tmux-ide 2.9.11 → 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.
- package/bin/cli.js +4158 -2545
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +4 -0
- package/packages/contracts/src/pane-stream.ts +60 -0
- package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
- package/packages/contracts/src/terminal-delivery.ts +17 -2
- package/packages/contracts/src/terminal-graphics.ts +68 -0
- package/packages/contracts/src/terminal-projection.ts +153 -0
- package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/terminal-delivery-binary.ts +160 -0
- package/packages/core/src/terminal-delivery.ts +371 -16
- package/packages/core/src/terminal-projection-vectors.mjs +82 -0
- package/packages/core/src/terminal-projection.ts +476 -0
- package/packages/core/src/terminal-replica.ts +1 -1
- package/packages/daemon/dist/lib/daemon-embed.js +39 -3
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
- package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +176 -16
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
- package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
- package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
- package/packages/daemon/src/lib/daemon-embed.ts +38 -2
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +3 -0
- package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
- package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +219 -15
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
- package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
- package/packages/daemon-client/src/pane-stream-client.ts +75 -1
|
@@ -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
|
|
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
|
},
|