diff --git a/packages/pi-teams/src/adapters/tmux-adapter.test.ts b/packages/pi-teams/src/adapters/tmux-adapter.test.ts index 1591ef8..8f07b9c 100644 --- a/packages/pi-teams/src/adapters/tmux-adapter.test.ts +++ b/packages/pi-teams/src/adapters/tmux-adapter.test.ts @@ -13,6 +13,7 @@ describe("TmuxAdapter", () => { delete process.env.ZELLIJ; delete process.env.WEZTERM_PANE; delete process.env.TERM_PROGRAM; + delete process.env.COLORTERM; }); afterEach(() => { @@ -30,6 +31,18 @@ describe("TmuxAdapter", () => { expect(mockExecCommand).toHaveBeenCalledWith("tmux", ["-V"]); }); + it("does not detect tmux in GUI terminals just because the binary exists", () => { + process.env.COLORTERM = "truecolor"; + mockExecCommand.mockReturnValue({ + stdout: "tmux 3.4", + stderr: "", + status: 0, + }); + + expect(adapter.detect()).toBe(false); + expect(mockExecCommand).not.toHaveBeenCalled(); + }); + it("creates a detached team session when not already inside tmux", () => { mockExecCommand.mockImplementation((_bin: string, args: string[]) => { if (args[0] === "has-session") { @@ -56,19 +69,44 @@ describe("TmuxAdapter", () => { ); }); + it("splits an existing detached session when not already inside tmux", () => { + mockExecCommand.mockImplementation((_bin: string, args: string[]) => { + if (args[0] === "has-session") { + return { stdout: "", stderr: "", status: 0 }; + } + if (args[0] === "split-window") { + return { stdout: "%2\n", stderr: "", status: 0 }; + } + return { stdout: "", stderr: "", status: 0 }; + }); + + expect( + adapter.spawn({ + name: "worker", + cwd: "/tmp/project", + command: "pi", + env: { PI_TEAM_NAME: "demo", PI_AGENT_NAME: "worker" }, + }), + ).toBe("%2"); + + expect(mockExecCommand).toHaveBeenCalledWith( + "tmux", + expect.arrayContaining(["split-window", "-t", "pi-teams-demo:0"]), + ); + }); + it("checks pane liveness by pane id", () => { mockExecCommand.mockReturnValue({ - stdout: "%7\n", + stdout: "%1\n%7\n", stderr: "", status: 0, }); expect(adapter.isAlive("%7")).toBe(true); expect(mockExecCommand).toHaveBeenCalledWith("tmux", [ - "display-message", - "-p", - "-t", - "%7", + "list-panes", + "-a", + "-F", "#{pane_id}", ]); }); diff --git a/packages/pi-teams/src/adapters/tmux-adapter.ts b/packages/pi-teams/src/adapters/tmux-adapter.ts index ae73b46..354077e 100644 --- a/packages/pi-teams/src/adapters/tmux-adapter.ts +++ b/packages/pi-teams/src/adapters/tmux-adapter.ts @@ -18,6 +18,7 @@ export class TmuxAdapter implements TerminalAdapter { if (process.env.ZELLIJ || process.env.TERM_PROGRAM === "iTerm.app") { return false; } + if (process.env.TERM_PROGRAM || process.env.COLORTERM) return false; if (process.env.WEZTERM_PANE) return false; return execCommand("tmux", ["-V"]).status === 0; } @@ -60,6 +61,7 @@ export class TmuxAdapter implements TerminalAdapter { ); } + // The first pane becomes window 0; layout only matters once later spawns split it. return result.stdout.trim(); } } @@ -130,13 +132,15 @@ export class TmuxAdapter implements TerminalAdapter { } const result = execCommand("tmux", [ - "display-message", - "-p", - "-t", - paneId.trim(), + "list-panes", + "-a", + "-F", "#{pane_id}", ]); - return result.status === 0 && result.stdout.trim() === paneId.trim(); + return ( + result.status === 0 && + result.stdout.split("\n").some((line) => line.trim() === paneId.trim()) + ); } setTitle(title: string): void {