tmux-ide 2.9.11 → 2.9.13

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 (84) hide show
  1. package/bin/cli.js +6036 -3047
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +6 -0
  4. package/packages/contracts/src/pane-stream.ts +77 -0
  5. package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
  6. package/packages/contracts/src/terminal-delivery.ts +19 -3
  7. package/packages/contracts/src/terminal-graphics-compressed.ts +66 -0
  8. package/packages/contracts/src/terminal-graphics.ts +73 -0
  9. package/packages/contracts/src/terminal-projection.ts +153 -0
  10. package/packages/contracts/src/terminal-surfaces.ts +137 -0
  11. package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
  12. package/packages/core/src/index.ts +2 -0
  13. package/packages/core/src/terminal-delivery-binary.ts +160 -0
  14. package/packages/core/src/terminal-delivery.ts +377 -16
  15. package/packages/core/src/terminal-projection-vectors.mjs +82 -0
  16. package/packages/core/src/terminal-projection.ts +476 -0
  17. package/packages/core/src/terminal-replica.ts +34 -1
  18. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +2 -0
  19. package/packages/daemon/dist/command-center/resources/saved-machine-watch.js +117 -0
  20. package/packages/daemon/dist/command-center/server.js +13 -0
  21. package/packages/daemon/dist/command-center/tmux-servers.js +9 -0
  22. package/packages/daemon/dist/lib/daemon-embed.js +71 -4
  23. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
  24. package/packages/daemon/dist/lib/native-surface-context.js +96 -0
  25. package/packages/daemon/dist/lib/tmux-server-owner.js +10 -0
  26. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  27. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  28. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  29. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  30. package/packages/daemon/dist/server/native-surface-provider-upgrade.js +88 -0
  31. package/packages/daemon/dist/terminal/graphics/compress-chunk.js +40 -0
  32. package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
  33. package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
  34. package/packages/daemon/dist/terminal/graphics/native-surface-demand.js +99 -0
  35. package/packages/daemon/dist/terminal/graphics/native-surface-provider-channel.js +162 -0
  36. package/packages/daemon/dist/terminal/graphics/native-surface-provider.js +275 -0
  37. package/packages/daemon/dist/terminal/graphics/native-surface-viewer.js +146 -0
  38. package/packages/daemon/dist/terminal/mirror/mirror-service.js +3 -0
  39. package/packages/daemon/dist/terminal/mirror/session-channel.js +32 -9
  40. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +369 -16
  41. package/packages/daemon/dist/terminal/pane-stream/runtime.js +10 -0
  42. package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
  43. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
  44. package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
  45. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
  46. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
  47. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
  48. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
  49. package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
  50. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
  51. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
  52. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +4 -0
  53. package/packages/daemon/src/command-center/resources/saved-machine-watch.ts +132 -0
  54. package/packages/daemon/src/command-center/server.ts +18 -0
  55. package/packages/daemon/src/command-center/tmux-servers.ts +14 -0
  56. package/packages/daemon/src/lib/daemon-embed.ts +81 -3
  57. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
  58. package/packages/daemon/src/lib/native-surface-context.ts +123 -0
  59. package/packages/daemon/src/lib/tmux-server-owner.ts +12 -0
  60. package/packages/daemon/src/server/native-surface-provider-upgrade.ts +103 -0
  61. package/packages/daemon/src/terminal/graphics/compress-chunk.ts +49 -0
  62. package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
  63. package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
  64. package/packages/daemon/src/terminal/graphics/native-surface-demand.ts +88 -0
  65. package/packages/daemon/src/terminal/graphics/native-surface-provider-channel.ts +161 -0
  66. package/packages/daemon/src/terminal/graphics/native-surface-provider.ts +308 -0
  67. package/packages/daemon/src/terminal/graphics/native-surface-viewer.ts +154 -0
  68. package/packages/daemon/src/terminal/mirror/mirror-service.ts +5 -0
  69. package/packages/daemon/src/terminal/mirror/session-channel.ts +31 -10
  70. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +446 -15
  71. package/packages/daemon/src/terminal/pane-stream/runtime.ts +14 -1
  72. package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
  73. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
  74. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
  75. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
  76. package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
  77. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
  78. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
  79. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
  80. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
  81. package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
  82. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
  83. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
  84. package/packages/daemon-client/src/pane-stream-client.ts +75 -1
@@ -1,3 +1,5 @@
1
+ import type { LocalImageOwner } from "../graphics/local-image-owner.ts";
2
+ import { NativeSurfaceProviderRegistry } from "../graphics/native-surface-provider.ts";
1
3
  import type { SemanticPaneCatalog } from "../attachments/semantic-pane-catalog.ts";
