mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 05:02:07 +00:00
191 lines
5.2 KiB
TypeScript
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}`);
|
|
}
|
|
}
|