/** * 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 " 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 " 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}`); } }