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.
- package/bin/cli.js +4321 -2596
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +4 -0
- package/packages/contracts/src/pane-stream.ts +72 -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-hash-cache.ts +8 -1
- 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/mirror/mirror-service.js +12 -2
- package/packages/daemon/dist/terminal/mirror/session-channel.js +46 -23
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +190 -19
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +19 -6
- 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-interpreter.js +95 -36
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +5 -0
- package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +2 -2
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-generation-starter.js +16 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agent-navigation.js +13 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/application-retained-agent.js +18 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +90 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +2 -2
- package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
- package/packages/daemon/dist/tui/mirror/runtime/runtime-layout-presentation.js +6 -3
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +7 -2
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-selection-waiter.js +36 -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/mirror/mirror-service.ts +19 -2
- package/packages/daemon/src/terminal/mirror/session-channel.ts +57 -9
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +248 -16
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +25 -3
- 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-interpreter.ts +102 -44
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +6 -0
- package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +3 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +6 -0
- package/packages/daemon/src/tui/mirror/runtime/application-generation-starter.ts +17 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agent-navigation.ts +15 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +18 -4
- package/packages/daemon/src/tui/mirror/runtime/application-retained-agent.ts +29 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +89 -6
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +2 -2
- package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
- package/packages/daemon/src/tui/mirror/runtime/runtime-layout-presentation.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-selection-waiter.ts +36 -0
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
- package/packages/daemon-client/src/pane-stream-client.ts +92 -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
|
+
}
|
|
@@ -11,7 +11,7 @@ export interface OpenTuiRuntimeLayoutPresentation {
|
|
|
11
11
|
getWindowSnapshot(): OpenTuiWorkspaceLayoutSnapshot;
|
|
12
12
|
subscribe(listener: (layout: OpenTuiTerminalLayout | null) => void): () => void;
|
|
13
13
|
subscribeWindows(listener: (layout: OpenTuiWorkspaceLayoutSnapshot) => void): () => void;
|
|
14
|
-
adopt(port: OpenTuiLayoutPort): () => void;
|
|
14
|
+
adopt(port: OpenTuiLayoutPort, rendererEpoch: number): () => void;
|
|
15
15
|
clear(): void;
|
|
16
16
|
dispose(): void;
|
|
17
17
|
}
|
|
@@ -66,21 +66,24 @@ export function createOpenTuiRuntimeLayoutPresentation(): OpenTuiRuntimeLayoutPr
|
|
|
66
66
|
listener(windows);
|
|
67
67
|
return () => windowListeners.delete(listener);
|
|
68
68
|
},
|
|
69
|
-
adopt(port) {
|
|
69
|
+
adopt(port, rendererEpoch) {
|
|
70
70
|
if (disposed) return () => undefined;
|
|
71
71
|
let candidate = true;
|
|
72
72
|
let candidateStop: (() => void) | null = null;
|
|
73
73
|
candidateStop = port.onLayout((layout) => {
|
|
74
74
|
if (disposed || !candidate || !layout.current) return;
|
|
75
|
+
// A port may reuse its snapshot object. Own the provenance wrapper so
|
|
76
|
+
// a preparing runtime cannot relabel the retained coherent frame.
|
|
77
|
+
const presented = Object.freeze({ ...layout, rendererEpoch });
|
|
75
78
|
if (currentPort !== port) {
|
|
76
79
|
const previousStop = stopLayout;
|
|
77
80
|
currentPort = port;
|
|
78
81
|
stopLayout = candidateStop;
|
|
79
|
-
publish(
|
|
82
|
+
publish(presented);
|
|
80
83
|
previousStop?.();
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
83
|
-
publish(
|
|
86
|
+
publish(presented);
|
|
84
87
|
});
|
|
85
88
|
// `onLayout` intentionally seeds synchronously. Capture the disposer
|
|
86
89
|
// after that first callback has promoted this candidate.
|
|
@@ -20,6 +20,7 @@ export type PaneSelectionFailure = Readonly<{
|
|
|
20
20
|
| "generation-replaced"
|
|
21
21
|
| "operation-timeout"
|
|
22
22
|
| "transport-rejected"
|
|
23
|
+
| "topology-changed"
|
|
23
24
|
| "receipt-invalid";
|
|
24
25
|
backendReason?:
|
|
25
26
|
| "pane_inventory_not_ready"
|
|
@@ -42,6 +43,7 @@ export async function selectTerminalPane(
|
|
|
42
43
|
operationId?: string,
|
|
43
44
|
onFailure?: (failure: PaneSelectionFailure) => void,
|
|
44
45
|
windowLink?: WindowLinkTarget,
|
|
46
|
+
onDispatch?: () => void,
|
|
45
47
|
): Promise<PaneSelectionReceipt | null> {
|
|
46
48
|
const fail = (failure: PaneSelectionFailure): null => {
|
|
47
49
|
try {
|
|
@@ -62,7 +64,12 @@ export async function selectTerminalPane(
|
|
|
62
64
|
: undefined;
|
|
63
65
|
return fail({
|
|
64
66
|
stage,
|
|
65
|
-
reason:
|
|
67
|
+
reason:
|
|
68
|
+
code === "operation-timeout"
|
|
69
|
+
? "operation-timeout"
|
|
70
|
+
: code === "topology-changed" && stage === "dispatch"
|
|
71
|
+
? "topology-changed"
|
|
72
|
+
: "transport-rejected",
|
|
66
73
|
...(backendReason ? { backendReason } : {}),
|
|
67
74
|
});
|
|
68
75
|
};
|
|
@@ -90,6 +97,7 @@ export async function selectTerminalPane(
|
|
|
90
97
|
if (!isCurrent()) return fail({ stage: "pre-dispatch", reason: "generation-replaced" });
|
|
91
98
|
let result: unknown;
|
|
92
99
|
try {
|
|
100
|
+
onDispatch?.();
|
|
93
101
|
result = await expected.client.dispatch({
|
|
94
102
|
kind: "semantic-intent",
|
|
95
103
|
...(operationId ? { operationId } : {}),
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** One bounded selection observation; never submits or repeats a mutation. */
|
|
2
|
+
export function createTerminalSelectionWaiter() {
|
|
3
|
+
let pending: {
|
|
4
|
+
check: () => boolean | "pending";
|
|
5
|
+
finish: (selected: boolean) => void;
|
|
6
|
+
} | null = null;
|
|
7
|
+
const cancel = () => pending?.finish(false);
|
|
8
|
+
const reconcile = () => {
|
|
9
|
+
const waiting = pending;
|
|
10
|
+
if (!waiting) return;
|
|
11
|
+
const result = waiting.check();
|
|
12
|
+
if (result !== "pending") waiting.finish(result);
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
cancel,
|
|
16
|
+
reconcile,
|
|
17
|
+
wait(check: () => boolean | "pending"): Promise<boolean> {
|
|
18
|
+
cancel();
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const timer = setTimeout(() => waiting.finish(false), 5000);
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
const waiting = {
|
|
23
|
+
check,
|
|
24
|
+
finish(selected: boolean) {
|
|
25
|
+
if (pending !== waiting) return;
|
|
26
|
+
pending = null;
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
resolve(selected);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
pending = waiting;
|
|
32
|
+
reconcile();
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -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,12 @@
|
|
|
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,
|
|
8
|
+
PaneStreamInputLifetimeSchemaZ,
|
|
9
|
+
type PaneStreamInputLifetime,
|
|
3
10
|
PANE_STREAM_PROTOCOL_VERSION,
|
|
4
11
|
PaneStreamClientFrameSchemaZ,
|
|
5
12
|
PaneStreamIssueMutationRequestSchemaZ,
|
|
@@ -116,6 +123,7 @@ function createRuntimeTimer(callback: () => void, delayMs: number): RuntimeTimer
|
|
|
116
123
|
|
|
117
124
|
export interface PaneStreamClientSocket {
|
|
118
125
|
readonly readyState: number;
|
|
126
|
+
binaryType?: string;
|
|
119
127
|
/** Optional native/socket queue watermark in bytes. */
|
|
120
128
|
readonly bufferedAmount?: number;
|
|
121
129
|
addEventListener(type: SocketEventType, listener: SocketListener): void;
|
|
@@ -343,6 +351,8 @@ export interface PaneStreamRuntimeClient {
|
|
|
343
351
|
readonly effectiveViewerMode: PaneStreamIssueDescriptor["effectiveViewerMode"];
|
|
344
352
|
readonly authoritySnapshot: SessionRuntimeAuthoritySnapshot | null;
|
|
345
353
|
readonly connectionClientId: string | null;
|
|
354
|
+
readonly supportsInputLifetime?: boolean;
|
|
355
|
+
readonly supportsInputBytes?: boolean;
|
|
346
356
|
ownsConnectionAuthority(authority: SessionRuntimeAuthorityKind): boolean;
|
|
347
357
|
connectionAuthorityClientId(authority: SessionRuntimeAuthorityKind): string | null;
|
|
348
358
|
setPresence(state: SessionRuntimePresenceState): void;
|
|
@@ -358,6 +368,7 @@ export interface PaneStreamRuntimeClient {
|
|
|
358
368
|
input: SessionRuntimeTerminalInput,
|
|
359
369
|
performanceTraceId?: string,
|
|
360
370
|
causalProbe?: CausalCellProbeRequestV1,
|
|
371
|
+
expectedLifetime?: PaneStreamInputLifetime,
|
|
361
372
|
): Promise<SessionRuntimeTerminalInputResult>;
|
|
362
373
|
sendText(pane: string, text: string, performanceTraceId?: string): void;
|
|
363
374
|
sendKey(pane: string, key: string, performanceTraceId?: string): void;
|
|
@@ -452,6 +463,7 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
452
463
|
"X-Tmux-Ide-Host-Client-Id": options.hostClientId,
|
|
453
464
|
"X-Tmux-Ide-Request-Id": descriptor.requestId,
|
|
454
465
|
});
|
|
466
|
+
socket.binaryType = "arraybuffer";
|
|
455
467
|
options.onConnectionDiagnostic?.("socket-created", { requestId: descriptor.requestId });
|
|
456
468
|
const releaseSocketResource = acquireRuntimeResource("pane-stream-socket");
|
|
457
469
|
const releaseSocketListenerResources = acquireRuntimeResource("socket-listener", 4);
|
|
@@ -470,6 +482,13 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
470
482
|
};
|
|
471
483
|
let closed = false;
|
|
472
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
|
+
);
|
|
473
492
|
let layoutTopologyEpoch = -1;
|
|
474
493
|
let windowLinks: WindowLinkTopology | null = null;
|
|
475
494
|
let resolveReady!: (client: PaneStreamRuntimeClient) => void;
|
|
@@ -984,13 +1003,24 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
984
1003
|
connectionAuthorityReleases.set(authority, release);
|
|
985
1004
|
return release;
|
|
986
1005
|
};
|
|
1006
|
+
let supportsInputLifetime = false;
|
|
1007
|
+
let supportsInputBytes = false;
|
|
987
1008
|
const sendTerminalInput = (
|
|
988
1009
|
target: TerminalReplicaAddress,
|
|
989
1010
|
rawInput: SessionRuntimeTerminalInput,
|
|
990
1011
|
performanceTraceId?: string,
|
|
991
1012
|
causalProbeRequest?: CausalCellProbeRequestV1,
|
|
1013
|
+
expectedLifetime?: PaneStreamInputLifetime,
|
|
992
1014
|
): Promise<SessionRuntimeTerminalInputResult> => {
|
|
1015
|
+
if (expectedLifetime && !supportsInputLifetime)
|
|
1016
|
+
return Promise.reject(new Error("Input lifetime capability unavailable"));
|
|
1017
|
+
const lifetime =
|
|
1018
|
+
expectedLifetime === undefined
|
|
1019
|
+
? undefined
|
|
1020
|
+
: PaneStreamInputLifetimeSchemaZ.parse(expectedLifetime);
|
|
993
1021
|
const input = SessionRuntimeTerminalInputSchemaZ.parse(rawInput);
|
|
1022
|
+
if (input.kind === "bytes" && !supportsInputBytes)
|
|
1023
|
+
return Promise.reject(new Error("Byte input capability unavailable"));
|
|
994
1024
|
const pane = target.semanticPaneId;
|
|
995
1025
|
if (!descriptor.panes.includes(pane)) {
|
|
996
1026
|
return Promise.reject(
|
|
@@ -1087,6 +1117,7 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
1087
1117
|
seq: sequence,
|
|
1088
1118
|
...(performanceTraceId ? { performanceTraceId } : {}),
|
|
1089
1119
|
...(causalProbe ? { causalProbe } : {}),
|
|
1120
|
+
...(lifetime ? { expectedLifetime: lifetime } : {}),
|
|
1090
1121
|
},
|
|
1091
1122
|
Boolean(transportStage),
|
|
1092
1123
|
);
|
|
@@ -1234,7 +1265,41 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
1234
1265
|
);
|
|
1235
1266
|
};
|
|
1236
1267
|
const onMessage: SocketListener = (event) => {
|
|
1237
|
-
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
|
+
}
|
|
1238
1303
|
const arrivedAtMicros = options.onTerminalFrameArrival
|
|
1239
1304
|
? Math.floor(performance.now() * 1_000)
|
|
1240
1305
|
: 0;
|
|
@@ -1285,6 +1350,12 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
1285
1350
|
)
|
|
1286
1351
|
return fail("Pane-stream peer client identity did not match the redeemed connection");
|
|
1287
1352
|
connectionClientId = frame.connectionClientId ?? null;
|
|
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
|
+
);
|
|
1288
1359
|
verified = true;
|
|
1289
1360
|
options.onConnectionDiagnostic?.("ready-frame", {
|
|
1290
1361
|
requestId: descriptor.requestId,
|
|
@@ -1370,7 +1441,21 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
1370
1441
|
options.onCausalCellFailure?.(frame.failure);
|
|
1371
1442
|
return;
|
|
1372
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
|
+
}
|
|
1373
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
|
+
}
|
|
1374
1459
|
if (frame.negotiation.accepted) {
|
|
1375
1460
|
negotiatedDeliveries.set(frame.pane, {
|
|
1376
1461
|
generation: frame.negotiation.negotiated.generation,
|
|
@@ -1553,6 +1638,12 @@ export async function connectIssuedPaneStreamRuntimeClient(
|
|
|
1553
1638
|
send({ type: "activity", generation: descriptor.daemonInstanceId, activity }),
|
|
1554
1639
|
requestAuthority,
|
|
1555
1640
|
releaseAuthority,
|
|
1641
|
+
get supportsInputBytes() {
|
|
1642
|
+
return supportsInputBytes;
|
|
1643
|
+
},
|
|
1644
|
+
get supportsInputLifetime() {
|
|
1645
|
+
return supportsInputLifetime;
|
|
1646
|
+
},
|
|
1556
1647
|
sendTerminalInput,
|
|
1557
1648
|
sendText: (pane, text, performanceTraceId) => {
|
|
1558
1649
|
void sendTerminalInput(
|