tmux-ide 2.9.11 → 2.9.13

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.
Files changed (84) hide show
  1. package/bin/cli.js +6036 -3047
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +6 -0
  4. package/packages/contracts/src/pane-stream.ts +77 -0
  5. package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
  6. package/packages/contracts/src/terminal-delivery.ts +19 -3
  7. package/packages/contracts/src/terminal-graphics-compressed.ts +66 -0
  8. package/packages/contracts/src/terminal-graphics.ts +73 -0
  9. package/packages/contracts/src/terminal-projection.ts +153 -0
  10. package/packages/contracts/src/terminal-surfaces.ts +137 -0
  11. package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
  12. package/packages/core/src/index.ts +2 -0
  13. package/packages/core/src/terminal-delivery-binary.ts +160 -0
  14. package/packages/core/src/terminal-delivery.ts +377 -16
  15. package/packages/core/src/terminal-projection-vectors.mjs +82 -0
  16. package/packages/core/src/terminal-projection.ts +476 -0
  17. package/packages/core/src/terminal-replica.ts +34 -1
  18. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +2 -0
  19. package/packages/daemon/dist/command-center/resources/saved-machine-watch.js +117 -0
  20. package/packages/daemon/dist/command-center/server.js +13 -0
  21. package/packages/daemon/dist/command-center/tmux-servers.js +9 -0
  22. package/packages/daemon/dist/lib/daemon-embed.js +71 -4
  23. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
  24. package/packages/daemon/dist/lib/native-surface-context.js +96 -0
  25. package/packages/daemon/dist/lib/tmux-server-owner.js +10 -0
  26. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  27. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  28. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  29. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  30. package/packages/daemon/dist/server/native-surface-provider-upgrade.js +88 -0
  31. package/packages/daemon/dist/terminal/graphics/compress-chunk.js +40 -0
  32. package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
  33. package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
  34. package/packages/daemon/dist/terminal/graphics/native-surface-demand.js +99 -0
  35. package/packages/daemon/dist/terminal/graphics/native-surface-provider-channel.js +162 -0
  36. package/packages/daemon/dist/terminal/graphics/native-surface-provider.js +275 -0
  37. package/packages/daemon/dist/terminal/graphics/native-surface-viewer.js +146 -0
  38. package/packages/daemon/dist/terminal/mirror/mirror-service.js +3 -0
  39. package/packages/daemon/dist/terminal/mirror/session-channel.js +32 -9
  40. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +369 -16
  41. package/packages/daemon/dist/terminal/pane-stream/runtime.js +10 -0
  42. package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
  43. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
  44. package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
  45. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
  46. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
  47. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
  48. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
  49. package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
  50. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
  51. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
  52. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +4 -0
  53. package/packages/daemon/src/command-center/resources/saved-machine-watch.ts +132 -0
  54. package/packages/daemon/src/command-center/server.ts +18 -0
  55. package/packages/daemon/src/command-center/tmux-servers.ts +14 -0
  56. package/packages/daemon/src/lib/daemon-embed.ts +81 -3
  57. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
  58. package/packages/daemon/src/lib/native-surface-context.ts +123 -0
  59. package/packages/daemon/src/lib/tmux-server-owner.ts +12 -0
  60. package/packages/daemon/src/server/native-surface-provider-upgrade.ts +103 -0
  61. package/packages/daemon/src/terminal/graphics/compress-chunk.ts +49 -0
  62. package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
  63. package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
  64. package/packages/daemon/src/terminal/graphics/native-surface-demand.ts +88 -0
  65. package/packages/daemon/src/terminal/graphics/native-surface-provider-channel.ts +161 -0
  66. package/packages/daemon/src/terminal/graphics/native-surface-provider.ts +308 -0
  67. package/packages/daemon/src/terminal/graphics/native-surface-viewer.ts +154 -0
  68. package/packages/daemon/src/terminal/mirror/mirror-service.ts +5 -0
  69. package/packages/daemon/src/terminal/mirror/session-channel.ts +31 -10
  70. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +446 -15
  71. package/packages/daemon/src/terminal/pane-stream/runtime.ts +14 -1
  72. package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
  73. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
  74. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
  75. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
  76. package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
  77. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
  78. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
  79. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
  80. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
  81. package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
  82. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
  83. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
  84. 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,14 @@
1
+ import {
2
+ TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION,
3
+ TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
4
+ TERMINAL_PROJECTION_CAPABILITY_VERSION,
5
+ TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
6
+ } from "@tmux-ide/contracts";
7
+ import {
8
+ BinaryTerminalWriter,
9
+ BinaryTerminalLimitError,
10
+ decodeBinaryTerminalTuple,
11
+ } from "./terminal-delivery-binary.ts";
1
12
  import { z } from "zod";
2
13
  import {
3
14
  isOwnedTerminalReplicaRow,
@@ -6,6 +17,8 @@ import {
6
17
  TERMINAL_REPLICA_SPACE_CELL,
7
18
  } from "./terminal-replica-owned-row.ts";
8
19
  import {
20
+ TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION,
21
+ TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION,
9
22
  TERMINAL_DELIVERY_CHUNK_BYTES,
10
23
  TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES,
11
24
  TERMINAL_DELIVERY_PROTOCOL_VERSION,
@@ -71,22 +84,52 @@ export function negotiateTerminalDelivery(
71
84
  ): TerminalDeliveryNegotiationResult {
72
85
  if (!offer.protocolVersions.includes(TERMINAL_DELIVERY_PROTOCOL_VERSION))
73
86
  return { accepted: false, reason: "protocol-version-mismatch" };
74
- const encoding = (
75
- ["semantic-compact-v1", "semantic-v1", "ansi-diff-v1", "ansi-raw-v1"] as const
76
- ).find((value) => offer.encodings.includes(value));
87
+ const binary =
88
+ offer.protocolVersions.some(
89
+ (v) =>
90
+ v === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
91
+ v === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
92
+ v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
93
+ v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
94
+ v === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
95
+ v === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
96
+ ) && offer.encodings.includes("semantic-compact-v1");
97
+ const encoding = binary
98
+ ? ("semantic-binary-v1" as const)
99
+ : (["semantic-compact-v1", "semantic-v1", "ansi-diff-v1", "ansi-raw-v1"] as const).find(
100
+ (value) => offer.encodings.includes(value),
101
+ );
77
102
  if (!encoding) return { accepted: false, reason: "encoding-mismatch" };
78
- if (offer.richPlacements && encoding !== "semantic-v1" && encoding !== "semantic-compact-v1")
103
+ if (
104
+ offer.richPlacements &&
105
+ encoding !== "semantic-v1" &&
106
+ encoding !== "semantic-compact-v1" &&
107
+ encoding !== "semantic-binary-v1"
108
+ )
79
109
  return { accepted: false, reason: "unsupported-capability-combination" };
80
110
  return {
81
111
  accepted: true,
82
112
  negotiated: {
83
113
  protocolVersion: TERMINAL_DELIVERY_PROTOCOL_VERSION,
84
114
  encoding,
115
+ ...(binary &&
116
+ offer.protocolVersions.some(
117
+ (v) =>
118
+ v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
119
+ v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
120
+ v === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
121
+ v === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
122
+ )
123
+ ? { alternateScreenProjection: "deferred-history-v1" as const }
124
+ : {}),
85
125
  ...(encoding === "semantic-compact-v1" && offer.encodings.includes("semantic-v1")
86
126
  ? { fallbackEncoding: "semantic-v1" as const }
87
127
  : {}),
88
128
  richPlacements:
89
- offer.richPlacements && (encoding === "semantic-v1" || encoding === "semantic-compact-v1"),
129
+ offer.richPlacements &&
130
+ (encoding === "semantic-v1" ||
131
+ encoding === "semantic-compact-v1" ||
132
+ encoding === "semantic-binary-v1"),
90
133
  generation,
91
134
  deliveryNonce,
92
135
  },
@@ -370,6 +413,12 @@ function hasOwnedSeedRow(input: TerminalSemanticDeliveryPayload): boolean {
370
413
  export function encodeCompactSemanticTerminalUpdate(
371
414
  input: TerminalSemanticDeliveryPayload,
372
415
  ): Uint8Array {
416
+ const wire = compactSemanticWire(input);
417
+ const bytes = UTF8_ENCODER.encode(JSON.stringify(wire));
418
+ assertRepresentationSize(bytes);
419
+ return bytes;
420
+ }
421
+ function compactSemanticWire(input: TerminalSemanticDeliveryPayload) {
373
422
  const update = (
374
423
  hasOwnedPatchRow(input)
375
424
  ? CompactPatchEncodingPayloadSchema
@@ -406,9 +455,172 @@ export function encodeCompactSemanticTerminalUpdate(
406
455
  t: update.tombstone.reason,
407
456
  v: 1,
408
457
  };
409
- const bytes = UTF8_ENCODER.encode(JSON.stringify(wire));
410
- assertRepresentationSize(bytes);
411
- return bytes;
458
+ return wire;
459
+ }
460
+
461
+ export function encodeBinarySemanticTerminalUpdate(
462
+ input: TerminalSemanticDeliveryPayload,
463
+ ): Uint8Array {
464
+ const wire = compactSemanticWire(input);
465
+ const writer = new BinaryTerminalWriter();
466
+ try {
467
+ writer.header(input.frame, wire.r, input.frame === "seed" ? undefined : input.baseRevision);
468
+ writer.value(wire.f === "s" ? wire.s : wire.f === "p" ? wire.p : wire.t);
469
+ return writer.finish();
470
+ } catch (error) {
471
+ if (error instanceof BinaryTerminalLimitError)
472
+ throw new TerminalDeliveryStateTooLargeError(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES + 1);
473
+ throw error;
474
+ }
475
+ }
476
+ export function decodeBinarySemanticTerminalUpdate(
477
+ bytes: Uint8Array,
478
+ ): TerminalSemanticDeliveryPayload {
479
+ const decoded = decodeBinaryTerminalTuple(bytes),
480
+ budget = { ...compactDecodeBudget(bytes.length), maxCells: COMPACT_MAX_EXPANDED_CELLS };
481
+ if (decoded.frame === "seed")
482
+ return Object.freeze({
483
+ frame: "seed",
484
+ revision: decoded.revision,
485
+ snapshot: expandSnapshot(decoded.value, budget),
486
+ });
487
+ if (decoded.baseRevision === undefined || decoded.revision <= decoded.baseRevision)
488
+ throw new TypeError("Binary revision did not advance");
489
+ if (decoded.frame === "patch")
490
+ return Object.freeze({
491
+ frame: "patch",
492
+ revision: decoded.revision,
493
+ baseRevision: decoded.baseRevision,
494
+ patch: expandPatch(decoded.value, budget),
495
+ });
496
+ if (
497
+ typeof decoded.value !== "string" ||
498
+ !["pane-closed", "session-restarted", "runtime-disposed"].includes(decoded.value)
499
+ )
500
+ throw new TypeError("Invalid binary tombstone");
501
+ return Object.freeze({
502
+ frame: "tombstone",
503
+ revision: decoded.revision,
504
+ baseRevision: decoded.baseRevision,
505
+ tombstone: Object.freeze({
506
+ reason: decoded.value as "pane-closed" | "session-restarted" | "runtime-disposed",
507
+ }),
508
+ });
509
+ }
510
+ export async function encodeBinarySemanticTerminalUpdateCooperatively(
511
+ input: TerminalSemanticDeliveryPayload,
512
+ options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
513
+ ): Promise<Uint8Array> {
514
+ options.signal?.throwIfAborted();
515
+ if (input.frame !== "seed") return encodeBinarySemanticTerminalUpdate(input);
516
+ const { snapshot: source, ...headerInput } = input;
517
+ const header = CompactSeedHeaderSchema.parse(headerInput);
518
+ const { grid, history, placements, ...metadataInput } = source;
519
+ const metadata = CompactSeedMetadataSchema.parse(metadataInput);
520
+ if (
521
+ !Array.isArray(grid) ||
522
+ !Array.isArray(history) ||
523
+ !Array.isArray(placements) ||
524
+ grid.length !== metadata.rows ||
525
+ metadata.cols > COMPACT_MAX_DIMENSION ||
526
+ metadata.rows > COMPACT_MAX_DIMENSION
527
+ )
528
+ compactEncodingLimit();
529
+ let rowCount = 0,
530
+ cellCount = 0,
531
+ runCount = 0,
532
+ work = 0,
533
+ encodedWork = 0;
534
+ const checkpoint = (characters: number): Promise<void> | undefined => {
535
+ options.signal?.throwIfAborted();
536
+ work++;
537
+ encodedWork += characters + 64;
538
+ if (work >= 256 || encodedWork >= 32 * 1024) {
539
+ work = 0;
540
+ encodedWork = 0;
541
+ return options.yieldControl().then(() => options.signal?.throwIfAborted());
542
+ }
543
+ return undefined;
544
+ };
545
+ const writer = new BinaryTerminalWriter();
546
+ try {
547
+ writer.header("seed", header.revision);
548
+ writer.array(8);
549
+ writer.value(metadata.cols);
550
+ writer.value(metadata.rows);
551
+ for (const rows of [grid, history]) {
552
+ writer.array(rows.length);
553
+ for (const row of rows) {
554
+ const owned = isOwnedTerminalReplicaRow(row);
555
+ const validatedRow =
556
+ owned &&
557
+ (validatedOwnedEncodingRows.has(row) || isSchemaValidProjectedTerminalReplicaRow(row));
558
+ const { cells, ...rowHeaderInput } = row;
559
+ const rowHeader = CompactRowHeaderSchema.parse(rowHeaderInput);
560
+ if (
561
+ ++rowCount > COMPACT_MAX_ROWS ||
562
+ !Array.isArray(cells) ||
563
+ cells.length !== metadata.cols
564
+ )
565
+ compactEncodingLimit();
566
+ cellCount += cells.length;
567
+ if (cellCount > COMPACT_MAX_EXPANDED_CELLS) compactEncodingLimit();
568
+ const runs: CompactCellRun[] = [];
569
+ let prior: CompactCellRun | undefined;
570
+ for (let offset = 0; offset < cells.length; offset += 256) {
571
+ const slice = cells.slice(offset, offset + 256);
572
+ const validated =
573
+ validatedRow || (owned && isValidatedTrustedBlankSlice(slice))
574
+ ? slice
575
+ : owned
576
+ ? validateOwnedCellSlice(slice)
577
+ : CompactCellSliceSchema.parse(slice);
578
+ for (const cell of validated) {
579
+ const encoded = compactCell(cell);
580
+ if (prior && compactRunCellEqual(prior, encoded)) prior[0]++;
581
+ else {
582
+ if (++runCount > COMPACT_MAX_RUNS) compactEncodingLimit();
583
+ runs.push(encoded);
584
+ prior = encoded;
585
+ }
586
+ const pending = checkpoint(cell.grapheme.length);
587
+ if (pending) await pending;
588
+ }
589
+ }
590
+ writer.array(2);
591
+ writer.value(rowHeader.wrapped ? 1 : 0);
592
+ writer.array(runs.length);
593
+ for (const run of runs) {
594
+ writer.value(run);
595
+ const pending = checkpoint(run[1].length);
596
+ if (pending) await pending;
597
+ }
598
+ if (owned) validatedOwnedEncodingRows.add(row);
599
+ }
600
+ }
601
+ writer.value(compactCursor(metadata.cursor));
602
+ writer.value(compactModes(metadata.modes));
603
+ if (placements.length > COMPACT_MAX_PLACEMENTS) compactEncodingLimit();
604
+ writer.array(placements.length);
605
+ for (const value of placements) {
606
+ const placement = compactPlacement(TerminalReplicaPlacementSchemaZ.parse(value));
607
+ writer.value(placement);
608
+ const pending = checkpoint(
609
+ placement.reduce<number>(
610
+ (size, value) => size + (typeof value === "string" ? value.length : 8),
611
+ 0,
612
+ ),
613
+ );
614
+ if (pending) await pending;
615
+ }
616
+ writer.value(compactBootstrap(metadata.bootstrap));
617
+ options.signal?.throwIfAborted();
618
+ return writer.finish();
619
+ } catch (error) {
620
+ if (error instanceof BinaryTerminalLimitError)
621
+ throw new TerminalDeliveryStateTooLargeError(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES + 1);
622
+ throw error;
623
+ }
412
624
  }
413
625
 
414
626
  // Keep the ordinary small-patch encoder unchanged. Large seeds are validated
@@ -3177,15 +3389,21 @@ export function commitTerminalDelivery(
3177
3389
  throw new TypeError("Terminal delivery representation hash mismatch");
3178
3390
  let canonicalSnapshot = state.canonicalSnapshot;
3179
3391
  let semanticUpdate: TerminalSemanticDeliveryPayload | null = null;
3180
- if (envelope.encoding === "semantic-v1" || envelope.encoding === "semantic-compact-v1") {
3392
+ if (
3393
+ envelope.encoding === "semantic-v1" ||
3394
+ envelope.encoding === "semantic-compact-v1" ||
3395
+ envelope.encoding === "semantic-binary-v1"
3396
+ ) {
3181
3397
  const payload =
3182
- envelope.encoding === "semantic-compact-v1"
3183
- ? decodeVerifiedCompactSemanticTerminalUpdate(
3184
- staged.bytes,
3185
- canonicalSnapshot,
3186
- envelope.canonicalStateHash,
3187
- ).payload
3188
- : decodeSemanticTerminalUpdate(staged.bytes);
3398
+ envelope.encoding === "semantic-binary-v1"
3399
+ ? decodeBinarySemanticTerminalUpdate(staged.bytes)
3400
+ : envelope.encoding === "semantic-compact-v1"
3401
+ ? decodeVerifiedCompactSemanticTerminalUpdate(
3402
+ staged.bytes,
3403
+ canonicalSnapshot,
3404
+ envelope.canonicalStateHash,
3405
+ ).payload
3406
+ : decodeSemanticTerminalUpdate(staged.bytes);
3189
3407
  semanticUpdate = payload;
3190
3408
  if (payload.frame !== envelope.frame || payload.revision !== envelope.canonicalRevision)
3191
3409
  throw new TypeError("Semantic frame or revision mismatch");
@@ -3407,3 +3625,146 @@ function isNewerIncarnation(current: string, candidate: string): boolean {
3407
3625
  Number(candidateEpoch) > Number(currentEpoch)
3408
3626
  );
3409
3627
  }
3628
+
3629
+ /** Internal bounded viewport codec shared by the explicitly partial TCP1 protocol.
3630
+ * Callers validate projection metadata; these adapters never construct a complete snapshot. */
3631
+ export function encodeTerminalProjectionViewportTuple(
3632
+ snapshot: Omit<TerminalReplicaSnapshot, "history">,
3633
+ ): readonly unknown[] {
3634
+ const budget = {
3635
+ ...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
3636
+ maxCells: COMPACT_MAX_EXPANDED_CELLS,
3637
+ };
3638
+ if (
3639
+ snapshot.cols > COMPACT_MAX_DIMENSION ||
3640
+ snapshot.rows > COMPACT_MAX_DIMENSION ||
3641
+ snapshot.grid.length !== snapshot.rows
3642
+ )
3643
+ compactEncodingLimit();
3644
+ return [
3645
+ snapshot.cols,
3646
+ snapshot.rows,
3647
+ compactRows(snapshot.grid, snapshot.cols, budget),
3648
+ compactCursor(snapshot.cursor),
3649
+ compactModes(snapshot.modes),
3650
+ compactPlacements(snapshot.placements, budget),
3651
+ compactBootstrap(snapshot.bootstrap),
3652
+ ];
3653
+ }
3654
+ export function decodeTerminalProjectionViewportTuple(
3655
+ value: unknown,
3656
+ ): Omit<TerminalReplicaSnapshot, "history"> {
3657
+ const input = compactArray(value, 7, "projection viewport");
3658
+ const budget = {
3659
+ ...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
3660
+ maxCells: COMPACT_MAX_EXPANDED_CELLS,
3661
+ };
3662
+ const cols = compactInteger(input[0], 1, COMPACT_MAX_DIMENSION, "columns"),
3663
+ rows = compactInteger(input[1], 1, COMPACT_MAX_DIMENSION, "rows");
3664
+ return {
3665
+ cols,
3666
+ rows,
3667
+ grid: expandRows(input[2], budget, cols, rows, "grid"),
3668
+ cursor: expandCursor(input[3], cols, rows),
3669
+ modes: expandModes(input[4]),
3670
+ placements: expandPlacements(input[5], budget),
3671
+ bootstrap: expandBootstrap(input[6]),
3672
+ };
3673
+ }
3674
+ export function encodeTerminalProjectionPatchTuple(
3675
+ patch: Omit<TerminalReplicaPatchPayload, "history" | "historyDelta">,
3676
+ ): readonly unknown[] {
3677
+ const tuple = compactPatch(patch);
3678
+ return [tuple[0], tuple[1], tuple[4], tuple[5], tuple[6], tuple[7]];
3679
+ }
3680
+ export function decodeTerminalProjectionPatchTuple(
3681
+ value: unknown,
3682
+ ): Omit<TerminalReplicaPatchPayload, "history" | "historyDelta"> {
3683
+ const tuple = compactArray(value, 6, "projection patch");
3684
+ const budget = {
3685
+ ...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
3686
+ maxCells: COMPACT_MAX_EXPANDED_CELLS,
3687
+ };
3688
+ // Null means absent patch operations, never an empty canonical history.
3689
+ return expandPatch(
3690
+ [tuple[0], tuple[1], null, null, tuple[2], tuple[3], tuple[4], tuple[5]],
3691
+ budget,
3692
+ );
3693
+ }
3694
+
3695
+ /** Writes one bounded projection row, reusing immutable-row schema provenance. */
3696
+ export async function writeTerminalProjectionRowCooperatively(
3697
+ writer: BinaryTerminalWriter,
3698
+ row: TerminalReplicaRow,
3699
+ cols: number | null,
3700
+ options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
3701
+ ): Promise<void> {
3702
+ options.signal?.throwIfAborted();
3703
+ const owned = isOwnedTerminalReplicaRow(row);
3704
+ const validatedRow =
3705
+ owned && (validatedOwnedEncodingRows.has(row) || isSchemaValidProjectedTerminalReplicaRow(row));
3706
+ const { cells, ...header } = row;
3707
+ const rowHeader = CompactRowHeaderSchema.parse(header);
3708
+ if (
3709
+ !Array.isArray(cells) ||
3710
+ cells.length > COMPACT_MAX_DIMENSION ||
3711
+ (cols !== null && cells.length !== cols)
3712
+ )
3713
+ compactEncodingLimit();
3714
+ const runs: CompactCellRun[] = [];
3715
+ let prior: CompactCellRun | undefined,
3716
+ work = 0,
3717
+ characters = 0;
3718
+ const checkpoint = (length: number): Promise<void> | undefined => {
3719
+ options.signal?.throwIfAborted();
3720
+ work++;
3721
+ characters += length + 64;
3722
+ if (work < 256 && characters < 32 * 1024) return undefined;
3723
+ work = 0;
3724
+ characters = 0;
3725
+ return options.yieldControl().then(() => options.signal?.throwIfAborted());
3726
+ };
3727
+ for (let offset = 0; offset < cells.length; offset += 256) {
3728
+ const slice = cells.slice(offset, offset + 256);
3729
+ const validated =
3730
+ validatedRow || (owned && isValidatedTrustedBlankSlice(slice))
3731
+ ? slice
3732
+ : owned
3733
+ ? validateOwnedCellSlice(slice)
3734
+ : CompactCellSliceSchema.parse(slice);
3735
+ for (const cell of validated) {
3736
+ const encoded = compactCell(cell);
3737
+ if (prior && compactRunCellEqual(prior, encoded)) prior[0]++;
3738
+ else {
3739
+ runs.push(encoded);
3740
+ prior = encoded;
3741
+ }
3742
+ const pause = checkpoint(cell.grapheme.length);
3743
+ if (pause) await pause;
3744
+ }
3745
+ }
3746
+ writer.array(2);
3747
+ writer.value(rowHeader.wrapped ? 1 : 0);
3748
+ writer.array(runs.length);
3749
+ for (const run of runs) {
3750
+ writer.value(run);
3751
+ const pause = checkpoint(run[1].length);
3752
+ if (pause) await pause;
3753
+ }
3754
+ if (owned) validatedOwnedEncodingRows.add(row);
3755
+ }
3756
+ /** Metadata-only tuples; no row graph, history, or JSON cell serialization. */
3757
+ export function encodeTerminalProjectionMetadataTuple(
3758
+ value: Omit<TerminalReplicaSnapshot, "grid" | "history">,
3759
+ ): readonly unknown[] {
3760
+ const budget = {
3761
+ ...compactDecodeBudget(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES),
3762
+ maxCells: COMPACT_MAX_EXPANDED_CELLS,
3763
+ };
3764
+ return [
3765
+ compactCursor(value.cursor),
3766
+ compactModes(value.modes),
3767
+ compactPlacements(value.placements, budget),
3768
+ compactBootstrap(value.bootstrap),
3769
+ ];
3770
+ }
@@ -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
+ );