co-mono/packages/pi-teams/src/adapters/cmux-adapter.ts
2026-03-05 17:36:25 -08:00

191 lines
5.2 KiB
TypeScript

/**
* CMUX Terminal Adapter
*
* Implements the TerminalAdapter interface for CMUX (cmux.dev).
*/
import { execCommand, type SpawnOptions, type TerminalAdapter } from "../utils/terminal-adapter";
export class CmuxAdapter implements TerminalAdapter {
readonly name = "cmux";
detect(): boolean {
// Check for CMUX specific environment variables
return !!process.env.CMUX_SOCKET_PATH || !!process.env.CMUX_WORKSPACE_ID;
}
spawn(options: SpawnOptions): string {
// We use new-split to create a new pane in CMUX.
// CMUX doesn't have a direct 'spawn' that returns a pane ID and runs a command
// in one go while also returning the ID in a way we can easily capture for 'isAlive'.
// However, 'new-split' returns the new surface ID.
// Construct the command with environment variables
const envPrefix = Object.entries(options.env)
.filter(([k]) => k.startsWith("PI_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");
const fullCommand = envPrefix ? `env ${envPrefix} ${options.command}` : options.command;
// CMUX new-split returns "OK <UUID>"
const splitResult = execCommand("cmux", ["new-split", "right", "--command", fullCommand]);
if (splitResult.status !== 0) {
throw new Error(`cmux new-split failed with status ${splitResult.status}: ${splitResult.stderr}`);
}
const output = splitResult.stdout.trim();
if (output.startsWith("OK ")) {
const surfaceId = output.substring(3).trim();
return surfaceId;
}
throw new Error(`cmux new-split returned unexpected output: ${output}`);
}
kill(paneId: string): void {
if (!paneId) return;
try {
// CMUX calls them surfaces
execCommand("cmux", ["close-surface", "--surface", paneId]);
} catch {
// Ignore errors during kill
}
}
isAlive(paneId: string): boolean {
if (!paneId) return false;
try {
// We can use list-pane-surfaces and grep for the ID
// Or just 'identify' if we want to be precise, but list-pane-surfaces is safer
const result = execCommand("cmux", ["list-pane-surfaces"]);
return result.stdout.includes(paneId);
} catch {
return false;
}
}
setTitle(title: string): void {
try {
// rename-tab or rename-workspace?
// Usually agents want to rename their current "tab" or "surface"
execCommand("cmux", ["rename-tab", title]);
} catch {
// Ignore errors
}
}
/**
* CMUX supports spawning separate OS windows
*/
supportsWindows(): boolean {
return true;
}
/**
* Spawn a new separate OS window.
*/
spawnWindow(options: SpawnOptions): string {
// CMUX new-window returns "OK <UUID>"
const result = execCommand("cmux", ["new-window"]);
if (result.status !== 0) {
throw new Error(`cmux new-window failed with status ${result.status}: ${result.stderr}`);
}
const output = result.stdout.trim();
if (output.startsWith("OK ")) {
const windowId = output.substring(3).trim();
// Now we need to run the command in this window.
// Usually new-window creates a default workspace/surface.
// We might need to find the workspace in that window.
// For now, let's just use 'new-workspace' in that window if possible,
// but CMUX commands usually target the current window unless specified.
// Wait a bit for the window to be ready?
const envPrefix = Object.entries(options.env)
.filter(([k]) => k.startsWith("PI_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");
const fullCommand = envPrefix ? `env ${envPrefix} ${options.command}` : options.command;
// Target the new window
execCommand("cmux", ["new-workspace", "--window", windowId, "--command", fullCommand]);
if (options.teamName) {
this.setWindowTitle(windowId, options.teamName);
}
return windowId;
}
throw new Error(`cmux new-window returned unexpected output: ${output}`);
}
/**
* Set the title of a specific window.
*/
setWindowTitle(windowId: string, title: string): void {
try {
execCommand("cmux", ["rename-window", "--window", windowId, title]);
} catch {
// Ignore
}
}
/**
* Kill/terminate a window.
*/
killWindow(windowId: string): void {
if (!windowId) return;
try {
execCommand("cmux", ["close-window", "--window", windowId]);
} catch {
// Ignore
}
}
/**
* Check if a window is still alive.
*/
isWindowAlive(windowId: string): boolean {
if (!windowId) return false;
try {
const result = execCommand("cmux", ["list-windows"]);
return result.stdout.includes(windowId);
} catch {
return false;
}
}
/**
* Custom CMUX capability: create a workspace for a problem.
* This isn't part of the TerminalAdapter interface but can be used via the adapter.
*/
createProblemWorkspace(title: string, command?: string): string {
const args = ["new-workspace"];
if (command) {
args.push("--command", command);
}
const result = execCommand("cmux", args);
if (result.status !== 0) {
throw new Error(`cmux new-workspace failed: ${result.stderr}`);
}
const output = result.stdout.trim();
if (output.startsWith("OK ")) {
const workspaceId = output.substring(3).trim();
execCommand("cmux", ["workspace-action", "--action", "rename", "--title", title, "--workspace", workspaceId]);
return workspaceId;
}
throw new Error(`cmux new-workspace returned unexpected output: ${output}`);
}
}