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
@@ -507,6 +507,8 @@ export class SessionChannel {
507
507
  private readonly fittedWindows = new Map<string, { cols: number; rows: number }>();
508
508
  private cancelSync: (() => void) | null = null;
509
509
  private syncOrdinal = 0;
510
+ private identityJoinOrdinal = 0;
511
+ private trustedInventoryJoinsMissing = false;
510
512
  private completedSyncOrdinal = 0;
511
513
  private lastDisplayNameSyncAtMs = 0;
512
514
  private disposed = false;
@@ -799,22 +801,36 @@ export class SessionChannel {
799
801
 
800
802
  /**
801
803
  * Strict daemon-internal inventory from this channel's current tmux truth.
802
- * Unlike the background discovery path, this query awaits descriptor
803
- * reconciliation before projecting and rejects incomplete identity rather
804
- * than returning the previous descriptor snapshot.
804
+ * Unlike background discovery, this query verifies current native membership
805
+ * before projecting and rejects incomplete identity. Stream admission may
806
+ * explicitly join missing pane records after that verification; ordinary
807
+ * inventory readers retain their existing fail-closed no-join contract.
805
808
  */
806
809
  describeTrustedInventory(
807
810
  expectedRuntimeSessionId: string,
811
+ joinMissingPanes = false,
808
812
  ): Promise<TrustedMirrorSessionInventory> {
809
813
  if (this.disposed) {
810
814
  return Promise.reject(new Error(`mirror session ${this.opts.session} is disposed`));
811
815
  }
812
816
  if (this.trustedInventoryFlight) {
817
+ // A default read can fail precisely because it does not join new panes.
818
+ // Preserve that failure for its caller, then qualify admission separately.
819
+ if (
820
+ joinMissingPanes &&
821
+ !this.trustedInventoryJoinsMissing &&
822
+ this.trustedInventoryFlightSessionId === expectedRuntimeSessionId
823
+ )
824
+ return this.trustedInventoryFlight.then(
825
+ () => this.describeTrustedInventory(expectedRuntimeSessionId, true),
826
+ () => this.describeTrustedInventory(expectedRuntimeSessionId, true),
827
+ );
813
828
  return this.trustedInventoryFlightSessionId === expectedRuntimeSessionId
814
829
  ? this.trustedInventoryFlight
815
830
  : Promise.reject(new Error(`trusted inventory identity changed for ${this.opts.session}`));
816
831
  }
817
- const flight = this.refreshTrustedInventory(expectedRuntimeSessionId)
832
+ this.trustedInventoryJoinsMissing = joinMissingPanes;
833
+ const flight = this.refreshTrustedInventory(expectedRuntimeSessionId, 0, joinMissingPanes)
818
834
  .catch((error) => {
819
835
  this.windowLinkAuthority?.invalidate();
820
836
  this.latestWindowStage = null;
@@ -3387,6 +3403,7 @@ export class SessionChannel {
3387
3403
  private async refreshTrustedInventory(
3388
3404
  expectedRuntimeSessionId: string,
3389
3405
  attempt = 0,
3406
+ joinMissingPanes = false,
3390
3407
  ): Promise<TrustedMirrorSessionInventory> {
3391
3408
  const authorityOrdinal = this.windowIdentityOrdinal;
3392
3409
  const beforeLines = await this.io.request(
@@ -3486,7 +3503,11 @@ export class SessionChannel {
3486
3503
  ) {
3487
3504
  if (attempt >= 1)
3488
3505
  throw new Error(`trusted inventory for ${this.opts.session} did not settle`);
3489
- return await this.refreshTrustedInventory(expectedRuntimeSessionId, attempt + 1);
3506
+ return await this.refreshTrustedInventory(
3507
+ expectedRuntimeSessionId,
3508
+ attempt + 1,
3509
+ joinMissingPanes,
3510
+ );
3490
3511
  }
3491
3512
  if (this.disposed) throw new Error(`mirror session ${this.opts.session} is disposed`);
3492
3513
  const activeWindowId = activeWindowIds.values().next().value;
@@ -3521,7 +3542,28 @@ export class SessionChannel {
3521
3542
  stagedPaneIds.size !== descriptors.length ||
3522
3543
  [...computedWindowCounts.keys()].some(
3523
3544
  (runtimeWindowId) => !confirmedWindowStage.windows.has(runtimeWindowId),
3524
- ) ||
3545
+ )
3546
+ )
3547
+ throw new Error(`trusted inventory for ${this.opts.session} lacks verified identity`);
3548
+ // Fresh native inventory can beat background discovery after window creation.
3549
+ // Join verified stable membership before projecting semantic records.
3550
+ if (
3551
+ joinMissingPanes &&
3552
+ descriptors.some(
3553
+ (pane) => this.panesByRuntime.get(pane.runtimePaneId)?.semanticId !== pane.semanticPaneId,
3554
+ )
3555
+ ) {
3556
+ this.discovery.invalidate();
3557
+ await this.reconcileIdentity(
3558
+ descriptors,
3559
+ runtimePaneIds,
3560
+ () => authorityOrdinal === this.windowIdentityOrdinal,
3561
+ false,
3562
+ );
3563
+ }
3564
+ if (this.disposed || authorityOrdinal !== this.windowIdentityOrdinal)
3565
+ throw new Error(`trusted inventory for ${this.opts.session} changed during identity join`);
3566
+ if (
3525
3567
  descriptors.some((descriptor) => {
3526
3568
  const pane = this.panesByRuntime.get(descriptor.runtimePaneId);
3527
3569
  const window = confirmedWindowStage.windows.get(descriptor.windowId!);
@@ -3934,7 +3976,10 @@ export class SessionChannel {
3934
3976
  private async reconcileIdentity(
3935
3977
  descriptors: readonly SessionPaneDescriptor[],
3936
3978
  listed: ReadonlySet<string>,
3979
+ current: () => boolean = () => true,
3980
+ publish = true,
3937
3981
  ): Promise<boolean> {
3982
+ const joinOrdinal = ++this.identityJoinOrdinal;
3938
3983
  if (this.disposed) return false;
3939
3984
  if (this.opts.hasSharedWindowConflict?.()) {
3940
3985
  this.settleFirstJoin();
@@ -3973,7 +4018,8 @@ export class SessionChannel {
3973
4018
  ),
3974
4019
  ),
3975
4020
  );
3976
- if (this.disposed) return plan.stampEffects.length > 0;
4021
+ if (this.disposed || !current() || joinOrdinal !== this.identityJoinOrdinal)
4022
+ return plan.stampEffects.length > 0;
3977
4023
  const reconciliation = finalizeWorkspaceTmuxReconciliation(plan, outcomes);
3978
4024
  const descriptorByRuntime = new Map(descriptors.map((d) => [d.runtimePaneId, d]));
3979
4025
  for (const verified of reconciliation.panes) {
@@ -4080,8 +4126,10 @@ export class SessionChannel {
4080
4126
  * without this the new pane stays invisible until something else happens to
4081
4127
  * move a layout, and a split looks like it did not reach the view.
4082
4128
  */
4083
- for (const runtimeId of this.layoutByWindow.keys()) this.emitLayout(runtimeId);
4084
- this.emitLayoutAuthority();
4129
+ if (publish) {
4130
+ for (const runtimeId of this.layoutByWindow.keys()) this.emitLayout(runtimeId);
4131
+ this.emitLayoutAuthority();
4132
+ }
4085
4133
  this.settleFirstJoin();
4086
4134
  return plan.stampEffects.length > 0;
4087
4135
  }
@@ -1,3 +1,15 @@
1
+ import {
2
+ TERMINAL_PROJECTION_CAPABILITY_VERSION,
3
+ TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
4
+ type TerminalProjectionReceipt,
5
+ type TerminalProjectionMaterialize,
6
+ } from "@tmux-ide/contracts";
7
+ import type { LocalImageOwner } from "../graphics/local-image-owner.ts";
8
+ import {
9
+ PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION,
10
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
11
+ type PaneStreamInputLifetime,
12
+ } from "@tmux-ide/contracts";
1
13
  import { inflateSync } from "node:zlib";
2
14
  import { PaneStreamLayoutSnapshotFrameSchemaZ, type WindowLinkTopology } from "@tmux-ide/contracts";
3
15
  import type { TerminalDeliveryGraphics } from "../session-runtime/terminal-delivery-hub.ts";
@@ -11,6 +23,12 @@ import {
11
23
  PANE_STREAM_REDEEM_PATH,
12
24
  PANE_STREAM_WEBSOCKET_SUBPROTOCOL,
13
25
  TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES,
26
+ TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION,
27
+ TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION,
28
+ TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION,
29
+ TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION,
30
+ encodeTerminalGraphicsBinary,
31
+ encodeTerminalDeliveryBinaryChunk,
14
32
  TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION,
15
33
  TERMINAL_GRAPHICS_PRECISE_PIXELS_CAPABILITY_VERSION,
16
34
  TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES,
@@ -149,6 +167,7 @@ export interface PaneStreamMirror {
149
167
  describeSession(session: string): Promise<MirrorSessionDescription>;
150
168
  describeSessionAuthority?(
151
169
  session: string,
170
+ expectedSemanticPaneIds?: readonly string[],
152
171
  ): Promise<{ readonly description: MirrorSessionDescription; readonly runtimeSessionId: string }>;
153
172
  subscribe(request: MirrorSubscribeRequest): Promise<MirrorSubscription>;
154
173
  subscribeLayout?(
@@ -240,6 +259,7 @@ export interface PaneStreamAdmissionSnapshot {
240
259
  }
241
260
 
242
261
  export interface PaneStreamAdmissionCoordinatorOptions {
262
+ readonly localImages?: LocalImageOwner;
243
263
  readonly daemonInstanceId: string;
244
264
  readonly webSocketUrl: string;
245
265
  readonly leaseManager: PaneStreamLeaseAuthority;
@@ -304,6 +324,7 @@ export interface SessionRuntimePaneStreamTransportBinding {
304
324
  performanceTraceId?: string,
305
325
  causalProbe?: CausalCellProbeV1,
306
326
  onCausalResult?: (result: CausalCellLedgerResult) => void,
327
+ expectedLifetime?: PaneStreamInputLifetime,
307
328
  ): void;
308
329
  fitViewport(
309
330
  lease: SessionRuntimeAuthorityLease,
@@ -320,6 +341,8 @@ export interface SessionRuntimeTerminalDeliveryConnection {
320
341
  readonly closed?: Promise<"closed" | "sink-failed">;
321
342
  readonly negotiation: TerminalDeliveryNegotiationResult;
322
343
  ack(ack: TerminalDeliveryAck): void;
344
+ projectionReceipt?(receipt: TerminalProjectionReceipt): void;
345
+ materialize?(request: TerminalProjectionMaterialize): void;
323
346
  nack(nack: TerminalDeliveryNack): void;
324
347
  setVisibility(visibility: TerminalDeliveryVisibility): void;
325
348
  close(): Promise<void>;
@@ -382,6 +405,7 @@ function errorFrameCode(error: unknown): PaneStreamErrorFrameCode {
382
405
  * ticket digests; a daemon restart makes every prior descriptor inert.
383
406
  */
384
407
  export class PaneStreamAdmissionCoordinator {
408
+ readonly #localImages: LocalImageOwner | undefined;
385
409
  readonly #instanceId: string;
386
410
  readonly #webSocketUrl: string;
387
411
  readonly #leaseManager: PaneStreamLeaseAuthority;
@@ -415,6 +439,7 @@ export class PaneStreamAdmissionCoordinator {
415
439
  #shutdownPromise: Promise<void> | null = null;
416
440
 
417
441
  constructor(options: PaneStreamAdmissionCoordinatorOptions) {
442
+ this.#localImages = options.localImages;
418
443
  this.#instanceId = BindingIdSchemaZ.parse(options.daemonInstanceId);
419
444
  this.#webSocketUrl = PaneStreamLoopbackWebSocketUrlSchemaZ.parse(options.webSocketUrl);
420
445
  this.#leaseManager = options.leaseManager;
@@ -493,7 +518,10 @@ export class PaneStreamAdmissionCoordinator {
493
518
  try {
494
519
  const describeStartedAtMicros = trace ? this.#observability!.nowMicros() : 0;
495
520
  if (this.#mirror.describeSessionAuthority) {
496
- const authority = await this.#mirror.describeSessionAuthority(context.sessionName);
521
+ const authority = await this.#mirror.describeSessionAuthority(
522
+ context.sessionName,
523
+ request.panes,
524
+ );
497
525
  described = authority.description;
498
526
  runtimeSessionId = authority.runtimeSessionId;
499
527
  } else {
@@ -783,6 +811,7 @@ export class PaneStreamAdmissionCoordinator {
783
811
  let live: PaneStreamLiveConnection;
784
812
  try {
785
813
  live = new PaneStreamLiveConnection({
814
+ localImages: this.#localImages,
786
815
  clientId: `psc-${this.#clientCounter}`,
787
816
  socket,
788
817
  descriptor: structuredClone(descriptor),
@@ -1001,6 +1030,7 @@ class PreAuthAdmission implements PaneStreamPreAuthAdmission {
1001
1030
  }
1002
1031
 
1003
1032
  interface LiveConnectionOptions {
1033
+ readonly localImages?: LocalImageOwner;
1004
1034
  readonly clientId: string;
1005
1035
  readonly socket: DirectTerminalSocket;
1006
1036
  readonly descriptor: PaneStreamLeaseDescriptor;
@@ -1057,6 +1087,10 @@ interface PaneChannel {
1057
1087
  graphicsServed: Map<number, string>;
1058
1088
  graphicsState: TerminalGraphicsState | null;
1059
1089
  graphicsZlib?: boolean;
1090
+ graphicsBinary?: boolean;
1091
+ binaryCells?: boolean;
1092
+ graphicsLocal?: boolean;
1093
+ graphicsInline?: Set<string>;
1060
1094
  }
1061
1095
 
1062
1096
  interface QueuedSend {
@@ -1088,6 +1122,8 @@ export function rawGraphicsState(state: TerminalGraphicsState): TerminalGraphics
1088
1122
  const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
1089
1123
 
1090
1124
  export class PaneStreamLiveConnection {
1125
+ readonly #localImages: LocalImageOwner | undefined;
1126
+ readonly #localLeases = new Map<string, { pane: string; expiresAt: number }>();
1091
1127
  readonly #clientId: string;
1092
1128
  readonly #socket: DirectTerminalSocket;
1093
1129
  readonly #descriptor: PaneStreamLeaseDescriptor;
@@ -1212,6 +1248,7 @@ export class PaneStreamLiveConnection {
1212
1248
  }
1213
1249
 
1214
1250
  constructor(options: LiveConnectionOptions) {
1251
+ this.#localImages = options.localImages;
1215
1252
  this.#clientId = options.clientId;
1216
1253
  this.#socket = options.socket;
1217
1254
  this.#descriptor = options.descriptor;
@@ -1279,6 +1316,18 @@ export class PaneStreamLiveConnection {
1279
1316
  try {
1280
1317
  sendControl(this.#socket, {
1281
1318
  type: "ready",
1319
+ ...(this.#sessionRuntimeBinding &&
1320
+ this.#descriptor.terminalDelivery?.protocolVersions.includes(
1321
+ PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION,
1322
+ )
1323
+ ? { inputLifetime: true as const }
1324
+ : {}),
1325
+ ...(this.#sessionRuntimeBinding &&
1326
+ this.#descriptor.terminalDelivery?.protocolVersions.includes(
1327
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
1328
+ )
1329
+ ? { inputBytes: true as const }
1330
+ : {}),
1282
1331
  protocolVersion: PANE_STREAM_PROTOCOL_VERSION,
1283
1332
  daemonInstanceId: this.#binding.daemonInstanceId,
1284
1333
  requestId: this.#binding.requestId,
@@ -1321,6 +1370,14 @@ export class PaneStreamLiveConnection {
1321
1370
  this.#stopAuthoritySnapshots?.();
1322
1371
  this.#stopAuthoritySnapshots = null;
1323
1372
  const closures: Promise<unknown>[] = [];
1373
+ this.#localLeases.clear();
1374
+ if (this.#localImages)
1375
+ closures.push(
1376
+ this.#localImages.releaseClient({
1377
+ generation: this.#localImages.identity.generation,
1378
+ client: this.#descriptor.leaseId,
1379
+ }),
1380
+ );
1324
1381
  const layoutSubscription = this.#layoutSubscription;
1325
1382
  this.#layoutSubscription = null;
1326
1383
  if (layoutSubscription) closures.push(layoutSubscription.close().catch(() => undefined));
@@ -1549,7 +1606,8 @@ export class PaneStreamLiveConnection {
1549
1606
  if (negotiated.generation !== this.#binding.daemonInstanceId) return true;
1550
1607
  let incarnation: string | null = null;
1551
1608
  for (const message of pending) {
1552
- if (message.type !== "terminal.delivery") continue;
1609
+ if (message.type !== "terminal.delivery" && message.type !== "terminal.projection")
1610
+ continue;
1553
1611
  if (
1554
1612
  message.workspaceName !== this.#descriptor.workspaceName ||
1555
1613
  message.semanticPaneId !== channel.semanticPaneId ||
@@ -1671,13 +1729,19 @@ export class PaneStreamLiveConnection {
1671
1729
  }
1672
1730
 
1673
1731
  async #sendTerminalDelivery(pane: string, message: TerminalDeliveryServerMessage): Promise<void> {
1674
- if (message.type === "terminal.delivery") {
1732
+ if (message.type === "terminal.projection") {
1733
+ const channel = this.#panes.get(pane);
1734
+ if (channel?.deliveryAddress) channel.deliveryAddress.incarnation = message.incarnation;
1735
+ if (channel) channel.binaryCells = true;
1736
+ this.#sendFrame(pane, { type: "terminal-projection-envelope", pane, envelope: message });
1737
+ } else if (message.type === "terminal.delivery") {
1675
1738
  if (!this.#diagnosticFirstSeedObserved) {
1676
1739
  this.#diagnosticFirstSeedObserved = true;
1677
1740
  this.#recordDiagnosticLifecycle("pane-stream-first-seed");
1678
1741
  }
1679
1742
  const channel = this.#panes.get(pane);
1680
1743
  if (channel?.deliveryAddress) channel.deliveryAddress.incarnation = message.incarnation;
1744
+ if (channel) channel.binaryCells = message.encoding === "semantic-binary-v1";
1681
1745
  // The trusted lease address was fixed at delivery admission; preserve the
1682
1746
  // exact envelope and its observation identity through transmission.
1683
1747
  const detailedDeliveryObservation = this.#observability?.enabled === true;
@@ -1760,13 +1824,26 @@ export class PaneStreamLiveConnection {
1760
1824
  }
1761
1825
  }
1762
1826
  } else if (message.type === "terminal.delivery.chunk") {
1763
- this.#sendFrame(pane, {
1764
- type: "terminal-delivery-chunk",
1765
- pane,
1766
- transactionId: message.transactionId,
1767
- index: message.index,
1768
- data: Buffer.from(message.bytes).toString("base64"),
1769
- });
1827
+ if (this.#panes.get(pane)?.binaryCells) {
1828
+ this.#sendEncoded(
1829
+ pane,
1830
+ Buffer.from(
1831
+ encodeTerminalDeliveryBinaryChunk(
1832
+ { pane, transactionId: message.transactionId, index: message.index },
1833
+ message.bytes,
1834
+ ),
1835
+ ),
1836
+ true,
1837
+ );
1838
+ } else {
1839
+ this.#sendFrame(pane, {
1840
+ type: "terminal-delivery-chunk",
1841
+ pane,
1842
+ transactionId: message.transactionId,
1843
+ index: message.index,
1844
+ data: Buffer.from(message.bytes).toString("base64"),
1845
+ });
1846
+ }
1770
1847
  } else {
1771
1848
  this.#sendFrame(pane, { type: "terminal-delivery-fault", pane, fault: message });
1772
1849
  // Lifecycle/control truth must never queue behind bulk terminal bytes.
@@ -1788,6 +1865,25 @@ export class PaneStreamLiveConnection {
1788
1865
  TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION,
1789
1866
  ) === true;
1790
1867
  channel.graphicsZlib = zlib;
1868
+ channel.graphicsLocal =
1869
+ !!this.#localImages &&
1870
+ this.#descriptor.terminalDelivery?.protocolVersions.some(
1871
+ (version) =>
1872
+ version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
1873
+ version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
1874
+ version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
1875
+ ) === true;
1876
+ channel.graphicsInline = new Set();
1877
+ channel.graphicsBinary =
1878
+ this.#descriptor.terminalDelivery?.protocolVersions.some(
1879
+ (version) =>
1880
+ version === TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION ||
1881
+ version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
1882
+ version === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
1883
+ version === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
1884
+ version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
1885
+ version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
1886
+ ) === true;
1791
1887
  // Renderers that report cell pixels learn up front that this daemon
1792
1888
  // accepts them, before any image exists.
1793
1889
  const cellPixels =
@@ -1819,6 +1915,8 @@ export class PaneStreamLiveConnection {
1819
1915
  ...(zlib ? state : rawGraphicsState(state)),
1820
1916
  ...(cellPixels ? { cellPixels: true as const } : {}),
1821
1917
  ...(preciseCellPixels ? { preciseCellPixels: true as const } : {}),
1918
+ ...(channel.graphicsBinary ? { binaryBlobs: true as const } : {}),
1919
+ ...(channel.graphicsLocal ? { localFiles: true as const } : {}),
1822
1920
  },
1823
1921
  });
1824
1922
  if (pushed.length > 0) this.#queueGraphics(channel, pushed);
@@ -1834,15 +1932,23 @@ export class PaneStreamLiveConnection {
1834
1932
  placements: [],
1835
1933
  cellPixels: true,
1836
1934
  ...(preciseCellPixels ? { preciseCellPixels: true as const } : {}),
1935
+ ...(channel.graphicsBinary ? { binaryBlobs: true as const } : {}),
1936
+ ...(channel.graphicsLocal ? { localFiles: true as const } : {}),
1837
1937
  },
1838
1938
  });
1839
1939
  }
1840
1940
  }
1841
1941
 
1842
- #requestGraphics(pane: string, digests: readonly string[]): void {
1942
+ #requestGraphics(pane: string, digests: readonly string[], preferInline = false): void {
1843
1943
  const channel = this.#panes.get(pane);
1844
1944
  // Requests are only meaningful after an announcement; ignore stray ones.
1845
1945
  if (!channel || channel.closed || !channel.graphicsAnnounced) return;
1946
+ if (preferInline && channel.graphicsLocal) {
1947
+ for (const digest of digests) {
1948
+ if ((channel.graphicsInline?.size ?? 0) < GRAPHICS_MAX_QUEUED_DIGESTS)
1949
+ channel.graphicsInline?.add(digest);
1950
+ }
1951
+ }
1846
1952
  this.#queueGraphics(channel, digests);
1847
1953
  }
1848
1954
 
@@ -1866,6 +1972,7 @@ export class PaneStreamLiveConnection {
1866
1972
  try {
1867
1973
  while (!this.#closed && !channel.closed && channel.graphicsQueue.length > 0) {
1868
1974
  const digest = channel.graphicsQueue.shift()!;
1975
+ const inline = channel.graphicsInline?.delete(digest) ?? false;
1869
1976
  const graphics = channel.delivery?.graphics;
1870
1977
  const image = graphics?.image(digest);
1871
1978
  if (!graphics || !image) continue;
@@ -1887,6 +1994,47 @@ export class PaneStreamLiveConnection {
1887
1994
  continue;
1888
1995
  const format = raw ? (image.format === "rgba-zlib" ? "rgba" : "rgb") : image.format;
1889
1996
  const total = data.byteLength;
1997
+ if (!inline && channel.graphicsLocal && !raw && this.#localImages) {
1998
+ for (const [id, lease] of this.#localLeases)
1999
+ if (lease.expiresAt <= Date.now()) this.#localLeases.delete(id);
2000
+ if (this.#localLeases.size < 64) {
2001
+ try {
2002
+ const lease = await this.#localImages.acquire({
2003
+ generation: this.#localImages.identity.generation,
2004
+ client: this.#descriptor.leaseId,
2005
+ digest,
2006
+ bytes: data,
2007
+ });
2008
+ if (this.#closed || channel.closed || graphics.image(digest) !== image) {
2009
+ await this.#localImages.release(lease);
2010
+ continue;
2011
+ }
2012
+ this.#localLeases.set(lease.id, {
2013
+ pane: channel.semanticPaneId,
2014
+ expiresAt: lease.expiresAt,
2015
+ });
2016
+ this.#sendFrame(null, {
2017
+ type: "terminal-graphics-local",
2018
+ pane: channel.semanticPaneId,
2019
+ generation: lease.generation,
2020
+ leaseId: lease.id,
2021
+ path: lease.path,
2022
+ expiresAt: lease.expiresAt,
2023
+ blob: {
2024
+ digest,
2025
+ format,
2026
+ width: image.width,
2027
+ height: image.height,
2028
+ total,
2029
+ offset: 0,
2030
+ },
2031
+ });
2032
+ continue;
2033
+ } catch {
2034
+ /* Optional transport: retain the negotiated inline fallback. */
2035
+ }
2036
+ }
2037
+ }
1890
2038
  for (let offset = 0; offset < total; offset += TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES) {
1891
2039
  while ((this.#socket.bufferedAmount ?? 0) > GRAPHICS_SEND_WINDOW_BYTES) {
1892
2040
  if (this.#closed || channel.closed) return;
@@ -1897,6 +2045,29 @@ export class PaneStreamLiveConnection {
1897
2045
  // the renderer drops the partial image and shows the newer one.
1898
2046
  if (offset > 0 && graphics.image(digest) !== image) break;
1899
2047
  const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
2048
+ if (channel.graphicsBinary) {
2049
+ this.#sendEncoded(
2050
+ null,
2051
+ Buffer.from(
2052
+ encodeTerminalGraphicsBinary(
2053
+ {
2054
+ pane: channel.semanticPaneId,
2055
+ blob: {
2056
+ digest,
2057
+ format,
2058
+ width: image.width,
2059
+ height: image.height,
2060
+ total,
2061
+ offset,
2062
+ },
2063
+ },
2064
+ data.subarray(offset, end),
2065
+ ),
2066
+ ),
2067
+ true,
2068
+ );
2069
+ continue;
2070
+ }
1900
2071
  this.#sendFrame(null, {
1901
2072
  type: "terminal-graphics-blob",
1902
2073
  pane: channel.semanticPaneId,
@@ -2230,7 +2401,12 @@ export class PaneStreamLiveConnection {
2230
2401
  #sendFrame(pane: string | null, frame: Record<string, unknown>): void {
2231
2402
  if (this.#closed || this.#socket.readyState !== WS_OPEN) return;
2232
2403
  const encoded = JSON.stringify(frame);
2233
- const bytes = Buffer.byteLength(encoded, "utf8");
2404
+ this.#sendEncoded(pane, encoded, false);
2405
+ }
2406
+
2407
+ #sendEncoded(pane: string | null, encoded: string | Buffer, binary: boolean): void {
2408
+ if (this.#closed || this.#socket.readyState !== WS_OPEN) return;
2409
+ const bytes = Buffer.byteLength(encoded);
2234
2410
  if (pane !== null) {
2235
2411
  this.#ledger.take(this.#clientId, pane, "ws-send-buffer", bytes);
2236
2412
  if (this.#deliveryAcks) this.#ledger.take(this.#clientId, pane, "renderer-backlog", 1);
@@ -2238,7 +2414,7 @@ export class PaneStreamLiveConnection {
2238
2414
  this.#sendQueue.push({ pane, remaining: bytes });
2239
2415
  this.#sentBytesTotal += bytes;
2240
2416
  try {
2241
- this.#socket.send(encoded, { binary: false });
2417
+ this.#socket.send(encoded, { binary });
2242
2418
  } catch {
2243
2419
  this.close(1011, "stream-unavailable");
2244
2420
  return;
@@ -2460,6 +2636,20 @@ export class PaneStreamLiveConnection {
2460
2636
  });
2461
2637
  return;
2462
2638
  }
2639
+ if (frame.type === "terminal-projection-receipt") {
2640
+ const channel = this.#deliveryChannel(frame.receipt);
2641
+ if (!channel) return;
2642
+ if (!channel.delivery?.projectionReceipt) return this.#failProtocol("protocol-error");
2643
+ channel.delivery.projectionReceipt(frame.receipt);
2644
+ return;
2645
+ }
2646
+ if (frame.type === "terminal-projection-materialize") {
2647
+ const channel = this.#deliveryChannel(frame.request);
2648
+ if (!channel) return;
2649
+ if (!channel.delivery?.materialize) return this.#failProtocol("protocol-error");
2650
+ channel.delivery.materialize(frame.request);
2651
+ return;
2652
+ }
2463
2653
  if (frame.type === "terminal-delivery-ack") {
2464
2654
  const channel = this.#deliveryChannel(frame.ack);
2465
2655
  if (!channel) return;
@@ -2489,7 +2679,25 @@ export class PaneStreamLiveConnection {
2489
2679
  return;
2490
2680
  }
2491
2681
  if (frame.type === "terminal-graphics-request") {
2492
- this.#requestGraphics(frame.pane, frame.digests);
2682
+ this.#requestGraphics(frame.pane, frame.digests, frame.preferInline === true);
2683
+ return;
2684
+ }
2685
+ if (frame.type === "terminal-graphics-local-release") {
2686
+ const lease = this.#localLeases.get(frame.leaseId);
2687
+ if (
2688
+ lease &&
2689
+ lease.pane === frame.pane &&
2690
+ frame.generation === this.#localImages?.identity.generation
2691
+ ) {
2692
+ this.#localLeases.delete(frame.leaseId);
2693
+ void this.#localImages
2694
+ ?.release({
2695
+ generation: frame.generation,
2696
+ client: this.#descriptor.leaseId,
2697
+ id: frame.leaseId,
2698
+ })
2699
+ .catch(() => {});
2700
+ }
2493
2701
  return;
2494
2702
  }
2495
2703
  if (frame.type === "terminal-cell-pixels") {
@@ -2560,6 +2768,7 @@ export class PaneStreamLiveConnection {
2560
2768
  ingressAtMicros,
2561
2769
  callbackAtSharedMicros,
2562
2770
  ingressAtSharedMicros,
2771
+ frame.expectedLifetime,
2563
2772
  );
2564
2773
  };
2565
2774
 
@@ -2704,7 +2913,7 @@ export class PaneStreamLiveConnection {
2704
2913
  #acceptInput(
2705
2914
  pane: string,
2706
2915
  seq: number,
2707
- kind: "text" | "key",
2916
+ kind: "text" | "key" | "bytes",
2708
2917
  data: string,
2709
2918
  frameBytes: number,
2710
2919
  performanceTraceId?: string,
@@ -2712,7 +2921,28 @@ export class PaneStreamLiveConnection {
2712
2921
  ingressAtMicros: number | null = null,
2713
2922
  callbackAtSharedMicros: number | null = null,
2714
2923
  ingressAtSharedMicros: number | null = null,
2924
+ expectedLifetime?: PaneStreamInputLifetime,
2715
2925
  ): void {
2926
+ if (
2927
+ kind === "bytes" &&
2928
+ (!this.#sessionRuntimeBinding ||
2929
+ !this.#descriptor.terminalDelivery?.protocolVersions.includes(
2930
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
2931
+ ))
2932
+ ) {
2933
+ this.#failProtocol("input-rejected");
2934
+ return;
2935
+ }
2936
+ if (
2937
+ expectedLifetime &&
2938
+ (!this.#sessionRuntimeBinding ||
2939
+ !this.#descriptor.terminalDelivery?.protocolVersions.includes(
2940
+ PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION,
2941
+ ))
2942
+ ) {
2943
+ this.#failProtocol("input-rejected");
2944
+ return;
2945
+ }
2716
2946
  if (this.#descriptor.viewerMode !== "interactive") {
2717
2947
  this.#failProtocol("input-rejected");
2718
2948
  return;
@@ -2832,9 +3062,11 @@ export class PaneStreamLiveConnection {
2832
3062
  }
2833
3063
  }
2834
3064
  : undefined,
3065
+ expectedLifetime,
2835
3066
  );
2836
3067
  else if (kind === "text") channel.sub!.sendText(data);
2837
- else channel.sub!.sendKey(data);
3068
+ else if (kind === "key") channel.sub!.sendKey(data);
3069
+ else throw new Error("Byte input requires semantic runtime");
2838
3070
  let ackStartedAtMicros: number | null = null;
2839
3071
  let ackStartedAtSharedMicros: number | null = null;
2840
3072
  if (trace) {
@@ -1,3 +1,4 @@
1
+ import type { LocalImageOwner } from "../graphics/local-image-owner.ts";
1
2
  import type { SemanticPaneCatalog } from "../attachments/semantic-pane-catalog.ts";
2
3
  import type { SessionRuntimeRegistry } from "../session-runtime/registry.ts";
3
4
  import { SessionRuntimeTransportBinder } from "../session-runtime/transport-binding.ts";
@@ -9,6 +10,7 @@ import {
9
10
  } from "./pane-stream-websocket.ts";
10
11
 
11
12
  export interface PaneStreamRuntimeOptions {
13
+ readonly localImages?: LocalImageOwner;
12
14
  readonly daemonInstanceId: string;
13
15
  readonly webSocketUrl: string;
14
16
  /** Canonical daemon-generation session/control owner. */
@@ -38,6 +40,7 @@ export class PaneStreamRuntime {
38
40
  const transportBinder = new SessionRuntimeTransportBinder(options.sessionRuntimeRegistry);
39
41
  this.coordinator = new PaneStreamAdmissionCoordinator({
40
42
  ...options.admission,
43
+ localImages: options.localImages,
41
44
  daemonInstanceId: options.daemonInstanceId,
42
45
  webSocketUrl: options.webSocketUrl,
43
46
  leaseManager,