tmux-ide 2.9.10 → 2.9.12

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 (81) hide show
  1. package/bin/cli.js +4321 -2596
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +4 -0
  4. package/packages/contracts/src/pane-stream.ts +72 -0
  5. package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
  6. package/packages/contracts/src/terminal-delivery.ts +17 -2
  7. package/packages/contracts/src/terminal-graphics.ts +68 -0
  8. package/packages/contracts/src/terminal-projection.ts +153 -0
  9. package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
  10. package/packages/core/src/index.ts +2 -0
  11. package/packages/core/src/terminal-delivery-binary.ts +160 -0
  12. package/packages/core/src/terminal-delivery.ts +371 -16
  13. package/packages/core/src/terminal-projection-vectors.mjs +82 -0
  14. package/packages/core/src/terminal-projection.ts +476 -0
  15. package/packages/core/src/terminal-replica-hash-cache.ts +8 -1
  16. package/packages/core/src/terminal-replica.ts +1 -1
  17. package/packages/daemon/dist/lib/daemon-embed.js +39 -3
  18. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
  19. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -0
  20. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  21. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  22. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  23. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  24. package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
  25. package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
  26. package/packages/daemon/dist/terminal/mirror/mirror-service.js +12 -2
  27. package/packages/daemon/dist/terminal/mirror/session-channel.js +46 -23
  28. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +190 -19
  29. package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
  30. package/packages/daemon/dist/terminal/session-runtime/registry.js +19 -6
  31. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
  32. package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
  33. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +95 -36
  34. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +5 -0
  35. package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +2 -2
  36. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
  37. package/packages/daemon/dist/tui/mirror/runtime/application-generation-starter.js +16 -1
  38. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agent-navigation.js +13 -0
  39. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +12 -4
  40. package/packages/daemon/dist/tui/mirror/runtime/application-retained-agent.js +18 -0
  41. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +90 -5
  42. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
  43. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +2 -2
  44. package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
  45. package/packages/daemon/dist/tui/mirror/runtime/runtime-layout-presentation.js +6 -3
  46. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +7 -2
  47. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
  48. package/packages/daemon/dist/tui/mirror/runtime/terminal-selection-waiter.js +36 -0
  49. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
  50. package/packages/daemon/src/lib/daemon-embed.ts +38 -2
  51. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
  52. package/packages/daemon/src/lib/tmux-server-owner.ts +3 -0
  53. package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
  54. package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
  55. package/packages/daemon/src/terminal/mirror/mirror-service.ts +19 -2
  56. package/packages/daemon/src/terminal/mirror/session-channel.ts +57 -9
  57. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +248 -16
  58. package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
  59. package/packages/daemon/src/terminal/session-runtime/registry.ts +25 -3
  60. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
  61. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
  62. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
  63. package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
  64. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +102 -44
  65. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +6 -0
  66. package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +3 -0
  67. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +6 -0
  68. package/packages/daemon/src/tui/mirror/runtime/application-generation-starter.ts +17 -1
  69. package/packages/daemon/src/tui/mirror/runtime/application-machine-agent-navigation.ts +15 -0
  70. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +18 -4
  71. package/packages/daemon/src/tui/mirror/runtime/application-retained-agent.ts +29 -0
  72. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +89 -6
  73. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
  74. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +2 -2
  75. package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
  76. package/packages/daemon/src/tui/mirror/runtime/runtime-layout-presentation.ts +7 -4
  77. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +9 -1
  78. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
  79. package/packages/daemon/src/tui/mirror/runtime/terminal-selection-waiter.ts +36 -0
  80. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
  81. package/packages/daemon-client/src/pane-stream-client.ts +92 -1
@@ -1,7 +1,10 @@
1
+ import { prepareProjection } from "./terminal-projection-delivery.js";
2
+ import { TerminalProjectionEnvelopeSchemaZ, TerminalProjectionReceiptSchemaZ, TerminalProjectionMaterializeSchemaZ, } from "@tmux-ide/contracts";
3
+ import { encodeTerminalProjectionCooperatively } from "@tmux-ide/core";
1
4
  import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.js";
2
5
  import { MAX_RETAINED_NATIVE_BACKING_BYTES, takeNativeSeedBacking, } from "./native-seed-backing.js";
3
6
  import { WorkspaceIdSchemaZ, TERMINAL_GRAPHICS_CAPABILITY_VERSION, TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES, TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES, TerminalDeliveryEnvelopeSchemaZ, TerminalDeliveryAckSchemaZ, TerminalDeliveryNackSchemaZ, TerminalDeliveryOfferSchemaZ, TerminalDeliveryVisibilitySchemaZ, } from "@tmux-ide/contracts";
