adhdev 0.8.50 → 0.8.54

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.
Files changed (53) hide show
  1. package/dist/cli/index.js +2115 -853
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/index.js +1391 -613
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -2
  6. package/vendor/session-host-daemon/index.d.mts +3 -0
  7. package/vendor/session-host-daemon/index.d.ts +3 -0
  8. package/vendor/session-host-daemon/index.js +33 -2
  9. package/vendor/session-host-daemon/index.js.map +1 -1
  10. package/vendor/session-host-daemon/index.mjs +34 -2
  11. package/vendor/session-host-daemon/index.mjs.map +1 -1
  12. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.d.mts +16 -1
  13. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.d.ts +16 -1
  14. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.js +59 -0
  15. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.js.map +1 -1
  16. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.mjs +54 -0
  17. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.mjs.map +1 -1
  18. package/vendor/terminal-mux-cli/index.d.mts +1 -0
  19. package/vendor/terminal-mux-cli/index.d.ts +1 -0
  20. package/vendor/terminal-mux-cli/index.js +2070 -0
  21. package/vendor/terminal-mux-cli/index.mjs +2062 -0
  22. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.mts +442 -0
  23. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.ts +442 -0
  24. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js +676 -0
  25. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js.map +1 -0
  26. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.mjs +627 -0
  27. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.mjs.map +1 -0
  28. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/package.json +7 -0
  29. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.d.mts +16 -0
  30. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.d.ts +16 -0
  31. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.js +206 -0
  32. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.mjs +17 -0
  33. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/chunk-7RNMRPVZ.mjs +183 -0
  34. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/chunk-R4EFW6W3.mjs +46 -0
  35. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/chunk-XZWWVN5W.mjs +164 -0
  36. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.d.mts +35 -0
  37. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.d.ts +35 -0
  38. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.js +219 -0
  39. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.mjs +13 -0
  40. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.d.mts +5 -0
  41. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.d.ts +5 -0
  42. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.js +427 -0
  43. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.mjs +34 -0
  44. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/package.json +33 -0
  45. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.d.mts +49 -0
  46. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.d.ts +49 -0
  47. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.js +222 -0
  48. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.mjs +16 -0
  49. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.d.mts +164 -0
  50. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.d.ts +164 -0
  51. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.js +993 -0
  52. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.mjs +957 -0
  53. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/package.json +7 -0
