adhdev 0.8.50 → 0.8.52

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 (41) hide show
  1. package/dist/cli/index.js +30 -5
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/index.js +30 -5
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -2
  6. package/vendor/terminal-mux-cli/index.d.mts +1 -0
  7. package/vendor/terminal-mux-cli/index.d.ts +1 -0
  8. package/vendor/terminal-mux-cli/index.js +2056 -0
  9. package/vendor/terminal-mux-cli/index.mjs +2048 -0
  10. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.mts +427 -0
  11. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.ts +427 -0
  12. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js +617 -0
  13. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js.map +1 -0
  14. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.mjs +573 -0
  15. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.mjs.map +1 -0
  16. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/package.json +7 -0
  17. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.d.mts +16 -0
  18. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.d.ts +16 -0
  19. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.js +206 -0
  20. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/api.mjs +17 -0
  21. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/chunk-7RNMRPVZ.mjs +183 -0
  22. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/chunk-R4EFW6W3.mjs +46 -0
  23. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/chunk-XZWWVN5W.mjs +164 -0
  24. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.d.mts +35 -0
  25. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.d.ts +35 -0
  26. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.js +219 -0
  27. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/control-socket.mjs +13 -0
  28. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.d.mts +5 -0
  29. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.d.ts +5 -0
  30. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.js +427 -0
  31. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/index.mjs +34 -0
  32. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/package.json +33 -0
  33. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.d.mts +49 -0
  34. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.d.ts +49 -0
  35. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.js +222 -0
  36. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-control/storage.mjs +16 -0
  37. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.d.mts +162 -0
  38. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.d.ts +162 -0
  39. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.js +985 -0
  40. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/index.mjs +948 -0
  41. package/vendor/terminal-mux-cli/node_modules/@adhdev/terminal-mux-core/package.json +7 -0
