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,5 +1,27 @@
1
+ import type { SurfaceViewerDemand } from "../graphics/native-surface-demand.ts";
2
+ import {
3
+ TERMINAL_SURFACE_MAX_CONTROL_BYTES,
4
+ TERMINAL_SURFACE_CAPABILITY_VERSION,
5
+ type TerminalSurfaceReceiver,
6
+ } from "@tmux-ide/contracts";
7
+ import type {
8
+ NativeSurfaceProviderRegistry,
9
+ NativeSurfaceSession,
10
+ } from "../graphics/native-surface-provider.ts";
11
+ import { NativeSurfaceViewer } from "../graphics/native-surface-viewer.ts";
12
+ import { compressGraphicsChunk } from "../graphics/compress-chunk.ts";
13
+ import {
14
+ TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION,
15
+ TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
16
+ TERMINAL_PROJECTION_CAPABILITY_VERSION,
17
+ TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
18
+ type TerminalProjectionReceipt,
19
+ type TerminalProjectionMaterialize,
20
+ } from "@tmux-ide/contracts";
21
+ import type { LocalImageOwner } from "../graphics/local-image-owner.ts";
1
22
  import {
2
23
  PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION,
24
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
3
25
  type PaneStreamInputLifetime,
4
26
  } from "@tmux-ide/contracts";
5
27
  import { inflateSync } from "node:zlib";
