mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-17 06:04:52 +00:00
teams tmux
This commit is contained in:
parent
9057c30726
commit
fb88f43573
2 changed files with 138 additions and 11 deletions
75
packages/pi-teams/src/adapters/tmux-adapter.test.ts
Normal file
75
packages/pi-teams/src/adapters/tmux-adapter.test.ts
Normal 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}",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
* Implements the TerminalAdapter interface for tmux terminal multiplexer.
|
* Implements the TerminalAdapter interface for tmux terminal multiplexer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "node:child_process";
|
|
||||||
import {
|
import {
|
||||||
execCommand,
|
execCommand,
|
||||||
type SpawnOptions,
|
type SpawnOptions,
|
||||||
|
|
@ -15,8 +14,12 @@ export class TmuxAdapter implements TerminalAdapter {
|
||||||
readonly name = "tmux";
|
readonly name = "tmux";
|
||||||
|
|
||||||
detect(): boolean {
|
detect(): boolean {
|
||||||
// tmux is available if TMUX environment variable is set
|
if (process.env.TMUX) return true;
|
||||||
return !!process.env.TMUX;
|
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 {
|
spawn(options: SpawnOptions): string {
|
||||||
|
|
@ -24,12 +27,50 @@ export class TmuxAdapter implements TerminalAdapter {
|
||||||
.filter(([k]) => k.startsWith("PI_"))
|
.filter(([k]) => k.startsWith("PI_"))
|
||||||
.map(([k, v]) => `${k}=${v}`);
|
.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 = [
|
const tmuxArgs = [
|
||||||
"split-window",
|
"split-window",
|
||||||
"-h",
|
"-h",
|
||||||
"-dP",
|
"-dP",
|
||||||
"-F",
|
"-F",
|
||||||
"#{pane_id}",
|
"#{pane_id}",
|
||||||
|
...(targetWindow ? ["-t", targetWindow] : []),
|
||||||
"-c",
|
"-c",
|
||||||
options.cwd,
|
options.cwd,
|
||||||
"env",
|
"env",
|
||||||
|
|
@ -48,8 +89,17 @@ export class TmuxAdapter implements TerminalAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply layout after spawning
|
// Apply layout after spawning
|
||||||
execCommand("tmux", ["set-window-option", "main-pane-width", "60%"]);
|
execCommand("tmux", [
|
||||||
execCommand("tmux", ["select-layout", "main-vertical"]);
|
"set-window-option",
|
||||||
|
...(targetWindow ? ["-t", targetWindow] : []),
|
||||||
|
"main-pane-width",
|
||||||
|
"60%",
|
||||||
|
]);
|
||||||
|
execCommand("tmux", [
|
||||||
|
"select-layout",
|
||||||
|
...(targetWindow ? ["-t", targetWindow] : []),
|
||||||
|
"main-vertical",
|
||||||
|
]);
|
||||||
|
|
||||||
return result.stdout.trim();
|
return result.stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
@ -79,12 +129,14 @@ export class TmuxAdapter implements TerminalAdapter {
|
||||||
return false; // Not a tmux pane
|
return false; // Not a tmux pane
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const result = execCommand("tmux", [
|
||||||
execSync(`tmux has-session -t ${paneId}`);
|
"display-message",
|
||||||
return true;
|
"-p",
|
||||||
} catch {
|
"-t",
|
||||||
return false;
|
paneId.trim(),
|
||||||
}
|
"#{pane_id}",
|
||||||
|
]);
|
||||||
|
return result.status === 0 && result.stdout.trim() === paneId.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle(title: string): void {
|
setTitle(title: string): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue