tmux-ide 2.9.6 → 2.9.8
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/bin/cli.js +2881 -1336
- package/package.json +2 -2
- package/packages/contracts/src/index.ts +5 -0
- package/packages/contracts/src/pane-stream.ts +54 -0
- package/packages/contracts/src/terminal-graphics.ts +118 -0
- package/packages/contracts/src/terminal-history-anchor.ts +61 -0
- package/packages/contracts/src/tmux-server-diagnostics.ts +16 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/kitty-placeholders.ts +208 -0
- package/packages/daemon/dist/command-center/tmux-server-diagnostics.js +24 -0
- package/packages/daemon/dist/command-center/tmux-server-history-anchor.js +48 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +11 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +23 -1
- package/packages/daemon/dist/lib/daemon-embed.js +21 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +10 -1
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +2 -1
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libevent_core-2.1.7.dylib +0 -0
- package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.3.0.dylib +0 -0
- package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +12 -4
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +9 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +9 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +25 -12
- package/packages/daemon/dist/terminal/graphics/kitty-graphics.js +699 -0
- package/packages/daemon/dist/terminal/mirror/control-channel.js +17 -1
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +7 -0
- package/packages/daemon/dist/terminal/mirror/native-grid-capture.js +8 -0
- package/packages/daemon/dist/terminal/mirror/native-grid-reader.js +6 -1
- package/packages/daemon/dist/terminal/mirror/session-channel.js +56 -4
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +180 -1
- package/packages/daemon/dist/terminal/protocol/session-descriptor-discovery.js +3 -2
- package/packages/daemon/dist/terminal/session-runtime/history-anchor-owner.js +171 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +29 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +20 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +85 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +30 -0
- package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +53 -21
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +11 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +7 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +35 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-paste.js +2 -2
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +2 -2
- package/packages/daemon/src/command-center/tmux-server-diagnostics.ts +38 -0
- package/packages/daemon/src/command-center/tmux-server-history-anchor.ts +69 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +11 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +20 -2
- package/packages/daemon/src/lib/daemon-embed.ts +26 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +10 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +2 -1
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
- package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client-recorded.ctl +87 -0
- package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client.ctl +10 -0
- package/packages/daemon/src/terminal/graphics/kitty-graphics.ts +756 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +18 -1
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +14 -0
- package/packages/daemon/src/terminal/mirror/native-grid-capture.ts +11 -0
- package/packages/daemon/src/terminal/mirror/native-grid-reader.ts +10 -1
- package/packages/daemon/src/terminal/mirror/session-channel.ts +75 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +199 -0
- package/packages/daemon/src/terminal/protocol/session-descriptor-discovery.ts +3 -2
- package/packages/daemon/src/terminal/session-runtime/history-anchor-owner.ts +190 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +38 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +38 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-interpreter-backend.ts +4 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +99 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +39 -0
- package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +58 -24
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +13 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +6 -1
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +44 -9
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-paste.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +5 -2
- package/packages/daemon-client/src/fleet-connection-status.ts +1 -1
- package/packages/daemon-client/src/pane-creation-refusal.ts +48 -0
- package/packages/daemon-client/src/pane-stream-client.ts +37 -0
- package/packages/daemon-client/src/tmux-server-client.ts +133 -6
- package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.2.3.dylib +0 -0
- /package/packages/daemon/dist/native/tmux/darwin-arm64/licenses/{libutf8proc.3.2.3.dylib.txt → libutf8proc.3.3.0.dylib.txt} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.js";
|
|
2
2
|
import { MAX_RETAINED_NATIVE_BACKING_BYTES, takeNativeSeedBacking, } from "./native-seed-backing.js";
|
|
3
|
-
import { WorkspaceIdSchemaZ, TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES, TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES, TerminalDeliveryEnvelopeSchemaZ, TerminalDeliveryAckSchemaZ, TerminalDeliveryNackSchemaZ, TerminalDeliveryOfferSchemaZ, TerminalDeliveryVisibilitySchemaZ, } from "@tmux-ide/contracts";
|
|
3
|
+
import { WorkspaceIdSchemaZ, TERMINAL_GRAPHICS_CAPABILITY_VERSION, TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES, TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES, TerminalDeliveryEnvelopeSchemaZ, TerminalDeliveryAckSchemaZ, TerminalDeliveryNackSchemaZ, TerminalDeliveryOfferSchemaZ, TerminalDeliveryVisibilitySchemaZ, } from "@tmux-ide/contracts";
|
|
4
4
|
import { TerminalDeliveryStateTooLargeError, terminalReplicaRowsEqual, TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
|
|
5
5
|
import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
|
|
6
6
|
import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
|
|
@@ -171,6 +171,9 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
171
171
|
this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
|
|
172
172
|
},
|
|
173
173
|
close: async () => this.#closeClient(client),
|
|
174
|
+
...(offer.protocolVersions.includes(TERMINAL_GRAPHICS_CAPABILITY_VERSION)
|
|
175
|
+
? { graphics: this.#graphicsFor(semanticPaneId, client, closed) }
|
|
176
|
+
: {}),
|
|
174
177
|
};
|
|
175
178
|
}
|
|
176
179
|
catch (error) {
|
|
@@ -196,6 +199,22 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
196
199
|
this.#pendingClients.delete(key);
|
|
197
200
|
}
|
|
198
201
|
}
|
|
202
|
+
#graphicsFor(semanticPaneId, client, closed) {
|
|
203
|
+
const owner = this.#ownerForPane(semanticPaneId);
|
|
204
|
+
return {
|
|
205
|
+
subscribe: (listener) => {
|
|
206
|
+
if (client.closed || !owner.subscribeGraphics)
|
|
207
|
+
return () => { };
|
|
208
|
+
const unsubscribe = owner.subscribeGraphics((state) => {
|
|
209
|
+
if (!client.closed)
|
|
210
|
+
listener(state);
|
|
211
|
+
});
|
|
212
|
+
void closed.then(unsubscribe);
|
|
213
|
+
return unsubscribe;
|
|
214
|
+
},
|
|
215
|
+
image: (digest) => (client.closed ? null : (owner.graphicsImage?.(digest) ?? null)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
199
218
|
/** Retire only this pane's delivery owners before its canonical owner is disposed. */
|
|
200
219
|
failPaneCapture(semanticPaneId, error) {
|
|
201
220
|
for (const client of this.#clients.values())
|
|
@@ -6,6 +6,7 @@ import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
|
|
|
6
6
|
import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
|
|
7
7
|
import { createXtermTerminalInterpreterBackend } from "./xterm-terminal-interpreter-backend.js";
|
|
8
8
|
import { CAUSAL_CELL_OSC, CausalCellLedger, cellsEqual, } from "./causal-cell-ledger.js";
|
|
9
|
+
import { KITTY_PLACEMENT_OSC, KittyApcScanner, KittyImageStore, } from "../graphics/kitty-graphics.js";
|
|
9
10
|
/** Daemon-owned, sans-I/O VT interpreter with one FIFO for bytes and geometry. */
|
|
10
11
|
export class TerminalReplicaInterpreter {
|
|
11
12
|
#nativeSeedBackingCandidate;
|
|
@@ -37,6 +38,13 @@ export class TerminalReplicaInterpreter {
|
|
|
37
38
|
#pendingWrites = [];
|
|
38
39
|
#writeFlushScheduled = false;
|
|
39
40
|
#widgetGate = false;
|
|
41
|
+
// kitty graphics: images outlive reseeds; the scanner owns split APCs.
|
|
42
|
+
#kittyScanner = new KittyApcScanner();
|
|
43
|
+
#kitty;
|
|
44
|
+
#kittyPending = new Map();
|
|
45
|
+
#graphicsListeners = new Set();
|
|
46
|
+
#graphics = null;
|
|
47
|
+
#graphicsKey = "";
|
|
40
48
|
#markerTail = "";
|
|
41
49
|
#pendingResize = null;
|
|
42
50
|
// null: no unpublished writes; undefined: a write requires full projection.
|
|
@@ -79,6 +87,8 @@ export class TerminalReplicaInterpreter {
|
|
|
79
87
|
rows: options.rows,
|
|
80
88
|
scrollback: this.#scrollback,
|
|
81
89
|
});
|
|
90
|
+
this.#kitty = new KittyImageStore(options.kittyImageBudget ? { budget: options.kittyImageBudget } : {});
|
|
91
|
+
this.#attachKitty(this.#backend);
|
|
82
92
|
this.#snapshot = blankTerminalReplicaSnapshot(options.cols, options.rows);
|
|
83
93
|
this.#seedReady = new Promise((resolve, reject) => {
|
|
84
94
|
this.#resolveSeedReady = resolve;
|
|
@@ -239,6 +249,18 @@ export class TerminalReplicaInterpreter {
|
|
|
239
249
|
}
|
|
240
250
|
const previous = this.#backend;
|
|
241
251
|
this.#backend = replacement;
|
|
252
|
+
this.#attachKitty(replacement);
|
|
253
|
+
// A capture replay does not erase graphics already observed by this owner.
|
|
254
|
+
// Keep anchors only when both backends agree on their coordinate basis;
|
|
255
|
+
// reflow, history replacement and authoritative stream resets are ambiguous.
|
|
256
|
+
const sameGraphicsBasis = operation.bootstrap === "painted-capture" &&
|
|
257
|
+
previous.cols === replacement.cols &&
|
|
258
|
+
previous.rows === replacement.rows &&
|
|
259
|
+
previous.graphicsViewportTop?.() !== undefined &&
|
|
260
|
+
previous.graphicsViewportTop?.() === replacement.graphicsViewportTop?.() &&
|
|
261
|
+
previous.modes().alternateScreen === replacement.modes().alternateScreen;
|
|
262
|
+
if (!sameGraphicsBasis)
|
|
263
|
+
this.#kitty.dropCursorPlacements();
|
|
242
264
|
this.#nativeReseedRequested = false;
|
|
243
265
|
this.#scrollback = scrollback;
|
|
244
266
|
this.#bootstrap = {
|
|
@@ -262,6 +284,7 @@ export class TerminalReplicaInterpreter {
|
|
|
262
284
|
this.#nativeSeedBackingCandidate = undefined;
|
|
263
285
|
}
|
|
264
286
|
previous.dispose();
|
|
287
|
+
this.#publishGraphics();
|
|
265
288
|
return;
|
|
266
289
|
}
|
|
267
290
|
if (operation.type === "cursor") {
|
|
@@ -276,6 +299,8 @@ export class TerminalReplicaInterpreter {
|
|
|
276
299
|
return;
|
|
277
300
|
}
|
|
278
301
|
if (operation.type === "resize") {
|
|
302
|
+
if (operation.cols !== this.#backend.cols || operation.rows !== this.#backend.rows)
|
|
303
|
+
this.#kitty.dropCursorPlacements();
|
|
279
304
|
this.#causalCell?.fail("geometry-drift");
|
|
280
305
|
if (this.#backend.modes().synchronizedOutput) {
|
|
281
306
|
// Geometry is part of the admitted FIFO even while publication is
|
|
@@ -287,6 +312,7 @@ export class TerminalReplicaInterpreter {
|
|
|
287
312
|
}
|
|
288
313
|
this.#backend.resize(operation.cols, operation.rows);
|
|
289
314
|
this.#commit(false);
|
|
315
|
+
this.#publishGraphics();
|
|
290
316
|
return;
|
|
291
317
|
}
|
|
292
318
|
const baseRevision = this.#revision;
|
|
@@ -304,6 +330,7 @@ export class TerminalReplicaInterpreter {
|
|
|
304
330
|
};
|
|
305
331
|
this.#revision = revision;
|
|
306
332
|
this.#closed = true;
|
|
333
|
+
this.#kitty.dispose();
|
|
307
334
|
this.#causalCell?.fail("transport-closed");
|
|
308
335
|
this.#clearSyncRecovery();
|
|
309
336
|
if (this.#needsSeed)
|
|
@@ -321,8 +348,20 @@ export class TerminalReplicaInterpreter {
|
|
|
321
348
|
this.#admitRaw(data);
|
|
322
349
|
this.#stats.parseBatches += 1;
|
|
323
350
|
this.#observeMarkerBytes(data);
|
|
351
|
+
const scanned = this.#kittyScanner.scan(data);
|
|
352
|
+
for (const command of scanned.commands)
|
|
353
|
+
this.#kittyPending.set(command.marker, command);
|
|
324
354
|
const parseStarted = this.#observability.enabled ? this.#observability.nowMicros() : 0;
|
|
325
|
-
|
|
355
|
+
// Always forward the scanner's output: tmux splits %output (~2 KiB) inside
|
|
356
|
+
// kitty APCs (~4 KiB chunks), so a chunk with no completed command can still
|
|
357
|
+
// be all image payload that must never reach the VT parser as text.
|
|
358
|
+
// A chunk entirely inside image payload changes nothing the parser can
|
|
359
|
+
// see. Skipping it matters: every parser write settles on a later timer
|
|
360
|
+
// turn, so a frame split into dozens of %output chunks would otherwise
|
|
361
|
+
// serialize dozens of turns and fall behind a 30 fps image stream.
|
|
362
|
+
if (scanned.forward.byteLength === 0)
|
|
363
|
+
return;
|
|
364
|
+
await this.#writeToBackend(this.#backend, scanned.forward);
|
|
326
365
|
if (this.#observability.enabled)
|
|
327
366
|
this.#observability.recordSpan("parse", "terminal-replica-write", parseStarted, this.#observability.nowMicros(), trace);
|
|
328
367
|
// The parser resets its dirty tracker per write. Synchronized output may
|
|
@@ -356,6 +395,7 @@ export class TerminalReplicaInterpreter {
|
|
|
356
395
|
else {
|
|
357
396
|
this.#commit(false, this.#backend.dirtyRange(), trace);
|
|
358
397
|
}
|
|
398
|
+
this.#publishGraphics();
|
|
359
399
|
}
|
|
360
400
|
#scheduleSyncRecovery() {
|
|
361
401
|
if (this.#syncRecovery || this.#closed)
|
|
@@ -541,6 +581,50 @@ export class TerminalReplicaInterpreter {
|
|
|
541
581
|
bootstrap: this.#bootstrap,
|
|
542
582
|
});
|
|
543
583
|
}
|
|
584
|
+
/** The pane's kitty image store (bytes for the graphics side channel). */
|
|
585
|
+
kittyImages() {
|
|
586
|
+
return this.#kitty;
|
|
587
|
+
}
|
|
588
|
+
/** Latest graphics state, or null while the pane has never held an image. */
|
|
589
|
+
currentGraphics() {
|
|
590
|
+
return this.#graphics;
|
|
591
|
+
}
|
|
592
|
+
onGraphics(listener) {
|
|
593
|
+
this.#graphicsListeners.add(listener);
|
|
594
|
+
return () => this.#graphicsListeners.delete(listener);
|
|
595
|
+
}
|
|
596
|
+
/** Publish graphics state when the store or the viewport-relative placements moved. */
|
|
597
|
+
#publishGraphics() {
|
|
598
|
+
if (this.#graphics === null && this.#kitty.revision === 0)
|
|
599
|
+
return;
|
|
600
|
+
const state = this.#kitty.state(this.#backend.graphicsViewportTop?.() ?? 0, this.#backend.rows, this.#backend.cols, this.#backend.modes().alternateScreen);
|
|
601
|
+
const key = JSON.stringify([state.revision, state.placements]);
|
|
602
|
+
if (key === this.#graphicsKey)
|
|
603
|
+
return;
|
|
604
|
+
this.#graphicsKey = key;
|
|
605
|
+
this.#graphics = state;
|
|
606
|
+
for (const listener of this.#graphicsListeners) {
|
|
607
|
+
try {
|
|
608
|
+
listener(state);
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
// Renderer callbacks never poison the authoritative parser FIFO.
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
#attachKitty(backend) {
|
|
616
|
+
// Minimal test backends omit the OSC seam; they simply never place images.
|
|
617
|
+
if (typeof backend.registerOscHandler !== "function")
|
|
618
|
+
return;
|
|
619
|
+
backend.registerOscHandler(KITTY_PLACEMENT_OSC, (data) => {
|
|
620
|
+
const command = this.#kittyPending.get(Number(data));
|
|
621
|
+
if (!command)
|
|
622
|
+
return true;
|
|
623
|
+
this.#kittyPending.delete(Number(data));
|
|
624
|
+
this.#kitty.apply(command, backend.graphicsCursor?.() ?? null);
|
|
625
|
+
return true;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
544
628
|
#currentSnapshotHash() {
|
|
545
629
|
return (this.#snapshotHash ??= hashTerminalReplicaSnapshot(this.#snapshot));
|
|
546
630
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HistoryAnchorOwner, } from "./history-anchor-owner.js";
|
|
1
2
|
import { layoutContentRows } from "../mirror/layout-content-rows.js";
|
|
2
3
|
import { terminalReplicaRowsEqual } from "@tmux-ide/core";
|
|
3
4
|
import { projectNativeGridRow } from "../mirror/native-grid-projection.js";
|
|
@@ -20,6 +21,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
20
21
|
#start;
|
|
21
22
|
#upstream = null;
|
|
22
23
|
#disposed = false;
|
|
24
|
+
#historyAnchors = new HistoryAnchorOwner();
|
|
23
25
|
#cols = 80;
|
|
24
26
|
#rows = 24;
|
|
25
27
|
#layoutEpoch = 0;
|
|
@@ -56,12 +58,14 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
56
58
|
if (this.#disposed || this.#reseed || this.#waitingForGeometryCapture)
|
|
57
59
|
return;
|
|
58
60
|
this.#waitingForGeometryCapture = true;
|
|
61
|
+
this.#historyAnchors.invalidate();
|
|
59
62
|
this.#supervise(this.#start.then(() => {
|
|
60
63
|
if (!this.#disposed && this.#waitingForGeometryCapture)
|
|
61
64
|
this.#upstream?.reseed();
|
|
62
65
|
}));
|
|
63
66
|
},
|
|
64
67
|
onUpdate: (update, trace) => {
|
|
68
|
+
this.#historyAnchors.observe(update);
|
|
65
69
|
this.#publishedIncarnation = update.incarnation;
|
|
66
70
|
if (update.type === "terminal.seed") {
|
|
67
71
|
this.#bootstrapped = true;
|
|
@@ -137,6 +141,16 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
137
141
|
failCausalCell(reason, traceId) {
|
|
138
142
|
this.#interpreter.failCausalCell(reason, traceId);
|
|
139
143
|
}
|
|
144
|
+
captureHistoryAnchor(proof, historyIndex) {
|
|
145
|
+
if (this.#disposed || this.#reseed || this.#waitingForGeometryCapture)
|
|
146
|
+
return { status: "unavailable" };
|
|
147
|
+
return this.#historyAnchors.capture(proof, historyIndex);
|
|
148
|
+
}
|
|
149
|
+
resolveHistoryAnchor(proof, token) {
|
|
150
|
+
if (this.#disposed || this.#reseed || this.#waitingForGeometryCapture)
|
|
151
|
+
return { status: "unavailable" };
|
|
152
|
+
return this.#historyAnchors.resolve(proof, token);
|
|
153
|
+
}
|
|
140
154
|
/** Scalar diagnostic only: no seed construction, hashing, or owner creation. */
|
|
141
155
|
inputDispatchIncarnation() {
|
|
142
156
|
return this.#disposed ? null : this.#publishedIncarnation;
|
|
@@ -248,6 +262,17 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
248
262
|
},
|
|
249
263
|
};
|
|
250
264
|
}
|
|
265
|
+
/** Kitty graphics side channel: current state now (if any), then every change. */
|
|
266
|
+
subscribeGraphics(listener) {
|
|
267
|
+
const current = this.#interpreter.currentGraphics();
|
|
268
|
+
if (current)
|
|
269
|
+
listener(current);
|
|
270
|
+
return this.#interpreter.onGraphics(listener);
|
|
271
|
+
}
|
|
272
|
+
/** Image bytes by content digest, while the pane's store still holds them. */
|
|
273
|
+
graphicsImage(digest) {
|
|
274
|
+
return this.#interpreter.kittyImages().imageByDigest(digest) ?? null;
|
|
275
|
+
}
|
|
251
276
|
async subscribeSource(listener, onRaw) {
|
|
252
277
|
const canonical = await this.subscribe(listener);
|
|
253
278
|
this.#rawListeners.add(onRaw);
|
|
@@ -268,6 +293,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
268
293
|
return;
|
|
269
294
|
this.#noteLifecycle(`dispose-${reason}`);
|
|
270
295
|
this.#disposed = true;
|
|
296
|
+
this.#historyAnchors.invalidate();
|
|
271
297
|
this.#historyTimer?.cancel();
|
|
272
298
|
this.#historyTimer = null;
|
|
273
299
|
this.#subscriptionEpoch += 1;
|
|
@@ -330,6 +356,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
330
356
|
// Deterministic capture point: reset opens the atomic capture. A probe
|
|
331
357
|
// armed after reset belongs to the next post-capture delta, never to this
|
|
332
358
|
// already-started reseed.
|
|
359
|
+
this.#historyAnchors.invalidate();
|
|
333
360
|
this.#reseed = {
|
|
334
361
|
nativeCols: event.cols,
|
|
335
362
|
nativeRows: event.rows,
|
|
@@ -447,6 +474,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
447
474
|
if (!this.#bootstrapped || this.#reseed || this.#waitingForGeometryCapture)
|
|
448
475
|
return;
|
|
449
476
|
this.#waitingForGeometryCapture = true;
|
|
477
|
+
this.#historyAnchors.invalidate();
|
|
450
478
|
void this.#start.then(() => {
|
|
451
479
|
if (!this.#disposed && this.#waitingForGeometryCapture)
|
|
452
480
|
this.#upstream?.reseed();
|
|
@@ -557,6 +585,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
557
585
|
this.#noteLifecycle("reseed-retry", diagnostic);
|
|
558
586
|
this.#reseedRetryCount += 1;
|
|
559
587
|
this.#waitingForGeometryCapture = true;
|
|
588
|
+
this.#historyAnchors.invalidate();
|
|
560
589
|
void this.#start.then(() => {
|
|
561
590
|
if (!this.#disposed)
|
|
562
591
|
this.#upstream?.reseed();
|
|
@@ -564,6 +593,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
564
593
|
}
|
|
565
594
|
#supervise(operation) {
|
|
566
595
|
void operation.catch((error) => {
|
|
596
|
+
this.#historyAnchors.invalidate();
|
|
567
597
|
this.#noteLifecycle("interpreter-fault");
|
|
568
598
|
this.#interpreter.abort(error);
|
|
569
599
|
this.#onFault?.(error);
|
|
@@ -29,6 +29,13 @@ function assertLiveControllerPrincipal(shared, lease) {
|
|
|
29
29
|
throw new SessionRuntimeControllerLeaseError("stale-controller-lease", "The host no longer owns session controller authority.");
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
function holdsCurrentLease(shared) {
|
|
33
|
+
const lease = shared.lease;
|
|
34
|
+
if (!lease)
|
|
35
|
+
return false;
|
|
36
|
+
const snapshot = shared.consumer.controllerSnapshot();
|
|
37
|
+
return snapshot.controllerClientId === lease.clientId && snapshot.revision === lease.revision;
|
|
38
|
+
}
|
|
32
39
|
const clientsByRegistry = new WeakMap();
|
|
33
40
|
function authenticatedSurface(transport, hostClientId) {
|
|
34
41
|
// The production host and development gateway mint these principals
|
|
@@ -290,6 +297,10 @@ export class SessionRuntimeTransportBinding {
|
|
|
290
297
|
throw error;
|
|
291
298
|
}
|
|
292
299
|
}
|
|
300
|
+
setCellPixels(width, height, precise) {
|
|
301
|
+
this.#assertOpen();
|
|
302
|
+
this.#shared.consumer.setCellPixels?.(width, height, precise);
|
|
303
|
+
}
|
|
293
304
|
fitViewport(expectedLease, cols, rows, semanticWindowId) {
|
|
294
305
|
this.#assertOpen();
|
|
295
306
|
if (this.#shared.geometryTransportLeaseIds.at(-1) !== this.#transportLeaseId) {
|
|
@@ -471,18 +482,33 @@ export class SessionRuntimeTransportBinder {
|
|
|
471
482
|
: this.registry.bindExecutionSource(base, sourceSemanticPaneId);
|
|
472
483
|
}
|
|
473
484
|
activateController(target) {
|
|
474
|
-
if (target.lease)
|
|
485
|
+
if (target.lease && holdsCurrentLease(target))
|
|
475
486
|
return;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
487
|
+
target.lease = null;
|
|
488
|
+
const controllerClientId = target.consumer.controllerSnapshot().controllerClientId;
|
|
489
|
+
const current = controllerClientId === null || controllerClientId === target.consumer.clientId
|
|
490
|
+
? undefined
|
|
491
|
+
: [...this.#clients.values()].find((candidate) => candidate !== target &&
|
|
492
|
+
candidate.consumer.session === target.consumer.session &&
|
|
493
|
+
candidate.consumer.clientId === controllerClientId);
|
|
494
|
+
if (!current) {
|
|
495
|
+
// Free, already ours, or held by a principal outside this binder (for
|
|
496
|
+
// example the command-center owner route): plain acquisition decides.
|
|
497
|
+
target.lease = target.consumer.acquireController();
|
|
483
498
|
return;
|
|
484
499
|
}
|
|
485
|
-
|
|
500
|
+
// A transport-bound holder whose binding lost its executable lease can
|
|
501
|
+
// never type again, yet it would keep every other viewer locked out. Re-
|
|
502
|
+
// derive its exact current lease so the handoff below reclaims it; a live
|
|
503
|
+
// holder simply hands off as before. Either way only one client can type.
|
|
504
|
+
const previous = current.lease && holdsCurrentLease(current)
|
|
505
|
+
? current.lease
|
|
506
|
+
: current.consumer.acquireController();
|
|
507
|
+
const handedOff = current.consumer.handoffController(previous, target.consumer.clientId);
|
|
508
|
+
// Drop the holder's lease only after the runtime accepted the handoff; a
|
|
509
|
+
// refusal (e.g. during topology verification) must leave it executable.
|
|
510
|
+
current.lease = null;
|
|
511
|
+
target.lease = handedOff;
|
|
486
512
|
}
|
|
487
513
|
async release(shared, grants, interactive) {
|
|
488
514
|
if (shared.refs <= 0)
|
|
@@ -500,19 +526,25 @@ export class SessionRuntimeTransportBinder {
|
|
|
500
526
|
shared.refs -= 1;
|
|
501
527
|
if (shared.refs > 0)
|
|
502
528
|
return;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
529
|
+
try {
|
|
530
|
+
if (shared.lease) {
|
|
531
|
+
// A same-host passive session channel keeps the controller PRINCIPAL
|
|
532
|
+
// continuous across interactive pane retargeting. It contributes no
|
|
533
|
+
// source-pane grant and cannot type; the lease retires with the last
|
|
534
|
+
// host/session ref, while a passive-only host never acquires one.
|
|
535
|
+
const retired = shared.lease;
|
|
536
|
+
shared.lease = null;
|
|
537
|
+
shared.consumer.releaseController(retired);
|
|
538
|
+
}
|
|
511
539
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
540
|
+
finally {
|
|
541
|
+
// A refused surrender must never strand the controller on a client with
|
|
542
|
+
// no transport: closing the consumer retires whatever it still owns.
|
|
543
|
+
for (const [key, candidate] of this.#clients) {
|
|
544
|
+
if (candidate === shared)
|
|
545
|
+
this.#clients.delete(key);
|
|
546
|
+
}
|
|
547
|
+
await shared.consumer.close();
|
|
515
548
|
}
|
|
516
|
-
await shared.consumer.close();
|
|
517
549
|
}
|
|
518
550
|
}
|
|
@@ -82,6 +82,17 @@ export class XtermTerminalInterpreterBackend {
|
|
|
82
82
|
get rows() {
|
|
83
83
|
return this.#terminal.rows;
|
|
84
84
|
}
|
|
85
|
+
graphicsCursor() {
|
|
86
|
+
const buffer = this.#terminal.buffer.active;
|
|
87
|
+
return {
|
|
88
|
+
x: Math.min(buffer.cursorX, this.#terminal.cols - 1),
|
|
89
|
+
line: buffer.baseY + buffer.cursorY,
|
|
90
|
+
alternate: buffer.type === "alternate",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
graphicsViewportTop() {
|
|
94
|
+
return this.#terminal.buffer.active.baseY;
|
|
95
|
+
}
|
|
85
96
|
write(data) {
|
|
86
97
|
return new Promise((resolve) => this.#terminal.write(data, resolve));
|
|
87
98
|
}
|
|
@@ -232,8 +232,14 @@ export function createApplicationHomeNavigationOwner(options) {
|
|
|
232
232
|
command === "whats-new" ||
|
|
233
233
|
command === "switch-session")
|
|
234
234
|
return null;
|
|
235
|
-
|
|
235
|
+
const status = options.sessionOwner()?.snapshot()?.status;
|
|
236
|
+
if (status !== "live") {
|
|
237
|
+
if (status === "rebinding")
|
|
238
|
+
return "Terminal is updating. Try again shortly.";
|
|
239
|
+
if (status === "connecting")
|
|
240
|
+
return "Opening terminal…";
|
|
236
241
|
return "Open a live session first";
|
|
242
|
+
}
|
|
237
243
|
return command !== "new-window" && command !== "new-agent" && !options.focusedPane()
|
|
238
244
|
? "Select a live pane first"
|
|
239
245
|
: null;
|
|
@@ -103,7 +103,14 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
103
103
|
}
|
|
104
104
|
: undefined;
|
|
105
105
|
recovery.inputs.push(async () => {
|
|
106
|
-
|
|
106
|
+
if (version !== recoveryVersion ||
|
|
107
|
+
!sameScope(target, scope()) ||
|
|
108
|
+
generation()?.status !== "live")
|
|
109
|
+
return false;
|
|
110
|
+
const framed = typeof inputs === "function" ? inputs() : inputs;
|
|
111
|
+
if (framed === null)
|
|
112
|
+
return false;
|
|
113
|
+
for (const [index, input] of framed.entries()) {
|
|
107
114
|
if (version !== recoveryVersion ||
|
|
108
115
|
!sameScope(target, scope()) ||
|
|
109
116
|
generation()?.status !== "live" ||
|
|
@@ -155,6 +162,24 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
155
162
|
}
|
|
156
163
|
})();
|
|
157
164
|
};
|
|
165
|
+
const pasteMode = () => {
|
|
166
|
+
const active = generation();
|
|
167
|
+
const pane = focusedPane();
|
|
168
|
+
if (active?.status !== "live" || !active.fastLane || !pane)
|
|
169
|
+
return null;
|
|
170
|
+
const mode = active.fastLane.lane.paneState(pane)?.snapshot?.modes.bracketedPaste;
|
|
171
|
+
return typeof mode === "boolean" ? mode : null;
|
|
172
|
+
};
|
|
173
|
+
const deliverPaste = (bytes) => {
|
|
174
|
+
const mode = pasteMode();
|
|
175
|
+
if (mode === null) {
|
|
176
|
+
// Do not let the successful drain of the surrounding FIFO hide refusal.
|
|
177
|
+
ownsNote = false;
|
|
178
|
+
setNote("terminal unavailable · paste was not sent");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
sendApplicationTerminalPaste(interaction, bytes, captureOrigin(), mode);
|
|
182
|
+
};
|
|
158
183
|
const captureOrigin = () => Boolean(currentTuiPerformanceEventSink()?.terminalInputOrigin);
|
|
159
184
|
const flush = () => {
|
|
160
185
|
const owner = sessionOwner();
|
|
@@ -243,7 +268,7 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
243
268
|
return;
|
|
244
269
|
const copy = Uint8Array.from(bytes);
|
|
245
270
|
try {
|
|
246
|
-
terminalInputsForPaste(Buffer.from(copy).toString("utf8"));
|
|
271
|
+
terminalInputsForPaste(Buffer.from(copy).toString("utf8"), false);
|
|
247
272
|
}
|
|
248
273
|
catch {
|
|
249
274
|
setNote("terminal unavailable · paste was rejected");
|
|
@@ -255,12 +280,17 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
255
280
|
active?.status === "live" &&
|
|
256
281
|
active.fastLane &&
|
|
257
282
|
focusedPane()) {
|
|
258
|
-
|
|
283
|
+
deliverPaste(copy);
|
|
259
284
|
return;
|
|
260
285
|
}
|
|
261
|
-
if (recover(
|
|
286
|
+
if (recover(() => {
|
|
287
|
+
const mode = pasteMode();
|
|
288
|
+
return mode === null
|
|
289
|
+
? null
|
|
290
|
+
: terminalInputsForPaste(Buffer.from(copy).toString("utf8"), mode);
|
|
291
|
+
}, copy, "paste"))
|
|
262
292
|
return;
|
|
263
|
-
noteAdmission(pending.enqueue(() =>
|
|
293
|
+
noteAdmission(pending.enqueue(() => deliverPaste(copy), copy.byteLength), "paste");
|
|
264
294
|
},
|
|
265
295
|
dispose() {
|
|
266
296
|
cancelInteractionInput();
|
|
@@ -10,9 +10,9 @@ export function sendApplicationTerminalKey(interaction, event, captureOrigin) {
|
|
|
10
10
|
void interaction.sendInput(input, parserOrigin);
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
|
-
export function sendApplicationTerminalPaste(interaction, bytes, captureOrigin) {
|
|
13
|
+
export function sendApplicationTerminalPaste(interaction, bytes, captureOrigin, bracketedPaste) {
|
|
14
14
|
const text = Buffer.from(bytes).toString("utf8");
|
|
15
|
-
const inputs = terminalInputsForPaste(text);
|
|
15
|
+
const inputs = terminalInputsForPaste(text, bracketedPaste);
|
|
16
16
|
for (let index = 0; index < inputs.length; index += 1) {
|
|
17
17
|
const parserOrigin = index === 0 && captureOrigin
|
|
18
18
|
? { origin: "bracketed-paste", payload: Buffer.from(bytes) }
|
|
@@ -33,12 +33,12 @@ export function terminalInputForOpenTuiKey(event) {
|
|
|
33
33
|
return SessionRuntimeTerminalInputSchemaZ.parse({ kind: "text", data });
|
|
34
34
|
}
|
|
35
35
|
/** Preserve paste ordering and bracketed-paste semantics without an unbounded frame. */
|
|
36
|
-
export function terminalInputsForPaste(text) {
|
|
36
|
+
export function terminalInputsForPaste(text, bracketedPaste) {
|
|
37
37
|
if (text.length === 0)
|
|
38
38
|
return [];
|
|
39
39
|
if (text.includes("\0"))
|
|
40
40
|
throw new TypeError("terminal paste must not contain NUL");
|
|
41
|
-
const framed = `\u001b[200~${text}\u001b[201
|
|
41
|
+
const framed = bracketedPaste ? `\u001b[200~${text}\u001b[201~` : text;
|
|
42
42
|
const inputs = [];
|
|
43
43
|
for (let offset = 0; offset < framed.length;) {
|
|
44
44
|
let end = Math.min(offset + SESSION_RUNTIME_MAX_TERMINAL_INPUT_TEXT_CHARS, framed.length);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import { TmuxServerScopeSchemaZ, TmuxServerDiagnosticsSchemaZ } from "@tmux-ide/contracts";
|
|
3
|
+
import { TmuxServerScopeError, type TmuxServerOwners } from "../lib/tmux-server-owners.ts";
|
|
4
|
+
import type { NativeTmuxServerOwner } from "../lib/tmux-server-owner.ts";
|
|
5
|
+
|
|
6
|
+
/** Mount beneath the existing owner-authenticated, no-store server router. */
|
|
7
|
+
export function mountTmuxServerDiagnosticsRoute(
|
|
8
|
+
app: Hono,
|
|
9
|
+
options: {
|
|
10
|
+
scopedPath: string;
|
|
11
|
+
owners: TmuxServerOwners<NativeTmuxServerOwner>;
|
|
12
|
+
},
|
|
13
|
+
) {
|
|
14
|
+
app.get(`${options.scopedPath}/diagnostics`, async (c) => {
|
|
15
|
+
const scope = TmuxServerScopeSchemaZ.safeParse({
|
|
16
|
+
serverId: c.req.param("serverId"),
|
|
17
|
+
generation: c.req.param("generation"),
|
|
18
|
+
});
|
|
19
|
+
if (!scope.success) return c.json({ error: { code: "invalid-request" } }, 400);
|
|
20
|
+
try {
|
|
21
|
+
c.req.raw.signal.throwIfAborted();
|
|
22
|
+
const tmuxVersion = await options.owners.withOwner(scope.data, (owner) =>
|
|
23
|
+
owner.readServerVersion(c.req.raw.signal),
|
|
24
|
+
);
|
|
25
|
+
c.req.raw.signal.throwIfAborted();
|
|
26
|
+
return c.json(
|
|
27
|
+
TmuxServerDiagnosticsSchemaZ.parse({ version: 1, server: scope.data, tmuxVersion }),
|
|
28
|
+
);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof TmuxServerScopeError)
|
|
31
|
+
return c.json(
|
|
32
|
+
{ error: { code: "scope-unavailable" } },
|
|
33
|
+
error.code === "not-found" ? 404 : 409,
|
|
34
|
+
);
|
|
35
|
+
return c.json({ error: { code: "diagnostics-unavailable" } }, 503);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
HistoryAnchorRequestSchemaZ,
|
|
4
|
+
HistoryAnchorResponseSchemaZ,
|
|
5
|
+
TmuxServerScopeSchemaZ,
|
|
6
|
+
} from "@tmux-ide/contracts";
|
|
7
|
+
import { TmuxServerScopeError, type TmuxServerOwners } from "../lib/tmux-server-owners.ts";
|
|
8
|
+
import type { NativeTmuxServerOwner } from "../lib/tmux-server-owner.ts";
|
|
9
|
+
/** Mounted below owner-authenticated no-store middleware. Never opens a runtime. */
|
|
10
|
+
export function mountTmuxServerHistoryAnchorRoute(
|
|
11
|
+
app: Hono,
|
|
12
|
+
options: {
|
|
13
|
+
scopedPath: string;
|
|
14
|
+
owners: TmuxServerOwners<NativeTmuxServerOwner>;
|
|
15
|
+
readBody: (request: Request) => Promise<unknown>;
|
|
16
|
+
},
|
|
17
|
+
) {
|
|
18
|
+
app.post(`${options.scopedPath}/history-anchor`, async (c) => {
|
|
19
|
+
const scope = TmuxServerScopeSchemaZ.safeParse({
|
|
20
|
+
serverId: c.req.param("serverId"),
|
|
21
|
+
generation: c.req.param("generation"),
|
|
22
|
+
});
|
|
23
|
+
if (!scope.success) return c.json({ error: { code: "invalid-request" } }, 400);
|
|
24
|
+
let body;
|
|
25
|
+
try {
|
|
26
|
+
body = HistoryAnchorRequestSchemaZ.parse(await options.readBody(c.req.raw));
|
|
27
|
+
} catch {
|
|
28
|
+
return c.json({ error: { code: "invalid-request" } }, 400);
|
|
29
|
+
}
|
|
30
|
+
if (body.proof.generation !== scope.data.generation)
|
|
31
|
+
return c.json({ error: { code: "stale-generation" } }, 409);
|
|
32
|
+
try {
|
|
33
|
+
c.req.raw.signal.throwIfAborted();
|
|
34
|
+
const result = await options.owners.withOwner(scope.data, async (owner) => {
|
|
35
|
+
const sessions = await owner.catalog();
|
|
36
|
+
c.req.raw.signal.throwIfAborted();
|
|
37
|
+
const workspace = owner.workspaceRegistry.get(body.proof.workspaceName);
|
|
38
|
+
if (
|
|
39
|
+
!workspace ||
|
|
40
|
+
!sessions.some(
|
|
41
|
+
(row) =>
|
|
42
|
+
row.sessionName === workspace.sessionName && row.liveSessionId === body.liveSessionId,
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return { status: "stale" as const };
|
|
46
|
+
const result = owner.sessionRuntimeRegistry.historyAnchor(
|
|
47
|
+
workspace.sessionName,
|
|
48
|
+
HistoryAnchorRequestSchemaZ.parse({
|
|
49
|
+
...body,
|
|
50
|
+
proof: { ...body.proof, workspaceName: workspace.sessionName },
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
// Delivery aliases are transport addresses, not canonical session names.
|
|
54
|
+
return result.status === "resolved"
|
|
55
|
+
? { ...result, proof: { ...result.proof, workspaceName: body.proof.workspaceName } }
|
|
56
|
+
: result;
|
|
57
|
+
});
|
|
58
|
+
c.req.raw.signal.throwIfAborted();
|
|
59
|
+
return c.json(HistoryAnchorResponseSchemaZ.parse({ version: 1, server: scope.data, result }));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof TmuxServerScopeError)
|
|
62
|
+
return c.json(
|
|
63
|
+
{ error: { code: "scope-unavailable" } },
|
|
64
|
+
error.code === "not-found" ? 404 : 409,
|
|
65
|
+
);
|
|
66
|
+
return c.json({ error: { code: "history-unavailable" } }, 503);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|