feat(daemon): CmuxAdapter layout RPC method extensions (slice 24 Checkpoint A)

Adds four new RPC method wrappers to CmuxAdapter powering the slice 24
layout flow per OPT-B (extend existing RPC adapter; chosen after
pre-scaffold spike confirmed cmux RPC exposes all required primitives):

- splitSurface(surfaceId, direction, workspaceId?): CmuxResult<string>
  Calls surface.split RPC; returns new surface handle. Direction is
  "left" | "right" | "up" | "down". Defensive handling of multiple
  response field name variants (created_surface_ref / created_surface_id
  / surface_ref / surface_id / id) via shared normalizeHandle helper.

- createWorkspace(name, cwd?): CmuxResult<string>
  Calls workspace.create RPC; returns new workspace handle. Optional
  cwd is omitted from params when not provided (cmux daemon picks a
  default).

- closeWorkspace(workspaceId): CmuxResult<void>
  Calls workspace.close RPC for cleanup paths (e.g., conflict-resolution
  retry, test teardown).

- listPaneSurfaces(paneId, workspaceId?): CmuxResult<CmuxSurface[]>
  Calls pane.surfaces RPC; returns surfaces in a pane. Used by the
  layout flow to discover the pane ID a new surface belongs to after
  surface.split (cmux returns surface handle; pane lookup is a separate
  call per the SOP).

Parameter convention: snake_case (surface_id, workspace_id, pane_id,
direction, name, cwd). Verified via slice 24 pre-scaffold spike against
live cmux daemon — `cmux rpc workspace.close { "workspace_id": ... }`
succeeds; camelCase `workspaceId` returns "Missing or invalid
workspace_id". Existing CmuxAdapter methods (sendText, focus, etc.)
use camelCase params and are unchanged at this checkpoint; orthogonal
to slice 24 contract.

Tests: 15/15 new layout-method tests green; 33/33 existing CmuxAdapter
tests still green (zero regression).

SC-29 EXCEPTION declared: NONE at this checkpoint
(adapter extension only; no new ConfigStore key, env var, daemon
endpoint, schema migration, or event type. SC-29 will land at slice 24
Checkpoint C when the new POST /api/rigs/:rigId/cmux/launch route is
added; declared in that commit body + slice ACK.)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-11 21:17:21 -07:00
1 parent 2835a283b3
commit cd3383b2df
1 file changed
+91
+91
View File
@@ -186,6 +186,97 @@ export class CmuxAdapter {
}
}
// Slice 24 layout primitives — see slice 24 pre-scaffold spike result.
// cmux RPC accepts these methods with snake_case params (verified via
// `cmux rpc workspace.close` probe). New methods adopt snake_case
// explicitly; existing camelCase callers (sendText, focus, etc.) are
// unchanged at this checkpoint.
async splitSurface(
surfaceId: string,
direction: "left" | "right" | "up" | "down",
workspaceId?: string,
): Promise<CmuxResult<string>> {
if (!this.transport) {
return { ok: false, code: "unavailable", message: "cmux is not connected" };
}
try {
const params: Record<string, unknown> = {
surface_id: surfaceId,
direction,
};
if (workspaceId != null) params["workspace_id"] = workspaceId;
const raw = (await this.transport.request("surface.split", params)) as Record<string, unknown>;
const handle = [
raw["created_surface_ref"],
raw["created_surface_id"],
raw["surface_ref"],
raw["surface_id"],
raw["id"],
]
.map((value) => normalizeHandle("surface", value))
.find((value): value is string => Boolean(value));
if (!handle) {
return { ok: false, code: "request_failed", message: "cmux surface.split returned no surface handle" };
}
return { ok: true, data: handle };
} catch (err) {
return { ok: false, code: "request_failed", message: err instanceof Error ? err.message : String(err) };
}
}
async createWorkspace(name: string, cwd?: string): Promise<CmuxResult<string>> {
if (!this.transport) {
return { ok: false, code: "unavailable", message: "cmux is not connected" };
}
try {
const params: Record<string, unknown> = { name };
if (cwd != null) params["cwd"] = cwd;
const raw = (await this.transport.request("workspace.create", params)) as Record<string, unknown>;
const handle = [
raw["workspace_ref"],
raw["workspace_id"],
raw["id"],
]
.map((value) => normalizeHandle("workspace", value))
.find((value): value is string => Boolean(value));
if (!handle) {
return { ok: false, code: "request_failed", message: "cmux workspace.create returned no workspace handle" };
}
return { ok: true, data: handle };
} catch (err) {
return { ok: false, code: "request_failed", message: err instanceof Error ? err.message : String(err) };
}
}
async closeWorkspace(workspaceId: string): Promise<CmuxResult<void>> {
if (!this.transport) {
return { ok: false, code: "unavailable", message: "cmux is not connected" };
}
try {
await this.transport.request("workspace.close", { workspace_id: workspaceId });
return { ok: true, data: undefined };
} catch (err) {
return { ok: false, code: "request_failed", message: err instanceof Error ? err.message : String(err) };
}
}
async listPaneSurfaces(paneId: string, workspaceId?: string): Promise<CmuxResult<CmuxSurface[]>> {
if (!this.transport) {
return { ok: false, code: "unavailable", message: "cmux is not connected" };
}
try {
const params: Record<string, unknown> = { pane_id: paneId };
if (workspaceId != null) params["workspace_id"] = workspaceId;
const result = (await this.transport.request("pane.surfaces", params)) as {
surfaces?: CmuxSurface[];
};
return { ok: true, data: result.surfaces ?? [] };
} catch (err) {
return { ok: false, code: "request_failed", message: err instanceof Error ? err.message : String(err) };
}
}
/** Query cmux for agent PIDs (sidebar metadata). Returns Map<pid, { runtime, pid }>. */
async queryAgentPIDs(): Promise<CmuxResult<Map<number, { runtime: string; pid: number }>>> {
if (!this.transport) {