tmux-ide 2.9.4 → 2.9.5
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 +4354 -3376
- package/package.json +7 -6
- package/packages/contracts/src/session-runtime.ts +2 -0
- package/packages/contracts/src/terminal-delivery.ts +4 -0
- package/packages/core/src/terminal-compact-capability.ts +31 -1
- package/packages/core/src/terminal-delivery.ts +234 -32
- package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
- package/packages/core/src/terminal-replica-owned-row.ts +165 -10
- package/packages/core/src/terminal-replica.ts +43 -21
- package/packages/daemon/dist/lib/daemon-embed.js +19 -34
- package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
- package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
- package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
- 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/native/tmux/darwin-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
- package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
- package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
- package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
- package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
- package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
- package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
- package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
- package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
- package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
- package/packages/daemon/src/lib/daemon-embed.ts +22 -34
- package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
- package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
- package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
- package/packages/daemon/src/terminal/mirror/events.ts +7 -7
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
- package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
- package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
- package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
- package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
- package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
- package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
- package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
- package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
- package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
- package/packages/daemon-client/src/workspace-client-types.ts +1 -0
- package/packages/daemon-client/src/workspace-client.ts +78 -5
- package/packages/tmux-bridge/src/index.ts +1 -0
- package/packages/tmux-bridge/src/runner.ts +25 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/** Tiny independent visual oracle for the SSH fixture; no production decoder. */
|
|
2
|
+
export type VisualCell = { text: string; width: number; fg: string; bg: string; bold: boolean };
|
|
3
|
+
export type VisualFrame = {
|
|
4
|
+
cells: VisualCell[][];
|
|
5
|
+
cursor: { x: number; y: number; visible: boolean };
|
|
6
|
+
};
|
|
7
|
+
export type CompletedFrame = {
|
|
8
|
+
cols: number;
|
|
9
|
+
rows: number;
|
|
10
|
+
char: number[];
|
|
11
|
+
fg: number[];
|
|
12
|
+
bg: number[];
|
|
13
|
+
attributes: number[];
|
|
14
|
+
text: string;
|
|
15
|
+
cursor: { x: number; y: number; visible: boolean };
|
|
16
|
+
};
|
|
17
|
+
function check(condition: unknown, message: string): asserts condition {
|
|
18
|
+
if (!condition) throw Error(message);
|
|
19
|
+
}
|
|
20
|
+
const blank = (): VisualCell => ({ text: "", width: 1, fg: "ffffff", bg: "000000", bold: false });
|
|
21
|
+
export function literalStyledFrame(marker: string, cols: 24 | 40 = 40): VisualFrame {
|
|
22
|
+
check(cols === 24 || cols === 40, "literal geometry");
|
|
23
|
+
check(marker.length <= cols, "marker width");
|
|
24
|
+
const cells = Array.from({ length: 8 }, () => Array.from({ length: cols }, blank));
|
|
25
|
+
[...marker].forEach((text, x) => (cells[0]![x] = { ...blank(), text }));
|
|
26
|
+
const initial = marker === "BEFORE_SSH";
|
|
27
|
+
const glyphs = initial ? ["X", "界", "", "é", "Z"] : ["Y", "q", "", "é", "R"];
|
|
28
|
+
glyphs.forEach(
|
|
29
|
+
(text, x) =>
|
|
30
|
+
(cells[1]![x] = {
|
|
31
|
+
text,
|
|
32
|
+
width: initial ? (x === 1 ? 2 : x === 2 ? 0 : 1) : 1,
|
|
33
|
+
fg: text ? "123456" : "ffffff",
|
|
34
|
+
bg: "214365",
|
|
35
|
+
bold: !!text,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
for (let x = 0; x < cols; x++)
|
|
39
|
+
cells[3]![x] = { ...blank(), text: x < 2 ? String(x + 1) : "", bg: "445566" };
|
|
40
|
+
return { cells, cursor: { x: 4, y: 2, visible: true } };
|
|
41
|
+
}
|
|
42
|
+
export function styledBytes(marker: string): string {
|
|
43
|
+
return `\x1b[0m\x1b[2J\x1b[H${marker}\x1b[2;1H\x1b[1;38;2;18;52;86;48;2;33;67;101m${marker === "BEFORE_SSH" ? "X界éZ" : "Yq éR"}\x1b[0m\x1b[4;1H\x1b[48;2;68;85;102m123456\x1b[4;3H\x1b[K\x1b[0m\x1b[3;5H\x1b]2;${marker}\x07`;
|
|
44
|
+
}
|
|
45
|
+
export function readCompletedFrame(raw: CompletedFrame): VisualFrame {
|
|
46
|
+
check((raw.cols === 24 || raw.cols === 40) && raw.rows === 8, "geometry");
|
|
47
|
+
const size = raw.cols * raw.rows;
|
|
48
|
+
check(
|
|
49
|
+
raw.char.length === size &&
|
|
50
|
+
raw.attributes.length === size &&
|
|
51
|
+
raw.fg.length === size * 4 &&
|
|
52
|
+
raw.bg.length === size * 4,
|
|
53
|
+
"buffer lengths",
|
|
54
|
+
);
|
|
55
|
+
const lines = raw.text.replace(/\n$/u, "").split("\n");
|
|
56
|
+
check(lines.length === 8, "line count");
|
|
57
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
58
|
+
const rgb = (a: number[], i: number) => {
|
|
59
|
+
const c = a.slice(i * 4, i * 4 + 4);
|
|
60
|
+
check(
|
|
61
|
+
c.length === 4 && c.every((v) => Number.isInteger(v) && v >= 0 && v <= 255) && c[3] === 255,
|
|
62
|
+
"RGBA",
|
|
63
|
+
);
|
|
64
|
+
return c
|
|
65
|
+
.slice(0, 3)
|
|
66
|
+
.map((v) => v.toString(16).padStart(2, "0"))
|
|
67
|
+
.join("");
|
|
68
|
+
};
|
|
69
|
+
const continuation = (n: number) => n === 0 || n >>> 30 === 3;
|
|
70
|
+
const cells = lines.map((line, y) => {
|
|
71
|
+
const text = [...segmenter.segment(line)].map((s) => s.segment);
|
|
72
|
+
let pos = 0;
|
|
73
|
+
const row = Array.from({ length: raw.cols }, (_, x) => {
|
|
74
|
+
const i = y * raw.cols + x,
|
|
75
|
+
cp = raw.char[i]!;
|
|
76
|
+
const cont =
|
|
77
|
+
continuation(cp) && x > 0 && !continuation(raw.char[i - 1]!) && raw.char[i - 1] !== 32;
|
|
78
|
+
let glyph = "";
|
|
79
|
+
if (cont) {
|
|
80
|
+
if (cp === 0) {
|
|
81
|
+
check(text[pos] === " ", "direct continuation text");
|
|
82
|
+
pos++;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
glyph = text[pos++] ?? "";
|
|
86
|
+
check(/^(?:[\x20-\x7e]|界|é)$/u.test(glyph), "unsupported grapheme");
|
|
87
|
+
}
|
|
88
|
+
const width = cont ? 0 : x < raw.cols - 1 && continuation(raw.char[i + 1]!) ? 2 : 1;
|
|
89
|
+
const kind = cp >>> 30;
|
|
90
|
+
if (cont) {
|
|
91
|
+
const prev = raw.char[i - 1]!;
|
|
92
|
+
check(
|
|
93
|
+
prev === 0x754c || (prev >>> 30 === 2 && ((prev >>> 28) & 3) === 1),
|
|
94
|
+
"continuation owner",
|
|
95
|
+
);
|
|
96
|
+
if (kind === 3)
|
|
97
|
+
check(
|
|
98
|
+
((cp >>> 26) & 3) === 1 &&
|
|
99
|
+
((cp >>> 28) & 3) === 0 &&
|
|
100
|
+
(cp & 0x03ffffff) === (prev & 0x03ffffff),
|
|
101
|
+
"interned continuation",
|
|
102
|
+
);
|
|
103
|
+
} else if (kind === 0) {
|
|
104
|
+
check(
|
|
105
|
+
cp !== 0 && glyph.codePointAt(0) === cp && [...glyph].length === 1,
|
|
106
|
+
"direct codepoint mismatch",
|
|
107
|
+
);
|
|
108
|
+
check(width === (glyph === "界" ? 2 : 1), "direct width");
|
|
109
|
+
} else if (kind === 2) {
|
|
110
|
+
check(
|
|
111
|
+
glyph === "é" && ((cp >>> 26) & 3) === 0 && ((cp >>> 28) & 3) === 0 && width === 1,
|
|
112
|
+
"interned grapheme shape",
|
|
113
|
+
);
|
|
114
|
+
} else check(false, "unsupported raw char flags");
|
|
115
|
+
const visible = glyph !== " " && glyph !== "";
|
|
116
|
+
check((raw.attributes[i]! & ~1) === 0, "unsupported attributes");
|
|
117
|
+
return {
|
|
118
|
+
text: visible ? glyph : "",
|
|
119
|
+
width,
|
|
120
|
+
fg: visible ? rgb(raw.fg, i) : "ffffff",
|
|
121
|
+
bg: rgb(raw.bg, i),
|
|
122
|
+
bold: visible && (raw.attributes[i]! & 1) !== 0,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
check(pos === text.length, "unconsumed text");
|
|
126
|
+
return row;
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
cells,
|
|
130
|
+
cursor: { x: raw.cursor.x - 1, y: raw.cursor.y - 1, visible: raw.cursor.visible },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function compareVisual(actual: VisualFrame, expected: VisualFrame): void {
|
|
134
|
+
check(JSON.stringify(actual.cursor) === JSON.stringify(expected.cursor), "cursor mismatch");
|
|
135
|
+
check(actual.cells.length === expected.cells.length, "rows mismatch");
|
|
136
|
+
actual.cells.forEach((row, y) => {
|
|
137
|
+
check(row.length === expected.cells[y]!.length, "cols mismatch");
|
|
138
|
+
row.forEach((cell, x) => {
|
|
139
|
+
for (const key of ["text", "width", "fg", "bg", "bold"] as const)
|
|
140
|
+
check(cell[key] === expected.cells[y]![x]![key], `${y}:${x}.${key} mismatch`);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function nativeVisualFrame(frame: {
|
|
146
|
+
cols: number;
|
|
147
|
+
rows: number;
|
|
148
|
+
cursor: number[];
|
|
149
|
+
cells: {
|
|
150
|
+
text: string;
|
|
151
|
+
width: number;
|
|
152
|
+
foreground: string;
|
|
153
|
+
background: string;
|
|
154
|
+
attributes: string[];
|
|
155
|
+
}[][];
|
|
156
|
+
}): VisualFrame {
|
|
157
|
+
check((frame.cols === 24 || frame.cols === 40) && frame.rows === 8, "native geometry");
|
|
158
|
+
const color = (value: string, defaultValue: string) => {
|
|
159
|
+
if (value === "default") return defaultValue;
|
|
160
|
+
check(/^rgb:[0-9a-f]{6}$/u.test(value), "native color vocabulary");
|
|
161
|
+
return value.slice(4);
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
cells: frame.cells.map((row) =>
|
|
165
|
+
row.map((c) => {
|
|
166
|
+
const visible = c.text !== "" && c.text !== " ";
|
|
167
|
+
check(
|
|
168
|
+
c.attributes.every((a) => a === "bold"),
|
|
169
|
+
"native attribute vocabulary",
|
|
170
|
+
);
|
|
171
|
+
return {
|
|
172
|
+
text: visible ? c.text : "",
|
|
173
|
+
width: c.width,
|
|
174
|
+
fg: visible ? color(c.foreground, "ffffff") : "ffffff",
|
|
175
|
+
bg: color(c.background, "000000"),
|
|
176
|
+
bold: visible && c.attributes.includes("bold"),
|
|
177
|
+
};
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
cursor: { x: frame.cursor[0]!, y: frame.cursor[1]!, visible: true },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Declared workspace chrome: one tab row plus one title row, then eight native rows. */
|
|
185
|
+
export function cropWorkspaceCompletedFrame(raw: CompletedFrame): CompletedFrame {
|
|
186
|
+
check((raw.cols === 24 || raw.cols === 40) && raw.rows === 10, "workspace geometry");
|
|
187
|
+
const size = raw.cols * raw.rows;
|
|
188
|
+
check(
|
|
189
|
+
raw.char.length === size &&
|
|
190
|
+
raw.attributes.length === size &&
|
|
191
|
+
raw.fg.length === size * 4 &&
|
|
192
|
+
raw.bg.length === size * 4,
|
|
193
|
+
"workspace buffer lengths",
|
|
194
|
+
);
|
|
195
|
+
const lines = raw.text.replace(/\n$/u, "").split("\n");
|
|
196
|
+
check(lines.length === 10, "workspace line count");
|
|
197
|
+
check(raw.cursor.y >= 3 && raw.cursor.y <= 10, "workspace cursor outside content");
|
|
198
|
+
const offset = 2 * raw.cols;
|
|
199
|
+
return {
|
|
200
|
+
cols: raw.cols,
|
|
201
|
+
rows: 8,
|
|
202
|
+
char: raw.char.slice(offset),
|
|
203
|
+
attributes: raw.attributes.slice(offset),
|
|
204
|
+
fg: raw.fg.slice(offset * 4),
|
|
205
|
+
bg: raw.bg.slice(offset * 4),
|
|
206
|
+
text: lines.slice(2).join("\n"),
|
|
207
|
+
cursor: { ...raw.cursor, y: raw.cursor.y - 2 },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -29,6 +29,7 @@ export interface TerminalFastLaneSourcePort {
|
|
|
29
29
|
update: CanonicalTerminalReplicaUpdate,
|
|
30
30
|
metadata?: TerminalReplicaDeliveryMetadata,
|
|
31
31
|
) => void,
|
|
32
|
+
onUnavailable?: (message: string) => void,
|
|
32
33
|
): () => void;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -40,6 +41,7 @@ export interface CanonicalTerminalSubscriptionPort {
|
|
|
40
41
|
update: CanonicalTerminalReplicaUpdate,
|
|
41
42
|
metadata?: TerminalReplicaDeliveryMetadata,
|
|
42
43
|
) => void,
|
|
44
|
+
onUnavailable?: (message: string) => void,
|
|
43
45
|
) => () => void;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -173,7 +175,9 @@ export interface TerminalFastLane {
|
|
|
173
175
|
subscribePane(
|
|
174
176
|
semanticPaneId: string,
|
|
175
177
|
listener: (publication: TerminalFastLanePublication) => void,
|
|
178
|
+
onUnavailable?: (message: string) => void,
|
|
176
179
|
): () => void;
|
|
180
|
+
paneUnavailable(semanticPaneId: string): string | null;
|
|
177
181
|
paneState(semanticPaneId: string): TerminalReplicaState | null;
|
|
178
182
|
paneLastAcceptedUpdateType(semanticPaneId: string): CanonicalTerminalReplicaUpdate["type"] | null;
|
|
179
183
|
/** Request one coalesced generation repair for a pane whose retained state vanished. */
|
|
@@ -193,6 +197,8 @@ interface PaneInterest {
|
|
|
193
197
|
readonly semanticPaneId: string;
|
|
194
198
|
readonly listeners: Set<(publication: TerminalFastLanePublication) => void>;
|
|
195
199
|
state: TerminalReplicaState | null;
|
|
200
|
+
unavailable: string | null;
|
|
201
|
+
readonly unavailableListeners: Set<(message: string) => void>;
|
|
196
202
|
lastAcceptedUpdateType: CanonicalTerminalReplicaUpdate["type"] | null;
|
|
197
203
|
repairPending: boolean;
|
|
198
204
|
release: (() => void) | null;
|
|
@@ -384,7 +390,7 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
384
390
|
update: CanonicalTerminalReplicaUpdate,
|
|
385
391
|
metadata?: TerminalReplicaDeliveryMetadata,
|
|
386
392
|
): void => {
|
|
387
|
-
if (disposed) return;
|
|
393
|
+
if (disposed || interest.unavailable) return;
|
|
388
394
|
const traceEnabled = Boolean(metadata?.performanceTraceId && options.onTraceStage);
|
|
389
395
|
const identity = traceEnabled
|
|
390
396
|
? {
|
|
@@ -480,6 +486,25 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
480
486
|
(update, metadata) => {
|
|
481
487
|
if (!disposed && epoch === generationEpoch) accept(interest, update, metadata);
|
|
482
488
|
},
|
|
489
|
+
(message) => {
|
|
490
|
+
if (
|
|
491
|
+
disposed ||
|
|
492
|
+
epoch !== generationEpoch ||
|
|
493
|
+
panes.get(interest.semanticPaneId) !== interest
|
|
494
|
+
)
|
|
495
|
+
return;
|
|
496
|
+
interest.unavailable = message;
|
|
497
|
+
interest.state = null;
|
|
498
|
+
interest.repairPending = false;
|
|
499
|
+
for (const listener of [...interest.unavailableListeners]) {
|
|
500
|
+
try {
|
|
501
|
+
listener(message);
|
|
502
|
+
} catch {
|
|
503
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
drainInput();
|
|
507
|
+
},
|
|
483
508
|
);
|
|
484
509
|
};
|
|
485
510
|
|
|
@@ -506,6 +531,8 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
506
531
|
semanticPaneId,
|
|
507
532
|
listeners: new Set(),
|
|
508
533
|
state: null,
|
|
534
|
+
unavailable: null,
|
|
535
|
+
unavailableListeners: new Set<(message: string) => void>(),
|
|
509
536
|
lastAcceptedUpdateType: null,
|
|
510
537
|
repairPending: false,
|
|
511
538
|
release: null,
|
|
@@ -587,6 +614,10 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
587
614
|
rejectInput(input, "retired");
|
|
588
615
|
continue;
|
|
589
616
|
}
|
|
617
|
+
if (panes.get(input.semanticPaneId)?.unavailable) {
|
|
618
|
+
rejectInput(input, "authority-lost");
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
590
621
|
dispatchInput(input, epoch);
|
|
591
622
|
}
|
|
592
623
|
return;
|
|
@@ -635,6 +666,7 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
635
666
|
interest.release?.();
|
|
636
667
|
interest.release = null;
|
|
637
668
|
interest.state = null;
|
|
669
|
+
interest.unavailable = null;
|
|
638
670
|
interest.lastAcceptedUpdateType = null;
|
|
639
671
|
interest.repairPending = false;
|
|
640
672
|
open(interest);
|
|
@@ -669,7 +701,7 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
669
701
|
}
|
|
670
702
|
};
|
|
671
703
|
},
|
|
672
|
-
subscribePane(semanticPaneId, listener) {
|
|
704
|
+
subscribePane(semanticPaneId, listener, onUnavailable) {
|
|
673
705
|
if (disposed) return () => undefined;
|
|
674
706
|
if (retainedPaneIds !== null && !retainedPaneIds.has(semanticPaneId)) return () => undefined;
|
|
675
707
|
const interest =
|
|
@@ -678,18 +710,31 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
678
710
|
semanticPaneId,
|
|
679
711
|
listeners: new Set(),
|
|
680
712
|
state: null,
|
|
713
|
+
unavailable: null,
|
|
714
|
+
unavailableListeners: new Set<(message: string) => void>(),
|
|
681
715
|
lastAcceptedUpdateType: null,
|
|
682
716
|
repairPending: false,
|
|
683
717
|
release: null,
|
|
684
718
|
} satisfies PaneInterest);
|
|
685
719
|
panes.set(semanticPaneId, interest);
|
|
686
720
|
interest.listeners.add(listener);
|
|
721
|
+
if (onUnavailable) {
|
|
722
|
+
interest.unavailableListeners.add(onUnavailable);
|
|
723
|
+
if (interest.unavailable) {
|
|
724
|
+
try {
|
|
725
|
+
onUnavailable(interest.unavailable);
|
|
726
|
+
} catch {
|
|
727
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
687
731
|
open(interest);
|
|
688
732
|
let active = true;
|
|
689
733
|
return () => {
|
|
690
734
|
if (!active) return;
|
|
691
735
|
active = false;
|
|
692
736
|
interest.listeners.delete(listener);
|
|
737
|
+
if (onUnavailable) interest.unavailableListeners.delete(onUnavailable);
|
|
693
738
|
// Renderer visibility is not terminal-replica lifetime. A pane surface
|
|
694
739
|
// unmounts when its tmux window is not selected, but output can keep
|
|
695
740
|
// arriving while hidden. Retain the one canonical pane replica and its
|
|
@@ -699,19 +744,22 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
|
|
|
699
744
|
// bounded by the runtime inventory.
|
|
700
745
|
};
|
|
701
746
|
},
|
|
747
|
+
paneUnavailable: (semanticPaneId) => panes.get(semanticPaneId)?.unavailable ?? null,
|
|
702
748
|
paneState: (semanticPaneId) => panes.get(semanticPaneId)?.state ?? null,
|
|
703
749
|
paneLastAcceptedUpdateType: (semanticPaneId) =>
|
|
704
750
|
panes.get(semanticPaneId)?.lastAcceptedUpdateType ?? null,
|
|
705
751
|
requestRepair(semanticPaneId, reason) {
|
|
706
752
|
if (disposed) return false;
|
|
707
753
|
const interest = panes.get(semanticPaneId);
|
|
708
|
-
if (!interest || !isRetainedOrStaged(semanticPaneId)) return false;
|
|
754
|
+
if (!interest || interest.unavailable || !isRetainedOrStaged(semanticPaneId)) return false;
|
|
709
755
|
const revision = interest.state?.revision ?? 0;
|
|
710
756
|
queueRepair(interest, reason, revision, revision);
|
|
711
757
|
return interest.repairPending;
|
|
712
758
|
},
|
|
713
759
|
sendInput(semanticPaneId, rawInput, performanceTraceId, causalProbe) {
|
|
714
760
|
if (disposed) return Promise.resolve({ status: "rejected", reason: "disposed" });
|
|
761
|
+
if (panes.get(semanticPaneId)?.unavailable)
|
|
762
|
+
return Promise.resolve({ status: "rejected", reason: "authority-lost" });
|
|
715
763
|
const parsed = SessionRuntimeTerminalInputSchemaZ.safeParse(rawInput);
|
|
716
764
|
if (!parsed.success) {
|
|
717
765
|
mutableCounters.inputRejected += 1;
|
|
@@ -809,13 +857,14 @@ export function createWorkspaceClientTerminalSource(
|
|
|
809
857
|
client: CanonicalTerminalSubscriptionPort,
|
|
810
858
|
): TerminalFastLaneSourcePort {
|
|
811
859
|
return {
|
|
812
|
-
subscribe(address, listener) {
|
|
860
|
+
subscribe(address, listener, onUnavailable) {
|
|
813
861
|
return client.subscribeTerminal(
|
|
814
862
|
{
|
|
815
863
|
workspaceName: address.workspaceName,
|
|
816
864
|
semanticPaneId: address.semanticPaneId,
|
|
817
865
|
},
|
|
818
866
|
listener,
|
|
867
|
+
onUnavailable,
|
|
819
868
|
);
|
|
820
869
|
},
|
|
821
870
|
};
|
|
@@ -263,6 +263,7 @@ export interface WorkspaceClient<
|
|
|
263
263
|
update: TerminalReplicaUpdate<TerminalSnapshot, TerminalPatch, TerminalTombstone>,
|
|
264
264
|
metadata?: TerminalReplicaDeliveryMetadata,
|
|
265
265
|
) => void,
|
|
266
|
+
onUnavailable?: (message: string) => void,
|
|
266
267
|
): () => void;
|
|
267
268
|
requestTerminalRepair(
|
|
268
269
|
target: TerminalReplicaAddress,
|
|
@@ -75,6 +75,9 @@ interface TerminalInterest<Snapshot, Patch, Tombstone> {
|
|
|
75
75
|
ReturnType<WorkspaceClientRuntimePort<Snapshot, Patch, Tombstone>["subscribeTerminal"]>
|
|
76
76
|
> | null;
|
|
77
77
|
unsubscribeUpdate: (() => void) | null;
|
|
78
|
+
unsubscribeUnavailable: (() => void) | null;
|
|
79
|
+
unavailable: string | null;
|
|
80
|
+
readonly unavailableListeners: Set<(message: string) => void>;
|
|
78
81
|
opening: boolean;
|
|
79
82
|
}
|
|
80
83
|
|
|
@@ -89,6 +92,8 @@ interface PreparedTerminalBinding<Snapshot, Patch, Tombstone> {
|
|
|
89
92
|
ReturnType<WorkspaceClientRuntimePort<Snapshot, Patch, Tombstone>["subscribeTerminal"]>
|
|
90
93
|
>;
|
|
91
94
|
readonly unsubscribeUpdate: () => void;
|
|
95
|
+
readonly unsubscribeUnavailable: () => void;
|
|
96
|
+
unavailable: string | null;
|
|
92
97
|
readonly pending: Array<{
|
|
93
98
|
readonly update: TerminalReplicaUpdate<Snapshot, Patch, Tombstone>;
|
|
94
99
|
readonly metadata?: TerminalReplicaDeliveryMetadata;
|
|
@@ -518,6 +523,9 @@ export function createWorkspaceClient<
|
|
|
518
523
|
interest: TerminalInterest<TerminalSnapshot, TerminalPatch, TerminalTombstone>,
|
|
519
524
|
): Promise<void> => {
|
|
520
525
|
interest.opening = false;
|
|
526
|
+
interest.unsubscribeUnavailable?.();
|
|
527
|
+
interest.unsubscribeUnavailable = null;
|
|
528
|
+
interest.unavailable = null;
|
|
521
529
|
interest.unsubscribeUpdate?.();
|
|
522
530
|
interest.unsubscribeUpdate = null;
|
|
523
531
|
const subscription = interest.subscription;
|
|
@@ -534,6 +542,7 @@ export function createWorkspaceClient<
|
|
|
534
542
|
const closing: Promise<void>[] = [];
|
|
535
543
|
for (const binding of prepared.bindings.values()) {
|
|
536
544
|
binding.unsubscribeUpdate();
|
|
545
|
+
binding.unsubscribeUnavailable();
|
|
537
546
|
closing.push(closeSubscription(binding.subscription));
|
|
538
547
|
}
|
|
539
548
|
prepared.bindings.clear();
|
|
@@ -562,6 +571,19 @@ export function createWorkspaceClient<
|
|
|
562
571
|
}
|
|
563
572
|
}
|
|
564
573
|
};
|
|
574
|
+
const publishUnavailable = (
|
|
575
|
+
interest: TerminalInterest<TerminalSnapshot, TerminalPatch, TerminalTombstone>,
|
|
576
|
+
message: string,
|
|
577
|
+
): void => {
|
|
578
|
+
interest.unavailable = message;
|
|
579
|
+
for (const listener of [...interest.unavailableListeners]) {
|
|
580
|
+
try {
|
|
581
|
+
listener(message);
|
|
582
|
+
} catch {
|
|
583
|
+
/* One pane observer cannot interrupt siblings. */
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
565
587
|
const openTerminal = (
|
|
566
588
|
interest: TerminalInterest<TerminalSnapshot, TerminalPatch, TerminalTombstone>,
|
|
567
589
|
): void => {
|
|
@@ -591,8 +613,19 @@ export function createWorkspaceClient<
|
|
|
591
613
|
return closeSubscription(subscription);
|
|
592
614
|
}
|
|
593
615
|
interest.subscription = subscription;
|
|
616
|
+
interest.unsubscribeUnavailable =
|
|
617
|
+
subscription.onUnavailable?.((message) => {
|
|
618
|
+
if (
|
|
619
|
+
disposed ||
|
|
620
|
+
generation !== expectedGeneration ||
|
|
621
|
+
runtime !== expectedRuntime ||
|
|
622
|
+
interest.subscription !== subscription
|
|
623
|
+
)
|
|
624
|
+
return;
|
|
625
|
+
publishUnavailable(interest, message);
|
|
626
|
+
}) ?? null;
|
|
594
627
|
interest.unsubscribeUpdate = subscription.onUpdate((update, metadata) => {
|
|
595
|
-
if (generation !== expectedGeneration) return;
|
|
628
|
+
if (generation !== expectedGeneration || interest.unavailable) return;
|
|
596
629
|
publishTerminalUpdate(interest, expectedRuntime, update, metadata);
|
|
597
630
|
});
|
|
598
631
|
})
|
|
@@ -702,12 +735,17 @@ export function createWorkspaceClient<
|
|
|
702
735
|
for (const binding of bindings) {
|
|
703
736
|
binding.interest.subscription = binding.subscription;
|
|
704
737
|
binding.interest.unsubscribeUpdate = binding.unsubscribeUpdate;
|
|
738
|
+
binding.interest.unsubscribeUnavailable = binding.unsubscribeUnavailable;
|
|
705
739
|
binding.interest.opening = false;
|
|
706
740
|
}
|
|
707
741
|
prepared.bindings.clear();
|
|
708
742
|
owner.prepared = null;
|
|
709
743
|
prepared.activated = true;
|
|
710
744
|
for (const binding of bindings) {
|
|
745
|
+
if (binding.unavailable) {
|
|
746
|
+
publishUnavailable(binding.interest, binding.unavailable);
|
|
747
|
+
binding.pending.length = 0;
|
|
748
|
+
}
|
|
711
749
|
for (const delivery of binding.pending) {
|
|
712
750
|
publishTerminalUpdate(
|
|
713
751
|
binding.interest,
|
|
@@ -778,8 +816,9 @@ export function createWorkspaceClient<
|
|
|
778
816
|
TerminalPatch,
|
|
779
817
|
TerminalTombstone
|
|
780
818
|
>["pending"] = [];
|
|
819
|
+
let subscriptionUnavailable = false;
|
|
781
820
|
const unsubscribeUpdate = subscription.onUpdate((update, metadata) => {
|
|
782
|
-
if (prepared.closed) return;
|
|
821
|
+
if (prepared.closed || subscriptionUnavailable) return;
|
|
783
822
|
if (prepared.activated) {
|
|
784
823
|
publishTerminalUpdate(interest, nextRuntime, update, metadata);
|
|
785
824
|
return;
|
|
@@ -821,12 +860,31 @@ export function createWorkspaceClient<
|
|
|
821
860
|
await closeSubscription(subscription);
|
|
822
861
|
return;
|
|
823
862
|
}
|
|
824
|
-
|
|
863
|
+
const binding = {
|
|
825
864
|
interest,
|
|
826
865
|
subscription,
|
|
827
866
|
unsubscribeUpdate,
|
|
867
|
+
unsubscribeUnavailable: () => {},
|
|
868
|
+
unavailable: null as string | null,
|
|
828
869
|
pending,
|
|
829
|
-
}
|
|
870
|
+
};
|
|
871
|
+
binding.unsubscribeUnavailable =
|
|
872
|
+
subscription.onUnavailable?.((message) => {
|
|
873
|
+
if (
|
|
874
|
+
prepared.closed ||
|
|
875
|
+
disposed ||
|
|
876
|
+
generation !== owner.clientGeneration ||
|
|
877
|
+
(activeRuntimeOwner !== owner && candidateRuntimeOwner !== owner) ||
|
|
878
|
+
terminals.get(key) !== interest
|
|
879
|
+
)
|
|
880
|
+
return;
|
|
881
|
+
subscriptionUnavailable = true;
|
|
882
|
+
binding.unavailable = message;
|
|
883
|
+
binding.pending.length = 0;
|
|
884
|
+
if (prepared.activated && runtime === nextRuntime)
|
|
885
|
+
publishUnavailable(interest, message);
|
|
886
|
+
}) ?? (() => {});
|
|
887
|
+
prepared.bindings.set(key, binding);
|
|
830
888
|
}),
|
|
831
889
|
];
|
|
832
890
|
});
|
|
@@ -1327,7 +1385,7 @@ export function createWorkspaceClient<
|
|
|
1327
1385
|
throw error;
|
|
1328
1386
|
}
|
|
1329
1387
|
},
|
|
1330
|
-
subscribeTerminal(nextTarget, listener) {
|
|
1388
|
+
subscribeTerminal(nextTarget, listener, onUnavailable) {
|
|
1331
1389
|
if (disposed) return () => undefined;
|
|
1332
1390
|
const key = terminalKey(nextTarget);
|
|
1333
1391
|
const interest =
|
|
@@ -1337,19 +1395,34 @@ export function createWorkspaceClient<
|
|
|
1337
1395
|
listeners: new Set(),
|
|
1338
1396
|
subscription: null,
|
|
1339
1397
|
unsubscribeUpdate: null,
|
|
1398
|
+
unsubscribeUnavailable: null,
|
|
1399
|
+
unavailable: null,
|
|
1400
|
+
unavailableListeners: new Set<(message: string) => void>(),
|
|
1340
1401
|
opening: false,
|
|
1341
1402
|
} satisfies TerminalInterest<TerminalSnapshot, TerminalPatch, TerminalTombstone>);
|
|
1342
1403
|
terminals.set(key, interest);
|
|
1343
1404
|
interest.listeners.add(listener);
|
|
1405
|
+
if (onUnavailable) {
|
|
1406
|
+
interest.unavailableListeners.add(onUnavailable);
|
|
1407
|
+
if (interest.unavailable) {
|
|
1408
|
+
try {
|
|
1409
|
+
onUnavailable(interest.unavailable);
|
|
1410
|
+
} catch {
|
|
1411
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1344
1415
|
openTerminal(interest);
|
|
1345
1416
|
return () => {
|
|
1346
1417
|
interest.listeners.delete(listener);
|
|
1418
|
+
if (onUnavailable) interest.unavailableListeners.delete(onUnavailable);
|
|
1347
1419
|
if (interest.listeners.size > 0) return;
|
|
1348
1420
|
for (const owner of [activeRuntimeOwner, candidateRuntimeOwner]) {
|
|
1349
1421
|
const prepared = owner?.prepared?.bindings.get(key);
|
|
1350
1422
|
if (!prepared) continue;
|
|
1351
1423
|
owner!.prepared!.bindings.delete(key);
|
|
1352
1424
|
prepared.unsubscribeUpdate();
|
|
1425
|
+
prepared.unsubscribeUnavailable();
|
|
1353
1426
|
void closeSubscription(prepared.subscription);
|
|
1354
1427
|
}
|
|
1355
1428
|
terminals.delete(key);
|
|
@@ -58,11 +58,35 @@ export function _getSpawner(): Spawner {
|
|
|
58
58
|
return _spawner;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
type TmuxClientResolver = (
|
|
62
|
+
environment: NodeJS.ProcessEnv,
|
|
63
|
+
cwd?: string | URL,
|
|
64
|
+
) => {
|
|
65
|
+
executable: string;
|
|
66
|
+
environment: NodeJS.ProcessEnv;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let clientResolver: TmuxClientResolver | undefined;
|
|
70
|
+
|
|
71
|
+
/** Configure ordinary clients at the host composition root; pinned runners are unaffected. */
|
|
72
|
+
export function configureTmuxClientResolver(resolver: TmuxClientResolver): () => void {
|
|
73
|
+
const previous = clientResolver;
|
|
74
|
+
clientResolver = resolver;
|
|
75
|
+
return () => {
|
|
76
|
+
clientResolver = previous;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
export function runTmux(args: string[], options: ExecFileSyncOptions = {}): string | Buffer {
|
|
62
81
|
if ((options.env ?? process.env).TMUX_IDE_RUNTIME_MODE === "development") {
|
|
63
82
|
throw new Error("Development tmux operations require an explicitly pinned namespace runner");
|
|
64
83
|
}
|
|
65
|
-
|
|
84
|
+
const resolved = clientResolver?.(options.env ?? process.env, options.cwd);
|
|
85
|
+
return runTmuxBinary(
|
|
86
|
+
resolved?.executable ?? "tmux",
|
|
87
|
+
args,
|
|
88
|
+
resolved ? { ...options, env: resolved.environment } : options,
|
|
89
|
+
);
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
/**
|