@@ -0,0 +1,948 @@
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/session-host-mux-client.ts
348
+ import { randomUUID as randomUUID2 } from "crypto";
349
+ import {
350
+ resolveRuntimeRecord,
351
+ SessionHostClient
352
+ } from "@adhdev/session-host-core";
353
+
354
+ // src/workspace-persistence.ts
355
+ function serializeWorkspace(workspace) {
356
+ return {
357
+ workspaceId: workspace.workspaceId,
358
+ title: workspace.title,
359
+ focusedPaneId: workspace.focusedPaneId,
360
+ zoomedPaneId: workspace.zoomedPaneId || null,
361
+ root: workspace.root,
362
+ panes: Object.fromEntries(
363
+ Object.entries(workspace.panes).map(([paneId, pane]) => [
364
+ paneId,
365
+ {
366
+ runtimeId: pane.runtimeId,
367
+ runtimeKey: pane.runtimeKey,
368
+ paneKind: pane.paneKind,
369
+ accessMode: pane.accessMode
370
+ }
371
+ ])
372
+ )
373
+ };
374
+ }
375
+
376
+ // src/session-host-mux-client.ts
377
+ function paneFromRecord(paneId, record, surface, paneKind, accessMode) {
378
+ return {
379
+ paneId,
380
+ paneKind,
381
+ runtimeId: record.sessionId,
382
+ runtimeKey: record.runtimeKey,
383
+ displayName: record.displayName,
384
+ workspaceLabel: record.workspaceLabel,
385
+ accessMode,
386
+ lifecycle: record.lifecycle,
387
+ writeOwner: record.writeOwner,
388
+ attachedClients: record.attachedClients,
389
+ viewport: surface.getViewportState()
390
+ };
391
+ }
392
+ var SessionHostMuxClient = class {
393
+ clientId;
394
+ clientType = "local-terminal";
395
+ client;
396
+ paneById = /* @__PURE__ */ new Map();
397
+ paneIdsByRuntime = /* @__PURE__ */ new Map();
398
+ workspaceById = /* @__PURE__ */ new Map();
399
+ listeners = /* @__PURE__ */ new Set();
400
+ unsubEvents = null;
401
+ constructor(options = {}) {
402
+ this.client = new SessionHostClient(options);
403
+ this.clientId = options.clientId || `terminal-ui-${randomUUID2()}`;
404
+ }
405
+ async connect() {
406
+ await this.client.connect();
407
+ if (!this.unsubEvents) {
408
+ this.unsubEvents = this.client.onEvent((event) => {
409
+ void this.handleHostEvent(event);
410
+ });
411
+ }
412
+ }
413
+ onEvent(listener) {
414
+ this.listeners.add(listener);
415
+ return () => {
416
+ this.listeners.delete(listener);
417
+ };
418
+ }
419
+ async createWorkspace(target, options = {}) {
420
+ const pane = await this.openRuntime(target, options);
421
+ const workspace = createMuxWorkspace(pane, {
422
+ workspaceId: options.workspaceId,
423
+ title: options.title
424
+ });
425
+ this.workspaceById.set(workspace.workspaceId, workspace);
426
+ this.emit({ kind: "workspace", workspace });
427
+ return workspace;
428
+ }
429
+ async splitWorkspacePane(workspaceId, targetPaneId, runtimeTarget, options) {
430
+ const workspace = this.requireWorkspace(workspaceId);
431
+ const nextPane = await this.openRuntime(runtimeTarget, options);
432
+ const updated = splitMuxPane(workspace, targetPaneId, options.axis, nextPane);
433
+ this.workspaceById.set(workspaceId, updated);
434
+ this.emit({ kind: "workspace", workspace: updated });
435
+ return updated;
436
+ }
437
+ async splitWorkspaceMirror(workspaceId, targetPaneId, sourcePaneId, axis) {
438
+ const workspace = this.requireWorkspace(workspaceId);
439
+ const source = this.requirePane(sourcePaneId);
440
+ const paneId = randomUUID2();
441
+ const viewport = source.surface.getViewportState();
442
+ const surface = new GhosttyTerminalSurface({
443
+ cols: viewport.cols,
444
+ rows: viewport.rows
445
+ });
446
+ surface.resetFromText(viewport.text, viewport.snapshotSeq);
447
+ const paneState = paneFromRecord(paneId, source.record, surface, "mirror", "read-only");
448
+ this.paneById.set(paneId, {
449
+ record: source.record,
450
+ surface,
451
+ paneKind: "mirror",
452
+ accessMode: "read-only",
453
+ requestedReadOnly: true
454
+ });
455
+ const paneIds = this.paneIdsByRuntime.get(source.record.sessionId) || /* @__PURE__ */ new Set();
456
+ paneIds.add(paneId);
457
+ this.paneIdsByRuntime.set(source.record.sessionId, paneIds);
458
+ const updated = splitMuxPane(workspace, targetPaneId, axis, paneState);
459
+ this.workspaceById.set(workspaceId, updated);
460
+ this.emit({ kind: "runtime", pane: paneState });
461
+ this.emit({ kind: "workspace", workspace: updated });
462
+ return updated;
463
+ }
464
+ async replacePaneRuntime(workspaceId, paneId, runtimeTarget, options = {}) {
465
+ const workspace = this.requireWorkspace(workspaceId);
466
+ const existing = this.requirePane(paneId);
467
+ const previousRuntimeId = existing.record.sessionId;
468
+ const replacement = await this.openRuntime(runtimeTarget, {
469
+ ...options,
470
+ paneId
471
+ });
472
+ existing.surface.dispose();
473
+ if (previousRuntimeId !== replacement.runtimeId) {
474
+ const existingPaneIds = this.paneIdsByRuntime.get(previousRuntimeId);
475
+ existingPaneIds?.delete(paneId);
476
+ if (existingPaneIds && existingPaneIds.size === 0) {
477
+ this.paneIdsByRuntime.delete(previousRuntimeId);
478
+ await this.client.request({
479
+ type: "detach_session",
480
+ payload: {
481
+ sessionId: previousRuntimeId,
482
+ clientId: this.clientId
483
+ }
484
+ });
485
+ }
486
+ }
487
+ const updated = updateMuxPane(workspace, replacement);
488
+ this.workspaceById.set(workspaceId, updated);
489
+ this.emit({ kind: "workspace", workspace: updated });
490
+ return updated;
491
+ }
492
+ async restoreWorkspace(snapshot) {
493
+ await this.connect();
494
+ const panes = Object.entries(snapshot.panes);
495
+ if (panes.length === 0) {
496
+ throw new Error(`Workspace ${snapshot.workspaceId} has no panes`);
497
+ }
498
+ const orderedPanes = panes.sort(([, left], [, right]) => {
499
+ const leftKind = left.paneKind || "runtime";
500
+ const rightKind = right.paneKind || "runtime";
501
+ if (leftKind === rightKind) return 0;
502
+ return leftKind === "runtime" ? -1 : 1;
503
+ });
504
+ const restoredPanes = [];
505
+ for (const [paneId, pane] of orderedPanes) {
506
+ const paneKind = pane.paneKind || "runtime";
507
+ const opened = paneKind === "mirror" ? await this.openMirrorRuntime(pane.runtimeId || pane.runtimeKey, { paneId }) : await this.openRuntime(pane.runtimeId || pane.runtimeKey, {
508
+ paneId,
509
+ readOnly: pane.accessMode === "read-only",
510
+ takeover: false
511
+ });
512
+ restoredPanes.push([paneId, opened]);
513
+ }
514
+ const workspace = {
515
+ workspaceId: snapshot.workspaceId,
516
+ title: snapshot.title,
517
+ root: snapshot.root,
518
+ focusedPaneId: snapshot.focusedPaneId in snapshot.panes ? snapshot.focusedPaneId : restoredPanes[0][0],
519
+ zoomedPaneId: snapshot.zoomedPaneId && snapshot.zoomedPaneId in snapshot.panes ? snapshot.zoomedPaneId : null,
520
+ panes: Object.fromEntries(restoredPanes)
521
+ };
522
+ this.workspaceById.set(workspace.workspaceId, workspace);
523
+ this.emit({ kind: "workspace", workspace });
524
+ return workspace;
525
+ }
526
+ async closePane(workspaceId, paneId) {
527
+ const workspace = this.requireWorkspace(workspaceId);
528
+ const next = removeMuxPane(workspace, paneId);
529
+ const pane = this.paneById.get(paneId);
530
+ if (pane) {
531
+ pane.surface.dispose();
532
+ this.paneById.delete(paneId);
533
+ const paneIds = this.paneIdsByRuntime.get(pane.record.sessionId);
534
+ paneIds?.delete(paneId);
535
+ if (paneIds && paneIds.size === 0) {
536
+ this.paneIdsByRuntime.delete(pane.record.sessionId);
537
+ await this.client.request({
538
+ type: "detach_session",
539
+ payload: {
540
+ sessionId: pane.record.sessionId,
541
+ clientId: this.clientId
542
+ }
543
+ });
544
+ }
545
+ }
546
+ if (!next) {
547
+ this.workspaceById.delete(workspaceId);
548
+ return null;
549
+ }
550
+ this.workspaceById.set(workspaceId, next);
551
+ this.emit({ kind: "workspace", workspace: next });
552
+ return next;
553
+ }
554
+ async focusPane(workspaceId, paneId) {
555
+ const workspace = this.requireWorkspace(workspaceId);
556
+ const next = focusMuxPane(workspace, paneId);
557
+ this.workspaceById.set(workspaceId, next);
558
+ this.emit({ kind: "workspace", workspace: next });
559
+ return next;
560
+ }
561
+ async resizeLayoutPane(workspaceId, paneId, direction, amount = 0.05) {
562
+ const workspace = this.requireWorkspace(workspaceId);
563
+ const next = resizeMuxPane(workspace, paneId, direction, amount);
564
+ this.workspaceById.set(workspaceId, next);
565
+ this.emit({ kind: "workspace", workspace: next });
566
+ return next;
567
+ }
568
+ async rebalanceWorkspaceLayout(workspaceId) {
569
+ const workspace = this.requireWorkspace(workspaceId);
570
+ const next = rebalanceMuxLayout(workspace);
571
+ this.workspaceById.set(workspaceId, next);
572
+ this.emit({ kind: "workspace", workspace: next });
573
+ return next;
574
+ }
575
+ async applyLayoutPreset(workspaceId, preset) {
576
+ const workspace = this.requireWorkspace(workspaceId);
577
+ const next = applyMuxLayoutPreset(workspace, preset);
578
+ this.workspaceById.set(workspaceId, next);
579
+ this.emit({ kind: "workspace", workspace: next });
580
+ return next;
581
+ }
582
+ async swapPanePositions(workspaceId, firstPaneId, secondPaneId) {
583
+ const workspace = this.requireWorkspace(workspaceId);
584
+ const next = swapMuxPanePositions(workspace, firstPaneId, secondPaneId);
585
+ this.workspaceById.set(workspaceId, next);
586
+ this.emit({ kind: "workspace", workspace: next });
587
+ return next;
588
+ }
589
+ async togglePaneZoom(workspaceId, paneId) {
590
+ const workspace = this.requireWorkspace(workspaceId);
591
+ const next = toggleMuxPaneZoom(workspace, paneId);
592
+ this.workspaceById.set(workspaceId, next);
593
+ this.emit({ kind: "workspace", workspace: next });
594
+ return next;
595
+ }
596
+ async sendInput(paneId, data) {
597
+ const pane = this.requirePane(paneId);
598
+ if (pane.accessMode === "read-only" && !pane.requestedReadOnly && pane.paneKind === "runtime") {
599
+ await this.takeoverPane(paneId);
600
+ }
601
+ if (pane.accessMode === "read-only") {
602
+ throw new Error(`Pane ${paneId} is read-only`);
603
+ }
604
+ const response = await this.client.request({
605
+ type: "send_input",
606
+ payload: {
607
+ sessionId: pane.record.sessionId,
608
+ clientId: this.clientId,
609
+ data
610
+ }
611
+ });
612
+ if (!response.success) {
613
+ if ((response.error || "").includes("Write owned by") && !pane.requestedReadOnly && pane.paneKind === "runtime") {
614
+ await this.takeoverPane(paneId);
615
+ const retry = await this.client.request({
616
+ type: "send_input",
617
+ payload: {
618
+ sessionId: pane.record.sessionId,
619
+ clientId: this.clientId,
620
+ data
621
+ }
622
+ });
623
+ if (!retry.success) {
624
+ throw new Error(retry.error || `Failed to send input to pane ${paneId}`);
625
+ }
626
+ return;
627
+ }
628
+ throw new Error(response.error || `Failed to send input to pane ${paneId}`);
629
+ }
630
+ }
631
+ async resizePane(paneId, cols, rows) {
632
+ const pane = this.requirePane(paneId);
633
+ pane.surface.resize(cols, rows);
634
+ await this.client.request({
635
+ type: "resize_session",
636
+ payload: {
637
+ sessionId: pane.record.sessionId,
638
+ cols,
639
+ rows
640
+ }
641
+ });
642
+ this.publishPaneUpdate(paneId);
643
+ }
644
+ async takeoverPane(paneId) {
645
+ const pane = this.requirePane(paneId);
646
+ const response = await this.client.request({
647
+ type: "acquire_write",
648
+ payload: {
649
+ sessionId: pane.record.sessionId,
650
+ clientId: this.clientId,
651
+ ownerType: "user",
652
+ force: true
653
+ }
654
+ });
655
+ if (!response.success || !response.result) {
656
+ throw new Error(response.error || "Failed to acquire write owner");
657
+ }
658
+ pane.record = response.result;
659
+ pane.requestedReadOnly = false;
660
+ pane.accessMode = "interactive";
661
+ this.publishPaneUpdate(paneId);
662
+ }
663
+ async releasePane(paneId) {
664
+ const pane = this.requirePane(paneId);
665
+ const response = await this.client.request({
666
+ type: "release_write",
667
+ payload: {
668
+ sessionId: pane.record.sessionId,
669
+ clientId: this.clientId
670
+ }
671
+ });
672
+ if (!response.success || !response.result) {
673
+ throw new Error(response.error || "Failed to release write owner");
674
+ }
675
+ pane.record = response.result;
676
+ pane.requestedReadOnly = false;
677
+ pane.accessMode = this.computeAccessMode(pane.record, false, pane.paneKind);
678
+ this.publishPaneUpdate(paneId);
679
+ }
680
+ listWorkspaces() {
681
+ return Array.from(this.workspaceById.values());
682
+ }
683
+ async listRuntimes() {
684
+ await this.connect();
685
+ const list = await this.client.request({ type: "list_sessions" });
686
+ if (!list.success || !list.result) {
687
+ throw new Error(list.error || "Failed to list runtimes");
688
+ }
689
+ return list.result;
690
+ }
691
+ async resumeRuntime(target) {
692
+ await this.connect();
693
+ const record = resolveRuntimeRecord(await this.listRuntimes(), target);
694
+ const response = await this.client.request({
695
+ type: "resume_session",
696
+ payload: {
697
+ sessionId: record.sessionId
698
+ }
699
+ });
700
+ if (!response.success || !response.result) {
701
+ throw new Error(response.error || `Failed to resume runtime ${target}`);
702
+ }
703
+ for (const [paneId, pane] of this.paneById) {
704
+ if (pane.record.sessionId !== record.sessionId) continue;
705
+ pane.record = response.result;
706
+ this.publishPaneUpdate(paneId);
707
+ }
708
+ return response.result;
709
+ }
710
+ serializeWorkspace(workspaceId) {
711
+ return serializeWorkspace(this.requireWorkspace(workspaceId));
712
+ }
713
+ async close() {
714
+ for (const [paneId, pane] of this.paneById) {
715
+ try {
716
+ if (pane.record.writeOwner?.clientId === this.clientId) {
717
+ await this.client.request({
718
+ type: "release_write",
719
+ payload: {
720
+ sessionId: pane.record.sessionId,
721
+ clientId: this.clientId
722
+ }
723
+ });
724
+ }
725
+ const siblings = this.paneIdsByRuntime.get(pane.record.sessionId);
726
+ if (siblings?.size === 1) {
727
+ await this.client.request({
728
+ type: "detach_session",
729
+ payload: {
730
+ sessionId: pane.record.sessionId,
731
+ clientId: this.clientId
732
+ }
733
+ });
734
+ }
735
+ } catch {
736
+ }
737
+ pane.surface.dispose();
738
+ this.paneById.delete(paneId);
739
+ }
740
+ this.paneIdsByRuntime.clear();
741
+ this.workspaceById.clear();
742
+ this.unsubEvents?.();
743
+ this.unsubEvents = null;
744
+ await this.client.close();
745
+ }
746
+ async openRuntime(target, options) {
747
+ await this.connect();
748
+ let record = resolveRuntimeRecord(await this.listRuntimes(), target);
749
+ if (record.lifecycle === "interrupted") {
750
+ try {
751
+ record = await this.resumeRuntime(target);
752
+ } catch {
753
+ }
754
+ }
755
+ const readOnly = options.takeover ? false : !!options.readOnly || !!(record.writeOwner && record.writeOwner.clientId !== this.clientId);
756
+ const attachResponse = await this.client.request({
757
+ type: "attach_session",
758
+ payload: {
759
+ sessionId: record.sessionId,
760
+ clientId: this.clientId,
761
+ clientType: this.clientType,
762
+ readOnly
763
+ }
764
+ });
765
+ if (!attachResponse.success || !attachResponse.result) {
766
+ throw new Error(attachResponse.error || `Failed to attach runtime ${target}`);
767
+ }
768
+ record = attachResponse.result;
769
+ if (options.takeover) {
770
+ const takeoverResponse = await this.client.request({
771
+ type: "acquire_write",
772
+ payload: {
773
+ sessionId: record.sessionId,
774
+ clientId: this.clientId,
775
+ ownerType: "user",
776
+ force: true
777
+ }
778
+ });
779
+ if (!takeoverResponse.success || !takeoverResponse.result) {
780
+ throw new Error(takeoverResponse.error || `Failed to acquire runtime ${target}`);
781
+ }
782
+ record = takeoverResponse.result;
783
+ }
784
+ const snapshot = await this.client.request({
785
+ type: "get_snapshot",
786
+ payload: {
787
+ sessionId: record.sessionId
788
+ }
789
+ });
790
+ if (!snapshot.success || !snapshot.result) {
791
+ throw new Error(snapshot.error || "Failed to get runtime snapshot");
792
+ }
793
+ const paneId = options.paneId || randomUUID2();
794
+ const surface = new GhosttyTerminalSurface({
795
+ cols: options.cols ?? 120,
796
+ rows: options.rows ?? 36
797
+ });
798
+ surface.resetFromText(snapshot.result.text, snapshot.result.seq);
799
+ const runtimeRecord = {
800
+ ...record,
801
+ attachedClients: record.attachedClients
802
+ };
803
+ const accessMode = this.computeAccessMode(runtimeRecord, readOnly, "runtime");
804
+ const paneState = paneFromRecord(paneId, runtimeRecord, surface, "runtime", accessMode);
805
+ this.paneById.set(paneId, {
806
+ record: runtimeRecord,
807
+ surface,
808
+ paneKind: "runtime",
809
+ accessMode,
810
+ requestedReadOnly: readOnly
811
+ });
812
+ const paneIds = this.paneIdsByRuntime.get(runtimeRecord.sessionId) || /* @__PURE__ */ new Set();
813
+ paneIds.add(paneId);
814
+ this.paneIdsByRuntime.set(runtimeRecord.sessionId, paneIds);
815
+ this.emit({ kind: "runtime", pane: paneState });
816
+ return paneState;
817
+ }
818
+ async openMirrorRuntime(target, options = {}) {
819
+ const record = resolveRuntimeRecord(await this.listRuntimes(), target);
820
+ const runtimePane = Array.from(this.paneById.values()).find((pane) => pane.record.sessionId === record.sessionId);
821
+ if (!runtimePane) {
822
+ throw new Error(`Cannot mirror runtime ${record.runtimeKey} before it is attached`);
823
+ }
824
+ const paneId = options.paneId || randomUUID2();
825
+ const viewport = runtimePane.surface.getViewportState();
826
+ const surface = new GhosttyTerminalSurface({
827
+ cols: viewport.cols,
828
+ rows: viewport.rows
829
+ });
830
+ surface.resetFromText(viewport.text, viewport.snapshotSeq);
831
+ const paneState = paneFromRecord(paneId, record, surface, "mirror", "read-only");
832
+ this.paneById.set(paneId, {
833
+ record,
834
+ surface,
835
+ paneKind: "mirror",
836
+ accessMode: "read-only",
837
+ requestedReadOnly: true
838
+ });
839
+ const paneIds = this.paneIdsByRuntime.get(record.sessionId) || /* @__PURE__ */ new Set();
840
+ paneIds.add(paneId);
841
+ this.paneIdsByRuntime.set(record.sessionId, paneIds);
842
+ this.emit({ kind: "runtime", pane: paneState });
843
+ return paneState;
844
+ }
845
+ async handleHostEvent(event) {
846
+ const sessionId = "sessionId" in event ? event.sessionId : event.type === "runtime_transition" ? event.transition.sessionId : event.type === "request_trace" ? event.trace.sessionId : event.entry.sessionId;
847
+ if (!sessionId) return;
848
+ const paneIds = this.paneIdsByRuntime.get(sessionId);
849
+ if (!paneIds || paneIds.size === 0) return;
850
+ for (const paneId of paneIds) {
851
+ const pane = this.paneById.get(paneId);
852
+ if (!pane) continue;
853
+ switch (event.type) {
854
+ case "session_output":
855
+ pane.surface.write(event.data, event.seq);
856
+ break;
857
+ case "session_started":
858
+ case "session_resumed":
859
+ pane.record = { ...pane.record, lifecycle: "running", osPid: event.pid ?? pane.record.osPid };
860
+ break;
861
+ case "session_exit":
862
+ pane.record = { ...pane.record, lifecycle: event.exitCode === 0 ? "stopped" : "failed" };
863
+ break;
864
+ case "session_stopped":
865
+ pane.record = { ...pane.record, lifecycle: "stopped" };
866
+ break;
867
+ case "session_resized":
868
+ pane.surface.resize(event.cols, event.rows);
869
+ break;
870
+ case "write_owner_changed":
871
+ pane.record = { ...pane.record, writeOwner: event.owner };
872
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
873
+ break;
874
+ case "client_attached":
875
+ pane.record = {
876
+ ...pane.record,
877
+ attachedClients: [
878
+ ...pane.record.attachedClients.filter((client) => client.clientId !== event.client.clientId),
879
+ event.client
880
+ ]
881
+ };
882
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
883
+ break;
884
+ case "client_detached":
885
+ pane.record = {
886
+ ...pane.record,
887
+ attachedClients: pane.record.attachedClients.filter((client) => client.clientId !== event.clientId)
888
+ };
889
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
890
+ break;
891
+ case "session_created":
892
+ pane.record = event.record;
893
+ pane.accessMode = this.computeAccessMode(pane.record, pane.requestedReadOnly, pane.paneKind);
894
+ break;
895
+ }
896
+ this.publishPaneUpdate(paneId, event);
897
+ }
898
+ }
899
+ publishPaneUpdate(paneId, event) {
900
+ const pane = this.requirePane(paneId);
901
+ const paneState = paneFromRecord(paneId, pane.record, pane.surface, pane.paneKind, pane.accessMode);
902
+ this.emit({ kind: "runtime", pane: paneState, event });
903
+ for (const [workspaceId, workspace] of this.workspaceById) {
904
+ if (!workspace.panes[paneId]) continue;
905
+ const updated = updateMuxPane(workspace, paneState);
906
+ this.workspaceById.set(workspaceId, updated);
907
+ this.emit({ kind: "workspace", workspace: updated });
908
+ }
909
+ }
910
+ emit(event) {
911
+ for (const listener of this.listeners) {
912
+ listener(event);
913
+ }
914
+ }
915
+ requireWorkspace(workspaceId) {
916
+ const workspace = this.workspaceById.get(workspaceId);
917
+ if (!workspace) throw new Error(`Unknown workspace: ${workspaceId}`);
918
+ return workspace;
919
+ }
920
+ requirePane(paneId) {
921
+ const pane = this.paneById.get(paneId);
922
+ if (!pane) throw new Error(`Unknown pane: ${paneId}`);
923
+ return pane;
924
+ }
925
+ computeAccessMode(record, requestedReadOnly, paneKind) {
926
+ if (paneKind === "mirror") return "read-only";
927
+ if (requestedReadOnly) return "read-only";
928
+ if (record.writeOwner && record.writeOwner.clientId !== this.clientId) return "read-only";
929
+ const attachedClient = record.attachedClients.find((client) => client.clientId === this.clientId);
930
+ if (attachedClient?.readOnly) return "read-only";
931
+ return "interactive";
932
+ }
933
+ };
934
+ export {
935
+ GhosttyTerminalSurface,
936
+ SessionHostMuxClient,
937
+ applyMuxLayoutPreset,
938
+ createMuxWorkspace,
939
+ focusMuxPane,
940
+ rebalanceMuxLayout,
941
+ removeMuxPane,
942
+ resizeMuxPane,
943
+ serializeWorkspace,
944
+ splitMuxPane,
945
+ swapMuxPanePositions,
946
+ toggleMuxPaneZoom,
947
+ updateMuxPane
948
+ };