tmux-ide 2.9.3 → 2.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +120 -139
- package/bin/cli.js +4847 -3585
- package/package.json +16 -9
- package/packages/contracts/src/session-runtime.ts +2 -0
- package/packages/contracts/src/terminal-delivery.ts +4 -0
- package/packages/core/src/terminal-compact-capability.ts +31 -1
- package/packages/core/src/terminal-delivery.ts +234 -32
- package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
- package/packages/core/src/terminal-replica-owned-row.ts +165 -10
- package/packages/core/src/terminal-replica.ts +43 -21
- package/packages/daemon/dist/lib/daemon-embed.js +19 -34
- package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
- package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
- package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
- 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/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
- package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
- package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
- package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
- package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
- package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
- package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
- package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
- package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
- package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
- package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
- package/packages/daemon/src/lib/daemon-embed.ts +22 -34
- package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
- package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
- package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
- package/packages/daemon/src/terminal/mirror/events.ts +7 -7
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
- package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
- package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
- package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
- package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
- package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
- package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
- package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
- package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
- package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
- package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
- package/packages/daemon-client/src/workspace-client-types.ts +1 -0
- package/packages/daemon-client/src/workspace-client.ts +78 -5
- package/packages/tmux-bridge/src/index.ts +1 -0
- package/packages/tmux-bridge/src/runner.ts +25 -1
- package/skill/SKILL.md +30 -30
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.ts";
|
|
1
2
|
import {
|
|
2
3
|
MAX_RETAINED_NATIVE_BACKING_BYTES,
|
|
3
4
|
takeNativeSeedBacking,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
} from "@tmux-ide/contracts";
|
|
28
29
|
import {
|
|
29
30
|
TerminalDeliveryStateTooLargeError,
|
|
31
|
+
terminalReplicaRowsEqual,
|
|
32
|
+
TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS,
|
|
30
33
|
applyTerminalReplicaUpdate,
|
|
31
34
|
applyTerminalReplicaUpdateCooperatively,
|
|
32
35
|
terminalReplicaUpdateNeedsCooperativeReduction,
|
|
@@ -71,6 +74,11 @@ export interface TerminalDeliveryMetrics {
|
|
|
71
74
|
/** Unique delivery subscribers, independent of their pane count. */
|
|
72
75
|
readonly clients: number;
|
|
73
76
|
readonly connections: number;
|
|
77
|
+
readonly sourceSubscriptions: number;
|
|
78
|
+
readonly sourceCloseFailures: number;
|
|
79
|
+
readonly pendingSourceCloses: number;
|
|
80
|
+
readonly pendingSourceSubscriptions: number;
|
|
81
|
+
readonly pendingClients: number;
|
|
74
82
|
readonly inFlight: number;
|
|
75
83
|
readonly latestPointers: number;
|
|
76
84
|
readonly coalesced: number;
|
|
@@ -196,6 +204,8 @@ interface ClientState {
|
|
|
196
204
|
readonly outgoing: Array<() => TerminalDeliveryServerMessage>;
|
|
197
205
|
sending: boolean;
|
|
198
206
|
retireAfterDrain: boolean;
|
|
207
|
+
/** A capture capability failure is terminal before the fault sink drains. */
|
|
208
|
+
captureUnavailable: boolean;
|
|
199
209
|
lastAck: TerminalDeliveryAck | null;
|
|
200
210
|
/** Delivery displaced by authoritative source close; its racing ACK is benign. */
|
|
201
211
|
sourceClosedFlight: TerminalDeliveryEnvelope | null;
|
|
@@ -312,6 +322,8 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
312
322
|
readonly #observability: SessionRuntimeObservability;
|
|
313
323
|
readonly #panes = new Map<string, PaneState>();
|
|
314
324
|
readonly #clients = new Map<string, ClientState>();
|
|
325
|
+
#sourceCloseFailures = 0;
|
|
326
|
+
#pendingSourceCloses = 0;
|
|
315
327
|
/** Synchronous reservations held while an async pane source is starting. */
|
|
316
328
|
readonly #pendingClients = new Map<string, string>();
|
|
317
329
|
readonly #cache = new Map<string, CachedRepresentation>();
|
|
@@ -409,6 +421,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
409
421
|
outgoing: [],
|
|
410
422
|
sending: false,
|
|
411
423
|
retireAfterDrain: false,
|
|
424
|
+
captureUnavailable: false,
|
|
412
425
|
lastAck: null,
|
|
413
426
|
sourceClosedFlight: null,
|
|
414
427
|
backgroundTimer: null,
|
|
@@ -433,11 +446,37 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
433
446
|
},
|
|
434
447
|
close: async () => this.#closeClient(client),
|
|
435
448
|
};
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (!(error instanceof StockCaptureTabUnavailableError)) throw error;
|
|
451
|
+
// Negotiation still settles this pane; no invented canonical seed is published.
|
|
452
|
+
await accept({
|
|
453
|
+
type: "terminal.delivery.fault",
|
|
454
|
+
reason: "source-closed",
|
|
455
|
+
message: error.message,
|
|
456
|
+
deliveryNonce: negotiation.negotiated.deliveryNonce,
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
negotiation,
|
|
460
|
+
closed: Promise.resolve("closed" as const),
|
|
461
|
+
ack: () => {},
|
|
462
|
+
nack: () => {},
|
|
463
|
+
setVisibility: () => {},
|
|
464
|
+
close: async () => {},
|
|
465
|
+
};
|
|
436
466
|
} finally {
|
|
437
467
|
this.#pendingClients.delete(key);
|
|
438
468
|
}
|
|
439
469
|
}
|
|
440
470
|
|
|
471
|
+
/** Retire only this pane's delivery owners before its canonical owner is disposed. */
|
|
472
|
+
failPaneCapture(semanticPaneId: string, error: StockCaptureTabUnavailableError): void {
|
|
473
|
+
for (const client of this.#clients.values())
|
|
474
|
+
if (client.paneId === semanticPaneId && !client.closed && !client.captureUnavailable) {
|
|
475
|
+
client.captureUnavailable = true;
|
|
476
|
+
this.#fault(client, "source-closed", error.message);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
441
480
|
retainedNativeBacking(
|
|
442
481
|
paneId: string,
|
|
443
482
|
expected: NativeBackingIdentity,
|
|
@@ -506,6 +545,12 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
506
545
|
return Object.freeze({
|
|
507
546
|
clients: new Set([...this.#clients.values()].map((client) => client.clientId)).size,
|
|
508
547
|
connections: this.#clients.size,
|
|
548
|
+
sourceSubscriptions: [...this.#panes.values()].filter((pane) => pane.source !== null).length,
|
|
549
|
+
pendingSourceSubscriptions: [...this.#panes.values()].filter((pane) => pane.source === null)
|
|
550
|
+
.length,
|
|
551
|
+
pendingClients: this.#pendingClients.size,
|
|
552
|
+
sourceCloseFailures: this.#sourceCloseFailures,
|
|
553
|
+
pendingSourceCloses: this.#pendingSourceCloses,
|
|
509
554
|
inFlight: [...this.#clients.values()].filter((client) => client.inFlight).length,
|
|
510
555
|
latestPointers: [...this.#clients.values()].filter((client) => client.latestRevision !== null)
|
|
511
556
|
.length,
|
|
@@ -588,10 +633,22 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
588
633
|
pane.pendingCanonicalCells = 0;
|
|
589
634
|
pane.canonicalScheduled = false;
|
|
590
635
|
}
|
|
591
|
-
await Promise.allSettled(panes.map((pane) => pane.source
|
|
636
|
+
await Promise.allSettled(panes.map((pane) => pane.source && this.#closeSource(pane.source)));
|
|
592
637
|
this.#clearCache();
|
|
593
638
|
}
|
|
594
639
|
|
|
640
|
+
async #closeSource(source: TerminalReplicaSourceSubscription): Promise<void> {
|
|
641
|
+
this.#pendingSourceCloses += 1;
|
|
642
|
+
try {
|
|
643
|
+
await source.close();
|
|
644
|
+
} catch (error) {
|
|
645
|
+
this.#sourceCloseFailures += 1;
|
|
646
|
+
throw error;
|
|
647
|
+
} finally {
|
|
648
|
+
this.#pendingSourceCloses -= 1;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
595
652
|
async close(): Promise<void> {
|
|
596
653
|
if (this.#closed) return;
|
|
597
654
|
this.#closed = true;
|
|
@@ -646,7 +703,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
646
703
|
this.#panes.get(semanticPaneId) !== pane ||
|
|
647
704
|
pane?.canonicalAbort.signal.aborted
|
|
648
705
|
) {
|
|
649
|
-
await source
|
|
706
|
+
await this.#closeSource(source);
|
|
650
707
|
throw new Error("Terminal delivery source retired during startup");
|
|
651
708
|
}
|
|
652
709
|
pane!.source = source;
|
|
@@ -850,7 +907,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
850
907
|
pane.revisions.set(update.revision, record);
|
|
851
908
|
this.#pruneCanonicalRevisions(semanticPaneId);
|
|
852
909
|
for (const client of this.#clients.values()) {
|
|
853
|
-
if (client.paneId !== semanticPaneId || client.closed) continue;
|
|
910
|
+
if (client.paneId !== semanticPaneId || client.closed || client.captureUnavailable) continue;
|
|
854
911
|
if (!client.lifecycleOpenRecorded)
|
|
855
912
|
client.lifecycleOpenRecorded = this.#recordDeliveryLifecycle(client, pane, "open");
|
|
856
913
|
if (
|
|
@@ -880,7 +937,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
880
937
|
pane.pendingCanonical.length = 0;
|
|
881
938
|
pane.pendingCanonicalCells = 0;
|
|
882
939
|
pane.canonicalScheduled = false;
|
|
883
|
-
void pane.source
|
|
940
|
+
void (pane.source && this.#closeSource(pane.source).catch(() => undefined));
|
|
884
941
|
this.#clearCache();
|
|
885
942
|
}, 0);
|
|
886
943
|
}
|
|
@@ -923,6 +980,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
923
980
|
#schedule(client: ClientState): void {
|
|
924
981
|
if (
|
|
925
982
|
client.closed ||
|
|
983
|
+
client.captureUnavailable ||
|
|
926
984
|
client.inFlight ||
|
|
927
985
|
client.encoding ||
|
|
928
986
|
client.latestRevision === null ||
|
|
@@ -963,6 +1021,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
963
1021
|
!pane ||
|
|
964
1022
|
!target ||
|
|
965
1023
|
client.closed ||
|
|
1024
|
+
client.captureUnavailable ||
|
|
966
1025
|
client.inFlight ||
|
|
967
1026
|
client.encoding ||
|
|
968
1027
|
client.visibility === "hidden" ||
|
|
@@ -1330,7 +1389,12 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1330
1389
|
};
|
|
1331
1390
|
this.#reseeds += 1;
|
|
1332
1391
|
} else {
|
|
1333
|
-
const patchPayload = semanticPayload(
|
|
1392
|
+
const patchPayload = semanticPayload(
|
|
1393
|
+
client.baselineRevision,
|
|
1394
|
+
baseline,
|
|
1395
|
+
target,
|
|
1396
|
+
pane.revisions.get(client.baselineRevision)?.update.incarnation,
|
|
1397
|
+
);
|
|
1334
1398
|
const seedPayload = patchPayload.frame === "tombstone" ? null : semanticSeed(target);
|
|
1335
1399
|
const legacyAttempts = null;
|
|
1336
1400
|
if (patchPayload.frame !== "patch") {
|
|
@@ -1663,6 +1727,22 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1663
1727
|
|
|
1664
1728
|
#nack(client: ClientState, input: TerminalDeliveryNack): void {
|
|
1665
1729
|
const nack = TerminalDeliveryNackSchemaZ.parse(input);
|
|
1730
|
+
const closed = client.sourceClosedFlight;
|
|
1731
|
+
// A decoder may have sent this NACK before observing the pane-local fault.
|
|
1732
|
+
// Only the exact displaced transaction is inert; unrelated feedback still
|
|
1733
|
+
// follows the normal protocol-violation path below.
|
|
1734
|
+
if (
|
|
1735
|
+
client.captureUnavailable &&
|
|
1736
|
+
!client.inFlight &&
|
|
1737
|
+
closed &&
|
|
1738
|
+
nack.workspaceName === closed.workspaceName &&
|
|
1739
|
+
nack.semanticPaneId === closed.semanticPaneId &&
|
|
1740
|
+
nack.generation === closed.generation &&
|
|
1741
|
+
nack.incarnation === closed.incarnation &&
|
|
1742
|
+
nack.deliveryNonce === closed.deliveryNonce &&
|
|
1743
|
+
nack.transactionId === closed.transactionId
|
|
1744
|
+
)
|
|
1745
|
+
return;
|
|
1666
1746
|
const envelope = client.inFlight?.envelope;
|
|
1667
1747
|
if (
|
|
1668
1748
|
!envelope ||
|
|
@@ -1825,7 +1905,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1825
1905
|
pane.pendingCanonicalCells = 0;
|
|
1826
1906
|
pane.canonicalScheduled = false;
|
|
1827
1907
|
}
|
|
1828
|
-
|
|
1908
|
+
if (pane?.source) await this.#closeSource(pane.source).catch(() => undefined);
|
|
1829
1909
|
this.#clearCache();
|
|
1830
1910
|
}
|
|
1831
1911
|
|
|
@@ -2233,6 +2313,7 @@ function semanticPayload(
|
|
|
2233
2313
|
baselineRevision: number,
|
|
2234
2314
|
baseline: TerminalReplicaSnapshot | null,
|
|
2235
2315
|
target: RevisionRecord,
|
|
2316
|
+
baselineIncarnation?: string,
|
|
2236
2317
|
): TerminalSemanticDeliveryPayload {
|
|
2237
2318
|
const update = target.update;
|
|
2238
2319
|
if (!target.state.snapshot) {
|
|
@@ -2244,8 +2325,8 @@ function semanticPayload(
|
|
|
2244
2325
|
tombstone: update.tombstone,
|
|
2245
2326
|
};
|
|
2246
2327
|
}
|
|
2247
|
-
// Adjacent canonical patches are already the cheapest exact diff.
|
|
2248
|
-
//
|
|
2328
|
+
// Adjacent canonical patches are already the cheapest exact diff. Preserve
|
|
2329
|
+
// this direct path before considering an exact coalesced snapshot patch.
|
|
2249
2330
|
if (baseline && update.type === "terminal.patch" && update.baseRevision === baselineRevision)
|
|
2250
2331
|
return {
|
|
2251
2332
|
frame: "patch",
|
|
@@ -2253,6 +2334,59 @@ function semanticPayload(
|
|
|
2253
2334
|
revision: update.revision,
|
|
2254
2335
|
patch: update.patch,
|
|
2255
2336
|
};
|
|
2337
|
+
// A skipped revision may still have an exact small representation. Only
|
|
2338
|
+
// append-only history is proved here; trim/reflow and changed dimensions
|
|
2339
|
+
// retain the full seed boundary. Equality includes wrapped and all cells.
|
|
2340
|
+
const snapshot = target.state.snapshot;
|
|
2341
|
+
if (
|
|
2342
|
+
baseline &&
|
|
2343
|
+
baselineIncarnation === update.incarnation &&
|
|
2344
|
+
baseline.cols === snapshot.cols &&
|
|
2345
|
+
baseline.rows === snapshot.rows &&
|
|
2346
|
+
baseline.history.length <= snapshot.history.length
|
|
2347
|
+
) {
|
|
2348
|
+
// Identity reuse is cheap, but detached equal histories must not turn a
|
|
2349
|
+
// cooperative seed into an unbounded synchronous cell comparison.
|
|
2350
|
+
let comparisonCells = 0;
|
|
2351
|
+
const equal = (
|
|
2352
|
+
left: TerminalReplicaSnapshot["grid"][number],
|
|
2353
|
+
right: TerminalReplicaSnapshot["grid"][number],
|
|
2354
|
+
) => {
|
|
2355
|
+
if (left === right) return true;
|
|
2356
|
+
comparisonCells += right.cells.length;
|
|
2357
|
+
return comparisonCells <= TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS
|
|
2358
|
+
? terminalReplicaRowsEqual(left, right)
|
|
2359
|
+
: null;
|
|
2360
|
+
};
|
|
2361
|
+
for (let index = 0; index < baseline.history.length; index++)
|
|
2362
|
+
if (equal(baseline.history[index]!, snapshot.history[index]!) !== true)
|
|
2363
|
+
return semanticSeed(target);
|
|
2364
|
+
const rows: { index: number; row: TerminalReplicaSnapshot["grid"][number] }[] = [];
|
|
2365
|
+
for (let index = 0; index < snapshot.grid.length; index++) {
|
|
2366
|
+
const same = equal(baseline.grid[index]!, snapshot.grid[index]!);
|
|
2367
|
+
if (same === null) return semanticSeed(target);
|
|
2368
|
+
if (!same) rows.push({ index, row: snapshot.grid[index]! });
|
|
2369
|
+
}
|
|
2370
|
+
const appendedRows = snapshot.history.length - baseline.history.length;
|
|
2371
|
+
// Compact patches serialize synchronously. Do not move a large appended
|
|
2372
|
+
// suffix or repaint across the seed-only cooperative encoding boundary.
|
|
2373
|
+
if ((rows.length + appendedRows) * snapshot.cols >= TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS)
|
|
2374
|
+
return semanticSeed(target);
|
|
2375
|
+
const append = snapshot.history.slice(baseline.history.length);
|
|
2376
|
+
return {
|
|
2377
|
+
frame: "patch",
|
|
2378
|
+
baseRevision: baselineRevision,
|
|
2379
|
+
revision: update.revision,
|
|
2380
|
+
patch: {
|
|
2381
|
+
rows,
|
|
2382
|
+
...(append.length ? { historyDelta: { trim: 0, append } } : {}),
|
|
2383
|
+
cursor: snapshot.cursor,
|
|
2384
|
+
modes: snapshot.modes,
|
|
2385
|
+
placements: snapshot.placements,
|
|
2386
|
+
bootstrap: snapshot.bootstrap,
|
|
2387
|
+
},
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2256
2390
|
return semanticSeed(target);
|
|
2257
2391
|
}
|
|
2258
2392
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertStockCaptureRepresentable } from "./stock-capture-fidelity.ts";
|
|
1
2
|
import { rememberNativeSeedBacking } from "./native-seed-backing.ts";
|
|
2
3
|
import type { NativeGridCapture } from "../mirror/native-grid-capture.ts";
|
|
3
4
|
import type { MirrorObservedTerminalModes } from "../mirror/events.ts";
|
|
@@ -40,6 +41,7 @@ import { createXtermTerminalInterpreterBackend } from "./xterm-terminal-interpre
|
|
|
40
41
|
import {
|
|
41
42
|
CAUSAL_CELL_OSC,
|
|
42
43
|
CausalCellLedger,
|
|
44
|
+
cellsEqual,
|
|
43
45
|
type CausalCellLedgerResult,
|
|
44
46
|
} from "./causal-cell-ledger.ts";
|
|
45
47
|
|
|
@@ -72,6 +74,8 @@ export type TerminalReplicaInterpreterOperation =
|
|
|
72
74
|
readonly cols: number;
|
|
73
75
|
readonly rows: number;
|
|
74
76
|
readonly chunks: readonly Uint8Array[];
|
|
77
|
+
/** Explicit capture provenance; excludes post-capture live data. Legacy callers capture all chunks. */
|
|
78
|
+
readonly captureChunks?: readonly Uint8Array[];
|
|
75
79
|
readonly cursor: { readonly x: number; readonly y: number };
|
|
76
80
|
readonly wraparound?: boolean;
|
|
77
81
|
readonly observedModes?: MirrorObservedTerminalModes;
|
|
@@ -101,6 +105,8 @@ export interface TerminalReplicaInterpreterOptions {
|
|
|
101
105
|
readonly baseRevision: number;
|
|
102
106
|
readonly revision: number;
|
|
103
107
|
readonly chunks: readonly Uint8Array[];
|
|
108
|
+
/** Explicit capture provenance; excludes post-capture live data. Legacy callers capture all chunks. */
|
|
109
|
+
readonly captureChunks?: readonly Uint8Array[];
|
|
104
110
|
readonly contiguous: boolean;
|
|
105
111
|
}) => void;
|
|
106
112
|
/** Request native truth without publishing an unknown saved-buffer projection. */
|
|
@@ -243,7 +249,13 @@ export class TerminalReplicaInterpreter {
|
|
|
243
249
|
this.#flushWrites();
|
|
244
250
|
const admitted =
|
|
245
251
|
operation.type === "reseed"
|
|
246
|
-
? {
|
|
252
|
+
? {
|
|
253
|
+
...operation,
|
|
254
|
+
chunks: operation.chunks.map((chunk) => Uint8Array.from(chunk)),
|
|
255
|
+
...(operation.captureChunks
|
|
256
|
+
? { captureChunks: operation.captureChunks.map((chunk) => Uint8Array.from(chunk)) }
|
|
257
|
+
: {}),
|
|
258
|
+
}
|
|
247
259
|
: operation;
|
|
248
260
|
return this.#append(admitted);
|
|
249
261
|
}
|
|
@@ -280,8 +292,10 @@ export class TerminalReplicaInterpreter {
|
|
|
280
292
|
seed.incarnation !== probe.incarnation ||
|
|
281
293
|
this.#snapshot.cols !== probe.geometry.cols ||
|
|
282
294
|
this.#snapshot.rows !== probe.geometry.rows ||
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
!cellsEqual(
|
|
296
|
+
this.#snapshot.grid[probe.geometry.row]?.cells[probe.geometry.column],
|
|
297
|
+
probe.before,
|
|
298
|
+
)
|
|
285
299
|
)
|
|
286
300
|
throw new Error("Causal-cell baseline drifted before admission");
|
|
287
301
|
const ledger = new CausalCellLedger({
|
|
@@ -343,6 +357,8 @@ export class TerminalReplicaInterpreter {
|
|
|
343
357
|
): Promise<void> {
|
|
344
358
|
if (this.#closed) return;
|
|
345
359
|
if (operation.type === "reseed") {
|
|
360
|
+
if (operation.bootstrap === "painted-capture" && !operation.native)
|
|
361
|
+
assertStockCaptureRepresentable(operation.captureChunks ?? operation.chunks);
|
|
346
362
|
this.#causalCell?.fail("reseeded");
|
|
347
363
|
const nativeCols = operation.nativeCols ?? operation.cols;
|
|
348
364
|
const nativeRows = operation.nativeRows ?? operation.rows;
|
|
@@ -394,6 +410,13 @@ export class TerminalReplicaInterpreter {
|
|
|
394
410
|
this.#nativeSeedBackingCandidate = operation.native;
|
|
395
411
|
try {
|
|
396
412
|
this.#commit(true, undefined, operation.trace ?? null);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
try {
|
|
415
|
+
previous.dispose();
|
|
416
|
+
} catch {
|
|
417
|
+
// Preserve the commit failure; replacement remains owned until close.
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
397
420
|
} finally {
|
|
398
421
|
this.#nativeSeedBackingCandidate = undefined;
|
|
399
422
|
}
|
|
@@ -47,6 +47,11 @@ export interface TerminalReplicaQualificationSnapshot {
|
|
|
47
47
|
readonly revision: number | null;
|
|
48
48
|
readonly stateHash: string | null;
|
|
49
49
|
readonly stats: TerminalReplicaInterpreterStats;
|
|
50
|
+
readonly listeners: {
|
|
51
|
+
readonly canonical: number;
|
|
52
|
+
readonly raw: number;
|
|
53
|
+
readonly upstream: number;
|
|
54
|
+
};
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/** Daemon-private backing admission; this is not a client transport schema. */
|
|
@@ -72,6 +77,7 @@ interface ReseedCandidate {
|
|
|
72
77
|
readonly layoutLease: LayoutLease | null;
|
|
73
78
|
readonly subscriptionEpoch: number;
|
|
74
79
|
readonly chunks: Uint8Array[];
|
|
80
|
+
readonly captureChunks: Uint8Array[];
|
|
75
81
|
readonly trace: SessionRuntimeTraceContext | null;
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -98,6 +104,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
98
104
|
#reseedRetryCount = 0;
|
|
99
105
|
#reseed: ReseedCandidate | null = null;
|
|
100
106
|
#bootstrapped = false;
|
|
107
|
+
#publishedIncarnation: string | null = null;
|
|
101
108
|
#waitingForGeometryCapture = false;
|
|
102
109
|
#historyTimer: SessionRuntimeTimer | null = null;
|
|
103
110
|
#historyChecking = false;
|
|
@@ -144,6 +151,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
144
151
|
);
|
|
145
152
|
},
|
|
146
153
|
onUpdate: (update, trace) => {
|
|
154
|
+
this.#publishedIncarnation = update.incarnation;
|
|
147
155
|
if (update.type === "terminal.seed") {
|
|
148
156
|
this.#bootstrapped = true;
|
|
149
157
|
this.#reseedRetryCount = 0;
|
|
@@ -224,6 +232,11 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
224
232
|
this.#interpreter.failCausalCell(reason, traceId);
|
|
225
233
|
}
|
|
226
234
|
|
|
235
|
+
/** Scalar diagnostic only: no seed construction, hashing, or owner creation. */
|
|
236
|
+
inputDispatchIncarnation(): string | null {
|
|
237
|
+
return this.#disposed ? null : this.#publishedIncarnation;
|
|
238
|
+
}
|
|
239
|
+
|
|
227
240
|
qualificationSnapshot(): TerminalReplicaQualificationSnapshot {
|
|
228
241
|
const seed = this.#interpreter.currentSeed();
|
|
229
242
|
return Object.freeze({
|
|
@@ -231,6 +244,11 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
231
244
|
revision: seed?.revision ?? null,
|
|
232
245
|
stateHash: seed?.stateHash ?? null,
|
|
233
246
|
stats: this.#interpreter.stats(),
|
|
247
|
+
listeners: Object.freeze({
|
|
248
|
+
canonical: this.#listeners.size,
|
|
249
|
+
raw: this.#rawListeners.size,
|
|
250
|
+
upstream: this.#upstream ? 1 : 0,
|
|
251
|
+
}),
|
|
234
252
|
});
|
|
235
253
|
}
|
|
236
254
|
|
|
@@ -425,6 +443,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
425
443
|
layoutLease: this.#layoutLease,
|
|
426
444
|
subscriptionEpoch: this.#subscriptionEpoch,
|
|
427
445
|
chunks: [],
|
|
446
|
+
captureChunks: [],
|
|
428
447
|
trace: this.#consumeOutputTrace(),
|
|
429
448
|
};
|
|
430
449
|
} else if (event.type === "seed" || event.type === "delta") {
|
|
@@ -433,7 +452,11 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
433
452
|
if (event.type === "seed" && event.requiresNativeRecapture)
|
|
434
453
|
this.#reseed.requiresNativeRecapture = true;
|
|
435
454
|
if (event.type === "seed" && event.native) this.#reseed.native = event.native;
|
|
436
|
-
else
|
|
455
|
+
else {
|
|
456
|
+
const bytes = Uint8Array.from(event.data);
|
|
457
|
+
this.#reseed.chunks.push(bytes);
|
|
458
|
+
if (event.type === "seed") this.#reseed.captureChunks.push(bytes);
|
|
459
|
+
}
|
|
437
460
|
} else
|
|
438
461
|
this.#supervise(
|
|
439
462
|
this.#interpreter.enqueue({
|
|
@@ -456,6 +479,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
456
479
|
cols: reseed.nativeCols,
|
|
457
480
|
rows: reseed.nativeRows,
|
|
458
481
|
chunks: reseed.chunks,
|
|
482
|
+
captureChunks: reseed.captureChunks,
|
|
459
483
|
native: reseed.native,
|
|
460
484
|
historyLimit: event.historyLimit,
|
|
461
485
|
historySize: event.historySize,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
TerminalReplicaRow,
|
|
11
11
|
TerminalReplicaSnapshot,
|
|
12
12
|
} from "@tmux-ide/contracts";
|
|
13
|
-
import {
|
|
13
|
+
import { createProjectedTerminalReplicaRowBuilder } from "@tmux-ide/core";
|
|
14
14
|
import type {
|
|
15
15
|
TerminalInterpreterBackend,
|
|
16
16
|
TerminalInterpreterBackendFactoryOptions,
|
|
@@ -580,21 +580,28 @@ function projectRowCached(
|
|
|
580
580
|
return prior.row;
|
|
581
581
|
}
|
|
582
582
|
const cell = buffer.getNullCell();
|
|
583
|
-
const
|
|
583
|
+
const builder = createProjectedTerminalReplicaRowBuilder();
|
|
584
584
|
for (let column = 0; column < cols; column += 1) {
|
|
585
585
|
line?.getCell(column, cell);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
586
|
+
// Compute the same clipped cell values before selecting trusted blanks.
|
|
587
|
+
const grapheme = line && column + cell.getWidth() <= cols ? cell.getChars() : "";
|
|
588
|
+
const width = line && column + cell.getWidth() <= cols ? (cell.getWidth() as 0 | 1 | 2) : 1;
|
|
589
|
+
const foregroundKind = line ? cellColorKind(cell, "foreground") : "default";
|
|
590
|
+
const foregroundValue = line ? cell.getFgColor() : 0;
|
|
591
|
+
const backgroundKind = line ? cellColorKind(cell, "background") : "default";
|
|
592
|
+
const backgroundValue = line ? cell.getBgColor() : 0;
|
|
593
|
+
const attributes = line ? cellAttributes(cell) : 0;
|
|
594
|
+
builder.append(
|
|
595
|
+
grapheme,
|
|
596
|
+
width,
|
|
597
|
+
attributes,
|
|
598
|
+
foregroundKind,
|
|
599
|
+
foregroundValue,
|
|
600
|
+
backgroundKind,
|
|
601
|
+
backgroundValue,
|
|
602
|
+
);
|
|
596
603
|
}
|
|
597
|
-
const row =
|
|
604
|
+
const row = builder.finish(line?.isWrapped ?? false);
|
|
598
605
|
if (line && cacheKey)
|
|
599
606
|
cache.set(cacheKey, {
|
|
600
607
|
data: lineData(line)?.slice() ?? null,
|
|
@@ -633,16 +640,15 @@ function rawRowsEqual(left: Uint32Array | null, right: Uint32Array | null): bool
|
|
|
633
640
|
return true;
|
|
634
641
|
}
|
|
635
642
|
|
|
636
|
-
function
|
|
643
|
+
function cellColorKind(
|
|
637
644
|
cell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
|
|
638
645
|
channel: "foreground" | "background",
|
|
639
|
-
): TerminalReplicaColor {
|
|
646
|
+
): TerminalReplicaColor["kind"] {
|
|
640
647
|
const rgb = channel === "foreground" ? cell.isFgRGB() : cell.isBgRGB();
|
|
641
648
|
const palette = channel === "foreground" ? cell.isFgPalette() : cell.isBgPalette();
|
|
642
|
-
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
return { kind: "default" };
|
|
649
|
+
if (rgb) return "rgb";
|
|
650
|
+
if (palette) return "indexed";
|
|
651
|
+
return "default";
|
|
646
652
|
}
|
|
647
653
|
|
|
648
654
|
function cellAttributes(cell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>): number {
|
|
@@ -675,7 +681,7 @@ interface NativeImportBuffer {
|
|
|
675
681
|
x: number;
|
|
676
682
|
y: number;
|
|
677
683
|
}
|
|
678
|
-
/** Inverse of this pinned adapter's
|
|
684
|
+
/** Inverse of this pinned adapter's cellColorKind/cellAttributes projection. */
|
|
679
685
|
function nativeImportAttributes(cell: TerminalReplicaCell): [number, number] {
|
|
680
686
|
const color = (value: TerminalReplicaColor) =>
|
|
681
687
|
value.kind === "default"
|