@@ -0,0 +1,957 @@
1
+ // src/layout.ts
2
+ import { randomUUID } from "crypto";
3
+ function cloneNode(node) {
4
+ if (node.type === "pane") {
5
+ return { ...node };
6
+ }
7
+ return {
8
+ ...node,
9
+ first: cloneNode(node.first),
10
+ second: cloneNode(node.second)
11
+ };
12
+ }
13
+ function replaceNode(node, paneId, next) {
14
+ if (node.type === "pane") {
15
+ return node.paneId === paneId ? next : node;
16
+ }
17
+ return {
18
+ ...node,
19
+ first: replaceNode(node.first, paneId, next),
20
+ second: replaceNode(node.second, paneId, next)
21
+ };
22
+ }
23
+ function removeNode(node, paneId) {
24
+ if (node.type === "pane") {
25
+ return node.paneId === paneId ? null : node;
26
+ }
27
+ const first = removeNode(node.first, paneId);
28
+ const second = removeNode(node.second, paneId);
29
+ if (!first && !second) return null;
30
+ if (!first) return second;
31
+ if (!second) return first;
32
+ return {
33
+ ...node,
34
+ first,
35
+ second
36
+ };
37
+ }
38
+ function cloneWorkspace(workspace) {
39
+ return {
40
+ ...workspace,
41
+ root: cloneNode(workspace.root),
42
+ panes: Object.fromEntries(
43
+ Object.entries(workspace.panes).map(([paneId, pane]) => [paneId, { ...pane, viewport: { ...pane.viewport } }])
44
+ )
45
+ };
46
+ }
47
+ function nodeContainsPane(node, paneId) {
48
+ if (node.type === "pane") {
49
+ return node.paneId === paneId;
50
+ }
51
+ return nodeContainsPane(node.first, paneId) || nodeContainsPane(node.second, paneId);
52
+ }
53
+ function clampRatio(value) {
54
+ return Math.max(0.15, Math.min(0.85, value));
55
+ }
56
+ function adjustNodeForPane(node, paneId, axis, delta) {
57
+ if (node.type === "pane") {
58
+ return { node, changed: false };
59
+ }
60
+ const firstContains = nodeContainsPane(node.first, paneId);
61
+ const secondContains = !firstContains && nodeContainsPane(node.second, paneId);
62
+ let changed = false;
63
+ let nextNode = node;
64
+ if (node.axis === axis && (firstContains || secondContains)) {
65
+ const directionDelta = firstContains ? delta : -delta;
66
+ nextNode = {
67
+ ...node,
68
+ ratio: clampRatio(node.ratio + directionDelta)
69
+ };
70
+ changed = true;
71
+ }
72
+ if (firstContains) {
73
+ const adjusted = adjustNodeForPane(nextNode.first, paneId, axis, delta);
74
+ if (adjusted.changed) {
75
+ nextNode = {
76
+ ...nextNode,
77
+ first: adjusted.node
78
+ };
79
+ changed = true;
80
+ }
81
+ } else if (secondContains) {
82
+ const adjusted = adjustNodeForPane(nextNode.second, paneId, axis, delta);
83
+ if (adjusted.changed) {
84
+ nextNode = {
85
+ ...nextNode,
86
+ second: adjusted.node
87
+ };
88
+ changed = true;
89
+ }
90
+ }
91
+ return { node: nextNode, changed };
92
+ }
93
+ function rebalanceNode(node) {
94
+ if (node.type === "pane") return node;
95
+ return {
96
+ ...node,
97
+ ratio: 0.5,
98
+ first: rebalanceNode(node.first),
99
+ second: rebalanceNode(node.second)
100
+ };
101
+ }
102
+ function mapPaneIds(node, firstPaneId, secondPaneId) {
103
+ if (node.type === "pane") {
104
+ if (node.paneId === firstPaneId) return { ...node, paneId: secondPaneId };
105
+ if (node.paneId === secondPaneId) return { ...node, paneId: firstPaneId };
106
+ return node;
107
+ }
108
+ return {
109
+ ...node,
110
+ first: mapPaneIds(node.first, firstPaneId, secondPaneId),
111
+ second: mapPaneIds(node.second, firstPaneId, secondPaneId)
112
+ };
113
+ }
114
+ function collectPaneIds(node, acc = []) {
115
+ if (node.type === "pane") {
116
+ acc.push(node.paneId);
117
+ return acc;
118
+ }
119
+ collectPaneIds(node.first, acc);
120
+ collectPaneIds(node.second, acc);
121
+ return acc;
122
+ }
123
+ function buildEvenLayout(paneIds, axis = "vertical") {
124
+ if (paneIds.length === 0) throw new Error("Cannot build layout without panes");
125
+ if (paneIds.length === 1) return { type: "pane", paneId: paneIds[0] };
126
+ const midpoint = Math.ceil(paneIds.length / 2);
127
+ const nextAxis = axis === "vertical" ? "horizontal" : "vertical";
128
+ return {
129
+ type: "split",
130
+ axis,
131
+ ratio: 0.5,
132
+ first: buildEvenLayout(paneIds.slice(0, midpoint), nextAxis),
133
+ second: buildEvenLayout(paneIds.slice(midpoint), nextAxis)
134
+ };
135
+ }
136
+ function buildStackLayout(paneIds, axis) {
137
+ if (paneIds.length === 0) throw new Error("Cannot build stack layout without panes");
138
+ if (paneIds.length === 1) return { type: "pane", paneId: paneIds[0] };
139
+ const [first, ...rest] = paneIds;
140
+ return {
141
+ type: "split",
142
+ axis,
143
+ ratio: 0.5,
144
+ first: { type: "pane", paneId: first },
145
+ second: buildStackLayout(rest, axis)
146
+ };
147
+ }
148
+ function buildMainLayout(paneIds, rootAxis) {
149
+ if (paneIds.length === 0) throw new Error("Cannot build layout without panes");
150
+ if (paneIds.length === 1) return { type: "pane", paneId: paneIds[0] };
151
+ const [primary, ...rest] = paneIds;
152
+ const stackAxis = rootAxis === "vertical" ? "horizontal" : "vertical";
153
+ return {
154
+ type: "split",
155
+ axis: rootAxis,
156
+ ratio: rest.length === 1 ? 0.5 : 0.62,
157
+ first: { type: "pane", paneId: primary },
158
+ second: buildStackLayout(rest, stackAxis)
159
+ };
160
+ }
161
+ function createMuxWorkspace(initialPane, options = {}) {
162
+ return {
163
+ workspaceId: options.workspaceId || randomUUID(),
164
+ title: options.title || initialPane.displayName,
165
+ root: {
166
+ type: "pane",
167
+ paneId: initialPane.paneId
168
+ },
169
+ focusedPaneId: initialPane.paneId,
170
+ zoomedPaneId: null,
171
+ panes: {
172
+ [initialPane.paneId]: initialPane
173
+ }
174
+ };
175
+ }
176
+ function splitMuxPane(workspace, targetPaneId, axis, nextPane) {
177
+ if (!workspace.panes[targetPaneId]) {
178
+ throw new Error(`Unknown pane: ${targetPaneId}`);
179
+ }
180
+ const next = cloneWorkspace(workspace);
181
+ next.root = replaceNode(next.root, targetPaneId, {
182
+ type: "split",
183
+ axis,
184
+ ratio: 0.5,
185
+ first: { type: "pane", paneId: targetPaneId },
186
+ second: { type: "pane", paneId: nextPane.paneId }
187
+ });
188
+ next.panes[nextPane.paneId] = nextPane;
189
+ next.focusedPaneId = nextPane.paneId;
190
+ return next;
191
+ }
192
+ function removeMuxPane(workspace, paneId) {
193
+ if (!workspace.panes[paneId]) return workspace;
194
+ const root = removeNode(workspace.root, paneId);
195
+ if (!root) return null;
196
+ const next = cloneWorkspace(workspace);
197
+ delete next.panes[paneId];
198
+ next.root = root;
199
+ if (next.focusedPaneId === paneId) {
200
+ next.focusedPaneId = Object.keys(next.panes)[0] || "";
201
+ }
202
+ if (next.zoomedPaneId === paneId) {
203
+ next.zoomedPaneId = null;
204
+ }
205
+ return next;
206
+ }
207
+ function focusMuxPane(workspace, paneId) {
208
+ if (!workspace.panes[paneId]) {
209
+ throw new Error(`Unknown pane: ${paneId}`);
210
+ }
211
+ return {
212
+ ...workspace,
213
+ focusedPaneId: paneId
214
+ };
215
+ }
216
+ function updateMuxPane(workspace, pane) {
217
+ if (!workspace.panes[pane.paneId]) return workspace;
218
+ return {
219
+ ...workspace,
220
+ panes: {
221
+ ...workspace.panes,
222
+ [pane.paneId]: pane
223
+ }
224
+ };
225
+ }
226
+ function toggleMuxPaneZoom(workspace, paneId) {
227
+ if (!workspace.panes[paneId]) {
228
+ throw new Error(`Unknown pane: ${paneId}`);
229
+ }
230
+ return {
231
+ ...workspace,
232
+ focusedPaneId: paneId,
233
+ zoomedPaneId: workspace.zoomedPaneId === paneId ? null : paneId
234
+ };
235
+ }
236
+ function resizeMuxPane(workspace, paneId, direction, amount = 0.05) {
237
+ if (!workspace.panes[paneId]) {
238
+ throw new Error(`Unknown pane: ${paneId}`);
239
+ }
240
+ const axis = direction === "left" || direction === "right" ? "vertical" : "horizontal";
241
+ const delta = direction === "left" || direction === "up" ? -Math.abs(amount) : Math.abs(amount);
242
+ const adjusted = adjustNodeForPane(workspace.root, paneId, axis, delta);
243
+ if (!adjusted.changed) {
244
+ return workspace;
245
+ }
246
+ return {
247
+ ...workspace,
248
+ root: adjusted.node
249
+ };
250
+ }
251
+ function rebalanceMuxLayout(workspace) {
252
+ return {
253
+ ...workspace,
254
+ root: rebalanceNode(workspace.root)
255
+ };
256
+ }
257
+ function swapMuxPanePositions(workspace, firstPaneId, secondPaneId) {
258
+ if (firstPaneId === secondPaneId) return workspace;
259
+ if (!workspace.panes[firstPaneId]) throw new Error(`Unknown pane: ${firstPaneId}`);
260
+ if (!workspace.panes[secondPaneId]) throw new Error(`Unknown pane: ${secondPaneId}`);
261
+ return {
262
+ ...workspace,
263
+ root: mapPaneIds(workspace.root, firstPaneId, secondPaneId),
264
+ focusedPaneId: workspace.focusedPaneId === firstPaneId ? secondPaneId : workspace.focusedPaneId === secondPaneId ? firstPaneId : workspace.focusedPaneId,
265
+ zoomedPaneId: workspace.zoomedPaneId === firstPaneId ? secondPaneId : workspace.zoomedPaneId === secondPaneId ? firstPaneId : workspace.zoomedPaneId
266
+ };
267
+ }
268
+ function applyMuxLayoutPreset(workspace, preset) {
269
+ const paneIds = collectPaneIds(workspace.root);
270
+ const orderedPaneIds = [workspace.focusedPaneId, ...paneIds.filter((paneId) => paneId !== workspace.focusedPaneId)];
271
+ let root;
272
+ switch (preset) {
273
+ case "main-vertical":
274
+ root = buildMainLayout(orderedPaneIds, "vertical");
275
+ break;
276
+ case "main-horizontal":
277
+ root = buildMainLayout(orderedPaneIds, "horizontal");
278
+ break;
279
+ case "tiled":
280
+ case "even":
281
+ default:
282
+ root = buildEvenLayout(orderedPaneIds, "vertical");
283
+ break;
284
+ }
285
+ return { ...workspace, root };
286
+ }
287
+
288
+ // src/ghostty-terminal-surface.ts
289
+ import { createRequire } from "module";
290
+ var require2 = createRequire(
291
+ typeof __filename === "string" ? __filename : import.meta.url
292
+ );
293
+ var ghosttyBinding = require2("@adhdev/ghostty-vt-node");
294
+ var GhosttyTerminalSurface = class {
295
+ terminal;
296
+ cols;
297
+ rows;
298
+ snapshotSeq = 0;
299
+ constructor(options = {}) {
300
+ this.cols = Math.max(1, options.cols ?? 120);
301
+ this.rows = Math.max(1, options.rows ?? 36);
302
+ const terminalOptions = {
303
+ cols: this.cols,
304
+ rows: this.rows,
305
+ scrollback: Math.max(1024, options.scrollback ?? 32768)
306
+ };
307
+ this.terminal = ghosttyBinding.createTerminal(terminalOptions);
308
+ }
309
+ resetFromText(text, snapshotSeq = 0) {
310
+ this.terminal.dispose();
311
+ this.terminal = ghosttyBinding.createTerminal({
312
+ cols: this.cols,
313
+ rows: this.rows,
314
+ scrollback: 32768
315
+ });
316
+ if (text) {
317
+ this.terminal.write(text);
318
+ }
319
+ this.snapshotSeq = snapshotSeq;
320
+ }
321
+ write(data, snapshotSeq) {
322
+ if (data) {
323
+ this.terminal.write(data);
324
+ }
325
+ if (typeof snapshotSeq === "number") {
326
+ this.snapshotSeq = snapshotSeq;
327
+ }
328
+ }
329
+ resize(cols, rows) {
330
+ this.cols = Math.max(1, cols | 0);
331
+ this.rows = Math.max(1, rows | 0);
332
+ this.terminal.resize(this.cols, this.rows);
333
+ }
334
+ getViewportState() {
335
+ return {
336
+ cols: this.cols,
337
+ rows: this.rows,
338
+ snapshotSeq: this.snapshotSeq,
339
+ text: this.terminal.formatPlainText({ trim: true }) || ""
340
+ };
341
+ }
342
+ dispose() {
343
+ this.terminal.dispose();
344
+ }
345
+ };
346
+
347
+ // src/runtime-targeting.ts
348
+ import {
349
+ resolveAttachableRuntimeRecord
350
+ } from "@adhdev/session-host-core";
351
+ function resolveMuxOpenRuntimeRecord(records, identifier) {
352
+ return resolveAttachableRuntimeRecord(records, identifier);
353
+ }
354
+
355
+ // src/session-host-mux-client.ts
356
+ import { randomUUID as randomUUID2 } from "crypto";
357
+ import {
358
+ resolveRuntimeRecord,
359
+ SessionHostClient
360
+ } from "@adhdev/session-host-core";
361
+
362
+ // src/workspace-persistence.ts
363
+ function serializeWorkspace(workspace) {
364
+ return {
365
+ workspaceId: workspace.workspaceId,
366
+ title: workspace.title,
367
+ focusedPaneId: workspace.focusedPaneId,
368
+ zoomedPaneId: workspace.zoomedPaneId || null,
369
+ root: workspace.root,
370
+ panes: Object.fromEntries(
371
+ Object.entries(workspace.panes).map(([paneId, pane]) => [
372
+ paneId,
373
+ {
374
+ runtimeId: pane.runtimeId,
375
+ runtimeKey: pane.runtimeKey,
376
+ paneKind: pane.paneKind,
377
+ accessMode: pane.accessMode
378
+ }
379
+ ])
380
+ )
381
+ };
382
+ }
383
+
384
+ // src/session-host-mux-client.ts
385
+ function paneFromRecord(paneId, record, surface, paneKind, accessMode) {
386
+ return {
387
+ paneId,
388
+ paneKind,
389
+ runtimeId: record.sessionId,
390
+ runtimeKey: record.runtimeKey,
391
+ displayName: record.displayName,
392
+ workspaceLabel: record.workspaceLabel,
393
+ accessMode,
394
+ lifecycle: record.lifecycle,
395
+ writeOwner: record.writeOwner,
396
+ attachedClients: record.attachedClients,
397
+ viewport: surface.getViewportState()
398
+ };
399
+ }
400
+ var SessionHostMuxClient = class {
401
+ clientId;
402
+ clientType = "local-terminal";
403
+ client;
404
+ paneById = /* @__PURE__ */ new Map();
405
+ paneIdsByRuntime = /* @__PURE__ */ new Map();
406
+ workspaceById = /* @__PURE__ */ new Map();
407
+ listeners = /* @__PURE__ */ new Set();
408
+ unsubEvents = null;
409
+ constructor(options = {}) {
410
+ this.client = new SessionHostClient(options);
411
+ this.clientId = options.clientId || `terminal-ui-${randomUUID2()}`;
412
+ }
413
+ async connect() {
414
+ await this.client.connect();
415
+ if (!this.unsubEvents) {
416
+ this.unsubEvents = this.client.onEvent((event) => {
417
+ void this.handleHostEvent(event);
418
+ });
419
+ }
420
+ }
421
+ onEvent(listener) {
422
+ this.listeners.add(listener);
423
+ return () => {
424
+ this.listeners.delete(listener);
425
+ };
426
+ }
427
+ async createWorkspace(target, options = {}) {
428
+ const pane = await this.openRuntime(target, options);
429
+ const workspace = createMuxWorkspace(pane, {
430
+ workspaceId: options.workspaceId,
431
+ title: options.title
432
+ });
433
+ this.workspaceById.set(workspace.workspaceId, workspace);
434
+ this.emit({ kind: "workspace", workspace });
435
+ return workspace;
436
+ }
437
+ async splitWorkspacePane(workspaceId, targetPaneId, runtimeTarget, options) {
438
+ const workspace = this.requireWorkspace(workspaceId);
439
+ const nextPane = await this.openRuntime(runtimeTarget, options);
440
+ const updated = splitMuxPane(workspace, targetPaneId, options.axis, nextPane);
441
+ this.workspaceById.set(workspaceId, updated);
442
+ this.emit({ kind: "workspace", workspace: updated });
443
+ return updated;
444
+ }
445
+ async splitWorkspaceMirror(workspaceId, targetPaneId, sourcePaneId, axis) {
446
+ const workspace = this.requireWorkspace(workspaceId);
447
+ const source = this.requirePane(sourcePaneId);
448
+ const paneId = randomUUID2();
449
+ const viewport = source.surface.getViewportState();
450
+ const surface = new GhosttyTerminalSurface({
451
+ cols: viewport.cols,
452
+ rows: viewport.rows
453
+ });
454
+ surface.resetFromText(viewport.text, viewport.snapshotSeq);
455
+ const paneState = paneFromRecord(paneId, source.record, surface, "mirror", "read-only");
456
+ this.paneById.set(paneId, {
457
+ record: source.record,
458
+ surface,
459
+ paneKind: "mirror",
460
+ accessMode: "read-only",
461
+ requestedReadOnly: true
462
+ });
463
+ const paneIds = this.paneIdsByRuntime.get(source.record.sessionId) || /* @__PURE__ */ new Set();
464
+ paneIds.add(paneId);
465
+ this.paneIdsByRuntime.set(source.record.sessionId, paneIds);
466
+ const updated = splitMuxPane(workspace, targetPaneId, axis, paneState);
467
+ this.workspaceById.set(workspaceId, updated);
468
+ this.emit({ kind: "runtime", pane: paneState });
469
+ this.emit({ kind: "workspace", workspace: updated });
470
+ return updated;
471
+ }
472
+ async replacePaneRuntime(workspaceId, paneId, runtimeTarget, options = {}) {
473
+ const workspace = this.requireWorkspace(workspaceId);
474
+ const existing = this.requirePane(paneId);
475
+ const previousRuntimeId = existing.record.sessionId;
476
+ const replacement = await this.openRuntime(runtimeTarget, {
477
+ ...options,
478
+ paneId
479
+ });
480
+ existing.surface.dispose();
481
+ if (previousRuntimeId !== replacement.runtimeId) {
482
+ const existingPaneIds = this.paneIdsByRuntime.get(previousRuntimeId);
483
+ existingPaneIds?.delete(paneId);
484
+ if (existingPaneIds && existingPaneIds.size === 0) {
485
+ this.paneIdsByRuntime.delete(previousRuntimeId);
486
+ await this.client.request({
487
+ type: "detach_session",
488
+ payload: {
489
+ sessionId: previousRuntimeId,
490
+ clientId: this.clientId
491
+ }
492
+ });
493
+ }
494
+ }
495
+ const updated = updateMuxPane(workspace, replacement);
496
+ this.workspaceById.set(workspaceId, updated);
497
+ this.emit({ kind: "workspace", workspace: updated });
498
+ return updated;
499
+ }
500
+ async restoreWorkspace(snapshot) {
501
+ await this.connect();
502
+ const panes = Object.entries(snapshot.panes);
503
+ if (panes.length === 0) {
504
+ throw new Error(`Workspace ${snapshot.workspaceId} has no panes`);
505
+ }
506
+ const orderedPanes = panes.sort(([, left], [, right]) => {
507
+ const leftKind = left.paneKind || "runtime";
508
+ const rightKind = right.paneKind || "runtime";
509
+ if (leftKind === rightKind) return 0;
510
+ return leftKind === "runtime" ? -1 : 1;
511
+ });
512
+ const restoredPanes = [];
513
+ for (const [paneId, pane] of orderedPanes) {
514
+ const paneKind = pane.paneKind || "runtime";
515
+ const opened = paneKind === "mirror" ? await this.openMirrorRuntime(pane.runtimeId || pane.runtimeKey, { paneId }) : await this.openRuntime(pane.runtimeId || pane.runtimeKey, {
516
+ paneId,
517
+ readOnly: pane.accessMode === "read-only",
518
+ takeover: false
519
+ });
520
+ restoredPanes.push([paneId, opened]);
521
+ }
522
+ const workspace = {
523
+ workspaceId: snapshot.workspaceId,
524
+ title: snapshot.title,
525
+ root: snapshot.root,
526
+ focusedPaneId: snapshot.focusedPaneId in snapshot.panes ? snapshot.focusedPaneId : restoredPanes[0][0],
527
+ zoomedPaneId: snapshot.zoomedPaneId && snapshot.zoomedPaneId in snapshot.panes ? snapshot.zoomedPaneId : null,
528
+ panes: Object.fromEntries(restoredPanes)
529
+ };
530
+ this.workspaceById.set(workspace.workspaceId, workspace);
531
+ this.emit({ kind: "workspace", workspace });
532
+ return workspace;
533
+ }
534
+ async closePane(workspaceId, paneId) {
535
+ const workspace = this.requireWorkspace(workspaceId);
536
+ const next = removeMuxPane(workspace, paneId);
537
+ const pane = this.paneById.get(paneId);
538
+ if (pane) {
539
+ pane.surface.dispose();
540
+ this.paneById.delete(paneId);
541
+ const paneIds = this.paneIdsByRuntime.get(pane.record.sessionId);
542
+ paneIds?.delete(paneId);
543
+ if (paneIds && paneIds.size === 0) {
544
+ this.paneIdsByRuntime.delete(pane.record.sessionId);
545
+ await this.client.request({
546
+ type: "detach_session",
547
+ payload: {
548
+ sessionId: pane.record.sessionId,
549
+ clientId: this.clientId
550
+ }
551
+ });
552
+ }
553
+ }
554
+ if (!next) {
555
+ this.workspaceById.delete(workspaceId);
556
+ return null;
557
+ }
558
+ this.workspaceById.set(workspaceId, next);
559
+ this.emit({ kind: "workspace", workspace: next });
560
+ return next;
561
+ }
562
+ async focusPane(workspaceId, paneId) {
563
+ const workspace = this.requireWorkspace(workspaceId);
564
+ const next = focusMuxPane(workspace, paneId);
565
+ this.workspaceById.set(workspaceId, next);
566
+ this.emit({ kind: "workspace", workspace: next });
567
+ return next;
568
+ }
569
+ async resizeLayoutPane(workspaceId, paneId, direction, amount = 0.05) {
570
+ const workspace = this.requireWorkspace(workspaceId);
571
+ const next = resizeMuxPane(workspace, paneId, direction, amount);
572
+ this.workspaceById.set(workspaceId, next);
573
+ this.emit({ kind: "workspace", workspace: next });
574
+ return next;
575
+ }
576
+ async rebalanceWorkspaceLayout(workspaceId) {
577
+ const workspace = this.requireWorkspace(workspaceId);
578
+ const next = rebalanceMuxLayout(workspace);
579
+ this.workspaceById.set(workspaceId, next);
580
+ this.emit({ kind: "workspace", workspace: next });
581
+ return next;
582
+ }
583
+ async applyLayoutPreset(workspaceId, preset) {
584
+ const workspace = this.requireWorkspace(workspaceId);
585
+ const next = applyMuxLayoutPreset(workspace, preset);
586
+ this.workspaceById.set(workspaceId, next);
587
+ this.emit({ kind: "workspace", workspace: next });
588
+ return next;
589
+ }
590
+ async swapPanePositions(workspaceId, firstPaneId, secondPaneId) {
591
+ const workspace = this.requireWorkspace(workspaceId);
592
+ const next = swapMuxPanePositions(workspace, firstPaneId, secondPaneId);
593
+ this.workspaceById.set(workspaceId, next);
594
+ this.emit({ kind: "workspace", workspace: next });
595
+ return next;
596
+ }
597
+ async togglePaneZoom(workspaceId, paneId) {
598
+ const workspace = this.requireWorkspace(workspaceId);
599
+ const next = toggleMuxPaneZoom(workspace, paneId);
600
+ this.workspaceById.set(workspaceId, next);
601
+ this.emit({ kind: "workspace", workspace: next });
602
+ return next;
603
+ }
604
+ async sendInput(paneId, data) {
605
+ const pane = this.requirePane(paneId);
606
+ if (pane.accessMode === "read-only" && !pane.requestedReadOnly && pane.paneKind === "runtime") {
607
+ await this.takeoverPane(paneId);
608
+ }
609
+ if (pane.accessMode === "read-only") {
610
+ throw new Error(`Pane ${paneId} is read-only`);
611
+ }
612
+ const response = await this.client.request({
613
+ type: "send_input",
614
+ payload: {
615
+ sessionId: pane.record.sessionId,
616
+ clientId: this.clientId,
617
+ data
618
+ }
619
+ });
620
+ if (!response.success) {
621
+ if ((response.error || "").includes("Write owned by") && !pane.requestedReadOnly && pane.paneKind === "runtime") {
622
+ await this.takeoverPane(paneId);
623
+ const retry = await this.client.request({
624
+ type: "send_input",
625
+ payload: {
626
+ sessionId: pane.record.sessionId,
627
+ clientId: this.clientId,
628
+ data
629
+ }
630
+ });
631
+ if (!retry.success) {
632
+ throw new Error(retry.error || `Failed to send input to pane ${paneId}`);
633
+ }
634
+ return;
635
+ }
636
+ throw new Error(response.error || `Failed to send input to pane ${paneId}`);
637
+ }
638
+ }
639
+ async resizePane(paneId, cols, rows) {
640
+ const pane = this.requirePane(paneId);
641
+ pane.surface.resize(cols, rows);
642
+ await this.client.request({
643
+ type: "resize_session",
644
+ payload: {
645
+ sessionId: pane.record.sessionId,
646
+ cols,
647
+ rows
648
+ }
649
+ });
650
+ this.publishPaneUpdate(paneId);
651
+ }
652
+ async takeoverPane(paneId) {
653
+ const pane = this.requirePane(paneId);
654
+ const response = await this.client.request({
655
+ type: "acquire_write",
656
+ payload: {
657
+ sessionId: pane.record.sessionId,
658
+ clientId: this.clientId,
659
+ ownerType: "user",
660
+ force: true
661
+ }
662
+ });
663
+ if (!response.success || !response.result) {
664
+ throw new Error(response.error || "Failed to acquire write owner");
665
+ }
666
+ pane.record = response.result;
667
+ pane.requestedReadOnly = false;
668
+ pane.accessMode = "interactive";
669
+ this.publishPaneUpdate(paneId);
670
+ }
671
+ async releasePane(paneId) {
672
+ const pane = this.requirePane(paneId);
673
+ const response = await this.client.request({
674
+ type: "release_write",
675
+ payload: {
676
+ sessionId: pane.record.sessionId,
677
+ clientId: this.clientId
678
+ }
679
+ });
680
+ if (!response.success || !response.result) {
681
+ throw new Error(response.error || "Failed to release write owner");
682
+ }
683
+ pane.record = response.result;
684
+ pane.requestedReadOnly = false;
685
+ pane.accessMode = this.computeAccessMode(pane.record, false, pane.paneKind);
686
+ this.publishPaneUpdate(paneId);
687
+ }
688
+ listWorkspaces() {
689
+ return Array.from(this.workspaceById.values());
690
+ }
691
+ async listRuntimes() {
692
+ await this.connect();
693
+ const list = await this.client.request({ type: "list_sessions" });
694
+ if (!list.success || !list.result) {
695
+ throw new Error(list.error || "Failed to list runtimes");
696
+ }
697
+ return list.result;
698
+ }
699
+ async resumeRuntime(target) {
700
+ await this.connect();
701
+ const record = resolveRuntimeRecord(await this.listRuntimes(), target);
702
+ const response = await this.client.request({
703
+ type: "resume_session",
704
+ payload: {
705
+ sessionId: record.sessionId
706
+ }
707
+ });
708
+ if (!response.success || !response.result) {
709
+ throw new Error(response.error || `Failed to resume runtime ${target}`);
710
+ }
711
+ for (const [paneId, pane] of this.paneById) {
712
+ if (pane.record.sessionId !== record.sessionId) continue;
713
+ pane.record = response.result;
714
+ this.publishPaneUpdate(paneId);
715
+ }
716
+ return response.result;
717
+ }
718
+ serializeWorkspace(workspaceId) {
719
+ return serializeWorkspace(this.requireWorkspace(workspaceId));
720
+ }
721
+ async close() {
722
+ for (const [paneId, pane] of this.paneById) {
723
+ try {
724
+ if (pane.record.writeOwner?.clientId === this.clientId) {
725
+ await this.client.request({
726
+ type: "release_write",
727
+ payload: {
728
+ sessionId: pane.record.sessionId,
729
+ clientId: this.clientId
730
+ }
731
+ });
732
+ }
733
+ const siblings = this.paneIdsByRuntime.get(pane.record.sessionId);
734
+ if (siblings?.size === 1) {
735
+ await this.client.request({
736
+ type: "detach_session",
737
+ payload: {
738
+ sessionId: pane.record.sessionId,
739
+ clientId: this.clientId
740
+ }
741
+ });
742
+ }
743
+ } catch {
744
+ }
745
+ pane.surface.dispose();
746
+ this.paneById.delete(paneId);
747
+ }
748
+ this.paneIdsByRuntime.clear();
749
+ this.workspaceById.clear();
750
+ this.unsubEvents?.();
751
+ this.unsubEvents = null;
752
+ await this.client.close();
753
+ }
754
+ async openRuntime(target, options) {
755
+ await this.connect();
756
+ let record = resolveMuxOpenRuntimeRecord(await this.listRuntimes(), target);
757
+ if (record.lifecycle === "interrupted") {
758
+ try {
759
+ record = await this.resumeRuntime(target);
760
+ } catch {
761
+ }
762
+ }
763
+ const readOnly = options.takeover ? false : !!options.readOnly || !!(record.writeOwner && record.writeOwner.clientId !== this.clientId);
764
+ const attachResponse = await this.client.request({
765
+ type: "attach_session",
766
+ payload: {
767
+ sessionId: record.sessionId,
768
+ clientId: this.clientId,
769
+ clientType: this.clientType,
770
+ readOnly
771
+ }
772
+ });
773
+ if (!attachResponse.success || !attachResponse.result) {
774
+ throw new Error(attachResponse.error || `Failed to attach runtime ${target}`);
775
+ }
776
+ record = attachResponse.result;
777
+ if (options.takeover) {
778
+ const takeoverResponse = await this.client.request({
779
+ type: "acquire_write",
780
+ payload: {
781
+ sessionId: record.sessionId,
782
+ clientId: this.clientId,
783
+ ownerType: "user",
784
+ force: true
785
+ }
786
+ });
787
+ if (!takeoverResponse.success || !takeoverResponse.result) {
788
+ throw new Error(takeoverResponse.error || `Failed to acquire runtime ${target}`);
789
+ }
790
+ record = takeoverResponse.result;
791
+ }
792
+ const snapshot = await this.client.request({
793
+ type: "get_snapshot",
794
+ payload: {
795
+ sessionId: record.sessionId
796
+ }
797
+ });
798
+ if (!snapshot.success || !snapshot.result) {
799
+ throw new Error(snapshot.error || "Failed to get runtime snapshot");
800
+ }
801
+ const paneId = options.paneId || randomUUID2();
802
+ const surface = new GhosttyTerminalSurface({
803
+ cols: options.cols ?? 120,
804
+ rows: options.rows ?? 36
805
+ });
806
+ surface.resetFromText(snapshot.result.text, snapshot.result.seq);
807
+ const runtimeRecord = {
808
+ ...record,
809
+ attachedClients: record.attachedClients
810
+ };
811
+ const accessMode = this.computeAccessMode(runtimeRecord, readOnly, "runtime");
812
+ const paneState = paneFromRecord(paneId, runtimeRecord, surface, "runtime", accessMode);
813
+ this.paneById.set(paneId, {
814
+ record: runtimeRecord,
815
+ surface,
816
+ paneKind: "runtime",
817
+ accessMode,
818
+ requestedReadOnly: readOnly
819
+ });
820
+ const paneIds = this.paneIdsByRuntime.get(runtimeRecord.sessionId) || /* @__PURE__ */ new Set();
821
+ paneIds.add(paneId);
822
+ this.paneIdsByRuntime.set(runtimeRecord.sessionId, paneIds);
823
+ this.emit({ kind: "runtime", pane: paneState });
824
+ return paneState;
825
+ }
826
+ async openMirrorRuntime(target, options = {}) {
827
+ const record = resolveRuntimeRecord(await this.listRuntimes(), target);
828
+ const runtimePane = Array.from(this.paneById.values()).find((pane) => pane.record.sessionId === record.sessionId);
829
+ if (!runtimePane) {
830
+ throw new Error(`Cannot mirror runtime ${record.runtimeKey} before it is attached`);
831
+ }
832
+ const paneId = options.paneId || randomUUID2();
833
+ const viewport = runtimePane.surface.getViewportState();
834
+ const surface = new GhosttyTerminalSurface({
835
+ cols: viewport.cols,
836
+ rows: viewport.rows
837
+ });
838
+ surface.resetFromText(viewport.text, viewport.snapshotSeq);
839
+ const paneState = paneFromRecord(paneId, record, surface, "mirror", "read-only");
840
+ this.paneById.set(paneId, {
841
+ record,
842
+ surface,
843
+ paneKind: "mirror",
844
+ accessMode: "read-only",
845
+ requestedReadOnly: true
846
+ });
847
+ const paneIds = this.paneIdsByRuntime.get(record.sessionId) || /* @__PURE__ */ new Set();
848
+ paneIds.add(paneId);
849
+ this.paneIdsByRuntime.set(record.sessionId, paneIds);
850
+ this.emit({ kind: "runtime", pane: paneState });
851
+ return paneState;
852
+ }
853
+ async handleHostEvent(event) {
854
+ const sessionId = "sessionId" in event ? event.sessionId : event.type === "runtime_transition" ? event.transition.sessionId : event.type === "request_trace" ? event.trace.sessionId : event.entry.sessionId;
855
+ if (!sessionId) return;
856
+ const paneIds = this.paneIdsByRuntime.get(sessionId);
857
+ if (!paneIds || paneIds.size === 0) return;
858
+ for (const paneId of paneIds) {
859
+ const pane = this.paneById.get(paneId);
860
+ if (!pane) continue;
861
+ switch (event.type) {
862
+ case "session_output":
863
+ pane.surface.write(event.data, event.seq);
864
+ break;
865
+ case "session_started":
866
+ case "session_resumed":
867
+ pane.record = { ...pane.record, lifecycle: "running", osPid: event.pid ?? pane.record.osPid };
868
+ break;
869
+ case "session_exit":
870
+ pane.record = { ...pane.record, lifecycle: event.exitCode === 0 ? "stopped" : "failed" };
871
+ break;
872
+ case "session_stopped":
873
+ pane.record = { ...pane.record, lifecycle: "stopped" };
874
+ break;
875
+ case "session_resized":
876
+ pane.surface.resize(event.cols, event.rows);
877
+ break;
878
+ case "write_owner_changed":
879
+ pane.record = { ...pane.record, writeOwner: event.owner };
880
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
881
+ break;
882
+ case "client_attached":
883
+ pane.record = {
884
+ ...pane.record,
885
+ attachedClients: [
886
+ ...pane.record.attachedClients.filter((client) => client.clientId !== event.client.clientId),
887
+ event.client
888
+ ]
889
+ };
890
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
891
+ break;
892
+ case "client_detached":
893
+ pane.record = {
894
+ ...pane.record,
895
+ attachedClients: pane.record.attachedClients.filter((client) => client.clientId !== event.clientId)
896
+ };
897
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
898
+ break;
899
+ case "session_created":
900
+ pane.record = event.record;
901
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
902
+ break;
903
+ }
904
+ this.publishPaneUpdate(paneId, event);
905
+ }
906
+ }
907
+ publishPaneUpdate(paneId, event) {
908
+ const pane = this.requirePane(paneId);
909
+ const paneState = paneFromRecord(paneId, pane.record, pane.surface, pane.paneKind, pane.accessMode);
910
+ this.emit({ kind: "runtime", pane: paneState, event });
911
+ for (const [workspaceId, workspace] of this.workspaceById) {
912
+ if (!workspace.panes[paneId]) continue;
913
+ const updated = updateMuxPane(workspace, paneState);
914
+ this.workspaceById.set(workspaceId, updated);
915
+ this.emit({ kind: "workspace", workspace: updated });
916
+ }
917
+ }
918
+ emit(event) {
919
+ for (const listener of this.listeners) {
920
+ listener(event);
921
+ }
922
+ }
923
+ requireWorkspace(workspaceId) {
924
+ const workspace = this.workspaceById.get(workspaceId);
925
+ if (!workspace) throw new Error(`Unknown workspace: ${workspaceId}`);
926
+ return workspace;
927
+ }
928
+ requirePane(paneId) {
929
+ const pane = this.paneById.get(paneId);
930
+ if (!pane) throw new Error(`Unknown pane: ${paneId}`);
931
+ return pane;
932
+ }
933
+ computeAccessMode(record, requestedReadOnly, paneKind) {
934
+ if (paneKind === "mirror") return "read-only";
935
+ if (requestedReadOnly) return "read-only";
936
+ if (record.writeOwner && record.writeOwner.clientId !== this.clientId) return "read-only";
937
+ const attachedClient = record.attachedClients.find((client) => client.clientId === this.clientId);
938
+ if (attachedClient?.readOnly) return "read-only";
939
+ return "interactive";
940
+ }
941
+ };
942
+ export {
943
+ GhosttyTerminalSurface,
944
+ SessionHostMuxClient,
945
+ applyMuxLayoutPreset,
946
+ createMuxWorkspace,
947
+ focusMuxPane,
948
+ rebalanceMuxLayout,
949
+ removeMuxPane,
950
+ resizeMuxPane,
951
+ resolveMuxOpenRuntimeRecord,
952
+ serializeWorkspace,
953
+ splitMuxPane,
954
+ swapMuxPanePositions,
955
+ toggleMuxPaneZoom,
956
+ updateMuxPane
957
+ };