teams tmux

This commit is contained in:
Harivansh Rathi 2026-03-08 14:11:48 -07:00
parent 9057c30726
commit fb88f43573
2 changed files with 138 additions and 11 deletions

View file

@ -0,0 +1,75 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as terminalAdapter from "../utils/terminal-adapter";
import { TmuxAdapter } from "./tmux-adapter";
describe("TmuxAdapter", () => {
let adapter: TmuxAdapter;
let mockExecCommand: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
adapter = new TmuxAdapter();
mockExecCommand = vi.spyOn(terminalAdapter, "execCommand");
delete process.env.TMUX;
delete process.env.ZELLIJ;
delete process.env.WEZTERM_PANE;
delete process.env.TERM_PROGRAM;
});
afterEach(() => {
vi.clearAllMocks();
});
it("detects tmux in headless runtimes when the binary is available", () => {
mockExecCommand.mockReturnValue({
stdout: "tmux 3.4",
stderr: "",
status: 0,
});
expect(adapter.detect()).toBe(true);
expect(mockExecCommand).toHaveBeenCalledWith("tmux", ["-V"]);
});
it("creates a detached team session when not already inside tmux", () => {
mockExecCommand.mockImplementation((_bin: string, args: string[]) => {
if (args[0] === "has-session") {
return { stdout: "", stderr: "missing", status: 1 };
}
if (args[0] === "new-session") {
return { stdout: "%1\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("%1");
expect(mockExecCommand).toHaveBeenCalledWith(
"tmux",
expect.arrayContaining(["new-session", "-d", "-s", "pi-teams-demo"]),
);
});
it("checks pane liveness by pane id", () => {
mockExecCommand.mockReturnValue({
stdout: "%7\n",
stderr: "",
status: 0,
});
expect(adapter.isAlive("%7")).toBe(true);
expect(mockExecCommand).toHaveBeenCalledWith("tmux", [
"display-message",
"-p",
"-t",
"%7",
"#{pane_id}",
]);
});
});

View file

@ -4,7 +4,6 @@
* Implements the TerminalAdapter interface for tmux terminal multiplexer.
*/
import { execSync } from "node:child_process";
import {
execCommand,
type SpawnOptions,
@ -15,8 +14,12 @@ export class TmuxAdapter implements TerminalAdapter {
readonly name = "tmux";
detect(): boolean {
// tmux is available if TMUX environment variable is set
return !!process.env.TMUX;
if (process.env.TMUX) return true;
if (process.env.ZELLIJ || process.env.TERM_PROGRAM === "iTerm.app") {
return false;
}
if (process.env.WEZTERM_PANE) return false;
return execCommand("tmux", ["-V"]).status === 0;
}
spawn(options: SpawnOptions): string {
@ -24,12 +27,50 @@ export class TmuxAdapter implements TerminalAdapter {
.filter(([k]) => k.startsWith("PI_"))
.map(([k, v]) => `${k}=${v}`);
let targetWindow: string | null = null;
if (!process.env.TMUX) {
const sessionName = `pi-teams-${options.env.PI_TEAM_NAME || "default"}`;
targetWindow = `${sessionName}:0`;
const hasSession = execCommand("tmux", [
"has-session",
"-t",
sessionName,
]);
if (hasSession.status !== 0) {
const result = execCommand("tmux", [
"new-session",
"-d",
"-s",
sessionName,
"-P",
"-F",
"#{pane_id}",
"-c",
options.cwd,
"env",
...envArgs,
"sh",
"-c",
options.command,
]);
if (result.status !== 0) {
throw new Error(
`tmux spawn failed with status ${result.status}: ${result.stderr}`,
);
}
return result.stdout.trim();
}
}
const tmuxArgs = [
"split-window",
"-h",
"-dP",
"-F",
"#{pane_id}",
...(targetWindow ? ["-t", targetWindow] : []),
"-c",
options.cwd,
"env",
@ -48,8 +89,17 @@ export class TmuxAdapter implements TerminalAdapter {
}
// Apply layout after spawning
execCommand("tmux", ["set-window-option", "main-pane-width", "60%"]);
execCommand("tmux", ["select-layout", "main-vertical"]);
execCommand("tmux", [
"set-window-option",
...(targetWindow ? ["-t", targetWindow] : []),
"main-pane-width",
"60%",
]);
execCommand("tmux", [
"select-layout",
...(targetWindow ? ["-t", targetWindow] : []),
"main-vertical",
]);
return result.stdout.trim();
}
@ -79,12 +129,14 @@ export class TmuxAdapter implements TerminalAdapter {
return false; // Not a tmux pane
}
try {
execSync(`tmux has-session -t ${paneId}`);
return true;
} catch {
return false;
}
const result = execCommand("tmux", [
"display-message",
"-p",
"-t",
paneId.trim(),
"#{pane_id}",
]);
return result.status === 0 && result.stdout.trim() === paneId.trim();
}
setTitle(title: string): void {