tmux-ide 2.9.10 → 2.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/bin/cli.js +4321 -2596
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +4 -0
  4. package/packages/contracts/src/pane-stream.ts +72 -0
  5. package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
  6. package/packages/contracts/src/terminal-delivery.ts +17 -2
  7. package/packages/contracts/src/terminal-graphics.ts +68 -0
  8. package/packages/contracts/src/terminal-projection.ts +153 -0
  9. package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
  10. package/packages/core/src/index.ts +2 -0
  11. package/packages/core/src/terminal-delivery-binary.ts +160 -0
  12. package/packages/core/src/terminal-delivery.ts +371 -16
  13. package/packages/core/src/terminal-projection-vectors.mjs +82 -0
  14. package/packages/core/src/terminal-projection.ts +476 -0
  15. package/packages/core/src/terminal-replica-hash-cache.ts +8 -1
  16. package/packages/core/src/terminal-replica.ts +1 -1
  17. package/packages/daemon/dist/lib/daemon-embed.js +39 -3
  18. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
  19. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -0
  20. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  21. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  22. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  23. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  24. package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
  25. package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
  26. package/packages/daemon/dist/terminal/mirror/mirror-service.js +12 -2
  27. package/packages/daemon/dist/terminal/mirror/session-channel.js +46 -23
  28. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +190 -19
  29. package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
  30. package/packages/daemon/dist/terminal/session-runtime/registry.js +19 -6
  31. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
  32. package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
  33. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +95 -36
  34. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +5 -0
  35. package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +2 -2
  36. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
  37. package/packages/daemon/dist/tui/mirror/runtime/application-generation-starter.js +16 -1
  38. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agent-navigation.js +13 -0
  39. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +12 -4
  40. package/packages/daemon/dist/tui/mirror/runtime/application-retained-agent.js +18 -0
  41. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +90 -5
  42. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
  43. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +2 -2
  44. package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
  45. package/packages/daemon/dist/tui/mirror/runtime/runtime-layout-presentation.js +6 -3
  46. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +7 -2
  47. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
  48. package/packages/daemon/dist/tui/mirror/runtime/terminal-selection-waiter.js +36 -0
  49. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
  50. package/packages/daemon/src/lib/daemon-embed.ts +38 -2
  51. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
  52. package/packages/daemon/src/lib/tmux-server-owner.ts +3 -0
  53. package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
  54. package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
  55. package/packages/daemon/src/terminal/mirror/mirror-service.ts +19 -2
  56. package/packages/daemon/src/terminal/mirror/session-channel.ts +57 -9
  57. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +248 -16
  58. package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
  59. package/packages/daemon/src/terminal/session-runtime/registry.ts +25 -3
  60. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
  61. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
  62. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
  63. package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
  64. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +102 -44
  65. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +6 -0
  66. package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +3 -0
  67. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +6 -0
  68. package/packages/daemon/src/tui/mirror/runtime/application-generation-starter.ts +17 -1
  69. package/packages/daemon/src/tui/mirror/runtime/application-machine-agent-navigation.ts +15 -0
  70. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +18 -4
  71. package/packages/daemon/src/tui/mirror/runtime/application-retained-agent.ts +29 -0
  72. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +89 -6
  73. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
  74. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +2 -2
  75. package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
  76. package/packages/daemon/src/tui/mirror/runtime/runtime-layout-presentation.ts +7 -4
  77. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +9 -1
  78. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
  79. package/packages/daemon/src/tui/mirror/runtime/terminal-selection-waiter.ts +36 -0
  80. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
  81. package/packages/daemon-client/src/pane-stream-client.ts +92 -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 encoding = (
75
- ["semantic-compact-v1", "semantic-v1", "ansi-diff-v1", "ansi-raw-v1"] as const
76
- ).find((value) => offer.encodings.includes(value));
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 (offer.richPlacements && encoding !== "semantic-v1" && encoding !== "semantic-compact-v1")
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 && (encoding === "semantic-v1" || encoding === "semantic-compact-v1"),
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
- const bytes = UTF8_ENCODER.encode(JSON.stringify(wire));
410
- assertRepresentationSize(bytes);
411
- return bytes;
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 (envelope.encoding === "semantic-v1" || envelope.encoding === "semantic-compact-v1") {
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-compact-v1"
3183
- ? decodeVerifiedCompactSemanticTerminalUpdate(
3184
- staged.bytes,
3185
- canonicalSnapshot,
3186
- envelope.canonicalStateHash,
3187
- ).payload
3188
- : decodeSemanticTerminalUpdate(staged.bytes);
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
+ );