mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
fix(daemon): normalize cmux workspace list responses
Dogfood against cmux 0.63.2 showed `cmux list-workspaces --json` can return plain text even though the flag is accepted. Prefer the RPC workspace.list surface when available, normalize ref/title rows to the adapter's id/name shape, and keep a plaintext fallback for older command surfaces. Tests: - npm --workspace packages/daemon test -- cmux-transport.test.ts cmux-transport-layout-methods.test.ts rig-cmux-launch-route.test.ts - npx vitest run test/cmux-adapter-layout-methods.test.ts test/cmux-transport-layout-methods.test.ts test/cmux-adapter.test.ts test/cmux-transport.test.ts test/cmux-layout-service.test.ts test/rig-cmux-launch-route.test.ts - npm --workspace packages/daemon run build
This commit is contained in:
1 parent
a2bc4ba337
commit
20820b235a
2 files changed
+115
-4
No files matched your search
@@ -66,6 +66,9 @@ function buildCommand(
|
||||
return { cmd: "cmux capabilities --json", json: true };
|
||||
}
|
||||
if (method === "workspace.list") {
|
||||
if (ctx.supported.has("rpc")) {
|
||||
return { cmd: "cmux rpc workspace.list", json: true };
|
||||
}
|
||||
return { cmd: "cmux list-workspaces --json", json: true };
|
||||
}
|
||||
if (method === "workspace.current") {
|
||||
@@ -163,10 +166,21 @@ function buildCommand(
|
||||
/**
|
||||
* Normalize JSON payloads from version-adaptive commands so downstream
|
||||
* consumers see one stable shape regardless of which cmux command actually
|
||||
* answered. Currently aliases `panels` (cmux ≥0.63 `list-panels`) onto
|
||||
* `surfaces` (legacy `list-surfaces`); leaves other shapes untouched.
|
||||
* answered.
|
||||
*/
|
||||
function normalizePayload(method: string, raw: unknown): unknown {
|
||||
if (method === "workspace.list") {
|
||||
if (raw === null || typeof raw !== "object") return raw;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
if (!Array.isArray(obj.workspaces)) return raw;
|
||||
return {
|
||||
...obj,
|
||||
workspaces: obj.workspaces
|
||||
.map(normalizeWorkspaceRow)
|
||||
.filter((workspace): workspace is { id: string; name: string } => workspace !== null),
|
||||
};
|
||||
}
|
||||
|
||||
if (method !== "surface.list") return raw;
|
||||
if (raw === null || typeof raw !== "object") return raw;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
@@ -182,6 +196,29 @@ function normalizePayload(method: string, raw: unknown): unknown {
|
||||
return raw;
|
||||
}
|
||||
|
||||
function normalizeWorkspaceRow(row: unknown): { id: string; name: string } | null {
|
||||
if (row === null || typeof row !== "object") return null;
|
||||
const obj = row as Record<string, unknown>;
|
||||
const id =
|
||||
typeof obj.ref === "string"
|
||||
? obj.ref
|
||||
: typeof obj.workspace_ref === "string"
|
||||
? obj.workspace_ref
|
||||
: typeof obj.id === "string"
|
||||
? obj.id
|
||||
: typeof obj.workspace_id === "string"
|
||||
? obj.workspace_id
|
||||
: "";
|
||||
if (!id) return null;
|
||||
const name =
|
||||
typeof obj.name === "string" && obj.name.trim()
|
||||
? obj.name.trim()
|
||||
: typeof obj.title === "string" && obj.title.trim()
|
||||
? obj.title.trim()
|
||||
: id;
|
||||
return { id, name };
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-based CmuxTransportFactory.
|
||||
*
|
||||
@@ -262,9 +299,37 @@ function legacyJsonFallback(method: string, output: string): unknown | null {
|
||||
}
|
||||
}
|
||||
|
||||
if (method === "workspace.list") {
|
||||
const workspaces = parsePlainTextWorkspaceRows(trimmed);
|
||||
if (workspaces) {
|
||||
return { workspaces };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parsePlainTextWorkspaceRows(output: string): Array<{ id: string; name: string }> | null {
|
||||
const workspaces: Array<{ id: string; name: string }> = [];
|
||||
|
||||
for (const rawLine of output.split("\n")) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
|
||||
const rowMatch = line.match(/^(?:\*\s+)?(workspace:[^\s]+)(?:\s+(.*?))?\s*$/);
|
||||
if (!rowMatch || !rowMatch[1]) {
|
||||
return null;
|
||||
}
|
||||
const rawName = (rowMatch[2] ?? "").replace(/\s+\[[^\]]+\]\s*$/g, "").trim();
|
||||
workspaces.push({
|
||||
id: rowMatch[1],
|
||||
name: rawName || rowMatch[1],
|
||||
});
|
||||
}
|
||||
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
function parsePlainTextSurfaceRows(output: string): Array<{ id: string; title: string; type: string }> | null {
|
||||
const surfaces: Array<{ id: string; title: string; type: string }> = [];
|
||||
const knownSurfaceTypes = new Set(["terminal", "browser", "markdown"]);
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ExecFn } from "../src/adapters/tmux.js";
|
||||
* has removed the older `list-surfaces` and `agent-pids` commands.
|
||||
* cmux <0.63 exposed `list-surfaces` and `agent-pids` instead.
|
||||
*/
|
||||
function helpText(opts: { modern?: boolean; legacy?: boolean }): string {
|
||||
function helpText(opts: { modern?: boolean; legacy?: boolean; rpc?: boolean }): string {
|
||||
const lines = [
|
||||
"cmux - control cmux via Unix socket",
|
||||
"",
|
||||
@@ -31,6 +31,9 @@ function helpText(opts: { modern?: boolean; legacy?: boolean }): string {
|
||||
if (opts.legacy) {
|
||||
lines.push(" list-surfaces", " agent-pids");
|
||||
}
|
||||
if (opts.rpc) {
|
||||
lines.push(" rpc <method> [json-params]");
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
@@ -42,6 +45,7 @@ function helpText(opts: { modern?: boolean; legacy?: boolean }): string {
|
||||
function mockExec(opts: {
|
||||
modern?: boolean;
|
||||
legacy?: boolean;
|
||||
rpc?: boolean;
|
||||
overrides?: Record<string, string | ((cmd: string) => string)>;
|
||||
} = {}): ExecFn {
|
||||
const impl = async (cmd: string): Promise<string> => {
|
||||
@@ -86,7 +90,29 @@ describe("cmux CLI transport — stable commands (unchanged across versions)", (
|
||||
expect(exec).toHaveBeenCalledWith("cmux capabilities --json");
|
||||
});
|
||||
|
||||
it("request('workspace.list') -> exact: cmux list-workspaces --json", async () => {
|
||||
it("request('workspace.list') prefers rpc when available and maps titles to names", async () => {
|
||||
const exec = mockExec({
|
||||
modern: true,
|
||||
rpc: true,
|
||||
overrides: {
|
||||
"cmux rpc workspace.list":
|
||||
'{"workspaces":[{"ref":"workspace:7","title":"qa6-cmux"},{"id":"workspace-uuid","name":"manual-name"}]}',
|
||||
},
|
||||
});
|
||||
const transport = await createCmuxCliTransport(exec)();
|
||||
|
||||
const result = await transport.request("workspace.list");
|
||||
|
||||
expect(exec).toHaveBeenCalledWith("cmux rpc workspace.list");
|
||||
expect(result).toEqual({
|
||||
workspaces: [
|
||||
{ id: "workspace:7", name: "qa6-cmux" },
|
||||
{ id: "workspace-uuid", name: "manual-name" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("request('workspace.list') falls back to list-workspaces when rpc is unavailable", async () => {
|
||||
const exec = mockExec({ modern: true, overrides: { "cmux list-workspaces --json": '{"workspaces":[]}' } });
|
||||
const transport = await createCmuxCliTransport(exec)();
|
||||
|
||||
@@ -95,6 +121,26 @@ describe("cmux CLI transport — stable commands (unchanged across versions)", (
|
||||
expect(exec).toHaveBeenCalledWith("cmux list-workspaces --json");
|
||||
});
|
||||
|
||||
it("request('workspace.list') falls back to plain-text rows when list-workspaces --json prints text", async () => {
|
||||
const exec = mockExec({
|
||||
modern: true,
|
||||
overrides: {
|
||||
"cmux list-workspaces --json":
|
||||
"* workspace:1 qa-rig [selected]\n workspace:2 tmux attach -t qa-rig@openrig\n",
|
||||
},
|
||||
});
|
||||
const transport = await createCmuxCliTransport(exec)();
|
||||
|
||||
const result = await transport.request("workspace.list");
|
||||
|
||||
expect(result).toEqual({
|
||||
workspaces: [
|
||||
{ id: "workspace:1", name: "qa-rig" },
|
||||
{ id: "workspace:2", name: "tmux attach -t qa-rig@openrig" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("request('workspace.current') -> exact: cmux current-workspace --json", async () => {
|
||||
const exec = mockExec({
|
||||
modern: true,
|
||||
|
||||
Reference in new issue
Block a user