2
4
  import type { SessionRuntimeRegistry } from "../session-runtime/registry.ts";
3
5
  import { SessionRuntimeTransportBinder } from "../session-runtime/transport-binding.ts";
@@ -9,6 +11,8 @@ import {
9
11
  } from "./pane-stream-websocket.ts";
10
12
 
11
13
  export interface PaneStreamRuntimeOptions {
14
+ readonly nativeSurfaces?: NativeSurfaceProviderRegistry;
15
+ readonly localImages?: LocalImageOwner;
12
16
  readonly daemonInstanceId: string;
13
17
  readonly webSocketUrl: string;
14
18
  /** Canonical daemon-generation session/control owner. */
@@ -18,7 +22,7 @@ export interface PaneStreamRuntimeOptions {
18
22
  readonly observability?: SessionRuntimeObservability;
19
23
  readonly admission?: Omit<
20
24
  PaneStreamAdmissionCoordinatorOptions,
21
- "daemonInstanceId" | "webSocketUrl" | "leaseManager" | "mirror"
25
+ "daemonInstanceId" | "webSocketUrl" | "leaseManager" | "mirror" | "nativeSurfaces"
22
26
  >;
23
27
  }
24
28
 
@@ -29,15 +33,23 @@ export interface PaneStreamRuntimeOptions {
29
33
  */
30
34
  export class PaneStreamRuntime {
31
35
  readonly coordinator: PaneStreamAdmissionCoordinator;
36
+ readonly nativeSurfaces: NativeSurfaceProviderRegistry | undefined;
32
37
  #disposePromise: Promise<void> | null = null;
33
38
 
34
39
  constructor(options: PaneStreamRuntimeOptions) {
40
+ this.nativeSurfaces =
41
+ options.nativeSurfaces ??
42
+ (process.platform === "darwin"
43
+ ? new NativeSurfaceProviderRegistry(options.daemonInstanceId)
44
+ : undefined);
35
45
  const leaseManager = new PaneStreamLeaseManager({
36
46
  daemonInstanceId: options.daemonInstanceId,
37
47
  });
38
48
  const transportBinder = new SessionRuntimeTransportBinder(options.sessionRuntimeRegistry);
39
49
  this.coordinator = new PaneStreamAdmissionCoordinator({
40
50
  ...options.admission,
51
+ localImages: options.localImages,
52
+ nativeSurfaces: this.nativeSurfaces,
41
53
  daemonInstanceId: options.daemonInstanceId,
42
54
  webSocketUrl: options.webSocketUrl,
43
55
  leaseManager,
@@ -66,6 +78,7 @@ export class PaneStreamRuntime {
66
78
  dispose(): Promise<void> {
67
79
  if (!this.#disposePromise) {
68
80
  this.#disposePromise = (async () => {
81
+ this.nativeSurfaces?.close();
69
82
  await this.coordinator.shutdown();
70
83
  })();
71
84
  }
@@ -2472,6 +2472,8 @@ class SessionRuntimeConsumerImpl implements SessionRuntimeConsumer {
2472
2472
  negotiation: upstream.negotiation,
2473
2473
  closed: upstream.closed,
2474
2474
  ack: (ack: TerminalDeliveryAck) => upstream.ack(ack),
2475
+ ...(upstream.projectionReceipt ? { projectionReceipt: upstream.projectionReceipt } : {}),
2476
+ ...(upstream.materialize ? { materialize: upstream.materialize } : {}),
2475
2477
  nack: (nack: TerminalDeliveryNack) => upstream.nack(nack),
2476
2478
  setVisibility: (visibility: TerminalDeliveryVisibility) => upstream.setVisibility(visibility),
2477
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
+ }
@@ -259,11 +259,10 @@ export class SessionRuntimeTerminalReplicaOwner {
259
259
  return this.#disposed ? null : this.#publishedIncarnation;
260
260
  }
261
261
 
262
- /** Scalar admission fence; unavailable while canonical replacement is pending. */
262
+ /** Screen recapture does not replace the terminal lifetime. Keep its input fence
263
+ * while resizing; disposal or a new owner still invalidates held input. */
263
264
  inputAdmissionIncarnation(): string | null {
264
- return this.#disposed || this.#reseed || this.#waitingForGeometryCapture
265
- ? null
266
- : this.#publishedIncarnation;
265
+ return this.#disposed ? null : this.#publishedIncarnation;
267
266
  }
268
267
 
269
268
  qualificationSnapshot(): TerminalReplicaQualificationSnapshot {
@@ -532,6 +532,10 @@ class WireTerminalEndpoint {
532
532
  return;
533
533
  }
534
534
  if (this.#unavailable) return;
535
+ if (message.type === "terminal.projection") {
536
+ this.#failConnection(new Error("Unsolicited alternate-screen projection"));
537
+ return;
538
+ }
535
539
  if (message.type === "terminal.delivery") {
536
540
  this.#acceptEnvelope(message);
537
541
  return;
@@ -1,3 +1,4 @@
1
+ import { rendererProbe } from "./renderer-probe.ts";
1
2
  import { createTerminalSelectionWaiter } from "./terminal-selection-waiter.ts";
2
3
  import { WorkspaceWindowLinkSelectResultSchemaZ } from "@tmux-ide/contracts";
3
4
  import { windowLinkForPane, windowLinkTarget } from "./application-terminal-workspace-policy.ts";
@@ -1050,11 +1051,15 @@ export function createApplicationTerminalInteractionController(
1050
1051
  }
1051
1052
  return outcome.status === "sent";
1052
1053
  },
1053
- onFocusedPane: options.setFocusedPane,
1054
+ onFocusedPane: (pane) => {
1055
+ rendererProbe()?.cancel("selection-changed", nowMicros());
1056
+ options.setFocusedPane(pane);
1057
+ },
1054
1058
  });
1055
1059
 
1056
1060
  return {
1057
1061
  cancelPendingInput() {
1062
+ rendererProbe()?.cancel("input-cancelled", nowMicros());
1058
1063
  selectionWaiter.cancel();
1059
1064
  finishLinkSelection(false);
1060
1065
  paneInput.invalidateSelection();
@@ -1064,6 +1069,7 @@ export function createApplicationTerminalInteractionController(
1064
1069
  selectionGeneration = snapshot;
1065
1070
  const control = controlIdentity(snapshot);
1066
1071
  if (!sameControl(selectedControl, control)) {
1072
+ rendererProbe()?.cancel("control-changed", nowMicros());
1067
1073
  finishLinkSelection(false);
1068
1074
  paneInput.invalidateSelection();
1069
1075
  }
@@ -1118,6 +1124,7 @@ export function createApplicationTerminalInteractionController(
1118
1124
  next.fastLane !== inputAuthorityIdentity.fastLane ||
1119
1125
  next.adapter !== inputAuthorityIdentity.adapter);
1120
1126
  if (next === null || replaced) {
1127
+ rendererProbe()?.cancel("generation-changed", nowMicros());
1121
1128
  finishLinkSelection(false);
1122
1129
  pendingWindowSwitch = null;
1123
1130
  pendingWindowRename = null;
@@ -1364,10 +1371,25 @@ export function createApplicationTerminalInteractionController(
1364
1371
  }
1365
1372
  },
1366
1373
  sendInput: async (input, parserOrigin) => {
1367
- await paneInput.sendInput({ input, parserOrigin });
1374
+ const probe = rendererProbe();
1375
+ const state = probe
1376
+ ? (options.generation()?.fastLane?.lane.paneState(paneInput.focusedPane ?? "") ?? null)
1377
+ : null;
1378
+ const measured = probe?.admit(input, state, nowMicros());
1379
+ const sent = await paneInput.sendInput({ input: measured ?? input, parserOrigin });
1380
+ if (measured && !sent) probe?.cancel("dispatch-rejected", nowMicros());
1381
+ },
1382
+ sendInputToPane: async (paneId, input, parserOrigin) => {
1383
+ const probe = rendererProbe();
1384
+ const state = probe ? (options.generation()?.fastLane?.lane.paneState(paneId) ?? null) : null;
1385
+ const measured = probe?.admit(input, state, nowMicros());
1386
+ const sent = await paneInput.sendInputToPane(paneId, {
1387
+ input: measured ?? input,
1388
+ parserOrigin,
1389
+ });
1390
+ if (measured && !sent) probe?.cancel("dispatch-rejected", nowMicros());
1391
+ return sent;
1368
1392
  },
1369
- sendInputToPane: (paneId, input, parserOrigin) =>
1370
- paneInput.sendInputToPane(paneId, { input, parserOrigin }),
1371
1393
  beginResizePointerIngress(input) {
1372
1394
  if (!options.diagnosticsEnabled) return null;
1373
1395
  const atMicros = diagnosticNowMicros();