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
@@ -30,6 +30,7 @@ import {
30
30
  type CanonicalTerminalReplicaUpdate,
31
31
  type CausalCellFailureReasonV1,
32
32
  type CausalCellProbeV1,
33
+ type PaneStreamInputLifetime,
33
34
  type TerminalDeliveryAck,
34
35
  type TerminalDeliveryNack,
35
36
  type TerminalDeliveryOffer,
@@ -244,6 +245,7 @@ export interface SessionRuntimeConsumer {
244
245
  performanceTraceId?: string,
245
246
  causalProbe?: CausalCellProbeV1,
246
247
  onCausalResult?: (result: CausalCellLedgerResult) => void,
248
+ expectedLifetime?: PaneStreamInputLifetime,
247
249
  ): void;
248
250
  failCausalCellProbe(
249
251
  semanticPaneId: string,
@@ -896,13 +898,16 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
896
898
  return result;
897
899
  }
898
900
 
899
- async describeSessionAuthority(session: string): Promise<{
901
+ async describeSessionAuthority(
902
+ session: string,
903
+ expectedSemanticPaneIds?: readonly string[],
904
+ ): Promise<{
900
905
  readonly description: MirrorSessionDescription;
901
906
  readonly runtimeSessionId: string;
902
907
  }> {
903
908
  const runtime = this.#runtime(session);
904
909
  await runtime.whenReady();
905
- const authority = await this.#mirror.describeSessionAuthority(session);
910
+ const authority = await this.#mirror.describeSessionAuthority(session, expectedSemanticPaneIds);
906
911
  if (this.#disposed || this.#sessions.get(session) !== runtime) {
907
912
  const error = new Error(`session runtime ${session} changed topology`);
908
913
  error.name = "MirrorTopologyChangedError";
@@ -1589,6 +1594,7 @@ class SessionRuntime {
1589
1594
  performanceTraceId?: string,
1590
1595
  rawCausalProbe?: CausalCellProbeV1,
1591
1596
  onCausalResult?: (result: CausalCellLedgerResult) => void,
1597
+ expectedLifetime?: PaneStreamInputLifetime,
1592
1598
  ): void {
1593
1599
  this.assertController(lease, clientId);
1594
1600
  const inputLease = this.#authority.leaseFor(clientId, "input");
@@ -1626,7 +1632,10 @@ class SessionRuntime {
1626
1632
  })
1627
1633
  : null;
1628
1634
  const started = this.#observability.enabled ? this.#observability.nowMicros() : 0;
1629
- const replicaOwner = this.#terminalReplicaOwner(semanticPaneId);
1635
+ const replicaOwner = expectedLifetime
1636
+ ? this.#terminalReplicas.get(semanticPaneId)
1637
+ : this.#terminalReplicaOwner(semanticPaneId);
1638
+ if (!replicaOwner) throw new Error("Input target lifetime unavailable");
1630
1639
  if (causalProbe) {
1631
1640
  this.#activeCausalCellProbes += 1;
1632
1641
  let settled = false;
@@ -1649,6 +1658,15 @@ class SessionRuntime {
1649
1658
  this.#outputTraces.arm(semanticPaneId, trace);
1650
1659
  }
1651
1660
  try {
1661
+ // Synchronous final admission: never create a new owner to satisfy a stale
1662
+ // held-input fence, and never await between this check and mirror dispatch.
1663
+ if (
1664
+ expectedLifetime &&
1665
+ (expectedLifetime.generation !== this.generation ||
1666
+ this.#terminalReplicas.get(semanticPaneId) !== replicaOwner ||
1667
+ replicaOwner.inputAdmissionIncarnation() !== expectedLifetime.incarnation)
1668
+ )
1669
+ throw new Error("Input target lifetime changed");
1652
1670
  if (input.kind === "text")
1653
1671
  this.#mirror.sendText(
1654
1672
  this.session,
@@ -2323,6 +2341,7 @@ class SessionRuntimeConsumerImpl implements SessionRuntimeConsumer {
2323
2341
  performanceTraceId?: string,
2324
2342
  causalProbe?: CausalCellProbeV1,
2325
2343
  onCausalResult?: (result: CausalCellLedgerResult) => void,
2344
+ expectedLifetime?: PaneStreamInputLifetime,
2326
2345
  ): void {
2327
2346
  this.#assertOpen();
2328
2347
  this.#runtime.sendInput(
@@ -2333,6 +2352,7 @@ class SessionRuntimeConsumerImpl implements SessionRuntimeConsumer {
2333
2352
  performanceTraceId,
2334
2353
  causalProbe,
2335
2354
  onCausalResult,
2355
+ expectedLifetime,
2336
2356
  );
2337
2357
  }
2338
2358
 
@@ -2452,6 +2472,8 @@ class SessionRuntimeConsumerImpl implements SessionRuntimeConsumer {
2452
2472
  negotiation: upstream.negotiation,
2453
2473
  closed: upstream.closed,
2454
2474
  ack: (ack: TerminalDeliveryAck) => upstream.ack(ack),
2475
+ ...(upstream.projectionReceipt ? { projectionReceipt: upstream.projectionReceipt } : {}),
2476
+ ...(upstream.materialize ? { materialize: upstream.materialize } : {}),
2455
2477
  nack: (nack: TerminalDeliveryNack) => upstream.nack(nack),
2456
2478
  setVisibility: (visibility: TerminalDeliveryVisibility) => upstream.setVisibility(visibility),
2457
2479
  close: async () => {
@@ -81,7 +81,7 @@ export interface SessionRuntimeStageSpan {
81
81
  readonly attemptedLegacySeedSizeCapped?: boolean;
82
82
  readonly attemptedCompactPatchBytes?: number | null;
83
83
  readonly attemptedCompactSeedBytes?: number | null;
84
- readonly selectedEncoding?: "semantic-v1" | "semantic-compact-v1";
84
+ readonly selectedEncoding?: "semantic-v1" | "semantic-compact-v1" | "semantic-binary-v1";
85
85
  readonly selectionStatus?:
86
86
  | "patch-preferred"
87
87
  | "seed-preferred"
@@ -1,3 +1,15 @@
1
+ import { prepareProjection, type ProjectionBaseline } from "./terminal-projection-delivery.ts";
2
+ import {
3
+ TerminalProjectionEnvelopeSchemaZ,
4
+ TerminalProjectionReceiptSchemaZ,
5
+ TerminalProjectionMaterializeSchemaZ,
6
+ type TerminalProjectionEnvelope,
7
+ type TerminalProjectionReceipt,
8
+ type TerminalProjectionMaterialize,
9
+ type TerminalProjectionPayload,
10
+ } from "@tmux-ide/contracts";
11
+ import { encodeTerminalProjectionCooperatively } from "@tmux-ide/core";
12
+ type DeliveryEnvelope = TerminalDeliveryEnvelope | TerminalProjectionEnvelope;
1
13
  import type { KittyImage } from "../graphics/kitty-graphics.ts";
2
14
  import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.ts";
3
15
  import {
@@ -37,6 +49,8 @@ import {
37
49
  applyTerminalReplicaUpdate,
38
50
  applyTerminalReplicaUpdateCooperatively,
39
51
  terminalReplicaUpdateNeedsCooperativeReduction,
52
+ encodeBinarySemanticTerminalUpdate,
53
+ encodeBinarySemanticTerminalUpdateCooperatively,
40
54
  encodeCompactSemanticTerminalUpdate,
41
55
  encodeCompactSemanticTerminalUpdateCooperatively,
42
56
  terminalSemanticUpdateNeedsCooperativeEncoding,
@@ -124,6 +138,8 @@ export interface TerminalDeliveryConnection {
124
138
  readonly closed: Promise<"closed" | "sink-failed">;
125
139
  readonly negotiation: TerminalDeliveryNegotiationResult;
126
140
  ack(ack: TerminalDeliveryAck): void;
141
+ projectionReceipt?(receipt: TerminalProjectionReceipt): void;
142
+ materialize?(request: TerminalProjectionMaterialize): void;
127
143
  nack(nack: TerminalDeliveryNack): void;
128
144
  setVisibility(visibility: TerminalDeliveryVisibility): void;
129
145
  close(): Promise<void>;
@@ -205,9 +221,14 @@ interface ClientState {
205
221
  visibility: TerminalDeliveryVisibility;
206
222
  baselineRevision: number;
207
223
  baselineHash: string | null;
224
+ projectionBaseline: ProjectionBaseline | null;
225
+ materializeRequested: boolean;
226
+ lastProjectionReceipt: TerminalProjectionReceipt | null;
227
+ lastMaterialize: TerminalProjectionMaterialize | null;
208
228
  reseedRequired: boolean;
209
229
  inFlight: {
210
- envelope: TerminalDeliveryEnvelope;
230
+ envelope: DeliveryEnvelope;
231
+ projection: ProjectionBaseline | null;
211
232
  deliveryOrdinal: number;
212
233
  bytes: Uint8Array;
213
234
  nextChunk: number;
@@ -226,7 +247,7 @@ interface ClientState {
226
247
  captureUnavailable: boolean;
227
248
  lastAck: TerminalDeliveryAck | null;
228
249
  /** Delivery displaced by authoritative source close; its racing ACK is benign. */
229
- sourceClosedFlight: TerminalDeliveryEnvelope | null;
250
+ sourceClosedFlight: DeliveryEnvelope | null;
230
251
  backgroundTimer: SessionRuntimeTimer | null;
231
252
  }
232
253
 
@@ -246,6 +267,11 @@ interface PendingClientEncoding {
246
267
  readonly reseedRequired: boolean;
247
268
  readonly traceStarted: number;
248
269
  }
270
+ class DeferredProjectionEncoding extends Error {
271
+ constructor(readonly payload: TerminalProjectionPayload) {
272
+ super("Deferred projection encoding");
273
+ }
274
+ }
249
275
  class DeferredSeedEncoding extends Error {
250
276
  constructor(readonly payload: TerminalSemanticDeliveryPayload) {
251
277
  super("Deferred compact seed encoding");
@@ -254,10 +280,16 @@ class DeferredSeedEncoding extends Error {
254
280
 
255
281
  interface CachedRepresentation {
256
282
  readonly bytes: Uint8Array;
257
- readonly encoding?: "semantic-v1" | "semantic-compact-v1";
283
+ readonly encoding?:
284
+ | "semantic-v1"
285
+ | "semantic-compact-v1"
286
+ | "semantic-binary-v1"
287
+ | "alternate-projection-binary-v1";
288
+ readonly projection?: ProjectionBaseline;
289
+ readonly projectionBaseRevision?: number | null;
258
290
  readonly frame: "seed" | "patch" | "tombstone";
259
291
  readonly canonicalEquivalent: boolean;
260
- readonly history: "complete" | "truncated" | "not-applicable";
292
+ readonly history: "complete" | "truncated" | "not-applicable" | "deferred";
261
293
  readonly representationHash?: string;
262
294
  /** Internal reachability metadata; never serialized onto the wire. */
263
295
  readonly cachePaneId?: string;
@@ -274,7 +306,7 @@ interface CachedRepresentation {
274
306
  attemptedLegacySeedSizeCapped: boolean;
275
307
  attemptedCompactPatchBytes: number | null;
276
308
  attemptedCompactSeedBytes: number | null;
277
- selectedEncoding: "semantic-v1" | "semantic-compact-v1";
309
+ selectedEncoding: "semantic-v1" | "semantic-compact-v1" | "semantic-binary-v1";
278
310
  selectionStatus:
279
311
  | "patch-preferred"
280
312
  | "seed-preferred"
@@ -432,6 +464,10 @@ export class SessionRuntimeTerminalDeliveryHub {
432
464
  visibility: "visible",
433
465
  baselineRevision: -1,
434
466
  baselineHash: null,
467
+ projectionBaseline: null,
468
+ materializeRequested: false,
469
+ lastProjectionReceipt: null,
470
+ lastMaterialize: null,
435
471
  reseedRequired: false,
436
472
  inFlight: null,
437
473
  latestRevision: pane.latest?.update.revision ?? null,
@@ -456,6 +492,8 @@ export class SessionRuntimeTerminalDeliveryHub {
456
492
  negotiation,
457
493
  closed,
458
494
  ack: (ack) => this.#ack(client, ack),
495
+ projectionReceipt: (receipt) => this.#projectionReceipt(client, receipt),
496
+ materialize: (request) => this.#materialize(client, request),
459
497
  nack: (nack) => this.#nack(client, nack),
460
498
  setVisibility: (visibilityInput) => {
461
499
  if (client.closed) return;
@@ -956,7 +994,10 @@ export class SessionRuntimeTerminalDeliveryHub {
956
994
  client.lifecycleOpenRecorded = this.#recordDeliveryLifecycle(client, pane, "open");
957
995
  if (
958
996
  update.type === "terminal.tombstone" &&
959
- (client.inFlight !== null || client.encoding !== null || client.visibility !== "visible")
997
+ (client.inFlight !== null ||
998
+ client.encoding !== null ||
999
+ client.visibility !== "visible" ||
1000
+ client.projectionBaseline !== null)
960
1001
  ) {
961
1002
  this.#fault(client, "source-closed", "Terminal source closed before final state delivery");
962
1003
  continue;
@@ -1079,15 +1120,22 @@ export class SessionRuntimeTerminalDeliveryHub {
1079
1120
  let deferred = false;
1080
1121
  let encodedRepresentation: CachedRepresentation | null = null;
1081
1122
  let failedSelectionObservation: SemanticSelectionObservation | null = null;
1082
- let deliveryEnvelope: TerminalDeliveryEnvelope | null = null;
1123
+ let deliveryEnvelope: DeliveryEnvelope | null = null;
1083
1124
  let deliveryOrdinal: number | null = null;
1084
1125
  try {
1085
1126
  const representation = this.#representation(client, pane, target, completed?.outcome);
1086
1127
  encodedRepresentation = representation;
1087
1128
  const transactionId = this.#scheduler.createId();
1088
1129
  const chunkCount = Math.max(1, Math.ceil(representation.bytes.byteLength / (256 * 1024)));
1089
- const envelope = TerminalDeliveryEnvelopeSchemaZ.parse({
1090
- type: "terminal.delivery",
1130
+ const envelope = (
1131
+ representation.projection
1132
+ ? TerminalProjectionEnvelopeSchemaZ
1133
+ : TerminalDeliveryEnvelopeSchemaZ
1134
+ ).parse({
1135
+ type: representation.projection ? "terminal.projection" : "terminal.delivery",
1136
+ ...(representation.projection
1137
+ ? { projectionStateHash: representation.projection.hash }
1138
+ : {}),
1091
1139
  workspaceName: client.deliveryWorkspaceName,
1092
1140
  semanticPaneId: target.update.semanticPaneId,
1093
1141
  generation: target.update.generation,
@@ -1098,7 +1146,11 @@ export class SessionRuntimeTerminalDeliveryHub {
1098
1146
  protocolVersion: 1,
1099
1147
  encoding: representation.encoding ?? client.negotiated.encoding,
1100
1148
  frame: representation.frame,
1101
- baseRevision: representation.frame === "seed" ? null : Math.max(0, client.baselineRevision),
1149
+ baseRevision: representation.projection
1150
+ ? representation.projectionBaseRevision
1151
+ : representation.frame === "seed"
1152
+ ? null
1153
+ : Math.max(0, client.baselineRevision),
1102
1154
  canonicalRevision: target.update.revision,
1103
1155
  canonicalStateHash: target.update.stateHash,
1104
1156
  representationHash:
@@ -1115,6 +1167,7 @@ export class SessionRuntimeTerminalDeliveryHub {
1115
1167
  registerTerminalDeliveryObservationOrdinal(envelope, deliveryOrdinal);
1116
1168
  client.inFlight = {
1117
1169
  envelope,
1170
+ projection: representation.projection ?? null,
1118
1171
  deliveryOrdinal,
1119
1172
  bytes: representation.bytes,
1120
1173
  nextChunk: 0,
@@ -1125,9 +1178,16 @@ export class SessionRuntimeTerminalDeliveryHub {
1125
1178
  if (target.trace?.traceId === pane.pendingDeliveryTrace?.traceId)
1126
1179
  pane.pendingDeliveryTrace = null;
1127
1180
  } catch (error) {
1128
- if (error instanceof DeferredSeedEncoding) {
1181
+ if (error instanceof DeferredSeedEncoding || error instanceof DeferredProjectionEncoding) {
1129
1182
  deferred = true;
1130
- this.#startEncoding(client, pane, target, error.payload, traceStarted);
1183
+ this.#startEncoding(
1184
+ client,
1185
+ pane,
1186
+ target,
1187
+ error.payload,
1188
+ traceStarted,
1189
+ error instanceof DeferredProjectionEncoding,
1190
+ );
1131
1191
  return;
1132
1192
  }
1133
1193
  failedSelectionObservation = semanticSelectionObservationFromError(error);
@@ -1205,8 +1265,9 @@ export class SessionRuntimeTerminalDeliveryHub {
1205
1265
  client: ClientState,
1206
1266
  pane: PaneState,
1207
1267
  target: RevisionRecord,
1208
- payload: TerminalSemanticDeliveryPayload,
1268
+ payload: TerminalSemanticDeliveryPayload | TerminalProjectionPayload,
1209
1269
  traceStarted: number,
1270
+ projection = false,
1210
1271
  ): void {
1211
1272
  const key = cacheKey([
1212
1273
  target.update.workspaceName,
@@ -1217,6 +1278,9 @@ export class SessionRuntimeTerminalDeliveryHub {
1217
1278
  target.update.stateHash,
1218
1279
  client.negotiated.encoding,
1219
1280
  client.negotiated.richPlacements ? "rich" : "plain",
1281
+ projection
1282
+ ? `projection:${client.projectionBaseline?.revision ?? -1}:${client.projectionBaseline?.hash ?? "none"}`
1283
+ : "complete",
1220
1284
  ]);
1221
1285
  let job = this.#pendingEncodings.get(key);
1222
1286
  let created = false;
@@ -1250,10 +1314,19 @@ export class SessionRuntimeTerminalDeliveryHub {
1250
1314
  };
1251
1315
  owned.clients.add(client);
1252
1316
  if (!created) return;
1253
- void encodeCompactSemanticTerminalUpdateCooperatively(payload, {
1254
- signal: owned.abort.signal,
1255
- yieldControl: () => this.#yieldEncoding(owned),
1256
- }).then(
1317
+ const options = { signal: owned.abort.signal, yieldControl: () => this.#yieldEncoding(owned) };
1318
+ const result = projection
1319
+ ? encodeTerminalProjectionCooperatively(payload as TerminalProjectionPayload, options)
1320
+ : client.negotiated.encoding === "semantic-binary-v1"
1321
+ ? encodeBinarySemanticTerminalUpdateCooperatively(
1322
+ payload as TerminalSemanticDeliveryPayload,
1323
+ options,
1324
+ )
1325
+ : encodeCompactSemanticTerminalUpdateCooperatively(
1326
+ payload as TerminalSemanticDeliveryPayload,
1327
+ options,
1328
+ );
1329
+ void result.then(
1257
1330
  (bytes) => this.#completeEncoding(owned, { bytes }),
1258
1331
  (error) => this.#completeEncoding(owned, { error }),
1259
1332
  );
@@ -1328,6 +1401,32 @@ export class SessionRuntimeTerminalDeliveryHub {
1328
1401
  target: RevisionRecord,
1329
1402
  seedOutcome?: SeedEncodingOutcome,
1330
1403
  ): CachedRepresentation {
1404
+ const snapshotForProjection = target.state.snapshot;
1405
+ if (
1406
+ client.negotiated.alternateScreenProjection &&
1407
+ !client.materializeRequested &&
1408
+ (client.baselineRevision < 0 || client.projectionBaseline !== null) &&
1409
+ snapshotForProjection?.modes.alternateScreen &&
1410
+ snapshotForProjection.history.length > 0
1411
+ ) {
1412
+ const { payload, retained } = prepareProjection(
1413
+ snapshotForProjection,
1414
+ target.update.revision,
1415
+ target.update.incarnation,
1416
+ client.reseedRequired ? null : client.projectionBaseline,
1417
+ );
1418
+ if (!seedOutcome) throw new DeferredProjectionEncoding(payload);
1419
+ if ("error" in seedOutcome) throw seedOutcome.error;
1420
+ return {
1421
+ bytes: seedOutcome.bytes,
1422
+ encoding: "alternate-projection-binary-v1",
1423
+ frame: payload.frame,
1424
+ history: "deferred",
1425
+ canonicalEquivalent: false,
1426
+ projection: retained,
1427
+ projectionBaseRevision: payload.frame === "seed" ? null : payload.baseRevision,
1428
+ };
1429
+ }
1331
1430
  const key = cacheKey([
1332
1431
  target.update.workspaceName,
1333
1432
  target.update.semanticPaneId,
@@ -1351,17 +1450,27 @@ export class SessionRuntimeTerminalDeliveryHub {
1351
1450
  let result: CachedRepresentation | null = null;
1352
1451
  if (
1353
1452
  client.negotiated.encoding === "semantic-v1" ||
1354
- client.negotiated.encoding === "semantic-compact-v1"
1453
+ client.negotiated.encoding === "semantic-compact-v1" ||
1454
+ client.negotiated.encoding === "semantic-binary-v1"
1355
1455
  ) {
1356
1456
  const encodeSemantic = (payload: TerminalSemanticDeliveryPayload): Uint8Array => {
1357
- if (client.negotiated.encoding !== "semantic-compact-v1")
1457
+ if (
1458
+ client.negotiated.encoding !== "semantic-compact-v1" &&
1459
+ client.negotiated.encoding !== "semantic-binary-v1"
1460
+ )
1358
1461
  return encodeSemanticTerminalUpdate(payload);
1359
- if (terminalSemanticUpdateNeedsCooperativeEncoding(payload)) {
1462
+ if (
1463
+ payload.frame === "seed" &&
1464
+ (client.negotiated.encoding === "semantic-binary-v1" ||
1465
+ terminalSemanticUpdateNeedsCooperativeEncoding(payload))
1466
+ ) {
1360
1467
  if (!seedOutcome) throw new DeferredSeedEncoding(payload);
1361
1468
  if ("error" in seedOutcome) throw seedOutcome.error;
1362
1469
  return seedOutcome.bytes;
1363
1470
  }
1364
- return encodeCompactSemanticTerminalUpdate(payload);
1471
+ return client.negotiated.encoding === "semantic-binary-v1"
1472
+ ? encodeBinarySemanticTerminalUpdate(payload)
1473
+ : encodeCompactSemanticTerminalUpdate(payload);
1365
1474
  };
1366
1475
  if (client.reseedRequired) {
1367
1476
  const payload = semanticSeed(target);
@@ -1693,6 +1802,10 @@ export class SessionRuntimeTerminalDeliveryHub {
1693
1802
  return;
1694
1803
  }
1695
1804
  const envelope = flight.envelope;
1805
+ if (envelope.type !== "terminal.delivery") {
1806
+ this.#fault(client, "protocol-violation", "Canonical ACK cannot acknowledge a projection");
1807
+ return;
1808
+ }
1696
1809
  if (
1697
1810
  flight.nextChunk !== envelope.chunkCount ||
1698
1811
  ack.workspaceName !== envelope.workspaceName ||
@@ -1712,6 +1825,8 @@ export class SessionRuntimeTerminalDeliveryHub {
1712
1825
  this.#maxSlowClientMs,
1713
1826
  this.#scheduler.nowMs() - flight.sentAt,
1714
1827
  );
1828
+ client.projectionBaseline = null;
1829
+ client.lastProjectionReceipt = null;
1715
1830
  client.baselineRevision = ack.canonicalRevision;
1716
1831
  client.baselineHash = ack.canonicalStateHash;
1717
1832
  client.reseedRequired = false;
@@ -1769,6 +1884,85 @@ export class SessionRuntimeTerminalDeliveryHub {
1769
1884
  this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
1770
1885
  }
1771
1886
 
1887
+ #projectionReceipt(client: ClientState, input: TerminalProjectionReceipt): void {
1888
+ const receipt = TerminalProjectionReceiptSchemaZ.parse(input);
1889
+ if (
1890
+ client.lastProjectionReceipt &&
1891
+ JSON.stringify(client.lastProjectionReceipt) === JSON.stringify(receipt)
1892
+ )
1893
+ return;
1894
+ const flight = client.inFlight;
1895
+ const envelope = flight?.envelope;
1896
+ if (
1897
+ !flight ||
1898
+ !flight.projection ||
1899
+ envelope?.type !== "terminal.projection" ||
1900
+ flight.nextChunk !== envelope.chunkCount ||
1901
+ receipt.workspaceName !== envelope.workspaceName ||
1902
+ receipt.semanticPaneId !== envelope.semanticPaneId ||
1903
+ receipt.generation !== envelope.generation ||
1904
+ receipt.incarnation !== envelope.incarnation ||
1905
+ receipt.deliveryNonce !== envelope.deliveryNonce ||
1906
+ receipt.transactionId !== envelope.transactionId ||
1907
+ receipt.canonicalRevision !== envelope.canonicalRevision ||
1908
+ receipt.canonicalStateHash !== envelope.canonicalStateHash ||
1909
+ receipt.projectionStateHash !== envelope.projectionStateHash ||
1910
+ receipt.representationHash !== envelope.representationHash
1911
+ ) {
1912
+ this.#fault(
1913
+ client,
1914
+ "protocol-violation",
1915
+ "Projection receipt does not identify the in-flight projection",
1916
+ );
1917
+ return;
1918
+ }
1919
+ client.projectionBaseline = flight.projection;
1920
+ client.lastProjectionReceipt = receipt;
1921
+ client.reseedRequired = false;
1922
+ client.inFlight = null;
1923
+ if (client.latestRevision === receipt.canonicalRevision) client.latestRevision = null;
1924
+ if (client.materializeRequested)
1925
+ client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
1926
+ this.#pruneCanonicalRevisions(client.paneId);
1927
+ this.#schedule(client);
1928
+ this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
1929
+ }
1930
+
1931
+ #materialize(client: ClientState, input: TerminalProjectionMaterialize): void {
1932
+ const request = TerminalProjectionMaterializeSchemaZ.parse(input);
1933
+ if (
1934
+ client.lastMaterialize &&
1935
+ JSON.stringify(client.lastMaterialize) === JSON.stringify(request)
1936
+ )
1937
+ return;
1938
+ const baseline = client.projectionBaseline;
1939
+ if (
1940
+ !client.negotiated.alternateScreenProjection ||
1941
+ !baseline ||
1942
+ request.workspaceName !== client.deliveryWorkspaceName ||
1943
+ request.semanticPaneId !== client.paneId ||
1944
+ request.generation !== client.negotiated.generation ||
1945
+ request.incarnation !== baseline.incarnation ||
1946
+ request.deliveryNonce !== client.negotiated.deliveryNonce ||
1947
+ request.canonicalRevision !== baseline.revision ||
1948
+ request.projectionStateHash !== baseline.hash
1949
+ ) {
1950
+ this.#fault(
1951
+ client,
1952
+ "protocol-violation",
1953
+ "History request does not identify the retained projection",
1954
+ );
1955
+ return;
1956
+ }
1957
+ client.lastMaterialize = request;
1958
+ client.materializeRequested = true;
1959
+ this.#cancelEncoding(client);
1960
+ client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
1961
+ // A queued projection still owns its transaction until its receipt arrives.
1962
+ // The following complete seed then supersedes it, including at the same revision.
1963
+ this.#schedule(client);
1964
+ }
1965
+
1772
1966
  #nack(client: ClientState, input: TerminalDeliveryNack): void {
1773
1967
  const nack = TerminalDeliveryNackSchemaZ.parse(input);
1774
1968
  const closed = client.sourceClosedFlight;
@@ -2224,7 +2418,7 @@ export function selectExactSemanticRepresentation<T extends { readonly frame: st
2224
2418
 
2225
2419
  function completeSemanticSelectionObservation(
2226
2420
  observation: ExactSemanticSelectionObservation,
2227
- selectedEncoding: "semantic-v1" | "semantic-compact-v1",
2421
+ selectedEncoding: "semantic-v1" | "semantic-compact-v1" | "semantic-binary-v1",
2228
2422
  legacyAttempts: Readonly<{ patchBytes: number | null; seedBytes: number | null }> | null,
2229
2423
  ): SemanticSelectionObservation {
2230
2424
  const compact = selectedEncoding === "semantic-compact-v1";
@@ -1,9 +1,12 @@
1
- import type { TerminalDeliveryEnvelope } from "@tmux-ide/contracts";
1
+ import type { TerminalDeliveryEnvelope, TerminalProjectionEnvelope } from "@tmux-ide/contracts";
2
2
 
3
- const deliveryOrdinals = new WeakMap<TerminalDeliveryEnvelope, number>();
3
+ const deliveryOrdinals = new WeakMap<
4
+ TerminalDeliveryEnvelope | TerminalProjectionEnvelope,
5
+ number
6
+ >();
4
7
 
5
8
  export function registerTerminalDeliveryObservationOrdinal(
6
- envelope: TerminalDeliveryEnvelope,
9
+ envelope: TerminalDeliveryEnvelope | TerminalProjectionEnvelope,
7
10
  ordinal: number,
8
11
  ): void {
9
12
  if (!Number.isSafeInteger(ordinal) || ordinal < 1)
@@ -12,7 +15,7 @@ export function registerTerminalDeliveryObservationOrdinal(
12
15
  }
13
16
 
14
17
  export function terminalDeliveryObservationOrdinal(
15
- envelope: TerminalDeliveryEnvelope,
18
+ envelope: TerminalDeliveryEnvelope | TerminalProjectionEnvelope,
16
19
  ): number | null {
17
20
  return deliveryOrdinals.get(envelope) ?? null;
18
21
  }
@@ -0,0 +1,59 @@
1
+ import type {
2
+ TerminalProjectionPayload,
3
+ TerminalProjectionSnapshot,
4
+ TerminalReplicaSnapshot,
5
+ } from "@tmux-ide/contracts";
6
+ import {
7
+ hashTerminalProjection,
8
+ projectAlternateSnapshot,
9
+ terminalReplicaRowsEqual,
10
+ } from "@tmux-ide/core";
11
+
12
+ /** A projection receipt never becomes a complete canonical baseline. */
13
+ export interface ProjectionBaseline {
14
+ readonly revision: number;
15
+ readonly incarnation: string;
16
+ readonly hash: string;
17
+ readonly snapshot: TerminalProjectionSnapshot;
18
+ }
19
+
20
+ export function prepareProjection(
21
+ snapshot: TerminalReplicaSnapshot,
22
+ revision: number,
23
+ incarnation: string,
24
+ baseline: ProjectionBaseline | null,
25
+ ): { payload: TerminalProjectionPayload; retained: ProjectionBaseline } {
26
+ const projected = projectAlternateSnapshot(snapshot);
27
+ const retained = {
28
+ revision,
29
+ incarnation,
30
+ hash: hashTerminalProjection(projected),
31
+ snapshot: projected,
32
+ };
33
+ if (
34
+ !baseline ||
35
+ baseline.incarnation !== incarnation ||
36
+ baseline.snapshot.cols !== projected.cols ||
37
+ baseline.snapshot.rows !== projected.rows
38
+ )
39
+ return { payload: { frame: "seed", revision, projection: projected }, retained };
40
+ const rows = projected.grid.flatMap((row, index) =>
41
+ terminalReplicaRowsEqual(row, baseline.snapshot.grid[index]!) ? [] : [{ index, row }],
42
+ );
43
+ return {
44
+ retained,
45
+ payload: {
46
+ frame: "patch",
47
+ baseRevision: baseline.revision,
48
+ revision,
49
+ patch: {
50
+ rows,
51
+ deferredHistory: projected.deferredHistory,
52
+ cursor: projected.cursor,
53
+ modes: projected.modes,
54
+ placements: projected.placements,
55
+ bootstrap: projected.bootstrap,
56
+ },
57
+ },
58
+ };
59
+ }