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
|
@@ -1,16 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSchemaValidProjectedTerminalReplicaRow,
|
|
3
|
+
TERMINAL_REPLICA_DEFAULT_COLOR,
|
|
4
|
+
} from "./terminal-replica-owned-row.ts";
|
|
1
5
|
import type { TerminalReplicaColor, TerminalReplicaRow } from "@tmux-ide/contracts";
|
|
2
6
|
import { createBufferedFnv64, type BufferedFnv64 } from "./terminal-fnv64-wasm.ts";
|
|
3
7
|
|
|
4
8
|
const ROW_HASH_CACHE = new WeakMap<object, string>();
|
|
5
9
|
const DEEPLY_FROZEN_ROWS = new WeakSet<object>();
|
|
6
10
|
const UTF8_ENCODER = new TextEncoder();
|
|
11
|
+
// Fixed ASCII headers cover common terminal strings without retaining values.
|
|
12
|
+
const SHORT_ASCII_HEADERS = Array.from({ length: 33 }, (_, length) => `s${length}:`);
|
|
7
13
|
|
|
8
14
|
interface CanonicalKeyOrder {
|
|
9
|
-
readonly original: string[];
|
|
10
|
-
readonly sorted: string[];
|
|
15
|
+
readonly original: readonly string[];
|
|
16
|
+
readonly sorted: readonly string[];
|
|
11
17
|
readonly tokens: readonly (string | null)[] | null;
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
// Only this exact internally constructed singleton proves the fixed layout.
|
|
21
|
+
// Foreign defaults (including frozen lookalikes) retain generic key discovery.
|
|
22
|
+
const DEFAULT_COLOR_KEYS = Object.freeze(["kind"]);
|
|
23
|
+
const DEFAULT_COLOR_KEY_ORDER: CanonicalKeyOrder = Object.freeze({
|
|
24
|
+
original: DEFAULT_COLOR_KEYS,
|
|
25
|
+
sorted: DEFAULT_COLOR_KEYS,
|
|
26
|
+
tokens: Object.freeze(["s4:kind;"]),
|
|
27
|
+
});
|
|
28
|
+
|
|
14
29
|
/**
|
|
15
30
|
* Streaming FNV-1a64 writer for the canonical terminal encoding.
|
|
16
31
|
*
|
|
@@ -43,6 +58,7 @@ class CanonicalFnv64 {
|
|
|
43
58
|
#keyOrders: CanonicalKeyOrder[] | null = null;
|
|
44
59
|
|
|
45
60
|
keyOrder(record: Record<string, unknown>): CanonicalKeyOrder {
|
|
61
|
+
if (record === TERMINAL_REPLICA_DEFAULT_COLOR) return DEFAULT_COLOR_KEY_ORDER;
|
|
46
62
|
const keys = Object.keys(record);
|
|
47
63
|
if (keys.length > 32) return { original: keys, sorted: keys.sort(), tokens: null };
|
|
48
64
|
for (const order of this.#keyOrders ?? []) {
|
|
@@ -116,7 +132,7 @@ class CanonicalFnv64 {
|
|
|
116
132
|
}
|
|
117
133
|
}
|
|
118
134
|
if (ascii) {
|
|
119
|
-
this.ascii(`s${value.length}:`);
|
|
135
|
+
this.ascii(SHORT_ASCII_HEADERS[value.length] ?? `s${value.length}:`);
|
|
120
136
|
this.ascii(value);
|
|
121
137
|
this.ascii(";");
|
|
122
138
|
return value.length;
|
|
@@ -129,6 +145,12 @@ class CanonicalFnv64 {
|
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
number(value: number): void {
|
|
148
|
+
// Common terminal widths and default attributes share exact canonical bytes.
|
|
149
|
+
// Equality intentionally preserves String(-0) === "0"; all other numbers
|
|
150
|
+
// retain the original formatting path.
|
|
151
|
+
if (value === 0) return void this.ascii("d1:0;");
|
|
152
|
+
if (value === 1) return void this.ascii("d1:1;");
|
|
153
|
+
if (value === 2) return void this.ascii("d1:2;");
|
|
132
154
|
const text = String(value);
|
|
133
155
|
this.ascii(`d${text.length}:${text};`);
|
|
134
156
|
}
|
|
@@ -137,6 +159,36 @@ class CanonicalFnv64 {
|
|
|
137
159
|
this.ascii(value ? "b1;" : "b0;");
|
|
138
160
|
}
|
|
139
161
|
|
|
162
|
+
#projectedRow(row: TerminalReplicaRow): void {
|
|
163
|
+
this.ascii("o2:s5:cells;");
|
|
164
|
+
const cells = row.cells;
|
|
165
|
+
this.ascii(`a${cells.length}:`);
|
|
166
|
+
let index = 0;
|
|
167
|
+
// Preserve dynamic array iteration. A replaced inherited iterator can yield
|
|
168
|
+
// foreign values; only the exact immutable cell at this slot earns the path.
|
|
169
|
+
for (const cell of cells) {
|
|
170
|
+
if (index < cells.length && cell === cells[index]) {
|
|
171
|
+
this.ascii("o5:s10:attributes;");
|
|
172
|
+
this.number(cell.attributes);
|
|
173
|
+
this.ascii("s10:background;");
|
|
174
|
+
writeColor(this, cell.background);
|
|
175
|
+
this.ascii("s10:foreground;");
|
|
176
|
+
writeColor(this, cell.foreground);
|
|
177
|
+
this.ascii("s8:grapheme;");
|
|
178
|
+
this.string(cell.grapheme);
|
|
179
|
+
this.ascii("s5:width;");
|
|
180
|
+
this.number(cell.width);
|
|
181
|
+
this.ascii(";");
|
|
182
|
+
} else {
|
|
183
|
+
this.value(cell);
|
|
184
|
+
}
|
|
185
|
+
index++;
|
|
186
|
+
}
|
|
187
|
+
this.ascii(";s7:wrapped;");
|
|
188
|
+
this.boolean(row.wrapped);
|
|
189
|
+
this.ascii(";");
|
|
190
|
+
}
|
|
191
|
+
|
|
140
192
|
value(value: unknown): void {
|
|
141
193
|
if (value === null) {
|
|
142
194
|
this.ascii("n;");
|
|
@@ -160,6 +212,10 @@ class CanonicalFnv64 {
|
|
|
160
212
|
this.ascii(";");
|
|
161
213
|
return;
|
|
162
214
|
}
|
|
215
|
+
if (isSchemaValidProjectedTerminalReplicaRow(value)) {
|
|
216
|
+
this.#projectedRow(value as TerminalReplicaRow);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
163
219
|
const record = value as Record<string, unknown>;
|
|
164
220
|
const { sorted: keys, tokens } = this.keyOrder(record);
|
|
165
221
|
this.ascii(`o${keys.length}:`);
|
|
@@ -198,22 +254,22 @@ export async function hashCanonicalTerminalValueCooperatively(
|
|
|
198
254
|
work = 0;
|
|
199
255
|
return true;
|
|
200
256
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
if (checkpoint(hash.ascii(
|
|
257
|
+
// Parallel stacks preserve the traversal and yield order without allocating
|
|
258
|
+
// a wrapper object for every value/key/token. pop() drops consumed references.
|
|
259
|
+
const kinds: ("value" | "ascii" | "string")[] = ["value"];
|
|
260
|
+
const values: unknown[] = [value];
|
|
261
|
+
while (values.length > 0) {
|
|
262
|
+
const kind = kinds.pop()!;
|
|
263
|
+
const value = values.pop();
|
|
264
|
+
if (kind === "ascii") {
|
|
265
|
+
if (checkpoint(hash.ascii(value as string))) await yieldControl();
|
|
210
266
|
continue;
|
|
211
267
|
}
|
|
212
|
-
if (
|
|
213
|
-
if (checkpoint(hash.string(
|
|
268
|
+
if (kind === "string") {
|
|
269
|
+
if (checkpoint(hash.string(value as string) + 8)) await yieldControl();
|
|
214
270
|
continue;
|
|
215
271
|
}
|
|
216
|
-
const entry =
|
|
272
|
+
const entry = value;
|
|
217
273
|
if (entry === null) {
|
|
218
274
|
hash.ascii("n;");
|
|
219
275
|
if (checkpoint(2)) await yieldControl();
|
|
@@ -235,19 +291,23 @@ export async function hashCanonicalTerminalValueCooperatively(
|
|
|
235
291
|
}
|
|
236
292
|
if (Array.isArray(entry)) {
|
|
237
293
|
if (checkpoint(hash.ascii(`a${entry.length}:`) + 1)) await yieldControl();
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
294
|
+
kinds.push("ascii");
|
|
295
|
+
values.push(";");
|
|
296
|
+
for (let index = entry.length - 1; index >= 0; index -= 1) {
|
|
297
|
+
kinds.push("value");
|
|
298
|
+
values.push(entry[index]);
|
|
299
|
+
}
|
|
241
300
|
continue;
|
|
242
301
|
}
|
|
243
302
|
const record = entry as Record<string, unknown>;
|
|
244
303
|
const keys = hash.keyOrder(record).sorted;
|
|
245
304
|
if (checkpoint(hash.ascii(`o${keys.length}:`) + keys.length)) await yieldControl();
|
|
246
|
-
|
|
305
|
+
kinds.push("ascii");
|
|
306
|
+
values.push(";");
|
|
247
307
|
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
248
308
|
const key = keys[index]!;
|
|
249
|
-
|
|
250
|
-
|
|
309
|
+
kinds.push("value", "string");
|
|
310
|
+
values.push(record[key], key);
|
|
251
311
|
}
|
|
252
312
|
}
|
|
253
313
|
return hash.digest();
|
|
@@ -473,6 +533,87 @@ export async function hashTerminalReplicaRowRunsCooperatively(
|
|
|
473
533
|
return digest;
|
|
474
534
|
}
|
|
475
535
|
|
|
536
|
+
// Scalar colors: -1 default, -2 RGB negative zero, 0..255 indexed,
|
|
537
|
+
// and 0x1000000 + value for other RGB values. No caller-owned color is adopted.
|
|
538
|
+
const DECODED_DEFAULT_COLOR = Object.freeze({ kind: "default" } as const);
|
|
539
|
+
function decodedScalarColor(value: number): TerminalReplicaColor {
|
|
540
|
+
if (value === -1) return DECODED_DEFAULT_COLOR;
|
|
541
|
+
if (value === -2) return Object.freeze({ kind: "rgb", value: -0 });
|
|
542
|
+
if (Number.isInteger(value) && value >= 0 && value <= 255)
|
|
543
|
+
return Object.freeze({ kind: "indexed", index: value });
|
|
544
|
+
if (Number.isInteger(value) && value >= 0x1000000 && value <= 0x1ffffff)
|
|
545
|
+
return Object.freeze({ kind: "rgb", value: value - 0x1000000 });
|
|
546
|
+
throw new TypeError("Invalid decoded scalar color");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Internal scalar construction only; no supplied graph, digest, or hash cache. */
|
|
550
|
+
class DecodedTerminalReplicaRowBuilder {
|
|
551
|
+
readonly #cells: TerminalReplicaRow["cells"][number][] = [];
|
|
552
|
+
#finished = false;
|
|
553
|
+
|
|
554
|
+
appendRun(
|
|
555
|
+
count: number,
|
|
556
|
+
grapheme: string,
|
|
557
|
+
width: 0 | 1 | 2,
|
|
558
|
+
foreground: number,
|
|
559
|
+
background: number,
|
|
560
|
+
attributes: number,
|
|
561
|
+
): void {
|
|
562
|
+
this.#assertOpen();
|
|
563
|
+
if (
|
|
564
|
+
!Number.isSafeInteger(count) ||
|
|
565
|
+
count < 1 ||
|
|
566
|
+
count > 16384 ||
|
|
567
|
+
typeof grapheme !== "string" ||
|
|
568
|
+
(width !== 0 && width !== 1 && width !== 2) ||
|
|
569
|
+
!Number.isInteger(attributes) ||
|
|
570
|
+
attributes < 0 ||
|
|
571
|
+
attributes > 255
|
|
572
|
+
)
|
|
573
|
+
throw new TypeError("Invalid decoded scalar run");
|
|
574
|
+
const cell = Object.freeze({
|
|
575
|
+
grapheme,
|
|
576
|
+
width,
|
|
577
|
+
foreground: decodedScalarColor(foreground),
|
|
578
|
+
background: decodedScalarColor(background),
|
|
579
|
+
attributes,
|
|
580
|
+
});
|
|
581
|
+
for (let index = 0; index < count; index += 1) this.#cells.push(cell);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
finish(wrapped: boolean, cols: number | null): TerminalReplicaRow {
|
|
585
|
+
this.#assertOpen();
|
|
586
|
+
const cells = this.#cells;
|
|
587
|
+
// As in compact expansion, sum-width checks happen after every run parses.
|
|
588
|
+
if (cols !== null && cells.length !== cols) throw new TypeError("Compact row width mismatch");
|
|
589
|
+
if (cells.length < 1 || cells.length > 16384)
|
|
590
|
+
throw new TypeError("Compact row width is out of bounds");
|
|
591
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
592
|
+
if (cells[index]!.width === 2 && cells[index + 1]?.width !== 0)
|
|
593
|
+
throw new TypeError("Malformed compact wide cell");
|
|
594
|
+
if (cells[index]!.width === 0 && (index === 0 || cells[index - 1]?.width !== 2))
|
|
595
|
+
throw new TypeError("Malformed compact continuation cell");
|
|
596
|
+
}
|
|
597
|
+
if (typeof wrapped !== "boolean") throw new TypeError("Invalid decoded scalar wrapped");
|
|
598
|
+
this.#finished = true;
|
|
599
|
+
const row = Object.freeze({
|
|
600
|
+
cells: Object.freeze(cells),
|
|
601
|
+
wrapped,
|
|
602
|
+
}) as unknown as TerminalReplicaRow;
|
|
603
|
+
// Earn only deep immutability. Existing row hashing remains lazy and unchanged.
|
|
604
|
+
DEEPLY_FROZEN_ROWS.add(row);
|
|
605
|
+
return row;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#assertOpen(): void {
|
|
609
|
+
if (this.#finished) throw new Error("Decoded terminal row already finished");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function createDecodedTerminalReplicaRowBuilder() {
|
|
614
|
+
return new DecodedTerminalReplicaRowBuilder();
|
|
615
|
+
}
|
|
616
|
+
|
|
476
617
|
/** Package-private verified-decoder seam; this module is not a package export. */
|
|
477
618
|
export function primeTerminalReplicaRowHash(row: TerminalReplicaRow, digest: string): void {
|
|
478
619
|
if (!/^[0-9a-f]{16}$/u.test(digest) || !isTerminalReplicaRowDeeplyFrozen(row))
|
|
@@ -1,8 +1,60 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
TerminalReplicaCell,
|
|
3
|
+
TerminalReplicaColor,
|
|
4
|
+
TerminalReplicaRow,
|
|
5
|
+
} from "@tmux-ide/contracts";
|
|
2
6
|
|
|
3
7
|
// Ownership proves immutable, ordinary data properties, not schema validity.
|
|
4
8
|
// There is no registration API: callers can only obtain ownership by copying.
|
|
5
9
|
const OWNED_ROWS = new WeakSet<TerminalReplicaRow>();
|
|
10
|
+
// Earned only by exact scalar construction, never by immutable foreign copies.
|
|
11
|
+
const SCHEMA_VALID_PROJECTED_ROWS = new WeakSet<TerminalReplicaRow>();
|
|
12
|
+
|
|
13
|
+
export function isSchemaValidProjectedTerminalReplicaRow(row: unknown): boolean {
|
|
14
|
+
return (
|
|
15
|
+
typeof row === "object" &&
|
|
16
|
+
row !== null &&
|
|
17
|
+
SCHEMA_VALID_PROJECTED_ROWS.has(row as TerminalReplicaRow)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TERMINAL_REPLICA_DEFAULT_COLOR = Object.freeze({ kind: "default" } as const);
|
|
22
|
+
|
|
23
|
+
// Trusted immutable producer values only. Empty storage and a literal space
|
|
24
|
+
// remain distinct for reflow; foreign equivalent cells are still copied.
|
|
25
|
+
export const TERMINAL_REPLICA_EMPTY_CELL: Readonly<TerminalReplicaCell> = Object.freeze({
|
|
26
|
+
grapheme: "",
|
|
27
|
+
width: 1,
|
|
28
|
+
attributes: 0,
|
|
29
|
+
foreground: TERMINAL_REPLICA_DEFAULT_COLOR,
|
|
30
|
+
background: TERMINAL_REPLICA_DEFAULT_COLOR,
|
|
31
|
+
});
|
|
32
|
+
export const TERMINAL_REPLICA_SPACE_CELL: Readonly<TerminalReplicaCell> = Object.freeze({
|
|
33
|
+
...TERMINAL_REPLICA_EMPTY_CELL,
|
|
34
|
+
grapheme: " ",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function freezeColor(color: TerminalReplicaColor): TerminalReplicaColor {
|
|
38
|
+
if (color === TERMINAL_REPLICA_DEFAULT_COLOR) return color;
|
|
39
|
+
// Copy first: external objects (including getters) keep the existing detached
|
|
40
|
+
// data contract. Share only the exact default shape, without erasing fields.
|
|
41
|
+
const copy = { ...color };
|
|
42
|
+
return copy.kind === "default" && Reflect.ownKeys(copy).length === 1
|
|
43
|
+
? TERMINAL_REPLICA_DEFAULT_COLOR
|
|
44
|
+
: Object.freeze(copy);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Internal copy seam shared by row ownership and cooperative seed admission.
|
|
48
|
+
// Exact trusted values may be shared; foreign objects always remain detached.
|
|
49
|
+
export function freezeOwnedTerminalReplicaCell(cell: TerminalReplicaCell): TerminalReplicaCell {
|
|
50
|
+
return cell === TERMINAL_REPLICA_EMPTY_CELL || cell === TERMINAL_REPLICA_SPACE_CELL
|
|
51
|
+
? cell
|
|
52
|
+
: Object.freeze({
|
|
53
|
+
...cell,
|
|
54
|
+
foreground: freezeColor(cell.foreground),
|
|
55
|
+
background: freezeColor(cell.background),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
6
58
|
|
|
7
59
|
export function isOwnedTerminalReplicaRow(row: unknown): row is TerminalReplicaRow {
|
|
8
60
|
return typeof row === "object" && row !== null && OWNED_ROWS.has(row as TerminalReplicaRow);
|
|
@@ -12,16 +64,119 @@ export function freezeOwnedTerminalReplicaRow(row: TerminalReplicaRow): Terminal
|
|
|
12
64
|
if (OWNED_ROWS.has(row)) return row;
|
|
13
65
|
const frozen = Object.freeze({
|
|
14
66
|
wrapped: row.wrapped,
|
|
15
|
-
cells: Object.freeze(
|
|
16
|
-
Array.from(row.cells, (cell) =>
|
|
17
|
-
Object.freeze({
|
|
18
|
-
...cell,
|
|
19
|
-
foreground: Object.freeze({ ...cell.foreground }),
|
|
20
|
-
background: Object.freeze({ ...cell.background }),
|
|
21
|
-
}),
|
|
22
|
-
),
|
|
23
|
-
),
|
|
67
|
+
cells: Object.freeze(Array.from(row.cells, freezeOwnedTerminalReplicaCell)),
|
|
24
68
|
}) as unknown as TerminalReplicaRow;
|
|
25
69
|
OWNED_ROWS.add(frozen);
|
|
26
70
|
return frozen;
|
|
27
71
|
}
|
|
72
|
+
|
|
73
|
+
/** Internal incremental copy seam. No caller-owned array or row can be branded.
|
|
74
|
+
* Ownership establishes detached immutable data, never schema validity. */
|
|
75
|
+
export function createOwnedTerminalReplicaRowBuilder() {
|
|
76
|
+
const cells: TerminalReplicaCell[] = [];
|
|
77
|
+
let finished = false;
|
|
78
|
+
const assertOpen = () => {
|
|
79
|
+
if (finished) throw new Error("Terminal replica row builder already finished");
|
|
80
|
+
};
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
append(source: TerminalReplicaCell): TerminalReplicaCell {
|
|
83
|
+
assertOpen();
|
|
84
|
+
const cell = freezeOwnedTerminalReplicaCell(source);
|
|
85
|
+
cells.push(cell);
|
|
86
|
+
return cell;
|
|
87
|
+
},
|
|
88
|
+
finish(source: TerminalReplicaRow, wrapped: boolean): TerminalReplicaRow {
|
|
89
|
+
assertOpen();
|
|
90
|
+
finished = true;
|
|
91
|
+
const row = Object.freeze({
|
|
92
|
+
...source,
|
|
93
|
+
wrapped,
|
|
94
|
+
cells: Object.freeze(cells),
|
|
95
|
+
}) as unknown as TerminalReplicaRow;
|
|
96
|
+
OWNED_ROWS.add(row);
|
|
97
|
+
return row;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Parser construction seam: accepts primitives, never caller-owned objects or
|
|
103
|
+
* arrays. Ownership proves detached immutable data, not schema validity. */
|
|
104
|
+
class ProjectedTerminalReplicaRowBuilder {
|
|
105
|
+
readonly #cells: TerminalReplicaCell[] = [];
|
|
106
|
+
#finished = false;
|
|
107
|
+
#schemaValid = true;
|
|
108
|
+
|
|
109
|
+
append(
|
|
110
|
+
grapheme: string,
|
|
111
|
+
width: 0 | 1 | 2,
|
|
112
|
+
attributes: number,
|
|
113
|
+
foregroundKind: TerminalReplicaColor["kind"],
|
|
114
|
+
foregroundValue: number,
|
|
115
|
+
backgroundKind: TerminalReplicaColor["kind"],
|
|
116
|
+
backgroundValue: number,
|
|
117
|
+
): void {
|
|
118
|
+
this.#assertOpen();
|
|
119
|
+
if (typeof grapheme !== "string" || typeof width !== "number" || typeof attributes !== "number")
|
|
120
|
+
throw new TypeError("Projected terminal cell values must be primitive");
|
|
121
|
+
const foreground = projectedColor(foregroundKind, foregroundValue);
|
|
122
|
+
const background = projectedColor(backgroundKind, backgroundValue);
|
|
123
|
+
const cell =
|
|
124
|
+
width === 1 &&
|
|
125
|
+
attributes === 0 &&
|
|
126
|
+
foreground === TERMINAL_REPLICA_DEFAULT_COLOR &&
|
|
127
|
+
background === TERMINAL_REPLICA_DEFAULT_COLOR &&
|
|
128
|
+
(grapheme === "" || grapheme === " ")
|
|
129
|
+
? grapheme === ""
|
|
130
|
+
? TERMINAL_REPLICA_EMPTY_CELL
|
|
131
|
+
: TERMINAL_REPLICA_SPACE_CELL
|
|
132
|
+
: Object.freeze({ grapheme, width, foreground, background, attributes });
|
|
133
|
+
this.#cells.push(cell);
|
|
134
|
+
// Describe the emitted exact-key cell, preserving all original throws and
|
|
135
|
+
// invalid-but-owned construction. Default color does not emit its value.
|
|
136
|
+
this.#schemaValid =
|
|
137
|
+
this.#schemaValid &&
|
|
138
|
+
(width === 0 || width === 1 || width === 2) &&
|
|
139
|
+
Number.isInteger(attributes) &&
|
|
140
|
+
attributes >= 0 &&
|
|
141
|
+
attributes <= 0xff &&
|
|
142
|
+
projectedColorIsSchemaValid(foregroundKind, foregroundValue) &&
|
|
143
|
+
projectedColorIsSchemaValid(backgroundKind, backgroundValue);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
finish(wrapped: boolean): TerminalReplicaRow {
|
|
147
|
+
this.#assertOpen();
|
|
148
|
+
if (typeof wrapped !== "boolean") throw new TypeError("Projected row wrapped must be boolean");
|
|
149
|
+
this.#finished = true;
|
|
150
|
+
const row = Object.freeze({
|
|
151
|
+
wrapped,
|
|
152
|
+
cells: Object.freeze(this.#cells),
|
|
153
|
+
}) as unknown as TerminalReplicaRow;
|
|
154
|
+
OWNED_ROWS.add(row);
|
|
155
|
+
if (this.#schemaValid) SCHEMA_VALID_PROJECTED_ROWS.add(row);
|
|
156
|
+
return row;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#assertOpen(): void {
|
|
160
|
+
if (this.#finished) throw new Error("Projected terminal row already finished");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function projectedColorIsSchemaValid(kind: TerminalReplicaColor["kind"], value: number): boolean {
|
|
165
|
+
return (
|
|
166
|
+
kind === "default" ||
|
|
167
|
+
(Number.isInteger(value) && value >= 0 && value <= (kind === "indexed" ? 255 : 0xffffff))
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function projectedColor(kind: TerminalReplicaColor["kind"], value: number): TerminalReplicaColor {
|
|
172
|
+
if (typeof value !== "number")
|
|
173
|
+
throw new TypeError("Projected terminal color value must be primitive");
|
|
174
|
+
if (kind === "default") return TERMINAL_REPLICA_DEFAULT_COLOR;
|
|
175
|
+
if (kind === "indexed") return Object.freeze({ kind, index: value });
|
|
176
|
+
if (kind === "rgb") return Object.freeze({ kind, value });
|
|
177
|
+
throw new TypeError("Unknown projected terminal color kind");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createProjectedTerminalReplicaRowBuilder() {
|
|
181
|
+
return new ProjectedTerminalReplicaRowBuilder();
|
|
182
|
+
}
|
|
@@ -9,8 +9,22 @@ import type {
|
|
|
9
9
|
TerminalReplicaSnapshot,
|
|
10
10
|
TerminalReplicaTombstonePayload,
|
|
11
11
|
} from "@tmux-ide/contracts";
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
consumeCompactReplicaCapability,
|
|
14
|
+
consumeVerifiedTombstoneInterval,
|
|
15
|
+
} from "./terminal-compact-capability.ts";
|
|
16
|
+
import {
|
|
17
|
+
freezeOwnedTerminalReplicaRow as freezeRow,
|
|
18
|
+
createOwnedTerminalReplicaRowBuilder,
|
|
19
|
+
isOwnedTerminalReplicaRow,
|
|
20
|
+
TERMINAL_REPLICA_DEFAULT_COLOR,
|
|
21
|
+
} from "./terminal-replica-owned-row.ts";
|
|
22
|
+
export {
|
|
23
|
+
createProjectedTerminalReplicaRowBuilder,
|
|
24
|
+
TERMINAL_REPLICA_DEFAULT_COLOR,
|
|
25
|
+
TERMINAL_REPLICA_EMPTY_CELL,
|
|
26
|
+
TERMINAL_REPLICA_SPACE_CELL,
|
|
27
|
+
} from "./terminal-replica-owned-row.ts";
|
|
14
28
|
import {
|
|
15
29
|
hashCanonicalTerminalValue,
|
|
16
30
|
hashCanonicalTerminalValueCooperatively,
|
|
@@ -24,7 +38,6 @@ import {
|
|
|
24
38
|
// External Object.freeze calls do not establish this provenance.
|
|
25
39
|
const VALIDATED_PATCH_ROWS = new WeakSet<TerminalReplicaRow>();
|
|
26
40
|
|
|
27
|
-
const DEFAULT_COLOR = Object.freeze({ kind: "default" } as const);
|
|
28
41
|
const ROW_ARRAY_HASH_CACHE = new WeakMap<
|
|
29
42
|
object,
|
|
30
43
|
{ readonly hash: bigint; readonly length: number }
|
|
@@ -128,6 +141,10 @@ export function blankTerminalReplicaSnapshot(cols: number, rows: number): Termin
|
|
|
128
141
|
});
|
|
129
142
|
}
|
|
130
143
|
|
|
144
|
+
// Only accepted verified coalesced tombstones need their original interval for replay.
|
|
145
|
+
// Weak state keys preserve exact-duplicate idempotency without retaining retired replicas.
|
|
146
|
+
const verifiedTombstoneBases = new WeakMap<TerminalReplicaState, number>();
|
|
147
|
+
|
|
131
148
|
export function applyTerminalReplicaUpdate(
|
|
132
149
|
current: TerminalReplicaState | null,
|
|
133
150
|
update: CanonicalTerminalReplicaUpdate,
|
|
@@ -208,7 +225,8 @@ export function applyTerminalReplicaUpdate(
|
|
|
208
225
|
if (update.type === "terminal.tombstone") {
|
|
209
226
|
return complete(
|
|
210
227
|
update.revision === current.revision &&
|
|
211
|
-
update.baseRevision === current.revision - 1
|
|
228
|
+
(update.baseRevision === current.revision - 1 ||
|
|
229
|
+
verifiedTombstoneBases.get(current) === update.baseRevision) &&
|
|
212
230
|
update.stateHash === current.hash &&
|
|
213
231
|
current.tombstone?.reason === update.tombstone.reason &&
|
|
214
232
|
current.frameHash === receivedFrameHash
|
|
@@ -231,7 +249,20 @@ export function applyTerminalReplicaUpdate(
|
|
|
231
249
|
: { status: "stale", state: current },
|
|
232
250
|
);
|
|
233
251
|
}
|
|
234
|
-
|
|
252
|
+
const verifiedTombstoneJump =
|
|
253
|
+
update.type === "terminal.tombstone" &&
|
|
254
|
+
update.revision > current.revision + 1 &&
|
|
255
|
+
consumeVerifiedTombstoneInterval(
|
|
256
|
+
update.tombstone,
|
|
257
|
+
current.snapshot,
|
|
258
|
+
update.stateHash,
|
|
259
|
+
update.baseRevision,
|
|
260
|
+
update.revision,
|
|
261
|
+
);
|
|
262
|
+
if (
|
|
263
|
+
update.baseRevision !== current.revision ||
|
|
264
|
+
(update.revision !== current.revision + 1 && !verifiedTombstoneJump)
|
|
265
|
+
) {
|
|
235
266
|
return complete({
|
|
236
267
|
status: "gap",
|
|
237
268
|
state: current,
|
|
@@ -263,6 +294,7 @@ export function applyTerminalReplicaUpdate(
|
|
|
263
294
|
hash,
|
|
264
295
|
frameHash: receivedFrameHash,
|
|
265
296
|
});
|
|
297
|
+
if (verifiedTombstoneJump) verifiedTombstoneBases.set(state, update.baseRevision);
|
|
266
298
|
return complete({ status: "applied", state });
|
|
267
299
|
}
|
|
268
300
|
if (current.snapshot === null) {
|
|
@@ -498,33 +530,23 @@ export async function applyTerminalReplicaUpdateCooperatively(
|
|
|
498
530
|
] as const) {
|
|
499
531
|
for (const row of sources) {
|
|
500
532
|
if (row.cells.length !== cols) return complete(conflict());
|
|
501
|
-
const
|
|
533
|
+
const owned = isOwnedTerminalReplicaRow(row);
|
|
534
|
+
const builder = owned ? null : createOwnedTerminalReplicaRowBuilder();
|
|
502
535
|
let priorWidth = -1;
|
|
503
536
|
const wrapped = row.wrapped;
|
|
504
537
|
for (let index = 0; index < cols; index++) {
|
|
505
538
|
const sourceCell = row.cells[index]!;
|
|
506
|
-
const cell =
|
|
507
|
-
...sourceCell,
|
|
508
|
-
foreground: Object.freeze({ ...sourceCell.foreground }),
|
|
509
|
-
background: Object.freeze({ ...sourceCell.background }),
|
|
510
|
-
});
|
|
539
|
+
const cell = owned ? sourceCell : builder!.append(sourceCell);
|
|
511
540
|
if ((priorWidth === 2 && cell.width !== 0) || (cell.width === 0 && priorWidth !== 2))
|
|
512
541
|
return complete(conflict());
|
|
513
542
|
priorWidth = cell.width;
|
|
514
|
-
cells.push(cell);
|
|
515
543
|
if (++work >= 256) {
|
|
516
544
|
work = 0;
|
|
517
545
|
await yieldControl();
|
|
518
546
|
}
|
|
519
547
|
}
|
|
520
548
|
if (priorWidth === 2) return complete(conflict());
|
|
521
|
-
target.push(
|
|
522
|
-
Object.freeze({
|
|
523
|
-
...row,
|
|
524
|
-
wrapped,
|
|
525
|
-
cells: Object.freeze(cells),
|
|
526
|
-
}) as unknown as TerminalReplicaRow,
|
|
527
|
-
);
|
|
549
|
+
target.push(owned ? row : builder!.finish(row, wrapped));
|
|
528
550
|
}
|
|
529
551
|
}
|
|
530
552
|
const placements: TerminalReplicaSnapshot["placements"] = [];
|
|
@@ -827,8 +849,8 @@ function blankRow(cols: number): TerminalReplicaRow {
|
|
|
827
849
|
Object.freeze({
|
|
828
850
|
grapheme: " ",
|
|
829
851
|
width: 1 as const,
|
|
830
|
-
foreground:
|
|
831
|
-
background:
|
|
852
|
+
foreground: TERMINAL_REPLICA_DEFAULT_COLOR,
|
|
853
|
+
background: TERMINAL_REPLICA_DEFAULT_COLOR,
|
|
832
854
|
attributes: 0,
|
|
833
855
|
}),
|
|
834
856
|
),
|
|
@@ -37,6 +37,7 @@ import { startOwnedEmbeddedDaemon } from "./embedded-daemon-lifecycle.js";
|
|
|
37
37
|
import { execFileSync } from "node:child_process";
|
|
38
38
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
39
39
|
import { createWriteStream } from "node:fs";
|
|
40
|
+
import { createRuntimeTraceWriter } from "./runtime-trace-writer.js";
|
|
40
41
|
import { createServer } from "node:http";
|
|
41
42
|
import { createRequire } from "node:module";
|
|
42
43
|
import { performance } from "node:perf_hooks";
|
|
@@ -965,15 +966,11 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
965
966
|
throw new Error("Default tmux owner is retired");
|
|
966
967
|
},
|
|
967
968
|
});
|
|
968
|
-
let
|
|
969
|
-
const closeRuntimeTraceStream = async () => {
|
|
969
|
+
let runtimeTraceWriter = null;
|
|
970
|
+
const closeRuntimeTraceStream = async (incomplete = false) => {
|
|
970
971
|
externalInteractionObserver.setDiagnostics(null);
|
|
971
972
|
setFleetFactsObserverDiagnostics(null);
|
|
972
|
-
|
|
973
|
-
runtimeTraceStream = null;
|
|
974
|
-
if (!stream || stream.closed || stream.destroyed)
|
|
975
|
-
return;
|
|
976
|
-
await new Promise((resolve) => stream.end(resolve));
|
|
973
|
+
await runtimeTraceWriter?.close(incomplete);
|
|
977
974
|
};
|
|
978
975
|
let startedServer;
|
|
979
976
|
try {
|
|
@@ -1001,16 +998,9 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1001
998
|
return workspaceMultiplexer.mutate({ operationId, expectedDaemonInstanceId: instanceId, intent }, timing, execution);
|
|
1002
999
|
};
|
|
1003
1000
|
const runtimeTracePath = process.env.TMUX_IDE_SESSION_RUNTIME_TRACE_LOG;
|
|
1004
|
-
|
|
1005
|
-
? createWriteStream(runtimeTracePath, { flags: "a", highWaterMark: 64 *
|
|
1001
|
+
runtimeTraceWriter = runtimeTracePath
|
|
1002
|
+
? createRuntimeTraceWriter(createWriteStream(runtimeTracePath, { flags: "a", highWaterMark: 64 * 1024 }), { processId: `daemon:${process.pid}`, daemonInstanceId: instanceId })
|
|
1006
1003
|
: null;
|
|
1007
|
-
let runtimeTraceSaturated = false;
|
|
1008
|
-
runtimeTraceStream?.on("error", () => {
|
|
1009
|
-
runtimeTraceSaturated = true;
|
|
1010
|
-
});
|
|
1011
|
-
runtimeTraceStream?.on("drain", () => {
|
|
1012
|
-
runtimeTraceSaturated = false;
|
|
1013
|
-
});
|
|
1014
1004
|
const runtimeObservability = runtimeTracePath
|
|
1015
1005
|
? createSessionRuntimeObservability({
|
|
1016
1006
|
// The JSONL stream is the qualifying record. Keep only a small
|
|
@@ -1019,27 +1009,22 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1019
1009
|
// pauses on the daemon's input/output event loop.
|
|
1020
1010
|
capacity: 1_024,
|
|
1021
1011
|
onSpan: (span) => {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1012
|
+
runtimeTraceWriter?.append(() => ({
|
|
1013
|
+
version: 1,
|
|
1014
|
+
type: "performance.stage",
|
|
1015
|
+
...span,
|
|
1016
|
+
}));
|
|
1025
1017
|
},
|
|
1026
1018
|
})
|
|
1027
1019
|
: undefined;
|
|
1028
1020
|
if (runtimeTracePath) {
|
|
1029
1021
|
const publishObserverDiagnostic = (event) => {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
...event,
|
|
1037
|
-
generation: instanceId,
|
|
1038
|
-
})}\n`);
|
|
1039
|
-
}
|
|
1040
|
-
catch {
|
|
1041
|
-
// Qualification diagnostics never alter daemon observation.
|
|
1042
|
-
}
|
|
1022
|
+
runtimeTraceWriter?.append(() => ({
|
|
1023
|
+
version: 1,
|
|
1024
|
+
type: "performance.daemon-observer",
|
|
1025
|
+
...event,
|
|
1026
|
+
generation: instanceId,
|
|
1027
|
+
}));
|
|
1043
1028
|
};
|
|
1044
1029
|
const diagnostics = {
|
|
1045
1030
|
nowMicros: () => Math.floor(performance.now() * 1_000),
|
|
@@ -1438,7 +1423,7 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1438
1423
|
appWindowMutation.dispose(),
|
|
1439
1424
|
workspaceMultiplexer.dispose(),
|
|
1440
1425
|
disposeInteractionObservation(),
|
|
1441
|
-
closeRuntimeTraceStream(),
|
|
1426
|
+
closeRuntimeTraceStream(true),
|
|
1442
1427
|
]);
|
|
1443
1428
|
// The pane-stream coordinator may still hold runtime consumers while it
|
|
1444
1429
|
// drains. Preserve the normal shutdown order on startup rollback too:
|
|
@@ -1487,7 +1472,7 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1487
1472
|
appWindowMutationDisposal,
|
|
1488
1473
|
workspaceMultiplexerDisposal,
|
|
1489
1474
|
externalInteractionDisposal,
|
|
1490
|
-
closeRuntimeTraceStream(),
|
|
1475
|
+
closeRuntimeTraceStream(true),
|
|
1491
1476
|
Promise.resolve().then(() => closeClients()),
|
|
1492
1477
|
...[...sockets].map((socket) => Promise.resolve().then(() => socket.destroy())),
|
|
1493
1478
|
Promise.race([closePromise, delay(100)]),
|