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,6 +1,7 @@
1
+ import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.js";
1
2
  import { MAX_RETAINED_NATIVE_BACKING_BYTES, takeNativeSeedBacking, } from "./native-seed-backing.js";
2
3
  import { TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES, TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES, TerminalDeliveryEnvelopeSchemaZ, TerminalDeliveryAckSchemaZ, TerminalDeliveryNackSchemaZ, TerminalDeliveryOfferSchemaZ, TerminalDeliveryVisibilitySchemaZ, } from "@tmux-ide/contracts";
3
- import { TerminalDeliveryStateTooLargeError, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
4
+ import { TerminalDeliveryStateTooLargeError, terminalReplicaRowsEqual, TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
4
5
  import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
5
6
  import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
6
7
  import { registerTerminalDeliveryObservationOrdinal } from "./terminal-delivery-observation-identity.js";
@@ -64,6 +65,8 @@ export class SessionRuntimeTerminalDeliveryHub {
64
65
  #observability;
65
66
  #panes = new Map();
66
67
  #clients = new Map();
68
+ #sourceCloseFailures = 0;
69
+ #pendingSourceCloses = 0;
67
70
  /** Synchronous reservations held while an async pane source is starting. */
68
71
  #pendingClients = new Map();
69
72
  #cache = new Map();
@@ -140,6 +143,7 @@ export class SessionRuntimeTerminalDeliveryHub {
140
143
  outgoing: [],
141
144
  sending: false,
142
145
  retireAfterDrain: false,
146
+ captureUnavailable: false,
143
147
  lastAck: null,
144
148
  sourceClosedFlight: null,
145
149
  backgroundTimer: null,
@@ -166,10 +170,37 @@ export class SessionRuntimeTerminalDeliveryHub {
166
170
  close: async () => this.#closeClient(client),
167
171
  };
168
172
  }
173
+ catch (error) {
174
+ if (!(error instanceof StockCaptureTabUnavailableError))
175
+ throw error;
176
+ // Negotiation still settles this pane; no invented canonical seed is published.
177
+ await accept({
178
+ type: "terminal.delivery.fault",
179
+ reason: "source-closed",
180
+ message: error.message,
181
+ deliveryNonce: negotiation.negotiated.deliveryNonce,
182
+ });
183
+ return {
184
+ negotiation,
185
+ closed: Promise.resolve("closed"),
186
+ ack: () => { },
187
+ nack: () => { },
188
+ setVisibility: () => { },
189
+ close: async () => { },
190
+ };
191
+ }
169
192
  finally {
170
193
  this.#pendingClients.delete(key);
171
194
  }
172
195
  }
196
+ /** Retire only this pane's delivery owners before its canonical owner is disposed. */
197
+ failPaneCapture(semanticPaneId, error) {
198
+ for (const client of this.#clients.values())
199
+ if (client.paneId === semanticPaneId && !client.closed && !client.captureUnavailable) {
200
+ client.captureUnavailable = true;
201
+ this.#fault(client, "source-closed", error.message);
202
+ }
203
+ }
173
204
  retainedNativeBacking(paneId, expected) {
174
205
  const pane = this.#panes.get(paneId);
175
206
  const record = pane?.revisions.get(expected.revision);
@@ -230,6 +261,12 @@ export class SessionRuntimeTerminalDeliveryHub {
230
261
  return Object.freeze({
231
262
  clients: new Set([...this.#clients.values()].map((client) => client.clientId)).size,
232
263
  connections: this.#clients.size,
264
+ sourceSubscriptions: [...this.#panes.values()].filter((pane) => pane.source !== null).length,
265
+ pendingSourceSubscriptions: [...this.#panes.values()].filter((pane) => pane.source === null)
266
+ .length,
267
+ pendingClients: this.#pendingClients.size,
268
+ sourceCloseFailures: this.#sourceCloseFailures,
269
+ pendingSourceCloses: this.#pendingSourceCloses,
233
270
  inFlight: [...this.#clients.values()].filter((client) => client.inFlight).length,
234
271
  latestPointers: [...this.#clients.values()].filter((client) => client.latestRevision !== null)
235
272
  .length,
@@ -293,9 +330,22 @@ export class SessionRuntimeTerminalDeliveryHub {
293
330
  pane.pendingCanonicalCells = 0;
294
331
  pane.canonicalScheduled = false;
295
332
  }
296
- await Promise.allSettled(panes.map((pane) => pane.source?.close()));
333
+ await Promise.allSettled(panes.map((pane) => pane.source && this.#closeSource(pane.source)));
297
334
  this.#clearCache();
298
335
  }
336
+ async #closeSource(source) {
337
+ this.#pendingSourceCloses += 1;
338
+ try {
339
+ await source.close();
340
+ }
341
+ catch (error) {
342
+ this.#sourceCloseFailures += 1;
343
+ throw error;
344
+ }
345
+ finally {
346
+ this.#pendingSourceCloses -= 1;
347
+ }
348
+ }
299
349
  async close() {
300
350
  if (this.#closed)
301
351
  return;
@@ -346,7 +396,7 @@ export class SessionRuntimeTerminalDeliveryHub {
346
396
  if (this.#closed ||
347
397
  this.#panes.get(semanticPaneId) !== pane ||
348
398
  pane?.canonicalAbort.signal.aborted) {
349
- await source.close();
399
+ await this.#closeSource(source);
350
400
  throw new Error("Terminal delivery source retired during startup");
351
401
  }
352
402
  pane.source = source;
@@ -510,7 +560,7 @@ export class SessionRuntimeTerminalDeliveryHub {
510
560
  pane.revisions.set(update.revision, record);
511
561
  this.#pruneCanonicalRevisions(semanticPaneId);
512
562
  for (const client of this.#clients.values()) {
513
- if (client.paneId !== semanticPaneId || client.closed)
563
+ if (client.paneId !== semanticPaneId || client.closed || client.captureUnavailable)
514
564
  continue;
515
565
  if (!client.lifecycleOpenRecorded)
516
566
  client.lifecycleOpenRecorded = this.#recordDeliveryLifecycle(client, pane, "open");
@@ -540,7 +590,7 @@ export class SessionRuntimeTerminalDeliveryHub {
540
590
  pane.pendingCanonical.length = 0;
541
591
  pane.pendingCanonicalCells = 0;
542
592
  pane.canonicalScheduled = false;
543
- void pane.source?.close().catch(() => undefined);
593
+ void (pane.source && this.#closeSource(pane.source).catch(() => undefined));
544
594
  this.#clearCache();
545
595
  }, 0);
546
596
  }
@@ -581,6 +631,7 @@ export class SessionRuntimeTerminalDeliveryHub {
581
631
  }
582
632
  #schedule(client) {
583
633
  if (client.closed ||
634
+ client.captureUnavailable ||
584
635
  client.inFlight ||
585
636
  client.encoding ||
586
637
  client.latestRevision === null ||
@@ -613,6 +664,7 @@ export class SessionRuntimeTerminalDeliveryHub {
613
664
  if (!pane ||
614
665
  !target ||
615
666
  client.closed ||
667
+ client.captureUnavailable ||
616
668
  client.inFlight ||
617
669
  client.encoding ||
618
670
  client.visibility === "hidden" ||
@@ -936,7 +988,7 @@ export class SessionRuntimeTerminalDeliveryHub {
936
988
  this.#reseeds += 1;
937
989
  }
938
990
  else {
939
- const patchPayload = semanticPayload(client.baselineRevision, baseline, target);
991
+ const patchPayload = semanticPayload(client.baselineRevision, baseline, target, pane.revisions.get(client.baselineRevision)?.update.incarnation);
940
992
  const seedPayload = patchPayload.frame === "tombstone" ? null : semanticSeed(target);
941
993
  const legacyAttempts = null;
942
994
  if (patchPayload.frame !== "patch") {
@@ -1205,6 +1257,20 @@ export class SessionRuntimeTerminalDeliveryHub {
1205
1257
  }
1206
1258
  #nack(client, input) {
1207
1259
  const nack = TerminalDeliveryNackSchemaZ.parse(input);
1260
+ const closed = client.sourceClosedFlight;
1261
+ // A decoder may have sent this NACK before observing the pane-local fault.
1262
+ // Only the exact displaced transaction is inert; unrelated feedback still
1263
+ // follows the normal protocol-violation path below.
1264
+ if (client.captureUnavailable &&
1265
+ !client.inFlight &&
1266
+ closed &&
1267
+ nack.workspaceName === closed.workspaceName &&
1268
+ nack.semanticPaneId === closed.semanticPaneId &&
1269
+ nack.generation === closed.generation &&
1270
+ nack.incarnation === closed.incarnation &&
1271
+ nack.deliveryNonce === closed.deliveryNonce &&
1272
+ nack.transactionId === closed.transactionId)
1273
+ return;
1208
1274
  const envelope = client.inFlight?.envelope;
1209
1275
  if (!envelope ||
1210
1276
  nack.workspaceName !== envelope.workspaceName ||
@@ -1350,7 +1416,8 @@ export class SessionRuntimeTerminalDeliveryHub {
1350
1416
  pane.pendingCanonicalCells = 0;
1351
1417
  pane.canonicalScheduled = false;
1352
1418
  }
1353
- await pane?.source?.close().catch(() => undefined);
1419
+ if (pane?.source)
1420
+ await this.#closeSource(pane.source).catch(() => undefined);
1354
1421
  this.#clearCache();
1355
1422
  }
1356
1423
  #recordDeliveryLifecycle(client, pane, event) {
@@ -1689,7 +1756,7 @@ function semanticSelectionObservationFromError(error) {
1689
1756
  ? error.selectionObservation
1690
1757
  : null;
1691
1758
  }
1692
- function semanticPayload(baselineRevision, baseline, target) {
1759
+ function semanticPayload(baselineRevision, baseline, target, baselineIncarnation) {
1693
1760
  const update = target.update;
1694
1761
  if (!target.state.snapshot) {
1695
1762
  if (update.type !== "terminal.tombstone")
@@ -1701,8 +1768,8 @@ function semanticPayload(baselineRevision, baseline, target) {
1701
1768
  tombstone: update.tombstone,
1702
1769
  };
1703
1770
  }
1704
- // Adjacent canonical patches are already the cheapest exact diff. A skipped
1705
- // revision uses an atomic seed; the m56.2 adjacent reducer is never weakened.
1771
+ // Adjacent canonical patches are already the cheapest exact diff. Preserve
1772
+ // this direct path before considering an exact coalesced snapshot patch.
1706
1773
  if (baseline && update.type === "terminal.patch" && update.baseRevision === baselineRevision)
1707
1774
  return {
1708
1775
  frame: "patch",
@@ -1710,6 +1777,57 @@ function semanticPayload(baselineRevision, baseline, target) {
1710
1777
  revision: update.revision,
1711
1778
  patch: update.patch,
1712
1779
  };
1780
+ // A skipped revision may still have an exact small representation. Only
1781
+ // append-only history is proved here; trim/reflow and changed dimensions
1782
+ // retain the full seed boundary. Equality includes wrapped and all cells.
1783
+ const snapshot = target.state.snapshot;
1784
+ if (baseline &&
1785
+ baselineIncarnation === update.incarnation &&
1786
+ baseline.cols === snapshot.cols &&
1787
+ baseline.rows === snapshot.rows &&
1788
+ baseline.history.length <= snapshot.history.length) {
1789
+ // Identity reuse is cheap, but detached equal histories must not turn a
1790
+ // cooperative seed into an unbounded synchronous cell comparison.
1791
+ let comparisonCells = 0;
1792
+ const equal = (left, right) => {
1793
+ if (left === right)
1794
+ return true;
1795
+ comparisonCells += right.cells.length;
1796
+ return comparisonCells <= TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS
1797
+ ? terminalReplicaRowsEqual(left, right)
1798
+ : null;
1799
+ };
1800
+ for (let index = 0; index < baseline.history.length; index++)
1801
+ if (equal(baseline.history[index], snapshot.history[index]) !== true)
1802
+ return semanticSeed(target);
1803
+ const rows = [];
1804
+ for (let index = 0; index < snapshot.grid.length; index++) {
1805
+ const same = equal(baseline.grid[index], snapshot.grid[index]);
1806
+ if (same === null)
1807
+ return semanticSeed(target);
1808
+ if (!same)
1809
+ rows.push({ index, row: snapshot.grid[index] });
1810
+ }
1811
+ const appendedRows = snapshot.history.length - baseline.history.length;
1812
+ // Compact patches serialize synchronously. Do not move a large appended
1813
+ // suffix or repaint across the seed-only cooperative encoding boundary.
1814
+ if ((rows.length + appendedRows) * snapshot.cols >= TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS)
1815
+ return semanticSeed(target);
1816
+ const append = snapshot.history.slice(baseline.history.length);
1817
+ return {
1818
+ frame: "patch",
1819
+ baseRevision: baselineRevision,
1820
+ revision: update.revision,
1821
+ patch: {
1822
+ rows,
1823
+ ...(append.length ? { historyDelta: { trim: 0, append } } : {}),
1824
+ cursor: snapshot.cursor,
1825
+ modes: snapshot.modes,
1826
+ placements: snapshot.placements,
1827
+ bootstrap: snapshot.bootstrap,
1828
+ },
1829
+ };
1830
+ }
1713
1831
  return semanticSeed(target);
1714
1832
  }
1715
1833
  /** Account represented cells without traversing/serializing the snapshot. */
@@ -1,10 +1,11 @@
1
+ import { assertStockCaptureRepresentable } from "./stock-capture-fidelity.js";
1
2
  import { rememberNativeSeedBacking } from "./native-seed-backing.js";
2
3
  import { detectWidgetMarkerFromReplicaRows, } from "@tmux-ide/contracts";
3
4
  import { applyTerminalReplicaPatch, assembleTerminalReplicaSnapshot, blankTerminalReplicaSnapshot, hashTerminalReplicaSnapshot, hashTerminalReplicaTombstone, hashTerminalWidgetContent, terminalReplicaRowsEqual, } from "@tmux-ide/core";
4
5
  import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
5
6
  import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
6
7
  import { createXtermTerminalInterpreterBackend } from "./xterm-terminal-interpreter-backend.js";
7
- import { CAUSAL_CELL_OSC, CausalCellLedger, } from "./causal-cell-ledger.js";
8
+ import { CAUSAL_CELL_OSC, CausalCellLedger, cellsEqual, } from "./causal-cell-ledger.js";
8
9
  /** Daemon-owned, sans-I/O VT interpreter with one FIFO for bytes and geometry. */
9
10
  export class TerminalReplicaInterpreter {
10
11
  #nativeSeedBackingCandidate;
@@ -110,7 +111,13 @@ export class TerminalReplicaInterpreter {
110
111
  }
111
112
  this.#flushWrites();
112
113
  const admitted = operation.type === "reseed"
113
- ? { ...operation, chunks: operation.chunks.map((chunk) => chunk.slice()) }
114
+ ? {
115
+ ...operation,
116
+ chunks: operation.chunks.map((chunk) => Uint8Array.from(chunk)),
117
+ ...(operation.captureChunks
118
+ ? { captureChunks: operation.captureChunks.map((chunk) => Uint8Array.from(chunk)) }
119
+ : {}),
120
+ }
114
121
  : operation;
115
122
  return this.#append(admitted);
116
123
  }
@@ -141,8 +148,7 @@ export class TerminalReplicaInterpreter {
141
148
  seed.incarnation !== probe.incarnation ||
142
149
  this.#snapshot.cols !== probe.geometry.cols ||
143
150
  this.#snapshot.rows !== probe.geometry.rows ||
144
- JSON.stringify(this.#snapshot.grid[probe.geometry.row]?.cells[probe.geometry.column]) !==
145
- JSON.stringify(probe.before))
151
+ !cellsEqual(this.#snapshot.grid[probe.geometry.row]?.cells[probe.geometry.column], probe.before))
146
152
  throw new Error("Causal-cell baseline drifted before admission");
147
153
  const ledger = new CausalCellLedger({
148
154
  probe,
@@ -192,6 +198,8 @@ export class TerminalReplicaInterpreter {
192
198
  if (this.#closed)
193
199
  return;
194
200
  if (operation.type === "reseed") {
201
+ if (operation.bootstrap === "painted-capture" && !operation.native)
202
+ assertStockCaptureRepresentable(operation.captureChunks ?? operation.chunks);
195
203
  this.#causalCell?.fail("reseeded");
196
204
  const nativeCols = operation.nativeCols ?? operation.cols;
197
205
  const nativeRows = operation.nativeRows ?? operation.rows;
@@ -241,6 +249,15 @@ export class TerminalReplicaInterpreter {
241
249
  try {
242
250
  this.#commit(true, undefined, operation.trace ?? null);
243
251
  }
252
+ catch (error) {
253
+ try {
254
+ previous.dispose();
255
+ }
256
+ catch {
257
+ // Preserve the commit failure; replacement remains owned until close.
258
+ }
259
+ throw error;
260
+ }
244
261
  finally {
245
262
  this.#nativeSeedBackingCandidate = undefined;
246
263
  }
@@ -28,6 +28,7 @@ export class SessionRuntimeTerminalReplicaOwner {
28
28
  #reseedRetryCount = 0;
29
29
  #reseed = null;
30
30
  #bootstrapped = false;
31
+ #publishedIncarnation = null;
31
32
  #waitingForGeometryCapture = false;
32
33
  #historyTimer = null;
33
34
  #historyChecking = false;
@@ -61,6 +62,7 @@ export class SessionRuntimeTerminalReplicaOwner {
61
62
  }));
62
63
  },
63
64
  onUpdate: (update, trace) => {
65
+ this.#publishedIncarnation = update.incarnation;
64
66
  if (update.type === "terminal.seed") {
65
67
  this.#bootstrapped = true;
66
68
  this.#reseedRetryCount = 0;
@@ -135,6 +137,10 @@ export class SessionRuntimeTerminalReplicaOwner {
135
137
  failCausalCell(reason, traceId) {
136
138
  this.#interpreter.failCausalCell(reason, traceId);
137
139
  }
140
+ /** Scalar diagnostic only: no seed construction, hashing, or owner creation. */
141
+ inputDispatchIncarnation() {
142
+ return this.#disposed ? null : this.#publishedIncarnation;
143
+ }
138
144
  qualificationSnapshot() {
139
145
  const seed = this.#interpreter.currentSeed();
140
146
  return Object.freeze({
@@ -142,6 +148,11 @@ export class SessionRuntimeTerminalReplicaOwner {
142
148
  revision: seed?.revision ?? null,
143
149
  stateHash: seed?.stateHash ?? null,
144
150
  stats: this.#interpreter.stats(),
151
+ listeners: Object.freeze({
152
+ canonical: this.#listeners.size,
153
+ raw: this.#rawListeners.size,
154
+ upstream: this.#upstream ? 1 : 0,
155
+ }),
145
156
  });
146
157
  }
147
158
  async captureNativeBacking() {
@@ -325,6 +336,7 @@ export class SessionRuntimeTerminalReplicaOwner {
325
336
  layoutLease: this.#layoutLease,
326
337
  subscriptionEpoch: this.#subscriptionEpoch,
327
338
  chunks: [],
339
+ captureChunks: [],
328
340
  trace: this.#consumeOutputTrace(),
329
341
  };
330
342
  }
@@ -336,8 +348,12 @@ export class SessionRuntimeTerminalReplicaOwner {
336
348
  this.#reseed.requiresNativeRecapture = true;
337
349
  if (event.type === "seed" && event.native)
338
350
  this.#reseed.native = event.native;
339
- else
340
- this.#reseed.chunks.push(event.data.slice());
351
+ else {
352
+ const bytes = Uint8Array.from(event.data);
353
+ this.#reseed.chunks.push(bytes);
354
+ if (event.type === "seed")
355
+ this.#reseed.captureChunks.push(bytes);
356
+ }
341
357
  }
342
358
  else
343
359
  this.#supervise(this.#interpreter.enqueue({
@@ -359,6 +375,7 @@ export class SessionRuntimeTerminalReplicaOwner {
359
375
  cols: reseed.nativeCols,
360
376
  rows: reseed.nativeRows,
361
377
  chunks: reseed.chunks,
378
+ captureChunks: reseed.captureChunks,
362
379
  native: reseed.native,
363
380
  historyLimit: event.historyLimit,
364
381
  historySize: event.historySize,
@@ -2,7 +2,7 @@ import { isNativeBootstrapCapture } from "../mirror/native-grid-capture.js";
2
2
  import { projectNativeGridRow } from "../mirror/native-grid-projection.js";
3
3
  import { Terminal } from "@tmux-ide/xterm-headless";
4
4
  import { Unicode11Addon } from "@xterm/addon-unicode11";
5
- import { freezeTerminalReplicaRow } from "@tmux-ide/core";
5
+ import { createProjectedTerminalReplicaRowBuilder } from "@tmux-ide/core";
6
6
  /** Pinned xterm 6 adapter and the rollback oracle for native backends. */
7
7
  export class XtermTerminalInterpreterBackend {
8
8
  kind = "xterm";
@@ -429,21 +429,20 @@ function projectRowCached(cache, buffer, index, cols) {
429
429
  return prior.row;
430
430
  }
431
431
  const cell = buffer.getNullCell();
432
- const cells = [];
432
+ const builder = createProjectedTerminalReplicaRowBuilder();
433
433
  for (let column = 0; column < cols; column += 1) {
434
434
  line?.getCell(column, cell);
435
- cells.push({
436
- // Empty width-one cells are unused storage; literal spaces are content.
437
- // Both paint as blanks, but preserving the distinction is necessary for
438
- // reflow and for recognizing padding before a wrapped wide glyph.
439
- grapheme: line && column + cell.getWidth() <= cols ? cell.getChars() : "",
440
- width: line && column + cell.getWidth() <= cols ? cell.getWidth() : 1,
441
- foreground: line ? cellColor(cell, "foreground") : { kind: "default" },
442
- background: line ? cellColor(cell, "background") : { kind: "default" },
443
- attributes: line ? cellAttributes(cell) : 0,
444
- });
435
+ // Compute the same clipped cell values before selecting trusted blanks.
436
+ const grapheme = line && column + cell.getWidth() <= cols ? cell.getChars() : "";
437
+ const width = line && column + cell.getWidth() <= cols ? cell.getWidth() : 1;
438
+ const foregroundKind = line ? cellColorKind(cell, "foreground") : "default";
439
+ const foregroundValue = line ? cell.getFgColor() : 0;
440
+ const backgroundKind = line ? cellColorKind(cell, "background") : "default";
441
+ const backgroundValue = line ? cell.getBgColor() : 0;
442
+ const attributes = line ? cellAttributes(cell) : 0;
443
+ builder.append(grapheme, width, attributes, foregroundKind, foregroundValue, backgroundKind, backgroundValue);
445
444
  }
446
- const row = freezeTerminalReplicaRow({ cells, wrapped: line?.isWrapped ?? false });
445
+ const row = builder.finish(line?.isWrapped ?? false);
447
446
  if (line && cacheKey)
448
447
  cache.set(cacheKey, {
449
448
  data: lineData(line)?.slice() ?? null,
@@ -474,15 +473,14 @@ function rawRowsEqual(left, right) {
474
473
  return false;
475
474
  return true;
476
475
  }
477
- function cellColor(cell, channel) {
476
+ function cellColorKind(cell, channel) {
478
477
  const rgb = channel === "foreground" ? cell.isFgRGB() : cell.isBgRGB();
479
478
  const palette = channel === "foreground" ? cell.isFgPalette() : cell.isBgPalette();
480
- const value = channel === "foreground" ? cell.getFgColor() : cell.getBgColor();
481
479
  if (rgb)
482
- return { kind: "rgb", value };
480
+ return "rgb";
483
481
  if (palette)
484
- return { kind: "indexed", index: value };
485
- return { kind: "default" };
482
+ return "indexed";
483
+ return "default";
486
484
  }
487
485
  function cellAttributes(cell) {
488
486
  return ((cell.isBold() ? 1 : 0) |
@@ -494,7 +492,7 @@ function cellAttributes(cell) {
494
492
  (cell.isInvisible() ? 64 : 0) |
495
493
  (cell.isStrikethrough() ? 128 : 0));
496
494
  }
497
- /** Inverse of this pinned adapter's cellColor/cellAttributes projection. */
495
+ /** Inverse of this pinned adapter's cellColorKind/cellAttributes projection. */
498
496
  function nativeImportAttributes(cell) {
499
497
  const color = (value) => value.kind === "default"
500
498
  ? 0