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.
Files changed (85) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +120 -139
  3. package/bin/cli.js +4847 -3585
  4. package/package.json +16 -9
  5. package/packages/contracts/src/session-runtime.ts +2 -0
  6. package/packages/contracts/src/terminal-delivery.ts +4 -0
  7. package/packages/core/src/terminal-compact-capability.ts +31 -1
  8. package/packages/core/src/terminal-delivery.ts +234 -32
  9. package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
  10. package/packages/core/src/terminal-replica-owned-row.ts +165 -10
  11. package/packages/core/src/terminal-replica.ts +43 -21
  12. package/packages/daemon/dist/lib/daemon-embed.js +19 -34
  13. package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
  14. package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
  15. package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
  16. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  17. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  18. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  19. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  20. package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
  21. package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
  22. package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
  23. package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
  24. package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
  25. package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
  26. package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
  27. package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
  28. package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
  29. package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
  30. package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
  31. package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
  32. package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
  33. package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
  34. package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
  35. package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
  36. package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
  37. package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
  38. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
  39. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
  40. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
  41. package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
  42. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  43. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
  44. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
  45. package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
  46. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
  47. package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
  48. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
  49. package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
  50. package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
  51. package/packages/daemon/src/lib/daemon-embed.ts +22 -34
  52. package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
  53. package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
  54. package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
  55. package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
  56. package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
  57. package/packages/daemon/src/terminal/mirror/events.ts +7 -7
  58. package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
  59. package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
  60. package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
  61. package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
  62. package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
  63. package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
  64. package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
  65. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
  66. package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
  67. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
  68. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
  69. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
  70. package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
  71. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  72. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
  73. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
  74. package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
  75. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
  76. package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
  77. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
  78. package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
  79. package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
  80. package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
  81. package/packages/daemon-client/src/workspace-client-types.ts +1 -0
  82. package/packages/daemon-client/src/workspace-client.ts +78 -5
  83. package/packages/tmux-bridge/src/index.ts +1 -0
  84. package/packages/tmux-bridge/src/runner.ts +25 -1
  85. 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?.close()));
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.close();
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?.close().catch(() => undefined);
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(client.baselineRevision, baseline, target);
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
- await pane?.source?.close().catch(() => undefined);
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. A skipped
2248
- // revision uses an atomic seed; the m56.2 adjacent reducer is never weakened.
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
- ? { ...operation, chunks: operation.chunks.map((chunk) => chunk.slice()) }
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
- JSON.stringify(this.#snapshot.grid[probe.geometry.row]?.cells[probe.geometry.column]) !==
284
- JSON.stringify(probe.before)
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 this.#reseed.chunks.push(event.data.slice());
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 { freezeTerminalReplicaRow } from "@tmux-ide/core";
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 cells: TerminalReplicaCell[] = [];
583
+ const builder = createProjectedTerminalReplicaRowBuilder();
584
584
  for (let column = 0; column < cols; column += 1) {
585
585
  line?.getCell(column, cell);
586
- cells.push({
587
- // Empty width-one cells are unused storage; literal spaces are content.
588
- // Both paint as blanks, but preserving the distinction is necessary for
589
- // reflow and for recognizing padding before a wrapped wide glyph.
590
- grapheme: line && column + cell.getWidth() <= cols ? cell.getChars() : "",
591
- width: line && column + cell.getWidth() <= cols ? (cell.getWidth() as 0 | 1 | 2) : 1,
592
- foreground: line ? cellColor(cell, "foreground") : { kind: "default" },
593
- background: line ? cellColor(cell, "background") : { kind: "default" },
594
- attributes: line ? cellAttributes(cell) : 0,
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 = freezeTerminalReplicaRow({ cells, wrapped: line?.isWrapped ?? false });
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 cellColor(
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
- const value = channel === "foreground" ? cell.getFgColor() : cell.getBgColor();
643
- if (rgb) return { kind: "rgb", value };
644
- if (palette) return { kind: "indexed", index: value };
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 cellColor/cellAttributes projection. */
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"