tmux-ide 2.9.11 → 2.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +4158 -2545
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +4 -0
- package/packages/contracts/src/pane-stream.ts +60 -0
- package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
- package/packages/contracts/src/terminal-delivery.ts +17 -2
- package/packages/contracts/src/terminal-graphics.ts +68 -0
- package/packages/contracts/src/terminal-projection.ts +153 -0
- package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/terminal-delivery-binary.ts +160 -0
- package/packages/core/src/terminal-delivery.ts +371 -16
- package/packages/core/src/terminal-projection-vectors.mjs +82 -0
- package/packages/core/src/terminal-projection.ts +476 -0
- package/packages/core/src/terminal-replica.ts +1 -1
- package/packages/daemon/dist/lib/daemon-embed.js +39 -3
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
- package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +176 -16
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
- package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
- package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
- package/packages/daemon/src/lib/daemon-embed.ts +38 -2
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +3 -0
- package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
- package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +219 -15
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
- package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
- package/packages/daemon-client/src/pane-stream-client.ts +75 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** TCB1 typed RLE tuples. No JSON cell encoding; shape validation belongs to semantic delivery. */
|
|
2
|
+
const MAX_BYTES = 16 * 1024 * 1024,
|
|
3
|
+
MAX_STRING = 4096,
|
|
4
|
+
MAX_NODES = 8_000_000,
|
|
5
|
+
MAX_ARRAY = 1_000_000;
|
|
6
|
+
const encoder = new TextEncoder(),
|
|
7
|
+
decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
8
|
+
export class BinaryTerminalLimitError extends RangeError {}
|
|
9
|
+
export class BinaryTerminalWriter {
|
|
10
|
+
private data = new Uint8Array(4096);
|
|
11
|
+
private length = 0;
|
|
12
|
+
private nodes = 0;
|
|
13
|
+
private reserve(size: number) {
|
|
14
|
+
if (size > MAX_BYTES - this.length)
|
|
15
|
+
throw new BinaryTerminalLimitError("Binary terminal representation exceeds bound");
|
|
16
|
+
if (this.length + size > this.data.length) {
|
|
17
|
+
const next = new Uint8Array(
|
|
18
|
+
Math.min(MAX_BYTES, Math.max(this.length + size, this.data.length * 2)),
|
|
19
|
+
);
|
|
20
|
+
next.set(this.data);
|
|
21
|
+
this.data = next;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
byte(value: number) {
|
|
25
|
+
this.reserve(1);
|
|
26
|
+
this.data[this.length++] = value;
|
|
27
|
+
}
|
|
28
|
+
uint(value: number) {
|
|
29
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("Invalid binary integer");
|
|
30
|
+
do {
|
|
31
|
+
const low = value % 128;
|
|
32
|
+
value = Math.floor(value / 128);
|
|
33
|
+
this.byte(low + (value ? 128 : 0));
|
|
34
|
+
} while (value);
|
|
35
|
+
}
|
|
36
|
+
array(length: number) {
|
|
37
|
+
if (length > MAX_ARRAY) throw new BinaryTerminalLimitError("Binary array bound");
|
|
38
|
+
this.byte(3);
|
|
39
|
+
this.uint(length);
|
|
40
|
+
}
|
|
41
|
+
value(value: unknown, depth = 0): void {
|
|
42
|
+
if (depth > 16 || ++this.nodes > MAX_NODES)
|
|
43
|
+
throw new BinaryTerminalLimitError("Binary node bound");
|
|
44
|
+
if (value === null) {
|
|
45
|
+
this.byte(0);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "number") {
|
|
49
|
+
this.byte(1);
|
|
50
|
+
this.uint(value);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
const bytes = encoder.encode(value);
|
|
55
|
+
if (bytes.length > MAX_STRING) throw new BinaryTerminalLimitError("Binary string bound");
|
|
56
|
+
if (decoder.decode(bytes) !== value)
|
|
57
|
+
throw new TypeError("Binary strings require well-formed Unicode");
|
|
58
|
+
this.byte(2);
|
|
59
|
+
this.uint(bytes.length);
|
|
60
|
+
this.reserve(bytes.length);
|
|
61
|
+
this.data.set(bytes, this.length);
|
|
62
|
+
this.length += bytes.length;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
this.array(value.length);
|
|
67
|
+
for (const child of value) this.value(child, depth + 1);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new TypeError("Invalid binary tuple value");
|
|
71
|
+
}
|
|
72
|
+
header(
|
|
73
|
+
frame: "seed" | "patch" | "tombstone",
|
|
74
|
+
revision: number,
|
|
75
|
+
base?: number,
|
|
76
|
+
magic: "TCB1" | "TCP1" = "TCB1",
|
|
77
|
+
) {
|
|
78
|
+
for (const b of magic === "TCP1" ? [84, 67, 80, 49] : [84, 67, 66, 49]) this.byte(b);
|
|
79
|
+
this.byte(frame === "seed" ? 0 : frame === "patch" ? 1 : 2);
|
|
80
|
+
this.uint(revision);
|
|
81
|
+
if (frame !== "seed") this.uint(base!);
|
|
82
|
+
}
|
|
83
|
+
get byteLength(): number {
|
|
84
|
+
return this.length;
|
|
85
|
+
}
|
|
86
|
+
finish() {
|
|
87
|
+
return this.data.slice(0, this.length);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function decodeBinaryTerminalTuple(
|
|
91
|
+
bytes: Uint8Array,
|
|
92
|
+
magic: "TCB1" | "TCP1" = "TCB1",
|
|
93
|
+
): {
|
|
94
|
+
frame: "seed" | "patch" | "tombstone";
|
|
95
|
+
revision: number;
|
|
96
|
+
baseRevision?: number;
|
|
97
|
+
value: unknown;
|
|
98
|
+
} {
|
|
99
|
+
if (
|
|
100
|
+
bytes.length > MAX_BYTES ||
|
|
101
|
+
bytes.length < 7 ||
|
|
102
|
+
bytes[0] !== 84 ||
|
|
103
|
+
bytes[1] !== 67 ||
|
|
104
|
+
bytes[2] !== (magic === "TCP1" ? 80 : 66) ||
|
|
105
|
+
bytes[3] !== 49
|
|
106
|
+
)
|
|
107
|
+
throw new TypeError("Invalid binary terminal header");
|
|
108
|
+
let offset = 4,
|
|
109
|
+
nodes = 0;
|
|
110
|
+
const byte = () => {
|
|
111
|
+
if (offset >= bytes.length) throw new TypeError("Truncated binary terminal payload");
|
|
112
|
+
return bytes[offset++]!;
|
|
113
|
+
};
|
|
114
|
+
const uint = () => {
|
|
115
|
+
let value = 0,
|
|
116
|
+
scale = 1;
|
|
117
|
+
for (let index = 0; index < 8; index++) {
|
|
118
|
+
const b = byte();
|
|
119
|
+
value += (b & 127) * scale;
|
|
120
|
+
if (!Number.isSafeInteger(value)) throw new TypeError("Binary integer exceeds safe bound");
|
|
121
|
+
if (b < 128) {
|
|
122
|
+
if (index > 0 && b === 0) throw new TypeError("Noncanonical binary integer");
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
scale *= 128;
|
|
126
|
+
}
|
|
127
|
+
throw new TypeError("Binary integer is too long");
|
|
128
|
+
};
|
|
129
|
+
const read = (depth = 0): unknown => {
|
|
130
|
+
if (depth > 16 || ++nodes > MAX_NODES) throw new TypeError("Binary structure bound");
|
|
131
|
+
const tag = byte();
|
|
132
|
+
if (tag === 0) return null;
|
|
133
|
+
if (tag === 1) return uint();
|
|
134
|
+
if (tag === 2) {
|
|
135
|
+
const length = uint();
|
|
136
|
+
if (length > MAX_STRING || length > bytes.length - offset)
|
|
137
|
+
throw new TypeError("Binary string bound");
|
|
138
|
+
const result = decoder.decode(bytes.subarray(offset, offset + length));
|
|
139
|
+
offset += length;
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
if (tag === 3) {
|
|
143
|
+
const length = uint();
|
|
144
|
+
if (length > MAX_ARRAY || length > bytes.length - offset)
|
|
145
|
+
throw new TypeError("Binary array bound");
|
|
146
|
+
const result: unknown[] = [];
|
|
147
|
+
for (let i = 0; i < length; i++) result.push(read(depth + 1));
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
throw new TypeError("Unknown binary value tag");
|
|
151
|
+
};
|
|
152
|
+
const kind = byte();
|
|
153
|
+
if (kind > 2) throw new TypeError("Unknown binary frame");
|
|
154
|
+
const frame = kind === 0 ? "seed" : kind === 1 ? "patch" : "tombstone";
|
|
155
|
+
const revision = uint();
|
|
156
|
+
const baseRevision = kind === 0 ? undefined : uint();
|
|
157
|
+
const value = read();
|
|
158
|
+
if (offset !== bytes.length) throw new TypeError("Trailing binary data");
|
|
159
|
+
return { frame, revision, ...(baseRevision === undefined ? {} : { baseRevision }), value };
|
|
160
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TERMINAL_PROJECTION_CAPABILITY_VERSION,
|
|
3
|
+
TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
|
|
4
|
+
} from "@tmux-ide/contracts";
|
|
5
|
+
import {
|
|
6
|
+
BinaryTerminalWriter,
|
|
7
|
+
BinaryTerminalLimitError,
|
|
8
|
+
decodeBinaryTerminalTuple,
|
|
9
|
+
} from "./terminal-delivery-binary.ts";
|
|
1
10
|
import { z } from "zod";
|
|
2
11
|
import {
|
|
3
12
|
isOwnedTerminalReplicaRow,
|
|
@@ -6,6 +15,8 @@ import {
|
|
|
6
15
|
TERMINAL_REPLICA_SPACE_CELL,
|
|
7
16
|
} from "./terminal-replica-owned-row.ts";
|
|
8
17
|
import {
|
|
18
|
+
TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION,
|
|
19
|
+
TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION,
|
|
9
20
|
TERMINAL_DELIVERY_CHUNK_BYTES,
|
|
10
21
|
TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES,
|
|
11
22
|
TERMINAL_DELIVERY_PROTOCOL_VERSION,
|
|
@@ -71,22 +82,48 @@ export function negotiateTerminalDelivery(
|
|
|
71
82
|
): TerminalDeliveryNegotiationResult {
|
|
72
83
|
if (!offer.protocolVersions.includes(TERMINAL_DELIVERY_PROTOCOL_VERSION))
|
|
73
84
|
return { accepted: false, reason: "protocol-version-mismatch" };
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const binary =
|
|
86
|
+
offer.protocolVersions.some(
|
|
87
|
+
(v) =>
|
|
88
|
+
v === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
|
|
89
|
+
v === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
|
|
90
|
+
v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
|
|
91
|
+
v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
|
|
92
|
+
) && offer.encodings.includes("semantic-compact-v1");
|
|
93
|
+
const encoding = binary
|
|
94
|
+
? ("semantic-binary-v1" as const)
|
|
95
|
+
: (["semantic-compact-v1", "semantic-v1", "ansi-diff-v1", "ansi-raw-v1"] as const).find(
|
|
96
|
+
(value) => offer.encodings.includes(value),
|
|
97
|
+
);
|
|
77
98
|
if (!encoding) return { accepted: false, reason: "encoding-mismatch" };
|
|
78
|
-
if (
|
|
99
|
+
if (
|
|
100
|
+
offer.richPlacements &&
|
|
101
|
+
encoding !== "semantic-v1" &&
|
|
102
|
+
encoding !== "semantic-compact-v1" &&
|
|
103
|
+
encoding !== "semantic-binary-v1"
|
|
104
|
+
)
|
|
79
105
|
return { accepted: false, reason: "unsupported-capability-combination" };
|
|
80
106
|
return {
|
|
81
107
|
accepted: true,
|
|
82
108
|
negotiated: {
|
|
83
109
|
protocolVersion: TERMINAL_DELIVERY_PROTOCOL_VERSION,
|
|
84
110
|
encoding,
|
|
111
|
+
...(binary &&
|
|
112
|
+
offer.protocolVersions.some(
|
|
113
|
+
(v) =>
|
|
114
|
+
v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
|
|
115
|
+
v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
|
|
116
|
+
)
|
|
117
|
+
? { alternateScreenProjection: "deferred-history-v1" as const }
|
|
118
|
+
: {}),
|
|
85
119
|
...(encoding === "semantic-compact-v1" && offer.encodings.includes("semantic-v1")
|
|
86
120
|
? { fallbackEncoding: "semantic-v1" as const }
|
|
87
121
|
: {}),
|
|
88
122
|
richPlacements:
|
|
89
|
-
offer.richPlacements &&
|
|
123
|
+
offer.richPlacements &&
|
|
124
|
+
(encoding === "semantic-v1" ||
|
|
125
|
+
encoding === "semantic-compact-v1" ||
|
|
126
|
+
encoding === "semantic-binary-v1"),
|
|
90
127
|
generation,
|
|
91
128
|
deliveryNonce,
|
|
92
129
|
},
|
|
@@ -370,6 +407,12 @@ function hasOwnedSeedRow(input: TerminalSemanticDeliveryPayload): boolean {
|
|
|
370
407
|
export function encodeCompactSemanticTerminalUpdate(
|
|
371
408
|
input: TerminalSemanticDeliveryPayload,
|
|
372
409
|
): Uint8Array {
|
|
410
|
+
const wire = compactSemanticWire(input);
|
|
411
|
+
const bytes = UTF8_ENCODER.encode(JSON.stringify(wire));
|
|
412
|
+
assertRepresentationSize(bytes);
|
|
413
|
+
return bytes;
|
|
414
|
+
}
|
|
415
|
+
function compactSemanticWire(input: TerminalSemanticDeliveryPayload) {
|
|
373
416
|
const update = (
|
|
374
417
|
hasOwnedPatchRow(input)
|
|
375
418
|
? CompactPatchEncodingPayloadSchema
|
|
@@ -406,9 +449,172 @@ export function encodeCompactSemanticTerminalUpdate(
|
|
|
406
449
|
t: update.tombstone.reason,
|
|
407
450
|
v: 1,
|
|
408
451
|
};
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
452
|
+
return wire;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function encodeBinarySemanticTerminalUpdate(
|
|
456
|
+
input: TerminalSemanticDeliveryPayload,
|
|
457
|
+
): Uint8Array {
|
|
458
|
+
const wire = compactSemanticWire(input);
|
|
459
|
+
const writer = new BinaryTerminalWriter();
|
|
460
|
+
try {
|
|
461
|
+
writer.header(input.frame, wire.r, input.frame === "seed" ? undefined : input.baseRevision);
|
|
462
|
+
writer.value(wire.f === "s" ? wire.s : wire.f === "p" ? wire.p : wire.t);
|
|
463
|
+
return writer.finish();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error instanceof BinaryTerminalLimitError)
|
|
466
|
+
throw new TerminalDeliveryStateTooLargeError(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES + 1);
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
export function decodeBinarySemanticTerminalUpdate(
|
|
471
|
+
bytes: Uint8Array,
|
|
472
|
+
): TerminalSemanticDeliveryPayload {
|
|
473
|
+
const decoded = decodeBinaryTerminalTuple(bytes),
|
|
474
|
+
budget = { ...compactDecodeBudget(bytes.length), maxCells: COMPACT_MAX_EXPANDED_CELLS };
|
|
475
|
+
if (decoded.frame === "seed")
|
|
476
|
+
return Object.freeze({
|
|
477
|
+
frame: "seed",
|
|
478
|
+
revision: decoded.revision,
|
|
479
|
+
snapshot: expandSnapshot(decoded.value, budget),
|
|
480
|
+
});
|
|
481
|
+
if (decoded.baseRevision === undefined || decoded.revision <= decoded.baseRevision)
|
|
482
|
+
throw new TypeError("Binary revision did not advance");
|
|
483
|
+
if (decoded.frame === "patch")
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
frame: "patch",
|
|
486
|
+
revision: decoded.revision,
|
|
487
|
+
baseRevision: decoded.baseRevision,
|
|
488
|
+
patch: expandPatch(decoded.value, budget),
|
|
489
|
+
});
|
|
490
|
+
if (
|
|
491
|
+
typeof decoded.value !== "string" ||
|
|
492
|
+
!["pane-closed", "session-restarted", "runtime-disposed"].includes(decoded.value)
|
|
493
|
+
)
|
|
494
|
+
throw new TypeError("Invalid binary tombstone");
|
|
495
|
+
return Object.freeze({
|
|
496
|
+
frame: "tombstone",
|
|
497
|
+
revision: decoded.revision,
|
|
498
|
+
baseRevision: decoded.baseRevision,
|
|
499
|
+
tombstone: Object.freeze({
|
|
500
|
+
reason: decoded.value as "pane-closed" | "session-restarted" | "runtime-disposed",
|
|
501
|
+
}),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
export async function encodeBinarySemanticTerminalUpdateCooperatively(
|
|
505
|
+
input: TerminalSemanticDeliveryPayload,
|
|
506
|
+
options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
|
|
507
|
+
): Promise<Uint8Array> {
|
|
508
|
+
options.signal?.throwIfAborted();
|
|
509
|
+
if (input.frame !== "seed") return encodeBinarySemanticTerminalUpdate(input);
|
|
510
|
+
const { snapshot: source, ...headerInput } = input;
|
|
511
|
+
const header = CompactSeedHeaderSchema.parse(headerInput);
|
|
512
|
+
const { grid, history, placements, ...metadataInput } = source;
|
|
513
|
+
const metadata = CompactSeedMetadataSchema.parse(metadataInput);
|
|
514
|
+
if (
|
|
515
|
+
!Array.isArray(grid) ||
|
|
516
|
+
!Array.isArray(history) ||
|
|
517
|
+
!Array.isArray(placements) ||
|
|
518
|
+
grid.length !== metadata.rows ||
|
|
519
|
+
metadata.cols > COMPACT_MAX_DIMENSION ||
|
|
520
|
+
metadata.rows > COMPACT_MAX_DIMENSION
|
|
521
|
+
)
|
|
522
|
+
compactEncodingLimit();
|
|
523
|
+
let rowCount = 0,
|
|
524
|
+
cellCount = 0,
|
|
525
|
+
runCount = 0,
|
|
526
|
+
work = 0,
|
|
527
|
+
encodedWork = 0;
|
|
528
|
+
const checkpoint = (characters: number): Promise<void> | undefined => {
|
|
529
|
+
options.signal?.throwIfAborted();
|
|
530
|
+
work++;
|
|
531
|
+
encodedWork += characters + 64;
|
|
532
|
+
if (work >= 256 || encodedWork >= 32 * 1024) {
|
|
533
|
+
work = 0;
|
|
534
|
+
encodedWork = 0;
|
|
535
|
+
return options.yieldControl().then(() => options.signal?.throwIfAborted());
|
|
536
|
+
}
|
|
537
|
+
return undefined;
|
|
538
|
+
};
|
|
539
|
+
const writer = new BinaryTerminalWriter();
|
|
540
|
+
try {
|
|
541
|
+
writer.header("seed", header.revision);
|
|
542
|
+
writer.array(8);
|
|
543
|
+
writer.value(metadata.cols);
|
|
544
|
+
writer.value(metadata.rows);
|
|
545
|
+
for (const rows of [grid, history]) {
|
|
546
|
+
writer.array(rows.length);
|
|
547
|
+
for (const row of rows) {
|
|
548
|
+
const owned = isOwnedTerminalReplicaRow(row);
|
|
549
|
+
const validatedRow =
|
|
550
|
+
owned &&
|
|
551
|
+
(validatedOwnedEncodingRows.has(row) || isSchemaValidProjectedTerminalReplicaRow(row));
|
|
552
|
+
const { cells, ...rowHeaderInput } = row;
|
|
553
|
+
const rowHeader = CompactRowHeaderSchema.parse(rowHeaderInput);
|
|
554
|
+
if (
|
|
555
|
+
++rowCount > COMPACT_MAX_ROWS ||
|
|
556
|
+
!Array.isArray(cells) ||
|
|
557
|
+
cells.length !== metadata.cols
|
|
558
|
+
)
|
|
559
|
+
compactEncodingLimit();
|
|
560
|
+
cellCount += cells.length;
|
|
561
|
+
if (cellCount > COMPACT_MAX_EXPANDED_CELLS) compactEncodingLimit();
|
|
562
|
+
const runs: CompactCellRun[] = [];
|
|
563
|
+
let prior: CompactCellRun | undefined;
|
|
564
|
+
for (let offset = 0; offset < cells.length; offset += 256) {
|
|
565
|
+
const slice = cells.slice(offset, offset + 256);
|
|
566
|
+
const validated =
|
|
567
|
+
validatedRow || (owned && isValidatedTrustedBlankSlice(slice))
|
|
568
|
+
? slice
|
|
569
|
+
: owned
|
|
570
|
+
? validateOwnedCellSlice(slice)
|
|
571
|
+
: CompactCellSliceSchema.parse(slice);
|
|
572
|
+
for (const cell of validated) {
|
|
573
|
+
const encoded = compactCell(cell);
|
|
574
|
+
if (prior && compactRunCellEqual(prior, encoded)) prior[0]++;
|
|
575
|
+
else {
|
|
576
|
+
if (++runCount > COMPACT_MAX_RUNS) compactEncodingLimit();
|
|
577
|
+
runs.push(encoded);
|
|
578
|
+
prior = encoded;
|
|
579
|
+
}
|
|
580
|
+
const pending = checkpoint(cell.grapheme.length);
|
|
581
|
+
if (pending) await pending;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
writer.array(2);
|
|
585
|
+
writer.value(rowHeader.wrapped ? 1 : 0);
|
|
586
|
+
writer.array(runs.length);
|
|
587
|
+
for (const run of runs) {
|
|
588
|
+
writer.value(run);
|
|
589
|
+
const pending = checkpoint(run[1].length);
|
|
590
|
+
if (pending) await pending;
|
|
591
|
+
}
|
|
592
|
+
if (owned) validatedOwnedEncodingRows.add(row);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
writer.value(compactCursor(metadata.cursor));
|
|
596
|
+
writer.value(compactModes(metadata.modes));
|
|
597
|
+
if (placements.length > COMPACT_MAX_PLACEMENTS) compactEncodingLimit();
|
|
598
|
+
writer.array(placements.length);
|
|
599
|
+
for (const value of placements) {
|
|
600
|
+
const placement = compactPlacement(TerminalReplicaPlacementSchemaZ.parse(value));
|
|
601
|
+
writer.value(placement);
|
|
602
|
+
const pending = checkpoint(
|
|
603
|
+
placement.reduce<number>(
|
|
604
|
+
(size, value) => size + (typeof value === "string" ? value.length : 8),
|
|
605
|
+
0,
|
|
606
|
+
),
|
|
607
|
+
);
|
|
608
|
+
if (pending) await pending;
|
|
609
|
+
}
|
|
610
|
+
writer.value(compactBootstrap(metadata.bootstrap));
|
|
611
|
+
options.signal?.throwIfAborted();
|
|
612
|
+
return writer.finish();
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error instanceof BinaryTerminalLimitError)
|
|
615
|
+
throw new TerminalDeliveryStateTooLargeError(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES + 1);
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
412
618
|
}
|
|
413
619
|
|
|
414
620
|
// Keep the ordinary small-patch encoder unchanged. Large seeds are validated
|
|
@@ -3177,15 +3383,21 @@ export function commitTerminalDelivery(
|
|
|
3177
3383
|
throw new TypeError("Terminal delivery representation hash mismatch");
|
|
3178
3384
|
let canonicalSnapshot = state.canonicalSnapshot;
|
|
3179
3385
|
let semanticUpdate: TerminalSemanticDeliveryPayload | null = null;
|
|
3180
|
-
if (
|
|
3386
|
+
if (
|
|
3387
|
+
envelope.encoding === "semantic-v1" ||
|
|
3388
|
+
envelope.encoding === "semantic-compact-v1" ||
|
|
3389
|
+
envelope.encoding === "semantic-binary-v1"
|
|
3390
|
+
) {
|
|
3181
3391
|
const payload =
|
|
3182
|
-
envelope.encoding === "semantic-
|
|
3183
|
-
?
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3392
|
+
envelope.encoding === "semantic-binary-v1"
|
|
3393
|
+
? decodeBinarySemanticTerminalUpdate(staged.bytes)
|
|
3394
|
+
: envelope.encoding === "semantic-compact-v1"
|
|
3395
|
+
? decodeVerifiedCompactSemanticTerminalUpdate(
|
|
3396
|
+
staged.bytes,
|
|
3397
|
+
canonicalSnapshot,
|
|
3398
|
+
envelope.canonicalStateHash,
|
|
3399
|
+
).payload
|
|
3400
|
+
: decodeSemanticTerminalUpdate(staged.bytes);
|
|
3189
3401
|
semanticUpdate = payload;
|
|
3190
3402
|
if (payload.frame !== envelope.frame || payload.revision !== envelope.canonicalRevision)
|
|
3191
3403
|
throw new TypeError("Semantic frame or revision mismatch");
|
|
@@ -3407,3 +3619,146 @@ function isNewerIncarnation(current: string, candidate: string): boolean {
|
|
|
3407
3619
|
Number(candidateEpoch) > Number(currentEpoch)
|
|
3408
3620
|
);
|
|
3409
3621
|
}
|
|
3622
|
+
|
|
3623
|
+
/** Internal bounded viewport codec shared by the explicitly partial TCP1 protocol.
|
|
3624
|
+
* Callers validate projection metadata; these adapters never construct a complete snapshot. */
|
|
3625
|
+
export function encodeTerminalProjectionViewportTuple(
|
|
3626
|
+
snapshot: Omit<TerminalReplicaSnapshot, "history">,
|
|
3627
|
+
): readonly unknown[] {
|
|
3628
|
+
const budget = {
|
|
3629
|
+
...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
|
|
3630
|
+
maxCells: COMPACT_MAX_EXPANDED_CELLS,
|
|
3631
|
+
};
|
|
3632
|
+
if (
|
|
3633
|
+
snapshot.cols > COMPACT_MAX_DIMENSION ||
|
|
3634
|
+
snapshot.rows > COMPACT_MAX_DIMENSION ||
|
|
3635
|
+
snapshot.grid.length !== snapshot.rows
|
|
3636
|
+
)
|
|
3637
|
+
compactEncodingLimit();
|
|
3638
|
+
return [
|
|
3639
|
+
snapshot.cols,
|
|
3640
|
+
snapshot.rows,
|
|
3641
|
+
compactRows(snapshot.grid, snapshot.cols, budget),
|
|
3642
|
+
compactCursor(snapshot.cursor),
|
|
3643
|
+
compactModes(snapshot.modes),
|
|
3644
|
+
compactPlacements(snapshot.placements, budget),
|
|
3645
|
+
compactBootstrap(snapshot.bootstrap),
|
|
3646
|
+
];
|
|
3647
|
+
}
|
|
3648
|
+
export function decodeTerminalProjectionViewportTuple(
|
|
3649
|
+
value: unknown,
|
|
3650
|
+
): Omit<TerminalReplicaSnapshot, "history"> {
|
|
3651
|
+
const input = compactArray(value, 7, "projection viewport");
|
|
3652
|
+
const budget = {
|
|
3653
|
+
...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
|
|
3654
|
+
maxCells: COMPACT_MAX_EXPANDED_CELLS,
|
|
3655
|
+
};
|
|
3656
|
+
const cols = compactInteger(input[0], 1, COMPACT_MAX_DIMENSION, "columns"),
|
|
3657
|
+
rows = compactInteger(input[1], 1, COMPACT_MAX_DIMENSION, "rows");
|
|
3658
|
+
return {
|
|
3659
|
+
cols,
|
|
3660
|
+
rows,
|
|
3661
|
+
grid: expandRows(input[2], budget, cols, rows, "grid"),
|
|
3662
|
+
cursor: expandCursor(input[3], cols, rows),
|
|
3663
|
+
modes: expandModes(input[4]),
|
|
3664
|
+
placements: expandPlacements(input[5], budget),
|
|
3665
|
+
bootstrap: expandBootstrap(input[6]),
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
export function encodeTerminalProjectionPatchTuple(
|
|
3669
|
+
patch: Omit<TerminalReplicaPatchPayload, "history" | "historyDelta">,
|
|
3670
|
+
): readonly unknown[] {
|
|
3671
|
+
const tuple = compactPatch(patch);
|
|
3672
|
+
return [tuple[0], tuple[1], tuple[4], tuple[5], tuple[6], tuple[7]];
|
|
3673
|
+
}
|
|
3674
|
+
export function decodeTerminalProjectionPatchTuple(
|
|
3675
|
+
value: unknown,
|
|
3676
|
+
): Omit<TerminalReplicaPatchPayload, "history" | "historyDelta"> {
|
|
3677
|
+
const tuple = compactArray(value, 6, "projection patch");
|
|
3678
|
+
const budget = {
|
|
3679
|
+
...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
|
|
3680
|
+
maxCells: COMPACT_MAX_EXPANDED_CELLS,
|
|
3681
|
+
};
|
|
3682
|
+
// Null means absent patch operations, never an empty canonical history.
|
|
3683
|
+
return expandPatch(
|
|
3684
|
+
[tuple[0], tuple[1], null, null, tuple[2], tuple[3], tuple[4], tuple[5]],
|
|
3685
|
+
budget,
|
|
3686
|
+
);
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
/** Writes one bounded projection row, reusing immutable-row schema provenance. */
|
|
3690
|
+
export async function writeTerminalProjectionRowCooperatively(
|
|
3691
|
+
writer: BinaryTerminalWriter,
|
|
3692
|
+
row: TerminalReplicaRow,
|
|
3693
|
+
cols: number | null,
|
|
3694
|
+
options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
|
|
3695
|
+
): Promise<void> {
|
|
3696
|
+
options.signal?.throwIfAborted();
|
|
3697
|
+
const owned = isOwnedTerminalReplicaRow(row);
|
|
3698
|
+
const validatedRow =
|
|
3699
|
+
owned && (validatedOwnedEncodingRows.has(row) || isSchemaValidProjectedTerminalReplicaRow(row));
|
|
3700
|
+
const { cells, ...header } = row;
|
|
3701
|
+
const rowHeader = CompactRowHeaderSchema.parse(header);
|
|
3702
|
+
if (
|
|
3703
|
+
!Array.isArray(cells) ||
|
|
3704
|
+
cells.length > COMPACT_MAX_DIMENSION ||
|
|
3705
|
+
(cols !== null && cells.length !== cols)
|
|
3706
|
+
)
|
|
3707
|
+
compactEncodingLimit();
|
|
3708
|
+
const runs: CompactCellRun[] = [];
|
|
3709
|
+
let prior: CompactCellRun | undefined,
|
|
3710
|
+
work = 0,
|
|
3711
|
+
characters = 0;
|
|
3712
|
+
const checkpoint = (length: number): Promise<void> | undefined => {
|
|
3713
|
+
options.signal?.throwIfAborted();
|
|
3714
|
+
work++;
|
|
3715
|
+
characters += length + 64;
|
|
3716
|
+
if (work < 256 && characters < 32 * 1024) return undefined;
|
|
3717
|
+
work = 0;
|
|
3718
|
+
characters = 0;
|
|
3719
|
+
return options.yieldControl().then(() => options.signal?.throwIfAborted());
|
|
3720
|
+
};
|
|
3721
|
+
for (let offset = 0; offset < cells.length; offset += 256) {
|
|
3722
|
+
const slice = cells.slice(offset, offset + 256);
|
|
3723
|
+
const validated =
|
|
3724
|
+
validatedRow || (owned && isValidatedTrustedBlankSlice(slice))
|
|
3725
|
+
? slice
|
|
3726
|
+
: owned
|
|
3727
|
+
? validateOwnedCellSlice(slice)
|
|
3728
|
+
: CompactCellSliceSchema.parse(slice);
|
|
3729
|
+
for (const cell of validated) {
|
|
3730
|
+
const encoded = compactCell(cell);
|
|
3731
|
+
if (prior && compactRunCellEqual(prior, encoded)) prior[0]++;
|
|
3732
|
+
else {
|
|
3733
|
+
runs.push(encoded);
|
|
3734
|
+
prior = encoded;
|
|
3735
|
+
}
|
|
3736
|
+
const pause = checkpoint(cell.grapheme.length);
|
|
3737
|
+
if (pause) await pause;
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
writer.array(2);
|
|
3741
|
+
writer.value(rowHeader.wrapped ? 1 : 0);
|
|
3742
|
+
writer.array(runs.length);
|
|
3743
|
+
for (const run of runs) {
|
|
3744
|
+
writer.value(run);
|
|
3745
|
+
const pause = checkpoint(run[1].length);
|
|
3746
|
+
if (pause) await pause;
|
|
3747
|
+
}
|
|
3748
|
+
if (owned) validatedOwnedEncodingRows.add(row);
|
|
3749
|
+
}
|
|
3750
|
+
/** Metadata-only tuples; no row graph, history, or JSON cell serialization. */
|
|
3751
|
+
export function encodeTerminalProjectionMetadataTuple(
|
|
3752
|
+
value: Omit<TerminalReplicaSnapshot, "grid" | "history">,
|
|
3753
|
+
): readonly unknown[] {
|
|
3754
|
+
const budget = {
|
|
3755
|
+
...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
|
|
3756
|
+
maxCells: COMPACT_MAX_EXPANDED_CELLS,
|
|
3757
|
+
};
|
|
3758
|
+
return [
|
|
3759
|
+
compactCursor(value.cursor),
|
|
3760
|
+
compactModes(value.modes),
|
|
3761
|
+
compactPlacements(value.placements, budget),
|
|
3762
|
+
compactBootstrap(value.bootstrap),
|
|
3763
|
+
];
|
|
3764
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// node --import tsx packages/core/src/terminal-projection-vectors.mjs
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
blankTerminalReplicaSnapshot,
|
|
6
|
+
freezeTerminalReplicaSnapshot,
|
|
7
|
+
hashTerminalReplicaSnapshot,
|
|
8
|
+
} from "./terminal-replica.ts";
|
|
9
|
+
import {
|
|
10
|
+
encodeBinarySemanticTerminalUpdate,
|
|
11
|
+
hashTerminalDeliveryRepresentation,
|
|
12
|
+
} from "./terminal-delivery.ts";
|
|
13
|
+
import {
|
|
14
|
+
projectAlternateSnapshot,
|
|
15
|
+
applyTerminalProjectionPatch,
|
|
16
|
+
hashTerminalProjection,
|
|
17
|
+
hashSourceSnapshotFromProjection,
|
|
18
|
+
hashTerminalProjectionTombstone,
|
|
19
|
+
encodeTerminalProjection,
|
|
20
|
+
} from "./terminal-projection.ts";
|
|
21
|
+
import { hashTerminalReplicaTombstone } from "./terminal-replica.ts";
|
|
22
|
+
const base = blankTerminalReplicaSnapshot(3, 2);
|
|
23
|
+
const source = freezeTerminalReplicaSnapshot({
|
|
24
|
+
...base,
|
|
25
|
+
modes: { ...base.modes, alternateScreen: true },
|
|
26
|
+
history: [base.grid[0], base.grid[1]],
|
|
27
|
+
});
|
|
28
|
+
const initial = projectAlternateSnapshot(source);
|
|
29
|
+
const row = {
|
|
30
|
+
...initial.grid[1],
|
|
31
|
+
cells: initial.grid[1].cells.map((cell, index) => (index ? cell : { ...cell, grapheme: "é" })),
|
|
32
|
+
};
|
|
33
|
+
const patch = { rows: [{ index: 1, row }], deferredHistory: initial.deferredHistory };
|
|
34
|
+
const next = applyTerminalProjectionPatch(initial, patch);
|
|
35
|
+
const entries = [
|
|
36
|
+
{ payload: { frame: "seed", revision: 7, projection: initial }, projection: initial },
|
|
37
|
+
{ payload: { frame: "patch", baseRevision: 7, revision: 8, patch }, projection: next },
|
|
38
|
+
{
|
|
39
|
+
payload: {
|
|
40
|
+
frame: "tombstone",
|
|
41
|
+
baseRevision: 8,
|
|
42
|
+
revision: 9,
|
|
43
|
+
tombstone: { reason: "pane-closed" },
|
|
44
|
+
},
|
|
45
|
+
projection: null,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
const vectors = entries.map(({ payload, projection }) => {
|
|
49
|
+
const bytes = encodeTerminalProjection(payload);
|
|
50
|
+
return {
|
|
51
|
+
payload,
|
|
52
|
+
bytes: [...bytes],
|
|
53
|
+
representationHash: hashTerminalDeliveryRepresentation(bytes),
|
|
54
|
+
projectionStateHash: projection
|
|
55
|
+
? hashTerminalProjection(projection)
|
|
56
|
+
: hashTerminalProjectionTombstone(payload.tombstone.reason),
|
|
57
|
+
canonicalStateHash: projection
|
|
58
|
+
? hashSourceSnapshotFromProjection(projection)
|
|
59
|
+
: hashTerminalReplicaTombstone(payload.tombstone.reason),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
const materialized = { ...source, grid: next.grid };
|
|
63
|
+
const materializedBytes = encodeBinarySemanticTerminalUpdate({
|
|
64
|
+
frame: "seed",
|
|
65
|
+
revision: 8,
|
|
66
|
+
snapshot: materialized,
|
|
67
|
+
});
|
|
68
|
+
writeFileSync(
|
|
69
|
+
new URL("fixtures/terminal-projection-vectors.json", import.meta.url),
|
|
70
|
+
JSON.stringify(
|
|
71
|
+
{
|
|
72
|
+
source,
|
|
73
|
+
sourceHash: hashTerminalReplicaSnapshot(source),
|
|
74
|
+
materialized,
|
|
75
|
+
materializedBytes: [...materializedBytes],
|
|
76
|
+
materializedRepresentationHash: hashTerminalDeliveryRepresentation(materializedBytes),
|
|
77
|
+
vectors,
|
|
78
|
+
},
|
|
79
|
+
null,
|
|
80
|
+
2,
|
|
81
|
+
) + "\n",
|
|
82
|
+
);
|