@@ -15,6 +37,12 @@ import {
15
37
  PANE_STREAM_REDEEM_PATH,
16
38
  PANE_STREAM_WEBSOCKET_SUBPROTOCOL,
17
39
  TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES,
40
+ TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION,
41
+ TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION,
42
+ TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION,
43
+ TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION,
44
+ encodeTerminalGraphicsBinary,
45
+ encodeTerminalDeliveryBinaryChunk,
18
46
  TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION,
19
47
  TERMINAL_GRAPHICS_PRECISE_PIXELS_CAPABILITY_VERSION,
20
48
  TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES,
@@ -245,6 +273,8 @@ export interface PaneStreamAdmissionSnapshot {
245
273
  }
246
274
 
247
275
  export interface PaneStreamAdmissionCoordinatorOptions {
276
+ readonly nativeSurfaces?: NativeSurfaceProviderRegistry;
277
+ readonly localImages?: LocalImageOwner;
248
278
  readonly daemonInstanceId: string;
249
279
  readonly webSocketUrl: string;
250
280
  readonly leaseManager: PaneStreamLeaseAuthority;
@@ -326,6 +356,8 @@ export interface SessionRuntimeTerminalDeliveryConnection {
326
356
  readonly closed?: Promise<"closed" | "sink-failed">;
327
357
  readonly negotiation: TerminalDeliveryNegotiationResult;
328
358
  ack(ack: TerminalDeliveryAck): void;
359
+ projectionReceipt?(receipt: TerminalProjectionReceipt): void;
360
+ materialize?(request: TerminalProjectionMaterialize): void;
329
361
  nack(nack: TerminalDeliveryNack): void;
330
362
  setVisibility(visibility: TerminalDeliveryVisibility): void;
331
363
  close(): Promise<void>;
@@ -388,6 +420,8 @@ function errorFrameCode(error: unknown): PaneStreamErrorFrameCode {
388
420
  * ticket digests; a daemon restart makes every prior descriptor inert.
389
421
  */
390
422
  export class PaneStreamAdmissionCoordinator {
423
+ readonly #nativeSurfaces: NativeSurfaceProviderRegistry | undefined;
424
+ readonly #localImages: LocalImageOwner | undefined;
391
425
  readonly #instanceId: string;
392
426
  readonly #webSocketUrl: string;
393
427
  readonly #leaseManager: PaneStreamLeaseAuthority;
@@ -421,6 +455,10 @@ export class PaneStreamAdmissionCoordinator {
421
455
  #shutdownPromise: Promise<void> | null = null;
422
456
 
423
457
  constructor(options: PaneStreamAdmissionCoordinatorOptions) {
458
+ this.#nativeSurfaces = options.nativeSurfaces;
459
+ if (this.#nativeSurfaces && this.#nativeSurfaces.generation !== options.daemonInstanceId)
460
+ throw new Error("Native surface provider generation mismatch");
461
+ this.#localImages = options.localImages;
424
462
  this.#instanceId = BindingIdSchemaZ.parse(options.daemonInstanceId);
425
463
  this.#webSocketUrl = PaneStreamLoopbackWebSocketUrlSchemaZ.parse(options.webSocketUrl);
426
464
  this.#leaseManager = options.leaseManager;
@@ -792,6 +830,8 @@ export class PaneStreamAdmissionCoordinator {
792
830
  let live: PaneStreamLiveConnection;
793
831
  try {
794
832
  live = new PaneStreamLiveConnection({
833
+ nativeSurfaces: this.#nativeSurfaces,
834
+ localImages: this.#localImages,
795
835
  clientId: `psc-${this.#clientCounter}`,
796
836
  socket,
797
837
  descriptor: structuredClone(descriptor),
@@ -1010,6 +1050,8 @@ class PreAuthAdmission implements PaneStreamPreAuthAdmission {
1010
1050
  }
1011
1051
 
1012
1052
  interface LiveConnectionOptions {
1053
+ readonly nativeSurfaces?: NativeSurfaceProviderRegistry;
1054
+ readonly localImages?: LocalImageOwner;
1013
1055
  readonly clientId: string;
1014
1056
  readonly socket: DirectTerminalSocket;
1015
1057
  readonly descriptor: PaneStreamLeaseDescriptor;
@@ -1066,6 +1108,11 @@ interface PaneChannel {
1066
1108
  graphicsServed: Map<number, string>;
1067
1109
  graphicsState: TerminalGraphicsState | null;
1068
1110
  graphicsZlib?: boolean;
1111
+ graphicsBinary?: boolean;
1112
+ graphicsCompressed?: boolean;
1113
+ binaryCells?: boolean;
1114
+ graphicsLocal?: boolean;
1115
+ graphicsInline?: Set<string>;
1069
1116
  }
1070
1117
 
1071
1118
  interface QueuedSend {
@@ -1097,6 +1144,15 @@ export function rawGraphicsState(state: TerminalGraphicsState): TerminalGraphics
1097
1144
  const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
1098
1145
 
1099
1146
  export class PaneStreamLiveConnection {
1147
+ // One bounded native negotiation per redeemed socket; never reuse on reconnect.
1148
+ readonly #nativeSurfaces: NativeSurfaceProviderRegistry | undefined;
1149
+ #surfaceViewer: NativeSurfaceViewer | null = null;
1150
+ #surfaceDemands = new Map<string, SurfaceViewerDemand>();
1151
+ #surfaceNative = false;
1152
+ #surfaceSession: NativeSurfaceSession | null = null;
1153
+ #surfaceReceiver: TerminalSurfaceReceiver | null = null;
1154
+ readonly #localImages: LocalImageOwner | undefined;
1155
+ readonly #localLeases = new Map<string, { pane: string; expiresAt: number }>();
1100
1156
  readonly #clientId: string;
1101
1157
  readonly #socket: DirectTerminalSocket;
1102
1158
  readonly #descriptor: PaneStreamLeaseDescriptor;
@@ -1221,6 +1277,8 @@ export class PaneStreamLiveConnection {
1221
1277
  }
1222
1278
 
1223
1279
  constructor(options: LiveConnectionOptions) {
1280
+ this.#nativeSurfaces = options.nativeSurfaces;
1281
+ this.#localImages = options.localImages;
1224
1282
  this.#clientId = options.clientId;
1225
1283
  this.#socket = options.socket;
1226
1284
  this.#descriptor = options.descriptor;
@@ -1294,6 +1352,12 @@ export class PaneStreamLiveConnection {
1294
1352
  )
1295
1353
  ? { inputLifetime: true as const }
1296
1354
  : {}),
1355
+ ...(this.#sessionRuntimeBinding &&
1356
+ this.#descriptor.terminalDelivery?.protocolVersions.includes(
1357
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
1358
+ )
1359
+ ? { inputBytes: true as const }
1360
+ : {}),
1297
1361
  protocolVersion: PANE_STREAM_PROTOCOL_VERSION,
1298
1362
  daemonInstanceId: this.#binding.daemonInstanceId,
1299
1363
  requestId: this.#binding.requestId,
@@ -1315,6 +1379,7 @@ export class PaneStreamLiveConnection {
1315
1379
  this.#sessionRuntimeBinding?.onAuthoritySnapshot?.((snapshot) =>
1316
1380
  this.#usesExplicitAuthority ? this.#sendAuthoritySnapshot(snapshot) : undefined,
1317
1381
  ) ?? null;
1382
+ if (!this.#descriptor.terminalDelivery) this.#observeSurfaceDemand(this.#descriptor.panes);
1318
1383
  void this.#subscribeAll();
1319
1384
  }
1320
1385
 
@@ -1322,6 +1387,12 @@ export class PaneStreamLiveConnection {
1322
1387
  if (this.#closed) return;
1323
1388
  this.#recordDiagnosticLifecycle("pane-stream-terminal", code, reason);
1324
1389
  this.#closed = true;
1390
+ for (const demand of this.#surfaceDemands.values()) demand.close();
1391
+ this.#surfaceDemands.clear();
1392
+ this.#surfaceViewer?.close();
1393
+ this.#surfaceViewer = null;
1394
+ this.#surfaceSession?.close();
1395
+ this.#surfaceSession = null;
1325
1396
  this.#cancelDrainTick?.();
1326
1397
  this.#cancelDrainTick = null;
1327
1398
  // Departure force-returns every ticket within this same tick.
@@ -1336,6 +1407,14 @@ export class PaneStreamLiveConnection {
1336
1407
  this.#stopAuthoritySnapshots?.();
1337
1408
  this.#stopAuthoritySnapshots = null;
1338
1409
  const closures: Promise<unknown>[] = [];
1410
+ this.#localLeases.clear();
1411
+ if (this.#localImages)
1412
+ closures.push(
1413
+ this.#localImages.releaseClient({
1414
+ generation: this.#localImages.identity.generation,
1415
+ client: this.#descriptor.leaseId,
1416
+ }),
1417
+ );
1339
1418
  const layoutSubscription = this.#layoutSubscription;
1340
1419
  this.#layoutSubscription = null;
1341
1420
  if (layoutSubscription) closures.push(layoutSubscription.close().catch(() => undefined));
@@ -1345,6 +1424,8 @@ export class PaneStreamLiveConnection {
1345
1424
  const delivery = channel.delivery;
1346
1425
  channel.delivery = null;
1347
1426
  channel.closed = true;
1427
+ this.#surfaceDemands.get(channel.semanticPaneId)?.close();
1428
+ this.#surfaceDemands.delete(channel.semanticPaneId);
1348
1429
  if (sub) closures.push(sub.close().catch(() => undefined));
1349
1430
  if (delivery) closures.push(delivery.close().catch(() => undefined));
1350
1431
  }
@@ -1564,7 +1645,8 @@ export class PaneStreamLiveConnection {
1564
1645
  if (negotiated.generation !== this.#binding.daemonInstanceId) return true;
1565
1646
  let incarnation: string | null = null;
1566
1647
  for (const message of pending) {
1567
- if (message.type !== "terminal.delivery") continue;
1648
+ if (message.type !== "terminal.delivery" && message.type !== "terminal.projection")
1649
+ continue;
1568
1650
  if (
1569
1651
  message.workspaceName !== this.#descriptor.workspaceName ||
1570
1652
  message.semanticPaneId !== channel.semanticPaneId ||
@@ -1686,13 +1768,19 @@ export class PaneStreamLiveConnection {
1686
1768
  }
1687
1769
 
1688
1770
  async #sendTerminalDelivery(pane: string, message: TerminalDeliveryServerMessage): Promise<void> {
1689
- if (message.type === "terminal.delivery") {
1771
+ if (message.type === "terminal.projection") {
1772
+ const channel = this.#panes.get(pane);
1773
+ if (channel?.deliveryAddress) channel.deliveryAddress.incarnation = message.incarnation;
1774
+ if (channel) channel.binaryCells = true;
1775
+ this.#sendFrame(pane, { type: "terminal-projection-envelope", pane, envelope: message });
1776
+ } else if (message.type === "terminal.delivery") {
1690
1777
  if (!this.#diagnosticFirstSeedObserved) {
1691
1778
  this.#diagnosticFirstSeedObserved = true;
1692
1779
  this.#recordDiagnosticLifecycle("pane-stream-first-seed");
1693
1780
  }
1694
1781
  const channel = this.#panes.get(pane);
1695
1782
  if (channel?.deliveryAddress) channel.deliveryAddress.incarnation = message.incarnation;
1783
+ if (channel) channel.binaryCells = message.encoding === "semantic-binary-v1";
1696
1784
  // The trusted lease address was fixed at delivery admission; preserve the
1697
1785
  // exact envelope and its observation identity through transmission.
1698
1786
  const detailedDeliveryObservation = this.#observability?.enabled === true;
@@ -1775,13 +1863,26 @@ export class PaneStreamLiveConnection {
1775
1863
  }
1776
1864
  }
1777
1865
  } else if (message.type === "terminal.delivery.chunk") {
1778
- this.#sendFrame(pane, {
1779
- type: "terminal-delivery-chunk",
1780
- pane,
1781
- transactionId: message.transactionId,
1782
- index: message.index,
1783
- data: Buffer.from(message.bytes).toString("base64"),
1784
- });
1866
+ if (this.#panes.get(pane)?.binaryCells) {
1867
+ this.#sendEncoded(
1868
+ pane,
1869
+ Buffer.from(
1870
+ encodeTerminalDeliveryBinaryChunk(
1871
+ { pane, transactionId: message.transactionId, index: message.index },
1872
+ message.bytes,
1873
+ ),
1874
+ ),
1875
+ true,
1876
+ );
1877
+ } else {
1878
+ this.#sendFrame(pane, {
1879
+ type: "terminal-delivery-chunk",
1880
+ pane,
1881
+ transactionId: message.transactionId,
1882
+ index: message.index,
1883
+ data: Buffer.from(message.bytes).toString("base64"),
1884
+ });
1885
+ }
1785
1886
  } else {
1786
1887
  this.#sendFrame(pane, { type: "terminal-delivery-fault", pane, fault: message });
1787
1888
  // Lifecycle/control truth must never queue behind bulk terminal bytes.
@@ -1792,10 +1893,27 @@ export class PaneStreamLiveConnection {
1792
1893
  await this.#awaitSemanticCredit(pane);
1793
1894
  }
1794
1895
 
1896
+ #observeSurfaceDemand(panes: readonly string[]): void {
1897
+ for (const pane of panes) {
1898
+ if (this.#surfaceDemands.has(pane)) continue;
1899
+ const demand = this.#nativeSurfaces?.demand.viewer(this.#descriptor.sessionName, [pane]);
1900
+ if (!demand) return;
1901
+ demand.native(this.#surfaceNative);
1902
+ this.#surfaceDemands.set(pane, demand);
1903
+ }
1904
+ }
1905
+
1906
+ #surfaceControl(type: string): void {
1907
+ if (type === "terminal-surface-offer") return;
1908
+ this.#surfaceNative = type === "terminal-surface-peer";
1909
+ for (const demand of this.#surfaceDemands.values()) demand.native(this.#surfaceNative);
1910
+ }
1911
+
1795
1912
  /** Announce the pane's kitty graphics state; the client pulls bytes it lacks. */
1796
1913
  #startGraphics(channel: PaneChannel): void {
1797
1914
  const graphics = channel.delivery?.graphics;
1798
1915
  if (!graphics) return;
1916
+ this.#observeSurfaceDemand([channel.semanticPaneId]);
1799
1917
  // Renderers that cannot inflate see raw formats; their bytes are inflated
1800
1918
  // when served.
1801
1919
  const zlib =
@@ -1803,6 +1921,34 @@ export class PaneStreamLiveConnection {
1803
1921
  TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION,
1804
1922
  ) === true;
1805
1923
  channel.graphicsZlib = zlib;
1924
+ channel.graphicsLocal =
1925
+ !!this.#localImages &&
1926
+ this.#descriptor.terminalDelivery?.protocolVersions.some(
1927
+ (version) =>
1928
+ version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
1929
+ version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
1930
+ version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
1931
+ version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
1932
+ ) === true;
1933
+ channel.graphicsCompressed =
1934
+ this.#descriptor.terminalDelivery?.protocolVersions.some(
1935
+ (version) =>
1936
+ version === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
1937
+ version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
1938
+ ) === true;
1939
+ channel.graphicsInline = new Set();
1940
+ channel.graphicsBinary =
1941
+ this.#descriptor.terminalDelivery?.protocolVersions.some(
1942
+ (version) =>
1943
+ version === TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION ||
1944
+ version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
1945
+ version === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
1946
+ version === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
1947
+ version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
1948
+ version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
1949
+ version === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
1950
+ version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
1951
+ ) === true;
1806
1952
  // Renderers that report cell pixels learn up front that this daemon
1807
1953
  // accepts them, before any image exists.
1808
1954
  const cellPixels =
@@ -1834,6 +1980,9 @@ export class PaneStreamLiveConnection {
1834
1980
  ...(zlib ? state : rawGraphicsState(state)),
1835
1981
  ...(cellPixels ? { cellPixels: true as const } : {}),
1836
1982
  ...(preciseCellPixels ? { preciseCellPixels: true as const } : {}),
1983
+ ...(channel.graphicsBinary ? { binaryBlobs: true as const } : {}),
1984
+ ...(channel.graphicsCompressed ? { compressedBlobs: true as const } : {}),
1985
+ ...(channel.graphicsLocal ? { localFiles: true as const } : {}),
1837
1986
  },
1838
1987
  });
1839
1988
  if (pushed.length > 0) this.#queueGraphics(channel, pushed);
@@ -1849,15 +1998,24 @@ export class PaneStreamLiveConnection {
1849
1998
  placements: [],
1850
1999
  cellPixels: true,
1851
2000
  ...(preciseCellPixels ? { preciseCellPixels: true as const } : {}),
2001
+ ...(channel.graphicsBinary ? { binaryBlobs: true as const } : {}),
2002
+ ...(channel.graphicsCompressed ? { compressedBlobs: true as const } : {}),
2003
+ ...(channel.graphicsLocal ? { localFiles: true as const } : {}),
1852
2004
  },
1853
2005
  });
1854
2006
  }
1855
2007
  }
1856
2008
 
1857
- #requestGraphics(pane: string, digests: readonly string[]): void {
2009
+ #requestGraphics(pane: string, digests: readonly string[], preferInline = false): void {
1858
2010
  const channel = this.#panes.get(pane);
1859
2011
  // Requests are only meaningful after an announcement; ignore stray ones.
1860
2012
  if (!channel || channel.closed || !channel.graphicsAnnounced) return;
2013
+ if (preferInline && channel.graphicsLocal) {
2014
+ for (const digest of digests) {
2015
+ if ((channel.graphicsInline?.size ?? 0) < GRAPHICS_MAX_QUEUED_DIGESTS)
2016
+ channel.graphicsInline?.add(digest);
2017
+ }
2018
+ }
1861
2019
  this.#queueGraphics(channel, digests);
1862
2020
  }
1863
2021
 
@@ -1881,6 +2039,7 @@ export class PaneStreamLiveConnection {
1881
2039
  try {
1882
2040
  while (!this.#closed && !channel.closed && channel.graphicsQueue.length > 0) {
1883
2041
  const digest = channel.graphicsQueue.shift()!;
2042
+ const inline = channel.graphicsInline?.delete(digest) ?? false;
1884
2043
  const graphics = channel.delivery?.graphics;
1885
2044
  const image = graphics?.image(digest);
1886
2045
  if (!graphics || !image) continue;
@@ -1902,6 +2061,47 @@ export class PaneStreamLiveConnection {
1902
2061
  continue;
1903
2062
  const format = raw ? (image.format === "rgba-zlib" ? "rgba" : "rgb") : image.format;
1904
2063
  const total = data.byteLength;
2064
+ if (!inline && channel.graphicsLocal && !raw && this.#localImages) {
2065
+ for (const [id, lease] of this.#localLeases)
2066
+ if (lease.expiresAt <= Date.now()) this.#localLeases.delete(id);
2067
+ if (this.#localLeases.size < 64) {
2068
+ try {
2069
+ const lease = await this.#localImages.acquire({
2070
+ generation: this.#localImages.identity.generation,
2071
+ client: this.#descriptor.leaseId,
2072
+ digest,
2073
+ bytes: data,
2074
+ });
2075
+ if (this.#closed || channel.closed || graphics.image(digest) !== image) {
2076
+ await this.#localImages.release(lease);
2077
+ continue;
2078
+ }
2079
+ this.#localLeases.set(lease.id, {
2080
+ pane: channel.semanticPaneId,
2081
+ expiresAt: lease.expiresAt,
2082
+ });
2083
+ this.#sendFrame(null, {
2084
+ type: "terminal-graphics-local",
2085
+ pane: channel.semanticPaneId,
2086
+ generation: lease.generation,
2087
+ leaseId: lease.id,
2088
+ path: lease.path,
2089
+ expiresAt: lease.expiresAt,
2090
+ blob: {
2091
+ digest,
2092
+ format,
2093
+ width: image.width,
2094
+ height: image.height,
2095
+ total,
2096
+ offset: 0,
2097
+ },
2098
+ });
2099
+ continue;
2100
+ } catch {
2101
+ /* Optional transport: retain the negotiated inline fallback. */
2102
+ }
2103
+ }
2104
+ }
1905
2105
  for (let offset = 0; offset < total; offset += TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES) {
1906
2106
  while ((this.#socket.bufferedAmount ?? 0) > GRAPHICS_SEND_WINDOW_BYTES) {
1907
2107
  if (this.#closed || channel.closed) return;
@@ -1912,6 +2112,50 @@ export class PaneStreamLiveConnection {
1912
2112
  // the renderer drops the partial image and shows the newer one.
1913
2113
  if (offset > 0 && graphics.image(digest) !== image) break;
1914
2114
  const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
2115
+ if (channel.graphicsBinary) {
2116
+ const header = {
2117
+ pane: channel.semanticPaneId,
2118
+ blob: {
2119
+ digest,
2120
+ format,
2121
+ width: image.width,
2122
+ height: image.height,
2123
+ total,
2124
+ offset,
2125
+ },
2126
+ };
2127
+ const compressed = channel.graphicsCompressed
2128
+ ? await compressGraphicsChunk(header, data.subarray(offset, end))
2129
+ : null;
2130
+ // Compression yields: a pane may close or replace this image meanwhile.
2131
+ if (this.#closed || channel.closed) return;
2132
+ if (graphics.image(digest) !== image) break;
2133
+ if (compressed) {
2134
+ this.#sendEncoded(null, Buffer.from(compressed), true);
2135
+ continue;
2136
+ }
2137
+ this.#sendEncoded(
2138
+ null,
2139
+ Buffer.from(
2140
+ encodeTerminalGraphicsBinary(
2141
+ {
2142
+ pane: channel.semanticPaneId,
2143
+ blob: {
2144
+ digest,
2145
+ format,
2146
+ width: image.width,
2147
+ height: image.height,
2148
+ total,
2149
+ offset,
2150
+ },
2151
+ },
2152
+ data.subarray(offset, end),
2153
+ ),
2154
+ ),
2155
+ true,
2156
+ );
2157
+ continue;
2158
+ }
1915
2159
  this.#sendFrame(null, {
1916
2160
  type: "terminal-graphics-blob",
1917
2161
  pane: channel.semanticPaneId,
@@ -2067,6 +2311,8 @@ export class PaneStreamLiveConnection {
2067
2311
  #emitClosed(channel: PaneChannel): void {
2068
2312
  if (channel.closed) return;
2069
2313
  channel.closed = true;
2314
+ this.#surfaceDemands.get(channel.semanticPaneId)?.close();
2315
+ this.#surfaceDemands.delete(channel.semanticPaneId);
2070
2316
  const sub = channel.sub;
2071
2317
  channel.sub = null;
2072
2318
  if (sub) void sub.close().catch(() => undefined);
@@ -2245,7 +2491,12 @@ export class PaneStreamLiveConnection {
2245
2491
  #sendFrame(pane: string | null, frame: Record<string, unknown>): void {
2246
2492
  if (this.#closed || this.#socket.readyState !== WS_OPEN) return;
2247
2493
  const encoded = JSON.stringify(frame);
2248
- const bytes = Buffer.byteLength(encoded, "utf8");
2494
+ this.#sendEncoded(pane, encoded, false);
2495
+ }
2496
+
2497
+ #sendEncoded(pane: string | null, encoded: string | Buffer, binary: boolean): void {
2498
+ if (this.#closed || this.#socket.readyState !== WS_OPEN) return;
2499
+ const bytes = Buffer.byteLength(encoded);
2249
2500
  if (pane !== null) {
2250
2501
  this.#ledger.take(this.#clientId, pane, "ws-send-buffer", bytes);
2251
2502
  if (this.#deliveryAcks) this.#ledger.take(this.#clientId, pane, "renderer-backlog", 1);
@@ -2253,7 +2504,7 @@ export class PaneStreamLiveConnection {
2253
2504
  this.#sendQueue.push({ pane, remaining: bytes });
2254
2505
  this.#sentBytesTotal += bytes;
2255
2506
  try {
2256
- this.#socket.send(encoded, { binary: false });
2507
+ this.#socket.send(encoded, { binary });
2257
2508
  } catch {
2258
2509
  this.close(1011, "stream-unavailable");
2259
2510
  return;
@@ -2475,6 +2726,20 @@ export class PaneStreamLiveConnection {
2475
2726
  });
2476
2727
  return;
2477
2728
  }
2729
+ if (frame.type === "terminal-projection-receipt") {
2730
+ const channel = this.#deliveryChannel(frame.receipt);
2731
+ if (!channel) return;
2732
+ if (!channel.delivery?.projectionReceipt) return this.#failProtocol("protocol-error");
2733
+ channel.delivery.projectionReceipt(frame.receipt);
2734
+ return;
2735
+ }
2736
+ if (frame.type === "terminal-projection-materialize") {
2737
+ const channel = this.#deliveryChannel(frame.request);
2738
+ if (!channel) return;
2739
+ if (!channel.delivery?.materialize) return this.#failProtocol("protocol-error");
2740
+ channel.delivery.materialize(frame.request);
2741
+ return;
2742
+ }
2478
2743
  if (frame.type === "terminal-delivery-ack") {
2479
2744
  const channel = this.#deliveryChannel(frame.ack);
2480
2745
  if (!channel) return;
@@ -2503,8 +2768,163 @@ export class PaneStreamLiveConnection {
2503
2768
  this.#acceptSemanticIntent(frame.operationId, frame.intent);
2504
2769
  return;
2505
2770
  }
2771
+ if (frame.type === "terminal-surface-watch") {
2772
+ if (
2773
+ byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
2774
+ frame.generation !== this.#sessionRuntimeBinding?.generation ||
2775
+ !this.#descriptor.terminalDelivery?.protocolVersions.includes(
2776
+ TERMINAL_SURFACE_CAPABILITY_VERSION,
2777
+ ) ||
2778
+ !this.#nativeSurfaces ||
2779
+ this.#surfaceReceiver
2780
+ )
2781
+ return this.#failProtocol("protocol-error");
2782
+ if (this.#surfaceViewer) return;
2783
+ this.#surfaceViewer = new NativeSurfaceViewer(
2784
+ this.#nativeSurfaces,
2785
+ this.#descriptor.panes,
2786
+ (control) => {
2787
+ if (this.#closed) return false;
2788
+ this.#surfaceControl(control.type);
2789
+ this.#sendFrame(null, control);
2790
+ return !this.#closed;
2791
+ },
2792
+ (pane, generation) => {
2793
+ const channel = this.#panes.get(pane);
2794
+ const address = channel?.deliveryAddress;
2795
+ return !this.#closed &&
2796
+ channel &&
2797
+ !channel.closed &&
2798
+ channel.delivery &&
2799
+ address?.generation === generation
2800
+ ? address.deliveryNonce
2801
+ : null;
2802
+ },
2803
+ );
2804
+ this.#surfaceViewer.start();
2805
+ return;
2806
+ }
2807
+ if (frame.type === "terminal-surface-receiver") {
2808
+ if (this.#surfaceViewer) {
2809
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.receive(frame))
2810
+ this.#failProtocol("protocol-error");
2811
+ return;
2812
+ }
2813
+ if (
2814
+ byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
2815
+ !this.#descriptor.terminalDelivery ||
2816
+ frame.generation !== this.#sessionRuntimeBinding?.generation
2817
+ ) {
2818
+ return this.#failProtocol("protocol-error");
2819
+ }
2820
+ if (this.#surfaceReceiver) {
2821
+ // Parsed schema order is deterministic. A duplicate cannot change the
2822
+ // challenge, kernel identity or endpoint, nor amplify fallback replies.
2823
+ if (JSON.stringify(frame) !== JSON.stringify(this.#surfaceReceiver)) {
2824
+ return this.#failProtocol("protocol-error");
2825
+ }
2826
+ return;
2827
+ }
2828
+ this.#surfaceReceiver = frame;
2829
+ if (
2830
+ this.#descriptor.terminalDelivery.protocolVersions.includes(
2831
+ TERMINAL_SURFACE_CAPABILITY_VERSION,
2832
+ )
2833
+ ) {
2834
+ const native =
2835
+ this.#nativeSurfaces?.open(
2836
+ frame,
2837
+ this.#descriptor.panes,
2838
+ (control) => {
2839
+ if (this.#closed) return false;
2840
+ this.#surfaceControl(control.type);
2841
+ this.#sendFrame(null, control);
2842
+ return !this.#closed;
2843
+ },
2844
+ (pane, generation) => {
2845
+ const channel = this.#panes.get(pane);
2846
+ const address = channel?.deliveryAddress;
2847
+ if (
2848
+ this.#closed ||
2849
+ !channel ||
2850
+ channel.closed ||
2851
+ !channel.delivery ||
2852
+ !address ||
2853
+ address.generation !== generation
2854
+ )
2855
+ return null;
2856
+ return address.deliveryNonce;
2857
+ },
2858
+ ) ?? null;
2859
+ if (this.#closed) {
2860
+ native?.close();
2861
+ return;
2862
+ }
2863
+ this.#surfaceSession = native;
2864
+ if (native) return;
2865
+ }
2866
+ // No provider/capability means no PID/slot lookup and no inferred locality.
2867
+ this.#sendFrame(null, {
2868
+ type: "terminal-surface-fallback",
2869
+ version: 1,
2870
+ generation: frame.generation,
2871
+ connectionNonce: frame.connectionNonce,
2872
+ reason: "unsupported",
2873
+ });
2874
+ return;
2875
+ }
2876
+ if (frame.type === "terminal-surface-fallback") {
2877
+ if (this.#surfaceViewer) {
2878
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.fallback(frame))
2879
+ this.#failProtocol("protocol-error");
2880
+ return;
2881
+ }
2882
+ if (
2883
+ byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
2884
+ frame.generation !== this.#surfaceReceiver?.generation ||
2885
+ frame.connectionNonce !== this.#surfaceReceiver.connectionNonce
2886
+ ) {
2887
+ return this.#failProtocol("protocol-error");
2888
+ }
2889
+ this.#surfaceSession?.fallback(frame.reason);
2890
+ return;
2891
+ }
2892
+ if (frame.type === "terminal-surface-retired") {
2893
+ if (this.#surfaceViewer) {
2894
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.retire(frame))
2895
+ this.#failProtocol("protocol-error");
2896
+ return;
2897
+ }
2898
+ if (
2899
+ byteLength <= TERMINAL_SURFACE_MAX_CONTROL_BYTES &&
2900
+ frame.generation === this.#surfaceReceiver?.generation &&
2901
+ frame.connectionNonce === this.#surfaceReceiver.connectionNonce &&
2902
+ this.#surfaceSession?.retire(frame.sequence)
2903
+ )
2904
+ return;
2905
+ // Unissued/replayed ACKs must not retire another socket's native storage.
2906
+ return this.#failProtocol("protocol-error");
2907
+ }
2506
2908
  if (frame.type === "terminal-graphics-request") {
2507
- this.#requestGraphics(frame.pane, frame.digests);
2909
+ this.#requestGraphics(frame.pane, frame.digests, frame.preferInline === true);
2910
+ return;
2911
+ }
2912
+ if (frame.type === "terminal-graphics-local-release") {
2913
+ const lease = this.#localLeases.get(frame.leaseId);
2914
+ if (
2915
+ lease &&
2916
+ lease.pane === frame.pane &&
2917
+ frame.generation === this.#localImages?.identity.generation
2918
+ ) {
2919
+ this.#localLeases.delete(frame.leaseId);
2920
+ void this.#localImages
2921
+ ?.release({
2922
+ generation: frame.generation,
2923
+ client: this.#descriptor.leaseId,
2924
+ id: frame.leaseId,
2925
+ })
2926
+ .catch(() => {});
2927
+ }
2508
2928
  return;
2509
2929
  }
2510
2930
  if (frame.type === "terminal-cell-pixels") {
@@ -2720,7 +3140,7 @@ export class PaneStreamLiveConnection {
2720
3140
  #acceptInput(
2721
3141
  pane: string,
2722
3142
  seq: number,
2723
- kind: "text" | "key",
3143
+ kind: "text" | "key" | "bytes",
2724
3144
  data: string,
2725
3145
  frameBytes: number,
2726
3146
  performanceTraceId?: string,
@@ -2730,6 +3150,16 @@ export class PaneStreamLiveConnection {
2730
3150
  ingressAtSharedMicros: number | null = null,
2731
3151
  expectedLifetime?: PaneStreamInputLifetime,
2732
3152
  ): void {
3153
+ if (
3154
+ kind === "bytes" &&
3155
+ (!this.#sessionRuntimeBinding ||
3156
+ !this.#descriptor.terminalDelivery?.protocolVersions.includes(
3157
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
3158
+ ))
3159
+ ) {
3160
+ this.#failProtocol("input-rejected");
3161
+ return;
3162
+ }
2733
3163
  if (
2734
3164
  expectedLifetime &&
2735
3165
  (!this.#sessionRuntimeBinding ||
@@ -2862,7 +3292,8 @@ export class PaneStreamLiveConnection {
2862
3292
  expectedLifetime,
2863
3293
  );
2864
3294
  else if (kind === "text") channel.sub!.sendText(data);
2865
- else channel.sub!.sendKey(data);
3295
+ else if (kind === "key") channel.sub!.sendKey(data);
3296
+ else throw new Error("Byte input requires semantic runtime");
2866
3297
  let ackStartedAtMicros: number | null = null;
2867
3298
  let ackStartedAtSharedMicros: number | null = null;
2868
3299
  if (trace) {