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,4 @@
1
+ import { rendererProbe } from "./renderer-probe.ts";
1
2
  import { paneInteractionDisplayDestination } from "../ui/pane-interaction-presentation.ts";
2
3
  import type { InteractionObservationStatus, NativePaneIdentity } from "@tmux-ide/contracts";
3
4
  import { interactionPaneEndpointKey } from "@tmux-ide/core";
@@ -1350,6 +1351,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1350
1351
  return;
1351
1352
  }
1352
1353
  if (event.type === "scroll") {
1354
+ const probeAt = rendererProbe() ? Math.floor(performance.now() * 1000) : 0;
1353
1355
  // A held selection owns the gesture even when the pointer crosses a sibling.
1354
1356
  const activeSelection = selecting;
1355
1357
  const selectionCell =
@@ -1359,7 +1361,10 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1359
1361
  activeSelection && selectionCell
1360
1362
  ? { frame: activeSelection.frame, ...selectionCell }
1361
1363
  : paneContentAt(point);
1362
- const before = props.onWheelObservation && hit ? scrollback.offset(hit.frame.paneId) : null;
1364
+ const before =
1365
+ (props.onWheelObservation || rendererProbe()) && hit
1366
+ ? scrollback.offset(hit.frame.paneId)
1367
+ : null;
1363
1368
  const observe = (route: string): void => {
1364
1369
  props.onWheelObservation?.({
1365
1370
  paneId: hit?.frame.paneId ?? null,
@@ -1457,6 +1462,20 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1457
1462
  scrollback.seek(hit.frame.paneId, { ...origin, y: next.originY });
1458
1463
  setKeyboardCopy({ ...keyboard, cursor: next.cursor });
1459
1464
  } else scrollback.move(hit.frame.paneId, motion.lines);
1465
+ if (rendererProbe() && identity && before !== null)
1466
+ rendererProbe()?.scroll(
1467
+ {
1468
+ pane: hit.frame.paneId,
1469
+ generation: identity.generation,
1470
+ incarnation: identity.incarnation,
1471
+ cols: snapshot.cols,
1472
+ rows: snapshot.rows,
1473
+ },
1474
+ snapshot,
1475
+ before,
1476
+ scrollback.offset(hit.frame.paneId),
1477
+ probeAt,
1478
+ );
1460
1479
  observe("local-history");
1461
1480
  if (selecting?.paneId === hit.frame.paneId) updatePointerSelection(selecting.pointer);
1462
1481
  else {
@@ -0,0 +1,257 @@
1
+ /** Opt-in controlled nonce workload; renderer-buffer completion, never visible pixels. */
2
+ import type { TerminalReplicaState } from "@tmux-ide/core";
3
+ import { visibleTerminalRowAt } from "../semantic-pane-render-source.ts";
4
+ import type { TerminalReplicaRow, TerminalReplicaSnapshot } from "@tmux-ide/contracts";
5
+ import type { SessionRuntimeTerminalInput } from "@tmux-ide/contracts";
6
+ import { appendFile } from "node:fs/promises";
7
+ import { isAbsolute } from "node:path";
8
+ export interface ProbeTrial {
9
+ id: string;
10
+ nonce: string;
11
+ kind?: "input" | "scroll" | "output";
12
+ }
13
+ export interface ProbeRecord extends ProbeTrial {
14
+ side: "tui";
15
+ kind: "input" | "scroll" | "output";
16
+ sourceScope: Scope;
17
+ expectedOffset: number;
18
+ sourceOffset: number;
19
+ expectedScope: Scope;
20
+ admittedAtMicros: number;
21
+ completedAtMicros: number;
22
+ witness?: { row: number; column: number; marker: string; revision: number; stateHash: string };
23
+ endpoint: "input-admission-to-renderer-work-complete";
24
+ status: "completed" | "failed";
25
+ durationMicros?: number;
26
+ reason?: string;
27
+ }
28
+ interface Scope {
29
+ pane: string;
30
+ generation: string;
31
+ incarnation: string;
32
+ cols: number;
33
+ rows: number;
34
+ }
35
+ function scope(state: TerminalReplicaState): Scope | null {
36
+ return state.snapshot
37
+ ? {
38
+ pane: state.semanticPaneId,
39
+ generation: state.generation,
40
+ incarnation: state.incarnation,
41
+ cols: state.snapshot.cols,
42
+ rows: state.snapshot.rows,
43
+ }
44
+ : null;
45
+ }
46
+ const key = (s: Scope) => JSON.stringify([s.pane, s.generation, s.incarnation, s.cols, s.rows]);
47
+ export class RendererProbe {
48
+ #next = 0;
49
+ #pending: {
50
+ trial: ProbeTrial;
51
+ scope: Scope;
52
+ at: number;
53
+ marker: string;
54
+ offset: number;
55
+ sourceOffset: number;
56
+ } | null = null;
57
+ constructor(
58
+ private readonly trials: readonly ProbeTrial[],
59
+ private readonly emit: (r: ProbeRecord) => void,
60
+ ) {
61
+ if (
62
+ trials.length > 64 ||
63
+ new Set(trials.map((t) => t.nonce)).size !== trials.length ||
64
+ trials.some(
65
+ (t) =>
66
+ !t.id ||
67
+ t.id.length > 128 ||
68
+ !/^[a-f0-9]{32}$/.test(t.nonce) ||
69
+ (t.kind !== undefined && !["input", "scroll", "output"].includes(t.kind)),
70
+ )
71
+ )
72
+ throw new Error("Invalid renderer probe trials");
73
+ }
74
+ get pending(): boolean {
75
+ return this.#pending !== null;
76
+ }
77
+ admit(
78
+ input: SessionRuntimeTerminalInput,
79
+ state: TerminalReplicaState | null,
80
+ at: number,
81
+ ): SessionRuntimeTerminalInput | null {
82
+ if (
83
+ input.kind !== "text" ||
84
+ input.data !== "!" ||
85
+ !state?.snapshot ||
86
+ this.#pending ||
87
+ !this.trials[this.#next]
88
+ )
89
+ return null;
90
+ if (this.trials[this.#next]?.kind === "scroll")
91
+ return { kind: "text", data: `PROBE-PREPARE-SCROLL:${this.trials[this.#next]!.nonce}\n` };
92
+ const trial = this.trials[this.#next++]!,
93
+ marker = `PROBE-OUTPUT:${trial.nonce}`,
94
+ identity = scope(state)!;
95
+ this.#pending = { trial, scope: identity, at, marker, offset: 0, sourceOffset: 0 };
96
+ if (
97
+ state.snapshot.cols < marker.length ||
98
+ state.snapshot.grid.some((r) =>
99
+ r.cells
100
+ .map((c) => c.grapheme)
101
+ .join("")
102
+ .includes(marker),
103
+ )
104
+ ) {
105
+ this.cancel("negative-control", at);
106
+ return null;
107
+ }
108
+ return {
109
+ kind: "text",
110
+ data: `${trial.kind === "output" ? "PROBE-BURST" : "PROBE-IN"}:${trial.nonce}\n`,
111
+ };
112
+ }
113
+ scroll(
114
+ identity: Scope,
115
+ snapshot: TerminalReplicaSnapshot,
116
+ before: number,
117
+ after: number,
118
+ at: number,
119
+ ): void {
120
+ const trial = this.trials[this.#next];
121
+ if (this.#pending || trial?.kind !== "scroll" || before === after) return;
122
+ const row = visibleTerminalRowAt(snapshot, after, 0);
123
+ const text =
124
+ row?.cells
125
+ .map((c) => c.grapheme)
126
+ .join("")
127
+ .trimEnd() ?? "";
128
+ if (!new RegExp(`^PROBE-ROW:${trial.nonce}:[0-9]{4}$`).test(text)) return;
129
+ for (let y = 0; y < snapshot.rows; y++)
130
+ if (
131
+ visibleTerminalRowAt(snapshot, before, y)
132
+ ?.cells.map((c) => c.grapheme)
133
+ .join("")
134
+ .trimEnd() === text
135
+ )
136
+ return;
137
+ this.#next++;
138
+ this.#pending = {
139
+ trial,
140
+ scope: identity,
141
+ at,
142
+ marker: text,
143
+ offset: after,
144
+ sourceOffset: before,
145
+ };
146
+ }
147
+ tick(at: number): void {
148
+ if (this.#pending && at - this.#pending.at > 5_000_000) this.cancel("timeout", at);
149
+ }
150
+ cancel(reason: string, at: number): void {
151
+ const p = this.#pending;
152
+ if (!p) return;
153
+ this.#pending = null;
154
+ this.emit({
155
+ ...p.trial,
156
+ side: "tui",
157
+ kind: p.trial.kind ?? "input",
158
+ sourceScope: p.scope,
159
+ expectedScope: p.scope,
160
+ sourceOffset: p.sourceOffset,
161
+ expectedOffset: p.offset,
162
+ admittedAtMicros: p.at,
163
+ completedAtMicros: at,
164
+ endpoint: "input-admission-to-renderer-work-complete",
165
+ status: "failed",
166
+ reason,
167
+ });
168
+ }
169
+ observe(state: TerminalReplicaState | null, at: number): void {
170
+ if (!this.#pending) return;
171
+ if (state && state.semanticPaneId !== this.#pending.scope.pane) return;
172
+ if (!state || !scope(state) || key(scope(state)!) !== key(this.#pending.scope))
173
+ this.cancel("scope-changed", at);
174
+ else if (at - this.#pending.at > 5_000_000) this.cancel("timeout", at);
175
+ }
176
+ row(
177
+ state: TerminalReplicaState,
178
+ row: TerminalReplicaRow | undefined,
179
+ viewportRow: number,
180
+ width: number,
181
+ height: number,
182
+ left: number,
183
+ scrollOffset: number,
184
+ at: number,
185
+ ): void {
186
+ this.observe(state, at);
187
+ const p = this.#pending;
188
+ if (!p || state.semanticPaneId !== p.scope.pane) return;
189
+ if (
190
+ scrollOffset !== p.offset ||
191
+ left !== 0 ||
192
+ viewportRow !== 0 ||
193
+ height < 1 ||
194
+ width < p.marker.length ||
195
+ !row
196
+ )
197
+ return;
198
+ const cells = row.cells.slice(0, p.marker.length);
199
+ if (cells.some((c) => c.width !== 1) || cells.map((c) => c.grapheme).join("") !== p.marker)
200
+ return;
201
+ if (!Number.isSafeInteger(at) || at < p.at) {
202
+ this.cancel("clock-invalid", at);
203
+ return;
204
+ }
205
+ this.#pending = null;
206
+ this.emit({
207
+ ...p.trial,
208
+ side: "tui",
209
+ kind: p.trial.kind ?? "input",
210
+ sourceScope: p.scope,
211
+ expectedScope: p.scope,
212
+ sourceOffset: p.sourceOffset,
213
+ expectedOffset: p.offset,
214
+ admittedAtMicros: p.at,
215
+ completedAtMicros: at,
216
+ endpoint: "input-admission-to-renderer-work-complete",
217
+ status: "completed",
218
+ durationMicros: at - p.at,
219
+ witness: {
220
+ row: viewportRow,
221
+ column: 0,
222
+ marker: p.marker,
223
+ revision: state.revision,
224
+ stateHash: state.hash,
225
+ },
226
+ });
227
+ }
228
+ }
229
+ let singleton: RendererProbe | null | undefined;
230
+ /** All filesystem work is asynchronous and deferred; at most64 short receipts. */
231
+ export function rendererProbe(): RendererProbe | null {
232
+ if (singleton !== undefined) return singleton;
233
+ singleton = null;
234
+ if (process.env.TMUX_IDE_RENDERER_FIXTURE !== "1") return null;
235
+ const raw = process.env.TMUX_IDE_RENDERER_PROBE_TRIALS,
236
+ path = process.env.TMUX_IDE_RENDERER_PROBE_LOG;
237
+ if (!raw || raw.length > 16384 || !path || !isAbsolute(path)) return null;
238
+ try {
239
+ const trials: unknown = JSON.parse(raw);
240
+ if (!Array.isArray(trials)) return null;
241
+ let writes = Promise.resolve(),
242
+ failed = false;
243
+ singleton = new RendererProbe(trials, (record) => {
244
+ if (failed) return;
245
+ writes = writes
246
+ .then(() => appendFile(path, JSON.stringify(record) + "\n", { mode: 0o600 }))
247
+ .catch(() => {
248
+ failed = true;
249
+ });
250
+ });
251
+ const timer = setInterval(() => singleton?.tick(Math.floor(performance.now() * 1000)), 250);
252
+ timer.unref();
253
+ } catch {
254
+ singleton = null;
255
+ }
256
+ return singleton;
257
+ }
@@ -1,3 +1,4 @@
1
+ import { rendererProbe } from "./renderer-probe.ts";
1
2
  import type { ReadNativeBacking } from "../../../terminal/protocol/native-backing-client.ts";
2
3
  import type { TerminalReplicaSnapshot } from "@tmux-ide/contracts";
3
4
  import type {
@@ -637,6 +638,8 @@ export class TerminalFastLaneRendererAdapter implements PaneScopedTerminalAdapte
637
638
  // live parsing and must keep following the native pane's actual size.
638
639
  if (interest.retainedView && next) this.#resizeRetainedView(interest, next);
639
640
  interest.state = publication.state;
641
+ if (rendererProbe()?.pending)
642
+ rendererProbe()?.observe(publication.state, Math.floor(performance.now() * 1000));
640
643
  // Preserve coordinates for an unchanged prefix or an exact retained suffix.
641
644
  // The suffix offset is fixed by the length difference; never search repeated
642
645
  // log lines for a guessed overlap. Changed grids/reflow need a separate map.
@@ -992,6 +995,18 @@ export class TerminalFastLaneRendererAdapter implements PaneScopedTerminalAdapte
992
995
  options.palette,
993
996
  options.viewportOrigin?.x ?? 0,
994
997
  );
998
+ if (interest?.state && rendererProbe()?.pending) {
999
+ rendererProbe()?.row(
1000
+ interest.retainedView?.state ?? interest.state,
1001
+ sourceRow,
1002
+ row,
1003
+ width,
1004
+ height,
1005
+ options.viewportOrigin?.x ?? 0,
1006
+ options.viewportOrigin ? Math.max(0, -options.viewportOrigin.y) : scrollOffset,
1007
+ Math.floor(performance.now() * 1_000),
1008
+ );
1009
+ }
995
1010
  options.dirtyRows.push(row);
996
1011
  paintedRows[row] = sourceRow;
997
1012
  }
@@ -202,6 +202,10 @@ export class SemanticPaneReplica {
202
202
  }
203
203
 
204
204
  accept(message: TerminalDeliveryServerMessage): void {
205
+ if (message.type === "terminal.projection") {
206
+ this.#fail("protocol-violation", message.transactionId);
207
+ return;
208
+ }
205
209
  if (message.type === "terminal.delivery.fault") {
206
210
  this.#assembler = null;
207
211
  this.#snapshot = null;
@@ -1,5 +1,10 @@
1
1
  import {
2
2
  PANE_STREAM_ISSUE_PATH,
3
+ decodeTerminalDeliveryBinaryChunk,
4
+ decodeTerminalGraphicsBinary,
5
+ TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION,
6
+ TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION,
7
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
3
8
  PaneStreamInputLifetimeSchemaZ,
4
9
  type PaneStreamInputLifetime,
5
10
  PANE_STREAM_PROTOCOL_VERSION,
@@ -118,6 +123,7 @@ function createRuntimeTimer(callback: () => void, delayMs: number): RuntimeTimer
118
123
 
119
124
  export interface PaneStreamClientSocket {
120
125
  readonly readyState: number;
126
+ binaryType?: string;
121
127
  /** Optional native/socket queue watermark in bytes. */
122
128
  readonly bufferedAmount?: number;
123
129
  addEventListener(type: SocketEventType, listener: SocketListener): void;
@@ -346,6 +352,7 @@ export interface PaneStreamRuntimeClient {
346
352
  readonly authoritySnapshot: SessionRuntimeAuthoritySnapshot | null;
347
353
  readonly connectionClientId: string | null;
348
354
  readonly supportsInputLifetime?: boolean;
355
+ readonly supportsInputBytes?: boolean;
349
356
  ownsConnectionAuthority(authority: SessionRuntimeAuthorityKind): boolean;
350
357
  connectionAuthorityClientId(authority: SessionRuntimeAuthorityKind): string | null;
351
358
  setPresence(state: SessionRuntimePresenceState): void;
@@ -456,6 +463,7 @@ export async function connectIssuedPaneStreamRuntimeClient(
456
463
  "X-Tmux-Ide-Host-Client-Id": options.hostClientId,
457
464
  "X-Tmux-Ide-Request-Id": descriptor.requestId,
458
465
  });
466
+ socket.binaryType = "arraybuffer";
459
467
  options.onConnectionDiagnostic?.("socket-created", { requestId: descriptor.requestId });
460
468
  const releaseSocketResource = acquireRuntimeResource("pane-stream-socket");
461
469
  const releaseSocketListenerResources = acquireRuntimeResource("socket-listener", 4);
@@ -474,6 +482,13 @@ export async function connectIssuedPaneStreamRuntimeClient(
474
482
  };
475
483
  let closed = false;
476
484
  let verified = false;
485
+ const binaryPanes = new Set<string>();
486
+ const binaryGraphicsPanes = new Set<string>();
487
+ const offeredBinary = options.stream.terminalDelivery.protocolVersions.some(
488
+ (version) =>
489
+ version === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
490
+ version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION,
491
+ );
477
492
  let layoutTopologyEpoch = -1;
478
493
  let windowLinks: WindowLinkTopology | null = null;
479
494
  let resolveReady!: (client: PaneStreamRuntimeClient) => void;
@@ -989,6 +1004,7 @@ export async function connectIssuedPaneStreamRuntimeClient(
989
1004
  return release;
990
1005
  };
991
1006
  let supportsInputLifetime = false;
1007
+ let supportsInputBytes = false;
992
1008
  const sendTerminalInput = (
993
1009
  target: TerminalReplicaAddress,
994
1010
  rawInput: SessionRuntimeTerminalInput,
@@ -1003,6 +1019,8 @@ export async function connectIssuedPaneStreamRuntimeClient(
1003
1019
  ? undefined
1004
1020
  : PaneStreamInputLifetimeSchemaZ.parse(expectedLifetime);
1005
1021
  const input = SessionRuntimeTerminalInputSchemaZ.parse(rawInput);
1022
+ if (input.kind === "bytes" && !supportsInputBytes)
1023
+ return Promise.reject(new Error("Byte input capability unavailable"));
1006
1024
  const pane = target.semanticPaneId;
1007
1025
  if (!descriptor.panes.includes(pane)) {
1008
1026
  return Promise.reject(
@@ -1247,7 +1265,41 @@ export async function connectIssuedPaneStreamRuntimeClient(
1247
1265
  );
1248
1266
  };
1249
1267
  const onMessage: SocketListener = (event) => {
1250
- if (closed || typeof event.data !== "string") return fail("Pane-stream frame was not text");
1268
+ if (closed) return;
1269
+ if (typeof event.data !== "string") {
1270
+ if (!verified) return fail("Binary pane-stream frame before readiness");
1271
+ const data =
1272
+ event.data instanceof ArrayBuffer
1273
+ ? new Uint8Array(event.data)
1274
+ : event.data instanceof Uint8Array
1275
+ ? event.data
1276
+ : null;
1277
+ if (!data) return fail("Unsupported binary pane-stream data");
1278
+ try {
1279
+ if (data[0] === 84 && data[1] === 68 && data[2] === 67 && data[3] === 49) {
1280
+ const { header, bytes } = decodeTerminalDeliveryBinaryChunk(data);
1281
+ if (!descriptor.panes.includes(header.pane) || !binaryPanes.has(header.pane))
1282
+ return fail("Unnegotiated binary terminal delivery");
1283
+ options.onTerminalDelivery(header.pane, {
1284
+ type: "terminal.delivery.chunk",
1285
+ transactionId: header.transactionId,
1286
+ index: header.index,
1287
+ bytes:
1288
+ bytes.buffer instanceof ArrayBuffer
1289
+ ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
1290
+ : bytes.slice(),
1291
+ });
1292
+ } else {
1293
+ const { header, bytes } = decodeTerminalGraphicsBinary(data);
1294
+ if (!descriptor.panes.includes(header.pane) || !binaryGraphicsPanes.has(header.pane))
1295
+ return fail("Unnegotiated binary graphics delivery");
1296
+ options.onTerminalGraphicsBlob?.(header.pane, header.blob, bytes);
1297
+ }
1298
+ } catch {
1299
+ return fail("Invalid binary pane-stream frame");
1300
+ }
1301
+ return;
1302
+ }
1251
1303
  const arrivedAtMicros = options.onTerminalFrameArrival
1252
1304
  ? Math.floor(performance.now() * 1_000)
1253
1305
  : 0;
@@ -1299,6 +1351,11 @@ export async function connectIssuedPaneStreamRuntimeClient(
1299
1351
  return fail("Pane-stream peer client identity did not match the redeemed connection");
1300
1352
  connectionClientId = frame.connectionClientId ?? null;
1301
1353
  supportsInputLifetime = frame.inputLifetime === true;
1354
+ supportsInputBytes =
1355
+ frame.inputBytes === true &&
1356
+ options.stream.terminalDelivery.protocolVersions.includes(
1357
+ PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION,
1358
+ );
1302
1359
  verified = true;
1303
1360
  options.onConnectionDiagnostic?.("ready-frame", {
1304
1361
  requestId: descriptor.requestId,
@@ -1384,7 +1441,21 @@ export async function connectIssuedPaneStreamRuntimeClient(
1384
1441
  options.onCausalCellFailure?.(frame.failure);
1385
1442
  return;
1386
1443
  }
1444
+ if (frame.type === "terminal-graphics") {
1445
+ if (!descriptor.panes.includes(frame.pane)) return fail("Unissued graphics pane");
1446
+ if (frame.state.binaryBlobs === true) binaryGraphicsPanes.add(frame.pane);
1447
+ else binaryGraphicsPanes.delete(frame.pane);
1448
+ }
1387
1449
  if (frame.type === "terminal-delivery-ready") {
1450
+ if (!descriptor.panes.includes(frame.pane)) return fail("Unissued terminal pane");
1451
+ binaryPanes.delete(frame.pane);
1452
+ if (
1453
+ frame.negotiation.accepted &&
1454
+ frame.negotiation.negotiated.encoding === "semantic-binary-v1"
1455
+ ) {
1456
+ if (!offeredBinary) return fail("Unsolicited binary terminal negotiation");
1457
+ binaryPanes.add(frame.pane);
1458
+ }
1388
1459
  if (frame.negotiation.accepted) {
1389
1460
  negotiatedDeliveries.set(frame.pane, {
1390
1461
  generation: frame.negotiation.negotiated.generation,
@@ -1567,6 +1638,9 @@ export async function connectIssuedPaneStreamRuntimeClient(
1567
1638
  send({ type: "activity", generation: descriptor.daemonInstanceId, activity }),
1568
1639
  requestAuthority,
1569
1640
  releaseAuthority,
1641
+ get supportsInputBytes() {
1642
+ return supportsInputBytes;
1643
+ },
1570
1644
  get supportsInputLifetime() {
1571
1645
  return supportsInputLifetime;
1572
1646
  },