diff --git a/src/features/team-mode/team-layout-tmux/layout.test.ts b/src/features/team-mode/team-layout-tmux/layout.test.ts index a561771b6..67a9fb0cf 100644 --- a/src/features/team-mode/team-layout-tmux/layout.test.ts +++ b/src/features/team-mode/team-layout-tmux/layout.test.ts @@ -155,7 +155,7 @@ describe("team-layout-tmux", () => { expect(literals.some((s) => s.includes("--session 's-m2'"))).toBe(true) }) - test("uses main-vertical layout with leader at 30%", async () => { + test("uses main-vertical layout with leader at 30% for up to 3 teammates", async () => { // given const { createTeamLayout } = await loadLayoutModule() const members = [ @@ -177,39 +177,48 @@ describe("team-layout-tmux", () => { expect(Object.keys(result?.focusPanesByMember ?? {}).sort()).toEqual(["m1", "m2", "m3"]) }) - test("sets pane title for each member", async () => { + test("#given 4 or more teammates #when createTeamLayout runs #then it switches to tiled layout and stops resizing the leader pane", async () => { // given const { createTeamLayout } = await loadLayoutModule() - const members = [ - { name: "lead", sessionId: "s-lead", worktreePath: "/tmp/lead" }, - { name: "m2", sessionId: "s-m2", worktreePath: "/tmp/m2" }, - ] + const members = Array.from({ length: 5 }, (_, index) => ({ + name: `m${index + 1}`, + sessionId: `s-m${index + 1}`, + worktreePath: `/tmp/m${index + 1}`, + })) // when - await createTeamLayout("run-title", members, tmuxMgr as never) + await createTeamLayout("run-tiled", members, tmuxMgr as never) // then const commands = getCommands() - const titleSetters = commands - .filter((args) => args[0] === "select-pane" && args.includes("-T")) - .map((args) => args[args.length - 1]) - expect(titleSetters).toContain("lead") - expect(titleSetters).toContain("m2") + const selectLayoutArgs = commands.filter((args) => args[0] === "select-layout").map((args) => args[args.length - 1]) + expect(selectLayoutArgs).toContain("tiled") + const tiledIndex = selectLayoutArgs.indexOf("tiled") + const layoutsAfterTiled = selectLayoutArgs.slice(tiledIndex) + expect(layoutsAfterTiled.every((layout) => layout === "tiled")).toBe(true) + const indexOfFirstTiled = commands.findIndex((args) => args[0] === "select-layout" && args[args.length - 1] === "tiled") + const resizesAfterTiled = commands + .slice(indexOfFirstTiled) + .filter((args) => args[0] === "resize-pane" && args.includes("30%")) + expect(resizesAfterTiled).toEqual([]) }) - test("#given caller inside tmux #when createTeamLayout runs #then it restores keyboard focus and avoids border status mutation", async () => { + test("#given caller inside tmux #when createTeamLayout runs #then it never steals focus or mutates window border options", async () => { // given const { createTeamLayout } = await loadLayoutModule() - const members = [{ name: "m1", sessionId: "s-m1", worktreePath: "/tmp/m1" }] + const members = Array.from({ length: 5 }, (_, index) => ({ + name: `m${index + 1}`, + sessionId: `s-m${index + 1}`, + worktreePath: `/tmp/m${index + 1}`, + })) // when - await createTeamLayout("run-options", members, tmuxMgr as never) + await createTeamLayout("run-no-focus", members, tmuxMgr as never) // then const commands = getCommands() - const focusRestores = commands.filter((args) => args[0] === "select-pane" && args[1] === "-t" && args[2] === "%42") - expect(focusRestores.length).toBe(1) - expect(commands.some((args) => args[0] === "set-option" && args.includes("pane-border-status"))).toBe(false) + expect(commands.some((args) => args[0] === "select-pane")).toBe(false) + expect(commands.some((args) => args[0] === "set-option")).toBe(false) }) test("#given ownedSession=false, focusWindowId=@10, gridWindowId=@11 #when removeTeamLayout runs #then tmux kill-window is called twice with -t @10 and -t @11 and kill-session is NEVER called", async () => { diff --git a/src/features/team-mode/team-layout-tmux/layout.ts b/src/features/team-mode/team-layout-tmux/layout.ts index d43941cbd..7c1d39d86 100644 --- a/src/features/team-mode/team-layout-tmux/layout.ts +++ b/src/features/team-mode/team-layout-tmux/layout.ts @@ -68,8 +68,14 @@ async function listPanesInWindow(tmuxPath: string, windowTarget: string): Promis async function rebalanceWithLeader(tmuxPath: string, windowTarget: string, leaderPaneId: string): Promise { const panes = await listPanesInWindow(tmuxPath, windowTarget) if (panes.length <= 1) return - await runTmuxCommand(tmuxPath, ["select-layout", "-t", windowTarget, "main-vertical"]) - await runTmuxCommand(tmuxPath, ["resize-pane", "-t", leaderPaneId, "-x", "30%"]) + // main-vertical with many teammates collapses each pane to ~10 rows, which the + // opencode TUI cannot render. Switch to tiled once the column gets too tall. + const teammateCount = panes.length - 1 + const layout = teammateCount >= 4 ? "tiled" : "main-vertical" + await runTmuxCommand(tmuxPath, ["select-layout", "-t", windowTarget, layout]) + if (layout === "main-vertical") { + await runTmuxCommand(tmuxPath, ["resize-pane", "-t", leaderPaneId, "-x", "30%"]) + } } async function createTeammatePaneInCurrentWindow( @@ -105,12 +111,6 @@ async function createTeammatePaneInCurrentWindow( if (!splitResult.success || !splitResult.output) return null const paneId = splitResult.output.trim() - await runTmuxCommand(tmuxPath, ["select-pane", "-t", paneId, "-T", member.name]) - await runTmuxCommand(tmuxPath, ["select-pane", "-t", leaderPaneId]) - await runTmuxCommand(tmuxPath, ["set-option", "-p", "-t", paneId, "pane-border-style", "fg=cyan"]) - await runTmuxCommand(tmuxPath, ["set-option", "-p", "-t", paneId, "pane-active-border-style", "fg=cyan"]) - await runTmuxCommand(tmuxPath, ["set-option", "-p", "-t", paneId, "pane-border-format", "#[fg=cyan,bold] #{pane_title} #[default]"]) - await rebalanceWithLeader(tmuxPath, windowTarget, leaderPaneId) await new Promise((resolve) => setTimeout(resolve, PANE_SHELL_INIT_DELAY_MS))