4
- import { TerminalDeliveryStateTooLargeError, terminalReplicaRowsEqual, TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
7
+ import { TerminalDeliveryStateTooLargeError, terminalReplicaRowsEqual, TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeBinarySemanticTerminalUpdate, encodeBinarySemanticTerminalUpdateCooperatively, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
5
8
  import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
6
9
  import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
7
10
  import { registerTerminalDeliveryObservationOrdinal } from "./terminal-delivery-observation-identity.js";
@@ -15,6 +18,13 @@ const MAX_PANES = 32;
15
18
  const MAX_PENDING_CANONICAL_CELLS = 1_000_000;
16
19
  const MAX_CONNECTIONS = MAX_CLIENTS * MAX_PANES;
17
20
  const BACKGROUND_CADENCE_MS = 100;
21
+ class DeferredProjectionEncoding extends Error {
22
+ payload;
23
+ constructor(payload) {
24
+ super("Deferred projection encoding");
25
+ this.payload = payload;
26
+ }
27
+ }
18
28
  class DeferredSeedEncoding extends Error {
19
29
  payload;
20
30
  constructor(payload) {
@@ -135,6 +145,10 @@ export class SessionRuntimeTerminalDeliveryHub {
135
145
  visibility: "visible",
136
146
  baselineRevision: -1,
137
147
  baselineHash: null,
148
+ projectionBaseline: null,
149
+ materializeRequested: false,
150
+ lastProjectionReceipt: null,
151
+ lastMaterialize: null,
138
152
  reseedRequired: false,
139
153
  inFlight: null,
140
154
  latestRevision: pane.latest?.update.revision ?? null,
@@ -159,6 +173,8 @@ export class SessionRuntimeTerminalDeliveryHub {
159
173
  negotiation,
160
174
  closed,
161
175
  ack: (ack) => this.#ack(client, ack),
176
+ projectionReceipt: (receipt) => this.#projectionReceipt(client, receipt),
177
+ materialize: (request) => this.#materialize(client, request),
162
178
  nack: (nack) => this.#nack(client, nack),
163
179
  setVisibility: (visibilityInput) => {
164
180
  if (client.closed)
@@ -587,7 +603,10 @@ export class SessionRuntimeTerminalDeliveryHub {
587
603
  if (!client.lifecycleOpenRecorded)
588
604
  client.lifecycleOpenRecorded = this.#recordDeliveryLifecycle(client, pane, "open");
589
605
  if (update.type === "terminal.tombstone" &&
590
- (client.inFlight !== null || client.encoding !== null || client.visibility !== "visible")) {
606
+ (client.inFlight !== null ||
607
+ client.encoding !== null ||
608
+ client.visibility !== "visible" ||
609
+ client.projectionBaseline !== null)) {
591
610
  this.#fault(client, "source-closed", "Terminal source closed before final state delivery");
592
611
  continue;
593
612
  }
@@ -705,8 +724,13 @@ export class SessionRuntimeTerminalDeliveryHub {
705
724
  encodedRepresentation = representation;
706
725
  const transactionId = this.#scheduler.createId();
707
726
  const chunkCount = Math.max(1, Math.ceil(representation.bytes.byteLength / (256 * 1024)));
708
- const envelope = TerminalDeliveryEnvelopeSchemaZ.parse({
709
- type: "terminal.delivery",
727
+ const envelope = (representation.projection
728
+ ? TerminalProjectionEnvelopeSchemaZ
729
+ : TerminalDeliveryEnvelopeSchemaZ).parse({
730
+ type: representation.projection ? "terminal.projection" : "terminal.delivery",
731
+ ...(representation.projection
732
+ ? { projectionStateHash: representation.projection.hash }
733
+ : {}),
710
734
  workspaceName: client.deliveryWorkspaceName,
711
735
  semanticPaneId: target.update.semanticPaneId,
712
736
  generation: target.update.generation,
@@ -717,7 +741,11 @@ export class SessionRuntimeTerminalDeliveryHub {
717
741
  protocolVersion: 1,
718
742
  encoding: representation.encoding ?? client.negotiated.encoding,
719
743
  frame: representation.frame,
720
- baseRevision: representation.frame === "seed" ? null : Math.max(0, client.baselineRevision),
744
+ baseRevision: representation.projection
745
+ ? representation.projectionBaseRevision
746
+ : representation.frame === "seed"
747
+ ? null
748
+ : Math.max(0, client.baselineRevision),
721
749
  canonicalRevision: target.update.revision,
722
750
  canonicalStateHash: target.update.stateHash,
723
751
  representationHash: representation.representationHash ??
@@ -733,6 +761,7 @@ export class SessionRuntimeTerminalDeliveryHub {
733
761
  registerTerminalDeliveryObservationOrdinal(envelope, deliveryOrdinal);
734
762
  client.inFlight = {
735
763
  envelope,
764
+ projection: representation.projection ?? null,
736
765
  deliveryOrdinal,
737
766
  bytes: representation.bytes,
738
767
  nextChunk: 0,
@@ -744,9 +773,9 @@ export class SessionRuntimeTerminalDeliveryHub {
744
773
  pane.pendingDeliveryTrace = null;
745
774
  }
746
775
  catch (error) {
747
- if (error instanceof DeferredSeedEncoding) {
776
+ if (error instanceof DeferredSeedEncoding || error instanceof DeferredProjectionEncoding) {
748
777
  deferred = true;
749
- this.#startEncoding(client, pane, target, error.payload, traceStarted);
778
+ this.#startEncoding(client, pane, target, error.payload, traceStarted, error instanceof DeferredProjectionEncoding);
750
779
  return;
751
780
  }
752
781
  failedSelectionObservation = semanticSelectionObservationFromError(error);
@@ -804,7 +833,7 @@ export class SessionRuntimeTerminalDeliveryHub {
804
833
  }
805
834
  }
806
835
  }
807
- #startEncoding(client, pane, target, payload, traceStarted) {
836
+ #startEncoding(client, pane, target, payload, traceStarted, projection = false) {
808
837
  const key = cacheKey([
809
838
  target.update.workspaceName,
810
839
  client.paneId,
@@ -814,6 +843,9 @@ export class SessionRuntimeTerminalDeliveryHub {
814
843
  target.update.stateHash,
815
844
  client.negotiated.encoding,
816
845
  client.negotiated.richPlacements ? "rich" : "plain",
846
+ projection
847
+ ? `projection:${client.projectionBaseline?.revision ?? -1}:${client.projectionBaseline?.hash ?? "none"}`
848
+ : "complete",
817
849
  ]);
818
850
  let job = this.#pendingEncodings.get(key);
819
851
  let created = false;
@@ -844,10 +876,13 @@ export class SessionRuntimeTerminalDeliveryHub {
844
876
  owned.clients.add(client);
845
877
  if (!created)
846
878
  return;
847
- void encodeCompactSemanticTerminalUpdateCooperatively(payload, {
848
- signal: owned.abort.signal,
849
- yieldControl: () => this.#yieldEncoding(owned),
850
- }).then((bytes) => this.#completeEncoding(owned, { bytes }), (error) => this.#completeEncoding(owned, { error }));
879
+ const options = { signal: owned.abort.signal, yieldControl: () => this.#yieldEncoding(owned) };
880
+ const result = projection
881
+ ? encodeTerminalProjectionCooperatively(payload, options)
882
+ : client.negotiated.encoding === "semantic-binary-v1"
883
+ ? encodeBinarySemanticTerminalUpdateCooperatively(payload, options)
884
+ : encodeCompactSemanticTerminalUpdateCooperatively(payload, options);
885
+ void result.then((bytes) => this.#completeEncoding(owned, { bytes }), (error) => this.#completeEncoding(owned, { error }));
851
886
  }
852
887
  #yieldEncoding(job) {
853
888
  if (this.#scheduler.nowMs() - job.yieldStartedAt < 2)
@@ -914,6 +949,27 @@ export class SessionRuntimeTerminalDeliveryHub {
914
949
  this.#pruneCanonicalRevisions(client.paneId);
915
950
  }
916
951
  #representation(client, pane, target, seedOutcome) {
952
+ const snapshotForProjection = target.state.snapshot;
953
+ if (client.negotiated.alternateScreenProjection &&
954
+ !client.materializeRequested &&
955
+ (client.baselineRevision < 0 || client.projectionBaseline !== null) &&
956
+ snapshotForProjection?.modes.alternateScreen &&
957
+ snapshotForProjection.history.length > 0) {
958
+ const { payload, retained } = prepareProjection(snapshotForProjection, target.update.revision, target.update.incarnation, client.reseedRequired ? null : client.projectionBaseline);
959
+ if (!seedOutcome)
960
+ throw new DeferredProjectionEncoding(payload);
961
+ if ("error" in seedOutcome)
962
+ throw seedOutcome.error;
963
+ return {
964
+ bytes: seedOutcome.bytes,
965
+ encoding: "alternate-projection-binary-v1",
966
+ frame: payload.frame,
967
+ history: "deferred",
968
+ canonicalEquivalent: false,
969
+ projection: retained,
970
+ projectionBaseRevision: payload.frame === "seed" ? null : payload.baseRevision,
971
+ };
972
+ }
917
973
  const key = cacheKey([
918
974
  target.update.workspaceName,
919
975
  target.update.semanticPaneId,
@@ -937,18 +993,24 @@ export class SessionRuntimeTerminalDeliveryHub {
937
993
  const snapshot = target.state.snapshot;
938
994
  let result = null;
939
995
  if (client.negotiated.encoding === "semantic-v1" ||
940
- client.negotiated.encoding === "semantic-compact-v1") {
996
+ client.negotiated.encoding === "semantic-compact-v1" ||
997
+ client.negotiated.encoding === "semantic-binary-v1") {
941
998
  const encodeSemantic = (payload) => {
942
- if (client.negotiated.encoding !== "semantic-compact-v1")
999
+ if (client.negotiated.encoding !== "semantic-compact-v1" &&
1000
+ client.negotiated.encoding !== "semantic-binary-v1")
943
1001
  return encodeSemanticTerminalUpdate(payload);
944
- if (terminalSemanticUpdateNeedsCooperativeEncoding(payload)) {
1002
+ if (payload.frame === "seed" &&
1003
+ (client.negotiated.encoding === "semantic-binary-v1" ||
1004
+ terminalSemanticUpdateNeedsCooperativeEncoding(payload))) {
945
1005
  if (!seedOutcome)
946
1006
  throw new DeferredSeedEncoding(payload);
947
1007
  if ("error" in seedOutcome)
948
1008
  throw seedOutcome.error;
949
1009
  return seedOutcome.bytes;
950
1010
  }
951
- return encodeCompactSemanticTerminalUpdate(payload);
1011
+ return client.negotiated.encoding === "semantic-binary-v1"
1012
+ ? encodeBinarySemanticTerminalUpdate(payload)
1013
+ : encodeCompactSemanticTerminalUpdate(payload);
952
1014
  };
953
1015
  if (client.reseedRequired) {
954
1016
  const payload = semanticSeed(target);
@@ -1213,6 +1275,10 @@ export class SessionRuntimeTerminalDeliveryHub {
1213
1275
  return;
1214
1276
  }
1215
1277
  const envelope = flight.envelope;
1278
+ if (envelope.type !== "terminal.delivery") {
1279
+ this.#fault(client, "protocol-violation", "Canonical ACK cannot acknowledge a projection");
1280
+ return;
1281
+ }
1216
1282
  if (flight.nextChunk !== envelope.chunkCount ||
1217
1283
  ack.workspaceName !== envelope.workspaceName ||
1218
1284
  ack.semanticPaneId !== envelope.semanticPaneId ||
@@ -1227,6 +1293,8 @@ export class SessionRuntimeTerminalDeliveryHub {
1227
1293
  return;
1228
1294
  }
1229
1295
  this.#maxSlowClientMs = Math.max(this.#maxSlowClientMs, this.#scheduler.nowMs() - flight.sentAt);
1296
+ client.projectionBaseline = null;
1297
+ client.lastProjectionReceipt = null;
1230
1298
  client.baselineRevision = ack.canonicalRevision;
1231
1299
  client.baselineHash = ack.canonicalStateHash;
1232
1300
  client.reseedRequired = false;
@@ -1277,6 +1345,68 @@ export class SessionRuntimeTerminalDeliveryHub {
1277
1345
  this.#schedule(client);
1278
1346
  this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
1279
1347
  }
1348
+ #projectionReceipt(client, input) {
1349
+ const receipt = TerminalProjectionReceiptSchemaZ.parse(input);
1350
+ if (client.lastProjectionReceipt &&
1351
+ JSON.stringify(client.lastProjectionReceipt) === JSON.stringify(receipt))
1352
+ return;
1353
+ const flight = client.inFlight;
1354
+ const envelope = flight?.envelope;
1355
+ if (!flight ||
1356
+ !flight.projection ||
1357
+ envelope?.type !== "terminal.projection" ||
1358
+ flight.nextChunk !== envelope.chunkCount ||
1359
+ receipt.workspaceName !== envelope.workspaceName ||
1360
+ receipt.semanticPaneId !== envelope.semanticPaneId ||
1361
+ receipt.generation !== envelope.generation ||
1362
+ receipt.incarnation !== envelope.incarnation ||
1363
+ receipt.deliveryNonce !== envelope.deliveryNonce ||
1364
+ receipt.transactionId !== envelope.transactionId ||
1365
+ receipt.canonicalRevision !== envelope.canonicalRevision ||
1366
+ receipt.canonicalStateHash !== envelope.canonicalStateHash ||
1367
+ receipt.projectionStateHash !== envelope.projectionStateHash ||
1368
+ receipt.representationHash !== envelope.representationHash) {
1369
+ this.#fault(client, "protocol-violation", "Projection receipt does not identify the in-flight projection");
1370
+ return;
1371
+ }
1372
+ client.projectionBaseline = flight.projection;
1373
+ client.lastProjectionReceipt = receipt;
1374
+ client.reseedRequired = false;
1375
+ client.inFlight = null;
1376
+ if (client.latestRevision === receipt.canonicalRevision)
1377
+ client.latestRevision = null;
1378
+ if (client.materializeRequested)
1379
+ client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
1380
+ this.#pruneCanonicalRevisions(client.paneId);
1381
+ this.#schedule(client);
1382
+ this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
1383
+ }
1384
+ #materialize(client, input) {
1385
+ const request = TerminalProjectionMaterializeSchemaZ.parse(input);
1386
+ if (client.lastMaterialize &&
1387
+ JSON.stringify(client.lastMaterialize) === JSON.stringify(request))
1388
+ return;
1389
+ const baseline = client.projectionBaseline;
1390
+ if (!client.negotiated.alternateScreenProjection ||
1391
+ !baseline ||
1392
+ request.workspaceName !== client.deliveryWorkspaceName ||
1393
+ request.semanticPaneId !== client.paneId ||
1394
+ request.generation !== client.negotiated.generation ||
1395
+ request.incarnation !== baseline.incarnation ||
1396
+ request.deliveryNonce !== client.negotiated.deliveryNonce ||
1397
+ request.canonicalRevision !== baseline.revision ||
1398
+ request.projectionStateHash !== baseline.hash) {
1399
+ this.#fault(client, "protocol-violation", "History request does not identify the retained projection");
1400
+ return;
1401
+ }
1402
+ client.lastMaterialize = request;
1403
+ client.materializeRequested = true;
1404
+ this.#cancelEncoding(client);
1405
+ client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
1406
+ // A queued projection still owns its transaction until its receipt arrives.
1407
+ // The following complete seed then supersedes it, including at the same revision.
1408
+ this.#schedule(client);
1409
+ }
1280
1410
  #nack(client, input) {
1281
1411
  const nack = TerminalDeliveryNackSchemaZ.parse(input);
1282
1412
  const closed = client.sourceClosedFlight;
@@ -0,0 +1,32 @@
1
+ import { hashTerminalProjection, projectAlternateSnapshot, terminalReplicaRowsEqual, } from "@tmux-ide/core";
2
+ export function prepareProjection(snapshot, revision, incarnation, baseline) {
3
+ const projected = projectAlternateSnapshot(snapshot);
4
+ const retained = {
5
+ revision,
6
+ incarnation,
7
+ hash: hashTerminalProjection(projected),
8
+ snapshot: projected,
9
+ };
10
+ if (!baseline ||
11
+ baseline.incarnation !== incarnation ||
12
+ baseline.snapshot.cols !== projected.cols ||
13
+ baseline.snapshot.rows !== projected.rows)
14
+ return { payload: { frame: "seed", revision, projection: projected }, retained };
15
+ const rows = projected.grid.flatMap((row, index) => terminalReplicaRowsEqual(row, baseline.snapshot.grid[index]) ? [] : [{ index, row }]);
16
+ return {
17
+ retained,
18
+ payload: {
19
+ frame: "patch",
20
+ baseRevision: baseline.revision,
21
+ revision,
22
+ patch: {
23
+ rows,
24
+ deferredHistory: projected.deferredHistory,
25
+ cursor: projected.cursor,
26
+ modes: projected.modes,
27
+ placements: projected.placements,
28
+ bootstrap: projected.bootstrap,
29
+ },
30
+ },
31
+ };
32
+ }
@@ -7,6 +7,9 @@ import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability
7
7
  import { createXtermTerminalInterpreterBackend } from "./xterm-terminal-interpreter-backend.js";
8
8
  import { CAUSAL_CELL_OSC, CausalCellLedger, cellsEqual, } from "./causal-cell-ledger.js";
9
9
  import { KITTY_PLACEMENT_OSC, KittyApcScanner, KittyImageStore, } from "../graphics/kitty-graphics.js";
10
+ // Bound parser work per turn without changing byte-stream semantics. This is a
11
+ // batch bound, not a producer queue cap: upstream pressure must preserve bytes.
12
+ const MAX_INGEST_BATCH_BYTES = 256 * 1024;
10
13
  /** Daemon-owned, sans-I/O VT interpreter with one FIFO for bytes and geometry. */
11
14
  export class TerminalReplicaInterpreter {
12
15
  #nativeSeedBackingCandidate;
@@ -35,8 +38,7 @@ export class TerminalReplicaInterpreter {
35
38
  #needsSeed = true;
36
39
  #closed = false;
37
40
  #walkCount = 0;
38
- #pendingWrites = [];
39
- #writeFlushScheduled = false;
41
+ #pendingWrites = null;
40
42
  #widgetGate = false;
41
43
  // kitty graphics: images outlive reseeds; the scanner owns split APCs.
42
44
  #kittyScanner = new KittyApcScanner();
@@ -106,16 +108,25 @@ export class TerminalReplicaInterpreter {
106
108
  if (operation.type === "write") {
107
109
  const data = operation.data.slice();
108
110
  const trace = operation.trace ?? null;
109
- const pendingTrace = this.#pendingWrites[0]?.trace ?? null;
110
- if (this.#pendingWrites.length > 0 &&
111
- (pendingTrace?.traceId ?? null) !== (trace?.traceId ?? null))
111
+ if (this.#pendingWrites &&
112
+ (this.#pendingWrites.trace?.traceId ?? null) !== (trace?.traceId ?? null))
112
113
  this.#flushWrites();
114
+ const group = (this.#pendingWrites ??= {
115
+ entries: [],
116
+ bytes: 0,
117
+ scheduled: false,
118
+ flushQueued: false,
119
+ trace,
120
+ });
113
121
  const promise = new Promise((resolve, reject) => {
114
- this.#pendingWrites.push({ data, trace, resolve, reject });
122
+ group.entries.push({ data, offset: 0, resolve, reject });
123
+ group.bytes += data.length;
115
124
  });
116
- if (!this.#writeFlushScheduled) {
117
- this.#writeFlushScheduled = true;
118
- this.#scheduler.microtask(() => this.#flushWrites());
125
+ // Leave this group open while the parser awaits its callback. Scheduling
126
+ // it once on the FIFO lets later arrivals join the next bounded batch.
127
+ if (!group.scheduled && !group.flushQueued) {
128
+ group.flushQueued = true;
129
+ this.#scheduler.microtask(() => this.#scheduleWriteGroup(group));
119
130
  }
120
131
  return promise;
121
132
  }
@@ -361,22 +372,26 @@ export class TerminalReplicaInterpreter {
361
372
  // serialize dozens of turns and fall behind a 30 fps image stream.
362
373
  if (scanned.forward.byteLength === 0)
363
374
  return;
364
- await this.#writeToBackend(this.#backend, scanned.forward);
375
+ // Scanning can prepend an escape retained from the previous chunk. Bound
376
+ // the actual parser calls too, and union damage across every fragment.
377
+ for (let offset = 0; offset < scanned.forward.length; offset += MAX_INGEST_BATCH_BYTES) {
378
+ await this.#writeToBackend(this.#backend, scanned.forward.subarray(offset, offset + MAX_INGEST_BATCH_BYTES));
379
+ // The parser resets its dirty tracker per write. Synchronized output and
380
+ // bounded fragments can both span writes before canonical publication.
381
+ const dirty = this.#backend.dirtyRange();
382
+ const pending = this.#pendingWriteDirty;
383
+ this.#pendingWriteDirty =
384
+ pending === null
385
+ ? dirty
386
+ : pending === undefined || dirty === undefined
387
+ ? undefined
388
+ : {
389
+ start: Math.min(pending.start, dirty.start),
390
+ end: Math.max(pending.end, dirty.end),
391
+ };
392
+ }
365
393
  if (this.#observability.enabled)
366
394
  this.#observability.recordSpan("parse", "terminal-replica-write", parseStarted, this.#observability.nowMicros(), trace);
367
- // The parser resets its dirty tracker per write. Synchronized output may
368
- // span many writes before publication, so retain their union until commit.
369
- const dirty = this.#backend.dirtyRange();
370
- const pending = this.#pendingWriteDirty;
371
- this.#pendingWriteDirty =
372
- pending === null
373
- ? dirty
374
- : pending === undefined || dirty === undefined
375
- ? undefined
376
- : {
377
- start: Math.min(pending.start, dirty.start),
378
- end: Math.max(pending.end, dirty.end),
379
- };
380
395
  // DEC synchronized-output is atomic: no intermediate frame leaks.
381
396
  if (this.#backend.requiresNativeReseed()) {
382
397
  this.#commit(false);
@@ -402,6 +417,7 @@ export class TerminalReplicaInterpreter {
402
417
  return;
403
418
  this.#syncRecovery = this.#scheduler.timer(() => {
404
419
  this.#syncRecovery = null;
420
+ this.#flushWrites();
405
421
  const run = this.#tail.then(async () => {
406
422
  if (this.#closed || !this.#backend.modes().synchronizedOutput)
407
423
  return;
@@ -661,22 +677,65 @@ export class TerminalReplicaInterpreter {
661
677
  }
662
678
  }
663
679
  }
680
+ /** Seal the current group at every trace, control and idle barrier. */
664
681
  #flushWrites() {
665
- this.#writeFlushScheduled = false;
666
- if (this.#pendingWrites.length === 0)
682
+ const group = this.#pendingWrites;
683
+ this.#pendingWrites = null;
684
+ if (group)
685
+ this.#scheduleWriteGroup(group);
686
+ }
687
+ #scheduleWriteGroup(group) {
688
+ if (group.scheduled)
667
689
  return;
668
- const pending = this.#pendingWrites.splice(0);
669
- const length = pending.reduce((total, entry) => total + entry.data.length, 0);
670
- const data = new Uint8Array(length);
671
- let offset = 0;
672
- for (const entry of pending) {
673
- data.set(entry.data, offset);
674
- offset += entry.data.length;
675
- }
676
- const trace = pending.find(({ trace }) => trace !== null)?.trace ?? null;
677
- const run = this.#tail.then(() => this.#write(data, trace));
690
+ group.scheduled = true;
691
+ const run = this.#tail.then(async () => {
692
+ try {
693
+ while (group.entries.length > 0) {
694
+ const length = Math.min(group.bytes, MAX_INGEST_BATCH_BYTES);
695
+ const data = new Uint8Array(length);
696
+ const completed = [];
697
+ let offset = 0;
698
+ for (const entry of group.entries) {
699
+ const take = Math.min(entry.data.length - entry.offset, length - offset);
700
+ data.set(entry.data.subarray(entry.offset, entry.offset + take), offset);
701
+ offset += take;
702
+ entry.offset += take;
703
+ if (entry.offset === entry.data.length)
704
+ completed.push(entry);
705
+ else
706
+ break;
707
+ if (offset === length && length > 0)
708
+ break;
709
+ }
710
+ group.entries.splice(0, completed.length);
711
+ group.bytes -= length;
712
+ try {
713
+ await this.#write(data, group.trace);
714
+ }
715
+ catch (error) {
716
+ for (const entry of completed)
717
+ entry.reject(error);
718
+ throw error;
719
+ }
720
+ for (const entry of completed)
721
+ entry.resolve();
722
+ }
723
+ }
724
+ catch (error) {
725
+ // Reject every remaining fragment in the failed group; later FIFO
726
+ // controls (including recovery reseeds and close) still execute.
727
+ for (const entry of group.entries)
728
+ entry.reject(error);
729
+ group.entries.length = 0;
730
+ group.bytes = 0;
731
+ throw error;
732
+ }
733
+ finally {
734
+ if (this.#pendingWrites === group)
735
+ this.#pendingWrites = null;
736
+ }
737
+ });
678
738
  this.#tail = run.catch(() => undefined);
679
- void run.then(() => pending.forEach((entry) => entry.resolve()), (error) => pending.forEach((entry) => entry.reject(error)));
680
739
  }
681
740
  #observeMarkerBytes(data) {
682
741
  if (this.#widgetGate)
@@ -155,6 +155,11 @@ export class SessionRuntimeTerminalReplicaOwner {
155
155
  inputDispatchIncarnation() {
156
156
  return this.#disposed ? null : this.#publishedIncarnation;
157
157
  }
158
+ /** Screen recapture does not replace the terminal lifetime. Keep its input fence
159
+ * while resizing; disposal or a new owner still invalidates held input. */
160
+ inputAdmissionIncarnation() {
161
+ return this.#disposed ? null : this.#publishedIncarnation;
162
+ }
158
163
  qualificationSnapshot() {
159
164
  const seed = this.#interpreter.currentSeed();
160
165
  return Object.freeze({
@@ -275,7 +275,7 @@ export class SessionRuntimeTransportBinding {
275
275
  authorize();
276
276
  return authorize;
277
277
  }
278
- sendInput(semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult) {
278
+ sendInput(semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult, expectedLifetime) {
279
279
  this.assertController(semanticPaneId);
280
280
  const lease = this.#shared.lease;
281
281
  if (!lease) {
@@ -289,7 +289,7 @@ export class SessionRuntimeTransportBinding {
289
289
  this.#causalCellProbes.delete(causalProbe.traceId);
290
290
  onCausalResult?.(result);
291
291
  }
292
- : onCausalResult);
292
+ : onCausalResult, expectedLifetime);
293
293
  }
294
294
  catch (error) {
295
295
  if (causalProbe)
@@ -335,6 +335,10 @@ class WireTerminalEndpoint {
335
335
  }
336
336
  if (this.#unavailable)
337
337
  return;
338
+ if (message.type === "terminal.projection") {
339
+ this.#failConnection(new Error("Unsolicited alternate-screen projection"));
340
+ return;
341
+ }
338
342
  if (message.type === "terminal.delivery") {
339
343
  this.#acceptEnvelope(message);
340
344
  return;
@@ -67,8 +67,23 @@ export function createApplicationAgentNavigator(options) {
67
67
  return async (sessionName, paneId, source = "mouse") => {
68
68
  const token = ++navigationToken;
69
69
  const currentOwner = options.sessionOwner();
70
+ const snapshot = currentOwner.snapshot();
71
+ // A renderer rebind keeps its semantic control connection. Reserve this
72
+ // exact target synchronously; the controller fences and bounds first send.
73
+ const retainedControl = (() => {
74
+ try {
75
+ return (snapshot?.status === "rebinding" &&
76
+ snapshot.connection?.workspaceName === sessionName &&
77
+ snapshot.daemonGeneration &&
78
+ snapshot.client &&
79
+ Number.isSafeInteger(snapshot.client.getSnapshot().generation));
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ })();
70
85
  if (currentOwner.sessionName() === sessionName &&
71
- applicationGenerationNavigationKey(currentOwner.snapshot()) !== null) {
86
+ (applicationGenerationNavigationKey(snapshot) !== null || retainedControl)) {
72
87
  // Sidebar agents belong to the already-live workspace. Select their pane
73
88
  // before returning control to OpenTUI so the very next key cannot leak to
74
89
  // the pane that was focused before the click.
@@ -1,3 +1,4 @@
1
+ import { retainsAgentControl } from "./application-retained-agent.js";
1
2
  import { createApplicationHomeAgentNavigator, } from "./application-home-agent-navigation.js";
2
3
  import { applicationGenerationNavigationKey } from "./application-generation-starter.js";
3
4
  import { waitForHomeAgentSemantic } from "./application-home-agents-owner.js";
@@ -16,6 +17,18 @@ export function createApplicationMachineAgentNavigator(options) {
16
17
  opening = true;
17
18
  const selected = () => options.selectedMachineId() === machineId;
18
19
  const generation = () => selected() && options.generationMachineId() === machineId ? options.generation() : null;
20
+ if (selected() &&
21
+ options.isCurrentTarget(machineId, target) &&
22
+ options.sessionName() === target.sessionName &&
23
+ retainsAgentControl(generation(), target)) {
24
+ // Reserve before the next key; the terminal controller waits for a fresh
25
+ // coherent runtime and remains the only owner of physical input.
26
+ options.selectPane(target.paneId, source);
27
+ options.showTerminals(source);
28
+ options.setNote(null);
29
+ opening = false;
30
+ return Promise.resolve({ opened: true });
31
+ }
19
32
  current = createApplicationHomeAgentNavigator({
20
33
  isCurrentTarget: (row) => selected() && options.isCurrentTarget(machineId, row),
21
34
  currentGeneration() {
@@ -1,3 +1,4 @@
1
+ import { retainsAgentControl } from "./application-retained-agent.js";
1
2
  import { fleetHostColor } from "./fleet-presentation.js";
2
3
  import { createFleetTabs } from "./application-fleet-tabs.js";
3
4
  import { saveMachineProfiles } from "../../../lib/local-fleet-request.js";
@@ -64,7 +65,7 @@ export function createApplicationMachineNavigation(options) {
64
65
  const [tabRevision, setTabRevision] = createSignal(0);
65
66
  const history = [...saved().recent].reverse();
66
67
  let historyIndex = history.length - 1;
67
- const select = (id, expectedLiveSessionId, server) => {
68
+ const select = (id, expectedLiveSessionId, server, retainedAgent) => {
68
69
  if (!manager.getMachine(id))
69
70
  return false;
70
71
  if (manager.snapshot().selectedMachineId !== id) {
@@ -78,8 +79,15 @@ export function createApplicationMachineNavigation(options) {
78
79
  manager.select(id);
79
80
  }
80
81
  else if (expectedLiveSessionId) {
81
- const attached = options.attachedTarget?.() ??
82
- attachedMachineNavigationTarget(options.attachedGeneration?.() ?? null);
82
+ const active = options.attachedGeneration?.() ?? null;
83
+ const retained = retainedAgent && retainsAgentControl(active, retainedAgent)
84
+ ? {
85
+ liveSessionId: active.connection.liveSessionId,
86
+ daemonGeneration: active.daemonGeneration,
87
+ ...(active.connection.server ? { server: active.connection.server } : {}),
88
+ }
89
+ : null;
90
+ const attached = options.attachedTarget?.() ?? attachedMachineNavigationTarget(active) ?? retained;
83
91
  const sameAuthority = attached?.liveSessionId === expectedLiveSessionId &&
84
92
  (server
85
93
  ? attached.server
@@ -302,7 +310,7 @@ export function createApplicationMachineNavigation(options) {
302
310
  return;
303
311
  }
304
312
  options.cancelOpen();
305
- if (!select(id, selectedSession?.liveSessionId, selectedSession?.server))
313
+ if (!select(id, selectedSession?.liveSessionId, selectedSession?.server, row))
306
314
  return;
307
315
  if (selectedSession)
308
316
  setActiveSessionKey(selectedSession.id);
@@ -0,0 +1,18 @@
1
+ /** Selection intent may wait on this exact retained control; input cannot use it yet. */
2
+ export function retainsAgentControl(active, target) {
3
+ if (active?.status !== "rebinding" ||
4
+ !active.client ||
5
+ !target.paneId ||
6
+ active.daemonGeneration !== target.daemonInstanceId ||
7
+ active.connection?.workspaceName !== target.sessionName ||
8
+ active.connection.liveSessionId !== target.liveSessionId)
9
+ return false;
10
+ try {
11
+ const state = active.client.getSnapshot();
12
+ return (Number.isSafeInteger(state.generation) &&
13
+ state.semantic?.sidebar.agents.some((agent) => agent.id === target.agentId && agent.paneId === target.paneId) === true);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }