@rigkit/provider-cmux 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,11 +52,16 @@ export default workflow("site", {
52
52
  sshOptions: ["ServerAliveInterval=15"],
53
53
  },
54
54
  cwd: "/workspace/site",
55
- command: "pnpm dev",
55
+ surfaceLayout: "tabs",
56
+ terminals: [{ command: "pnpm dev" }],
56
57
  url: "http://localhost:3000",
57
58
  });
58
59
  },
59
60
  });
60
61
  ```
61
62
 
63
+ Set `surfaceLayout: "tabs"` to open terminals and the optional browser URL as
64
+ tabs in the same cmux pane. Omit it, or set `"splits"`, to keep the default
65
+ split-pane behavior.
66
+
62
67
  Local hosts can import `@rigkit/provider-cmux/host` to register the trusted `cmux.open` handler. The Rigkit CLI registers this handler automatically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigkit/provider-cmux",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,8 @@
17
17
  "README.md"
18
18
  ],
19
19
  "dependencies": {
20
- "@rigkit/sdk": "0.2.3",
21
- "@rigkit/engine": "0.2.3"
20
+ "@rigkit/sdk": "0.2.5",
21
+ "@rigkit/engine": "0.2.5"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/bun": "latest",
@@ -1,7 +1,7 @@
1
1
  export const CMUX_OPEN_CAPABILITY_ID = "cmux.open";
2
2
 
3
3
  export const CMUX_OPEN_SCHEMA_HASH =
4
- "sha256:671373232fc79a7f75dd01c8c83c0c350af62b349a89bb3cfcc96af2cd76c878";
4
+ "sha256:3a2975cfe53089c6a607da751b55575dc8806bd90132242d4b7e9065a26ae3af";
5
5
 
6
6
  export const CMUX_OPEN_CAPABILITY = {
7
7
  id: CMUX_OPEN_CAPABILITY_ID,
@@ -29,24 +29,40 @@ export type CmuxRemoteReadyOptions = {
29
29
  requireProxy?: boolean;
30
30
  };
31
31
 
32
+ export type CmuxOpenTerminalDirection = "left" | "right" | "up" | "down";
33
+ export type CmuxOpenSurfaceLayout = "splits" | "tabs";
34
+
35
+ export type CmuxOpenTerminalInput = {
36
+ command: string;
37
+ cwd?: string;
38
+ direction?: CmuxOpenTerminalDirection;
39
+ focus?: boolean;
40
+ };
41
+
32
42
  export type CmuxOpenInput = {
33
43
  name: string;
34
44
  ssh?: CmuxOpenSshInput;
35
45
  cwd?: string;
36
- command?: string;
46
+ surfaceLayout?: CmuxOpenSurfaceLayout;
47
+ terminals?: readonly CmuxOpenTerminalInput[];
37
48
  url?: string;
38
49
  focus?: boolean;
39
50
  waitForRemoteReady?: boolean | CmuxRemoteReadyOptions;
40
51
  };
41
52
 
53
+ export type CmuxOpenPaneResult = {
54
+ paneId?: string;
55
+ paneRef?: string;
56
+ surfaceId?: string;
57
+ surfaceRef?: string;
58
+ };
59
+
42
60
  export type CmuxOpenResult = {
43
61
  sessionId: string;
44
62
  workspaceId: string;
45
63
  workspaceRef?: string;
46
- terminalPaneId?: string;
47
- terminalSurfaceId?: string;
48
- browserPaneId?: string;
49
- browserSurfaceId?: string;
64
+ terminalPanes: CmuxOpenPaneResult[];
65
+ browserPane?: CmuxOpenPaneResult;
50
66
  };
51
67
 
52
68
  export type CmuxOpenSession = CmuxOpenResult & {
package/src/host.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import {
2
2
  createCmuxClient,
3
3
  formatShellCommand,
4
- type CmuxBrowserOpenOptions,
5
4
  type CmuxClient,
6
5
  type CmuxClientOptions,
7
- type CmuxNewPaneOptions,
8
6
  type CmuxNewWorkspaceOptions,
9
7
  type CmuxPane,
10
8
  type CmuxPortsKickOptions,
@@ -13,12 +11,19 @@ import {
13
11
  type CmuxWaitForRemoteOptions,
14
12
  type CmuxWorkspace,
15
13
  } from "./index.ts";
16
- import { defineHostCapability, type HostCapabilityHandler } from "@rigkit/sdk/host";
14
+ import {
15
+ defineHostCapability,
16
+ type HostCapabilityContext,
17
+ type HostCapabilityHandler,
18
+ } from "@rigkit/sdk/host";
17
19
  import {
18
20
  CMUX_OPEN_CAPABILITY,
19
21
  type CmuxOpenInput,
22
+ type CmuxOpenPaneResult,
20
23
  type CmuxOpenResult,
21
24
  type CmuxOpenSshInput,
25
+ type CmuxOpenTerminalDirection,
26
+ type CmuxOpenTerminalInput,
22
27
  type CmuxRemoteReadyOptions,
23
28
  } from "./capabilities.ts";
24
29
 
@@ -29,6 +34,7 @@ export type CmuxOpenClient = Pick<
29
34
  | "newWorkspace"
30
35
  | "ssh"
31
36
  | "newPane"
37
+ | "newSurface"
32
38
  | "send"
33
39
  | "portsKick"
34
40
  | "browserOpen"
@@ -39,6 +45,7 @@ export type CmuxOpenClient = Pick<
39
45
  export type CmuxOpenHostOptions = {
40
46
  client?: CmuxOpenClient;
41
47
  clientOptions?: CmuxClientOptions;
48
+ logger?: (message: string) => void;
42
49
  };
43
50
 
44
51
  export function createCmuxOpenHostCapability(
@@ -46,7 +53,11 @@ export function createCmuxOpenHostCapability(
46
53
  ): CmuxHostCapabilityHandler {
47
54
  return defineHostCapability(CMUX_OPEN_CAPABILITY.id, {
48
55
  schemaHash: CMUX_OPEN_CAPABILITY.schemaHash,
49
- handle: async (params) => await openCmux(params, options),
56
+ handle: async (params, context) =>
57
+ await openCmux(params, {
58
+ ...options,
59
+ logger: options.logger ?? hostCapabilityLogger(context) ?? options.clientOptions?.logger,
60
+ }),
50
61
  });
51
62
  }
52
63
 
@@ -57,84 +68,120 @@ export async function openCmux(
57
68
  options: CmuxOpenHostOptions = {},
58
69
  ): Promise<CmuxOpenResult> {
59
70
  const input = parseCmuxOpenInput(params);
60
- const cmux = options.client ?? createCmuxClient(options.clientOptions);
61
- const command = commandForInput(input);
71
+ const logger = cmuxOpenLogger(options);
72
+ const cmux = options.client ?? createCmuxClient({
73
+ ...options.clientOptions,
74
+ ...(options.logger ? { logger: options.logger } : {}),
75
+ printCommands: options.clientOptions?.printCommands ?? false,
76
+ });
62
77
  let workspace: CmuxWorkspace;
63
- let terminalPane: CmuxPane | undefined;
78
+ const terminalPanes: CmuxPane[] = [];
64
79
 
80
+ logger?.(`cmux: opening ${input.name}`);
65
81
  if (input.ssh) {
82
+ logger?.("cmux: connecting remote workspace");
66
83
  workspace = await cmux.ssh({
67
84
  ...cmuxSshOptionsForInput(input.ssh),
68
85
  name: input.name,
69
86
  noFocus: input.focus === false,
70
87
  });
71
88
  } else {
89
+ logger?.("cmux: creating workspace");
72
90
  const workspaceOptions: CmuxNewWorkspaceOptions = {
73
91
  name: input.name,
74
92
  cwd: input.cwd,
75
- command: input.command,
76
93
  focus: input.focus,
77
94
  };
78
95
  workspace = await cmux.newWorkspace(workspaceOptions);
79
96
  }
80
97
 
81
98
  const workspaceId = workspace.id ?? workspace.handle;
99
+ const useTabLayout = input.surfaceLayout === "tabs";
82
100
 
83
- if (input.ssh && command) {
84
- const paneOptions: CmuxNewPaneOptions = {
85
- workspace: workspaceId,
86
- type: "terminal",
87
- direction: "down",
88
- focus: true,
89
- };
90
- terminalPane = await cmux.newPane(paneOptions);
101
+ for (const terminal of input.terminals ?? []) {
102
+ const cwd = terminal.cwd ?? input.cwd;
103
+ logger?.(cwd ? `cmux: starting terminal in ${cwd}` : "cmux: starting terminal");
104
+ const terminalPane = useTabLayout
105
+ ? await cmux.newSurface({
106
+ workspace: workspaceId,
107
+ type: "terminal",
108
+ focus: terminal.focus ?? true,
109
+ })
110
+ : await cmux.newPane({
111
+ workspace: workspaceId,
112
+ type: "terminal",
113
+ direction: terminal.direction ?? "down",
114
+ focus: terminal.focus ?? true,
115
+ });
116
+ terminalPanes.push(terminalPane);
91
117
  const sendOptions: CmuxSendOptions = {
92
118
  workspace: workspaceId,
93
119
  surface: terminalPane.surface,
94
- text: command,
120
+ text: commandForTerminal(terminal, input),
95
121
  };
96
122
  await cmux.send(sendOptions);
97
123
  }
98
124
 
99
125
  const waitOptions = remoteReadyOptionsForInput(input);
100
126
  if (input.ssh && waitOptions) {
127
+ logger?.("cmux: waiting for remote ports");
101
128
  await cmux.waitForRemoteReady(workspaceId, waitOptions);
102
129
  }
103
130
 
104
- if (input.ssh && terminalPane?.surface) {
105
- const kickOptions: CmuxPortsKickOptions = {
106
- workspace: workspaceId,
107
- surface: terminalPane.surface,
108
- reason: "command",
109
- };
110
- await cmux.portsKick(kickOptions);
131
+ if (input.ssh && terminalPanes.some((pane) => pane.surface)) {
132
+ logger?.("cmux: refreshing remote ports");
133
+ for (const pane of terminalPanes) {
134
+ if (!pane.surface) continue;
135
+ const kickOptions: CmuxPortsKickOptions = {
136
+ workspace: workspaceId,
137
+ surface: pane.surface,
138
+ reason: "command",
139
+ };
140
+ await cmux.portsKick(kickOptions);
141
+ }
111
142
  }
112
143
 
113
144
  let browserPane: CmuxPane | undefined;
114
145
  if (input.url) {
115
- const browserOptions: CmuxBrowserOpenOptions = {
116
- workspace: workspaceId,
117
- url: input.url,
118
- focus: input.focus !== false,
119
- };
120
- browserPane = await cmux.browserOpen(browserOptions);
146
+ logger?.(`cmux: opening ${input.url}`);
147
+ browserPane = useTabLayout
148
+ ? await cmux.newSurface({
149
+ workspace: workspaceId,
150
+ type: "browser",
151
+ url: input.url,
152
+ focus: input.focus !== false,
153
+ })
154
+ : await cmux.browserOpen({
155
+ workspace: workspaceId,
156
+ url: input.url,
157
+ focus: input.focus !== false,
158
+ });
121
159
  }
122
160
 
123
161
  if (input.focus !== false) {
162
+ logger?.("cmux: focusing workspace");
124
163
  await cmux.selectWorkspace(workspaceId);
125
164
  }
126
165
 
166
+ logger?.(`cmux: ready ${input.name}`);
127
167
  return {
128
168
  sessionId: workspaceId,
129
169
  workspaceId,
130
170
  ...(workspace.ref ? { workspaceRef: workspace.ref } : {}),
131
- ...(terminalPane?.pane ? { terminalPaneId: terminalPane.pane } : {}),
132
- ...(terminalPane?.surface ? { terminalSurfaceId: terminalPane.surface } : {}),
133
- ...(browserPane?.pane ? { browserPaneId: browserPane.pane } : {}),
134
- ...(browserPane?.surface ? { browserSurfaceId: browserPane.surface } : {}),
171
+ terminalPanes: terminalPanes.map(paneResultForCmuxPane),
172
+ ...(browserPane ? { browserPane: paneResultForCmuxPane(browserPane) } : {}),
135
173
  };
136
174
  }
137
175
 
176
+ function cmuxOpenLogger(options: CmuxOpenHostOptions): ((message: string) => void) | undefined {
177
+ return options.logger ?? options.clientOptions?.logger;
178
+ }
179
+
180
+ function hostCapabilityLogger(context: HostCapabilityContext | undefined): ((message: string) => void) | undefined {
181
+ if (!context) return undefined;
182
+ return (message) => context.log(message, { label: "cmux" });
183
+ }
184
+
138
185
  export function parseCmuxOpenInput(value: unknown): CmuxOpenInput {
139
186
  if (!isRecord(value)) throw new Error(`cmux.open requires an object input`);
140
187
  const name = requiredString(value, "name");
@@ -142,7 +189,8 @@ export function parseCmuxOpenInput(value: unknown): CmuxOpenInput {
142
189
  name,
143
190
  ...(value.ssh !== undefined ? { ssh: parseSshInput(value.ssh) } : {}),
144
191
  ...optionalStringField(value, "cwd"),
145
- ...optionalStringField(value, "command"),
192
+ ...optionalSurfaceLayoutField(value, "surfaceLayout"),
193
+ ...(value.terminals !== undefined ? { terminals: parseTerminalInputs(value.terminals) } : {}),
146
194
  ...optionalStringField(value, "url"),
147
195
  ...optionalBooleanField(value, "focus"),
148
196
  ...(value.waitForRemoteReady !== undefined
@@ -205,10 +253,34 @@ function sshDestination(ssh: Extract<CmuxOpenSshInput, object>): string {
205
253
  return `${ssh.username}@${ssh.host}`;
206
254
  }
207
255
 
208
- function commandForInput(input: CmuxOpenInput): string | undefined {
209
- if (!input.command) return undefined;
210
- const prefix = input.cwd ? `${formatShellCommand(["cd", input.cwd])} && ` : "";
211
- return `${prefix}${input.command}\n`;
256
+ function parseTerminalInputs(value: unknown): CmuxOpenTerminalInput[] {
257
+ if (!Array.isArray(value)) throw new Error(`cmux.open terminals must be an array`);
258
+ return value.map((item, index) => parseTerminalInput(item, index));
259
+ }
260
+
261
+ function parseTerminalInput(value: unknown, index: number): CmuxOpenTerminalInput {
262
+ if (!isRecord(value)) throw new Error(`cmux.open terminals[${index}] must be an object`);
263
+ return {
264
+ command: requiredString(value, "command"),
265
+ ...optionalStringField(value, "cwd"),
266
+ ...optionalTerminalDirectionField(value, "direction"),
267
+ ...optionalBooleanField(value, "focus"),
268
+ };
269
+ }
270
+
271
+ function commandForTerminal(terminal: CmuxOpenTerminalInput, input: CmuxOpenInput): string {
272
+ const cwd = terminal.cwd ?? input.cwd;
273
+ const prefix = cwd ? `${formatShellCommand(["cd", cwd])} && ` : "";
274
+ return `${prefix}${terminal.command}\n`;
275
+ }
276
+
277
+ function paneResultForCmuxPane(pane: CmuxPane): CmuxOpenPaneResult {
278
+ return {
279
+ ...(pane.pane ? { paneId: pane.pane } : {}),
280
+ ...(pane.paneRef ? { paneRef: pane.paneRef } : {}),
281
+ ...(pane.surface ? { surfaceId: pane.surface } : {}),
282
+ ...(pane.surfaceRef ? { surfaceRef: pane.surfaceRef } : {}),
283
+ };
212
284
  }
213
285
 
214
286
  function remoteReadyOptionsForInput(input: CmuxOpenInput): CmuxWaitForRemoteOptions | false {
@@ -269,6 +341,30 @@ function optionalBooleanField(record: Record<string, unknown>, key: string): Rec
269
341
  return { [key]: value };
270
342
  }
271
343
 
344
+ function optionalTerminalDirectionField(
345
+ record: Record<string, unknown>,
346
+ key: string,
347
+ ): Record<string, CmuxOpenTerminalDirection> {
348
+ const value = record[key];
349
+ if (value === undefined) return {};
350
+ if (value !== "left" && value !== "right" && value !== "up" && value !== "down") {
351
+ throw new Error(`cmux.open ${key} must be "left", "right", "up", or "down"`);
352
+ }
353
+ return { [key]: value };
354
+ }
355
+
356
+ function optionalSurfaceLayoutField(
357
+ record: Record<string, unknown>,
358
+ key: string,
359
+ ): Record<string, "splits" | "tabs"> {
360
+ const value = record[key];
361
+ if (value === undefined) return {};
362
+ if (value !== "splits" && value !== "tabs") {
363
+ throw new Error(`cmux.open ${key} must be "splits" or "tabs"`);
364
+ }
365
+ return { [key]: value };
366
+ }
367
+
272
368
  function isRecord(value: unknown): value is Record<string, unknown> {
273
369
  return typeof value === "object" && value !== null && !Array.isArray(value);
274
370
  }
package/src/index.test.ts CHANGED
@@ -135,7 +135,7 @@ describe("cmux sdk", () => {
135
135
  ]);
136
136
  });
137
137
 
138
- test("creates panes, opens browsers, and sends terminal text", async () => {
138
+ test("creates panes, surfaces, opens browsers, and sends terminal text", async () => {
139
139
  const calls: Array<{ method: string; params: CmuxRpcParams }> = [];
140
140
  const cmux = createCmuxClient({
141
141
  printCommands: false,
@@ -161,6 +161,16 @@ describe("cmux sdk", () => {
161
161
  pane_ref: "pane:11",
162
162
  };
163
163
  }
164
+ if (method === "surface.create") {
165
+ return {
166
+ workspace_id: "00000000-0000-0000-0000-000000000009",
167
+ workspace_ref: "workspace:9",
168
+ surface_id: "00000000-0000-0000-0000-000000000012",
169
+ surface_ref: "surface:12",
170
+ pane_id: "00000000-0000-0000-0000-000000000008",
171
+ pane_ref: "pane:8",
172
+ };
173
+ }
164
174
  return {};
165
175
  },
166
176
  });
@@ -181,6 +191,17 @@ describe("cmux sdk", () => {
181
191
  surface: pane.surface,
182
192
  reason: "refresh",
183
193
  });
194
+ const surface = await cmux.newSurface({
195
+ workspace: "00000000-0000-0000-0000-000000000009",
196
+ pane: pane.pane,
197
+ type: "terminal",
198
+ focus: false,
199
+ });
200
+ await cmux.send({
201
+ workspace: "00000000-0000-0000-0000-000000000009",
202
+ surface: surface.surface,
203
+ text: "codex\\n",
204
+ });
184
205
  await cmux.browserOpen({
185
206
  workspace: "00000000-0000-0000-0000-000000000009",
186
207
  url: "http://localhost:3000",
@@ -188,6 +209,7 @@ describe("cmux sdk", () => {
188
209
  });
189
210
 
190
211
  expect(pane.surface).toBe("00000000-0000-0000-0000-000000000007");
212
+ expect(surface.surface).toBe("00000000-0000-0000-0000-000000000012");
191
213
  expect(calls).toEqual([
192
214
  {
193
215
  method: "pane.create",
@@ -214,6 +236,23 @@ describe("cmux sdk", () => {
214
236
  reason: "refresh",
215
237
  },
216
238
  },
239
+ {
240
+ method: "surface.create",
241
+ params: {
242
+ type: "terminal",
243
+ pane_id: "00000000-0000-0000-0000-000000000008",
244
+ workspace_id: "00000000-0000-0000-0000-000000000009",
245
+ focus: false,
246
+ },
247
+ },
248
+ {
249
+ method: "surface.send_text",
250
+ params: {
251
+ workspace_id: "00000000-0000-0000-0000-000000000009",
252
+ surface_id: "00000000-0000-0000-0000-000000000012",
253
+ text: "codex\\n",
254
+ },
255
+ },
217
256
  {
218
257
  method: "browser.open_split",
219
258
  params: {
@@ -400,8 +439,9 @@ describe("cmux sdk", () => {
400
439
  expect(() => cmux.run(["bad"])).toThrow(CmuxCommandError);
401
440
  });
402
441
 
403
- test("handles cmux.open host capability for an ssh workspace", async () => {
442
+ test("handles cmux.open host capability for an ssh workspace with tabs", async () => {
404
443
  const calls: Array<{ method: string; params: unknown }> = [];
444
+ const logs: string[] = [];
405
445
  const client = fakeOpenClient(calls);
406
446
 
407
447
  const result = await openCmux({
@@ -412,18 +452,38 @@ describe("cmux sdk", () => {
412
452
  sshOptions: ["ServerAliveInterval=15"],
413
453
  },
414
454
  cwd: "/workspace/site",
415
- command: "pnpm dev",
455
+ surfaceLayout: "tabs",
456
+ terminals: [
457
+ { command: "pnpm dev" },
458
+ { command: "codex", focus: false },
459
+ ],
416
460
  url: "http://localhost:4321",
417
- }, { client });
461
+ }, { client, logger: (message) => logs.push(message) });
418
462
 
419
463
  expect(result).toEqual({
420
464
  sessionId: "workspace-1",
421
465
  workspaceId: "workspace-1",
422
466
  workspaceRef: "workspace:1",
423
- terminalPaneId: "pane-1",
424
- terminalSurfaceId: "surface-1",
425
- browserPaneId: "pane-2",
426
- browserSurfaceId: "surface-2",
467
+ terminalPanes: [
468
+ {
469
+ paneId: "pane-1",
470
+ paneRef: "pane:1",
471
+ surfaceId: "surface-1",
472
+ surfaceRef: "surface:1",
473
+ },
474
+ {
475
+ paneId: "pane-2",
476
+ paneRef: "pane:2",
477
+ surfaceId: "surface-2",
478
+ surfaceRef: "surface:2",
479
+ },
480
+ ],
481
+ browserPane: {
482
+ paneId: "pane-3",
483
+ paneRef: "pane:3",
484
+ surfaceId: "surface-3",
485
+ surfaceRef: "surface:3",
486
+ },
427
487
  });
428
488
  expect(calls).toEqual([
429
489
  {
@@ -435,11 +495,10 @@ describe("cmux sdk", () => {
435
495
  }),
436
496
  },
437
497
  {
438
- method: "newPane",
498
+ method: "newSurface",
439
499
  params: {
440
500
  workspace: "workspace-1",
441
501
  type: "terminal",
442
- direction: "down",
443
502
  focus: true,
444
503
  },
445
504
  },
@@ -451,6 +510,22 @@ describe("cmux sdk", () => {
451
510
  text: "cd /workspace/site && pnpm dev\n",
452
511
  },
453
512
  },
513
+ {
514
+ method: "newSurface",
515
+ params: {
516
+ workspace: "workspace-1",
517
+ type: "terminal",
518
+ focus: false,
519
+ },
520
+ },
521
+ {
522
+ method: "send",
523
+ params: {
524
+ workspace: "workspace-1",
525
+ surface: "surface-2",
526
+ text: "cd /workspace/site && codex\n",
527
+ },
528
+ },
454
529
  {
455
530
  method: "waitForRemoteReady",
456
531
  params: {
@@ -467,9 +542,18 @@ describe("cmux sdk", () => {
467
542
  },
468
543
  },
469
544
  {
470
- method: "browserOpen",
545
+ method: "portsKick",
546
+ params: {
547
+ workspace: "workspace-1",
548
+ surface: "surface-2",
549
+ reason: "command",
550
+ },
551
+ },
552
+ {
553
+ method: "newSurface",
471
554
  params: {
472
555
  workspace: "workspace-1",
556
+ type: "browser",
473
557
  url: "http://localhost:4321",
474
558
  focus: true,
475
559
  },
@@ -480,6 +564,17 @@ describe("cmux sdk", () => {
480
564
  },
481
565
  ]);
482
566
  expect(calls[0]?.params).not.toHaveProperty("terminalStartupCommand");
567
+ expect(logs).toEqual([
568
+ "cmux: opening website",
569
+ "cmux: connecting remote workspace",
570
+ "cmux: starting terminal in /workspace/site",
571
+ "cmux: starting terminal in /workspace/site",
572
+ "cmux: waiting for remote ports",
573
+ "cmux: refreshing remote ports",
574
+ "cmux: opening http://localhost:4321",
575
+ "cmux: focusing workspace",
576
+ "cmux: ready website",
577
+ ]);
483
578
  });
484
579
 
485
580
  test("forwards an explicit cmux ssh terminal startup command", async () => {
@@ -513,8 +608,10 @@ describe("cmux sdk", () => {
513
608
  const controller = await cmuxProviderPlugin.createProvider({
514
609
  provider: { providerId: "cmux", config: {} },
515
610
  storage: memoryProviderStorage("cmux"),
611
+ hostStorage: memoryProviderStorage("cmux"),
612
+ local: { open: async () => {} },
516
613
  });
517
- const requests: Array<{ capability: string; params: unknown }> = [];
614
+ const requests: Array<{ capability: string; params: unknown; options: unknown }> = [];
518
615
  const runtime = await controller.runtime({
519
616
  workflow: "test",
520
617
  nodePath: "operation.open",
@@ -525,8 +622,8 @@ describe("cmux sdk", () => {
525
622
  metadata: () => {},
526
623
  local: {
527
624
  open: async () => {},
528
- requestCapability: async <Result,>(capability: string, params: unknown) => {
529
- requests.push({ capability, params });
625
+ requestCapability: async <Result,>(capability: string, params: unknown, options: unknown) => {
626
+ requests.push({ capability, params, options });
530
627
  return { sessionId: "workspace-1", workspaceId: "workspace-1" } as Result;
531
628
  },
532
629
  },
@@ -538,6 +635,7 @@ describe("cmux sdk", () => {
538
635
  {
539
636
  capability: "cmux.open",
540
637
  params: { name: "workspace" },
638
+ options: { nodePath: "operation.open" },
541
639
  },
542
640
  ]);
543
641
  expect(session.sessionId).toBe("workspace-1");
@@ -554,6 +652,8 @@ describe("cmux sdk", () => {
554
652
  const controller = await cmuxProviderPlugin.createProvider({
555
653
  provider: { providerId: "cmux", config: {} },
556
654
  storage: memoryProviderStorage("cmux"),
655
+ hostStorage: memoryProviderStorage("cmux"),
656
+ local: { open: async () => {} },
557
657
  });
558
658
  let resolveClosed!: () => void;
559
659
  const runtime = await controller.runtime({
@@ -566,9 +666,10 @@ describe("cmux sdk", () => {
566
666
  metadata: () => {},
567
667
  local: {
568
668
  open: async () => {},
569
- requestCapabilitySession: async <Result,>(capability: string, params: unknown) => {
669
+ requestCapabilitySession: async <Result,>(capability: string, params: unknown, options: unknown) => {
570
670
  expect(capability).toBe("cmux.open");
571
671
  expect(params).toEqual({ name: "workspace" });
672
+ expect(options).toEqual({ nodePath: "operation.open" });
572
673
  return {
573
674
  result: { sessionId: "workspace-1", workspaceId: "workspace-1" } as Result,
574
675
  closed: new Promise<void>((resolve) => {
@@ -594,6 +695,7 @@ describe("cmux sdk", () => {
594
695
  });
595
696
 
596
697
  function fakeOpenClient(calls: Array<{ method: string; params: unknown }>): CmuxOpenClient {
698
+ let terminalPaneIndex = 0;
597
699
  return {
598
700
  async newWorkspace(params) {
599
701
  calls.push({ method: "newWorkspace", params });
@@ -605,13 +707,26 @@ function fakeOpenClient(calls: Array<{ method: string; params: unknown }>): Cmux
605
707
  },
606
708
  async newPane(params) {
607
709
  calls.push({ method: "newPane", params });
710
+ terminalPaneIndex += 1;
711
+ return {
712
+ workspace: "workspace-1",
713
+ workspaceRef: "workspace:1",
714
+ pane: `pane-${terminalPaneIndex}`,
715
+ paneRef: `pane:${terminalPaneIndex}`,
716
+ surface: `surface-${terminalPaneIndex}`,
717
+ surfaceRef: `surface:${terminalPaneIndex}`,
718
+ };
719
+ },
720
+ async newSurface(params) {
721
+ calls.push({ method: "newSurface", params });
722
+ terminalPaneIndex += 1;
608
723
  return {
609
724
  workspace: "workspace-1",
610
725
  workspaceRef: "workspace:1",
611
- pane: "pane-1",
612
- paneRef: "pane:1",
613
- surface: "surface-1",
614
- surfaceRef: "surface:1",
726
+ pane: `pane-${terminalPaneIndex}`,
727
+ paneRef: `pane:${terminalPaneIndex}`,
728
+ surface: `surface-${terminalPaneIndex}`,
729
+ surfaceRef: `surface:${terminalPaneIndex}`,
615
730
  };
616
731
  },
617
732
  async send(params) {
@@ -627,10 +742,10 @@ function fakeOpenClient(calls: Array<{ method: string; params: unknown }>): Cmux
627
742
  return {
628
743
  workspace: "workspace-1",
629
744
  workspaceRef: "workspace:1",
630
- pane: "pane-2",
631
- paneRef: "pane:2",
632
- surface: "surface-2",
633
- surfaceRef: "surface:2",
745
+ pane: "browser-pane-1",
746
+ paneRef: "pane:browser-1",
747
+ surface: "browser-surface-1",
748
+ surfaceRef: "surface:browser-1",
634
749
  };
635
750
  },
636
751
  async selectWorkspace(workspace) {
package/src/index.ts CHANGED
@@ -103,6 +103,14 @@ export type CmuxNewPaneOptions = {
103
103
  focus?: boolean;
104
104
  };
105
105
 
106
+ export type CmuxNewSurfaceOptions = {
107
+ workspace?: string;
108
+ pane?: string;
109
+ type?: "terminal" | "browser";
110
+ url?: string;
111
+ focus?: boolean;
112
+ };
113
+
106
114
  export type CmuxPane = {
107
115
  workspace?: string;
108
116
  workspaceRef?: string;
@@ -277,6 +285,17 @@ export class CmuxClient {
277
285
  return paneFromResult(await this.rpc("pane.create", params));
278
286
  }
279
287
 
288
+ async newSurface(options: CmuxNewSurfaceOptions = {}): Promise<CmuxPane> {
289
+ const params: CmuxRpcParams = {};
290
+ if (options.type) params.type = options.type;
291
+ if (options.pane) params.pane_id = options.pane;
292
+ if (options.workspace) params.workspace_id = options.workspace;
293
+ if (options.url) params.url = options.url;
294
+ if (options.focus !== undefined) params.focus = options.focus;
295
+
296
+ return paneFromResult(await this.rpc("surface.create", params));
297
+ }
298
+
280
299
  async listWorkspaces(): Promise<CmuxWorkspaceStatus[]> {
281
300
  const result = await this.rpc("workspace.list");
282
301
  const workspaces = Array.isArray(result.workspaces)
@@ -477,9 +496,13 @@ export {
477
496
  CMUX_OPEN_CAPABILITY_ID,
478
497
  CMUX_OPEN_SCHEMA_HASH,
479
498
  type CmuxOpenInput,
499
+ type CmuxOpenPaneResult,
480
500
  type CmuxOpenResult,
481
501
  type CmuxOpenSession,
502
+ type CmuxOpenSurfaceLayout,
482
503
  type CmuxOpenSshInput,
504
+ type CmuxOpenTerminalDirection,
505
+ type CmuxOpenTerminalInput,
483
506
  type CmuxRemoteReadyOptions,
484
507
  } from "./capabilities.ts";
485
508
  export {
package/src/provider.ts CHANGED
@@ -7,6 +7,7 @@ import type { BaseProviderPlugin, WorkflowProviderController } from "@rigkit/eng
7
7
  import {
8
8
  CMUX_OPEN_CAPABILITY_ID,
9
9
  type CmuxOpenInput,
10
+ type CmuxOpenPaneResult,
10
11
  type CmuxOpenResult,
11
12
  type CmuxOpenSession,
12
13
  } from "./capabilities.ts";
@@ -37,24 +38,25 @@ export const cmuxProviderPlugin: BaseProviderPlugin = {
37
38
  return {
38
39
  providerId: CMUX_PROVIDER_ID,
39
40
  runtime(context) {
40
- return createCmuxRuntime(context.local);
41
+ return createCmuxRuntime(context.local, context.nodePath);
41
42
  },
42
43
  };
43
44
  },
44
45
  };
45
46
 
46
- function createCmuxRuntime(local: LocalWorkspaceRuntime): CmuxRuntime {
47
+ function createCmuxRuntime(local: LocalWorkspaceRuntime, nodePath: string): CmuxRuntime {
47
48
  return {
48
- open: async (input) => await requestCmuxOpen(local, input),
49
+ open: async (input) => await requestCmuxOpen(local, input, { nodePath }),
49
50
  };
50
51
  }
51
52
 
52
53
  export async function requestCmuxOpen(
53
54
  local: LocalWorkspaceRuntime,
54
55
  input: CmuxOpenInput,
56
+ options: { nodePath?: string } = {},
55
57
  ): Promise<CmuxOpenSession> {
56
58
  if (local.requestCapabilitySession) {
57
- const session = await local.requestCapabilitySession<CmuxOpenResult>(CMUX_OPEN_CAPABILITY_ID, input);
59
+ const session = await local.requestCapabilitySession<CmuxOpenResult>(CMUX_OPEN_CAPABILITY_ID, input, options);
58
60
  return {
59
61
  ...parseCmuxOpenResult(session.result),
60
62
  closed: session.closed,
@@ -64,7 +66,7 @@ export async function requestCmuxOpen(
64
66
  throw new Error(`Host capability ${CMUX_OPEN_CAPABILITY_ID} is unavailable in this runtime`);
65
67
  }
66
68
  const result = parseCmuxOpenResult(
67
- await local.requestCapability(CMUX_OPEN_CAPABILITY_ID, input),
69
+ await local.requestCapability(CMUX_OPEN_CAPABILITY_ID, input, options),
68
70
  );
69
71
  return {
70
72
  ...result,
@@ -81,13 +83,32 @@ export function parseCmuxOpenResult(value: unknown): CmuxOpenResult {
81
83
  sessionId,
82
84
  workspaceId,
83
85
  ...optionalStringField(value, "workspaceRef"),
84
- ...optionalStringField(value, "terminalPaneId"),
85
- ...optionalStringField(value, "terminalSurfaceId"),
86
- ...optionalStringField(value, "browserPaneId"),
87
- ...optionalStringField(value, "browserSurfaceId"),
86
+ terminalPanes: arrayField(value, "terminalPanes", parseCmuxOpenPaneResult),
87
+ ...(value.browserPane !== undefined ? { browserPane: parseCmuxOpenPaneResult(value.browserPane) } : {}),
88
88
  };
89
89
  }
90
90
 
91
+ function parseCmuxOpenPaneResult(value: unknown): CmuxOpenPaneResult {
92
+ if (!isRecord(value)) throw new Error(`cmux.open returned a non-object pane result`);
93
+ return {
94
+ ...optionalStringField(value, "paneId"),
95
+ ...optionalStringField(value, "paneRef"),
96
+ ...optionalStringField(value, "surfaceId"),
97
+ ...optionalStringField(value, "surfaceRef"),
98
+ };
99
+ }
100
+
101
+ function arrayField<Item>(
102
+ record: Record<string, unknown>,
103
+ key: string,
104
+ parseItem: (value: unknown) => Item,
105
+ ): Item[] {
106
+ const value = record[key];
107
+ if (value === undefined) return [];
108
+ if (!Array.isArray(value)) throw new Error(`cmux.open result ${key} must be an array`);
109
+ return value.map(parseItem);
110
+ }
111
+
91
112
  function optionalStringField(record: Record<string, unknown>, key: string): Record<string, string> {
92
113
  const value = stringField(record, key);
93
114
  return value ? { [key]: value } : {};
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const RIGKIT_PROVIDER_CMUX_VERSION = "0.2.3";
1
+ export const RIGKIT_PROVIDER_CMUX_VERSION = "0.2.5";