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,476 @@
1
+ import { isOwnedTerminalReplicaRow } from "./terminal-replica-owned-row.ts";
2
+ import { z } from "zod";
3
+ import {
4
+ TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES,
5
+ TerminalProjectionSnapshotSchemaZ,
6
+ TerminalProjectionPatchSchemaZ,
7
+ TerminalProjectionPayloadSchemaZ,
8
+ TerminalDeferredHistorySchemaZ,
9
+ TerminalProjectionEnvelopeSchemaZ,
10
+ TerminalProjectionReceiptSchemaZ,
11
+ type TerminalReplicaSnapshot,
12
+ type TerminalProjectionSnapshot,
13
+ type TerminalProjectionPatch,
14
+ type TerminalProjectionPayload,
15
+ type TerminalDeferredHistory,
16
+ type TerminalProjectionEnvelope,
17
+ type TerminalProjectionReceipt,
18
+ } from "@tmux-ide/contracts";
19
+ import {
20
+ hashTerminalReplicaRows,
21
+ hashTerminalReplicaTombstone,
22
+ freezeTerminalReplicaRow,
23
+ terminalReplicaRowsEqual,
24
+ } from "./terminal-replica.ts";
25
+ import { hashCanonicalTerminalValue } from "./terminal-replica-hash-cache.ts";
26
+ import {
27
+ BinaryTerminalWriter,
28
+ BinaryTerminalLimitError,
29
+ decodeBinaryTerminalTuple,
30
+ } from "./terminal-delivery-binary.ts";
31
+ import {
32
+ TerminalDeliveryStateTooLargeError,
33
+ writeTerminalProjectionRowCooperatively,
34
+ encodeTerminalProjectionMetadataTuple,
35
+ encodeTerminalProjectionViewportTuple,
36
+ decodeTerminalProjectionViewportTuple,
37
+ encodeTerminalProjectionPatchTuple,
38
+ decodeTerminalProjectionPatchTuple,
39
+ hashTerminalDeliveryRepresentation,
40
+ } from "./terminal-delivery.ts";
41
+
42
+ export function terminalHistoryCommitment(
43
+ snapshot: TerminalReplicaSnapshot,
44
+ ): TerminalDeferredHistory {
45
+ return Object.freeze(
46
+ TerminalDeferredHistorySchemaZ.parse({
47
+ rows: snapshot.history.length,
48
+ digest: hashTerminalReplicaRows(snapshot.history),
49
+ }),
50
+ );
51
+ }
52
+ export function projectAlternateSnapshot(
53
+ snapshot: TerminalReplicaSnapshot,
54
+ ): TerminalProjectionSnapshot {
55
+ const { history: _history, ...viewport } = snapshot;
56
+ void _history;
57
+ const candidate = { ...viewport, deferredHistory: terminalHistoryCommitment(snapshot) };
58
+ if (snapshot.grid.every(isOwnedTerminalReplicaRow)) {
59
+ const metadata = projectionMetadata(candidate);
60
+ validateViewportShape(snapshot.grid, metadata);
61
+ return freezeProjection({ ...metadata, grid: snapshot.grid });
62
+ }
63
+ return freezeProjection(TerminalProjectionSnapshotSchemaZ.parse(candidate));
64
+ }
65
+ function freezeProjection(value: TerminalProjectionSnapshot): TerminalProjectionSnapshot {
66
+ return Object.freeze({
67
+ ...value,
68
+ grid: (Object.isFrozen(value.grid) && value.grid.every(isOwnedTerminalReplicaRow)
69
+ ? value.grid
70
+ : Object.freeze(
71
+ value.grid.map((row) =>
72
+ isOwnedTerminalReplicaRow(row) ? row : freezeTerminalReplicaRow(row),
73
+ ),
74
+ )) as unknown as TerminalProjectionSnapshot["grid"],
75
+ cursor: Object.freeze(value.cursor),
76
+ modes: Object.freeze(value.modes),
77
+ placements: Object.freeze(
78
+ value.placements.map((p) => Object.freeze(p)),
79
+ ) as unknown as TerminalProjectionSnapshot["placements"],
80
+ bootstrap: Object.freeze(value.bootstrap),
81
+ deferredHistory: Object.freeze(value.deferredHistory),
82
+ });
83
+ }
84
+ const { grid: _projectionGrid, ...projectionMetadataShape } =
85
+ TerminalProjectionSnapshotSchemaZ.shape;
86
+ void _projectionGrid;
87
+ const ProjectionMetadataSchema = z.object(projectionMetadataShape).strict();
88
+ function projectionMetadata(
89
+ value: Omit<TerminalReplicaSnapshot, "history"> & { deferredHistory: TerminalDeferredHistory },
90
+ ) {
91
+ const { grid: _grid, ...metadata } = value;
92
+ void _grid;
93
+ return ProjectionMetadataSchema.parse(metadata);
94
+ }
95
+ function validateViewportShape(
96
+ grid: TerminalProjectionSnapshot["grid"],
97
+ metadata: Omit<TerminalProjectionSnapshot, "grid">,
98
+ ): void {
99
+ if (
100
+ grid.length !== metadata.rows ||
101
+ metadata.cols * metadata.rows > 1_000_000 ||
102
+ grid.some((row) => !row || row.cells.length !== metadata.cols) ||
103
+ metadata.cursor.x >= metadata.cols ||
104
+ metadata.cursor.y >= metadata.rows ||
105
+ metadata.placements.some(
106
+ (p) => p.row + p.rows > metadata.rows || p.column + p.columns > metadata.cols,
107
+ )
108
+ )
109
+ throw new TypeError("Invalid projection viewport");
110
+ }
111
+ export function hashTerminalProjection(value: TerminalProjectionSnapshot): string {
112
+ return hashCanonicalTerminalValue([
113
+ "terminal-projection-v1",
114
+ value.cols,
115
+ value.rows,
116
+ hashTerminalReplicaRows(value.grid),
117
+ value.cursor,
118
+ value.modes,
119
+ value.placements,
120
+ value.bootstrap,
121
+ [value.deferredHistory.rows, value.deferredHistory.digest],
122
+ ]);
123
+ }
124
+ /** Checks the source root using its omitted-history commitment, not history materialization. */
125
+ export function hashSourceSnapshotFromProjection(value: TerminalProjectionSnapshot): string {
126
+ return hashCanonicalTerminalValue([
127
+ "terminal-replica-v1",
128
+ value.cols,
129
+ value.rows,
130
+ hashTerminalReplicaRows(value.grid),
131
+ value.deferredHistory.digest,
132
+ value.cursor,
133
+ value.modes,
134
+ value.placements,
135
+ value.bootstrap,
136
+ ]);
137
+ }
138
+ function projectionEncodingError(error: unknown): never {
139
+ if (error instanceof BinaryTerminalLimitError)
140
+ throw new TerminalDeliveryStateTooLargeError(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES + 1);
141
+ throw error;
142
+ }
143
+ export function encodeTerminalProjection(input: TerminalProjectionPayload): Uint8Array {
144
+ try {
145
+ return encodeProjection(input);
146
+ } catch (error) {
147
+ return projectionEncodingError(error);
148
+ }
149
+ }
150
+ function encodeProjection(input: TerminalProjectionPayload): Uint8Array {
151
+ const value = TerminalProjectionPayloadSchemaZ.parse(input);
152
+ const writer = new BinaryTerminalWriter();
153
+ writer.header(
154
+ value.frame,
155
+ value.revision,
156
+ value.frame === "seed" ? undefined : value.baseRevision,
157
+ "TCP1",
158
+ );
159
+ if (value.frame === "tombstone") writer.value(value.tombstone.reason);
160
+ else {
161
+ const data = value.frame === "seed" ? value.projection : value.patch;
162
+ const tuple =
163
+ value.frame === "seed"
164
+ ? encodeTerminalProjectionViewportTuple(value.projection)
165
+ : encodeTerminalProjectionPatchTuple(value.patch);
166
+ writer.value([...tuple, [data.deferredHistory.rows, data.deferredHistory.digest]]);
167
+ }
168
+ return writer.finish();
169
+ }
170
+ export function decodeTerminalProjection(bytes: Uint8Array): TerminalProjectionPayload {
171
+ const decoded = decodeBinaryTerminalTuple(bytes, "TCP1");
172
+ if (decoded.frame === "tombstone")
173
+ return TerminalProjectionPayloadSchemaZ.parse({
174
+ frame: decoded.frame,
175
+ baseRevision: decoded.baseRevision,
176
+ revision: decoded.revision,
177
+ tombstone: { reason: decoded.value },
178
+ });
179
+ const tuple = decoded.value;
180
+ const expected = decoded.frame === "seed" ? 8 : 7;
181
+ if (!Array.isArray(tuple) || tuple.length !== expected)
182
+ throw new TypeError("Invalid projection tuple");
183
+ const deferred = tuple[expected - 1];
184
+ if (!Array.isArray(deferred) || deferred.length !== 2)
185
+ throw new TypeError("Invalid deferred history tuple");
186
+ const deferredHistory = TerminalDeferredHistorySchemaZ.parse({
187
+ rows: deferred[0],
188
+ digest: deferred[1],
189
+ });
190
+ if (decoded.frame === "seed")
191
+ return {
192
+ frame: "seed",
193
+ revision: decoded.revision,
194
+ projection: freezeProjection(
195
+ TerminalProjectionSnapshotSchemaZ.parse({
196
+ ...decodeTerminalProjectionViewportTuple(tuple.slice(0, -1)),
197
+ deferredHistory,
198
+ }),
199
+ ),
200
+ };
201
+ return TerminalProjectionPayloadSchemaZ.parse({
202
+ frame: "patch",
203
+ baseRevision: decoded.baseRevision,
204
+ revision: decoded.revision,
205
+ patch: { ...decodeTerminalProjectionPatchTuple(tuple.slice(0, -1)), deferredHistory },
206
+ });
207
+ }
208
+ export function applyTerminalProjectionPatch(
209
+ current: TerminalProjectionSnapshot,
210
+ input: TerminalProjectionPatch,
211
+ ): TerminalProjectionSnapshot {
212
+ const patch = TerminalProjectionPatchSchemaZ.parse(input);
213
+ const cols = patch.dimensions?.cols ?? current.cols,
214
+ rows = patch.dimensions?.rows ?? current.rows;
215
+ // A resize requires every visible row, avoiding invented blank cells or implicit history.
216
+ if (cols !== current.cols || rows !== current.rows) {
217
+ if (patch.rows.length !== rows || new Set(patch.rows.map((r) => r.index)).size !== rows)
218
+ throw new TypeError("Projection resize requires complete visible rows");
219
+ }
220
+ const grid = current.grid.slice(0, rows);
221
+ const indices = new Set<number>();
222
+ for (const change of patch.rows) {
223
+ if (indices.has(change.index) || change.index >= rows || change.row.cells.length !== cols)
224
+ throw new TypeError("Invalid projection row patch");
225
+ indices.add(change.index);
226
+ const old = grid[change.index];
227
+ grid[change.index] =
228
+ old && terminalReplicaRowsEqual(old, change.row) ? old : freezeTerminalReplicaRow(change.row);
229
+ }
230
+ const candidate = {
231
+ ...current,
232
+ cols,
233
+ rows,
234
+ grid,
235
+ cursor: patch.cursor ?? current.cursor,
236
+ modes: patch.modes ?? current.modes,
237
+ placements: patch.placements ?? current.placements,
238
+ bootstrap: patch.bootstrap ?? current.bootstrap,
239
+ deferredHistory: patch.deferredHistory,
240
+ };
241
+ // Validate without using the parsed copy so unchanged immutable rows retain identity.
242
+ validateViewportShape(grid, projectionMetadata(candidate));
243
+ return freezeProjection(candidate);
244
+ }
245
+ export interface TerminalProjectionState {
246
+ readonly receipt: TerminalProjectionReceipt;
247
+ readonly projection: TerminalProjectionSnapshot | null;
248
+ }
249
+ export function hashTerminalProjectionTombstone(reason: string): string {
250
+ return hashCanonicalTerminalValue(["terminal-projection-tombstone-v1", reason]);
251
+ }
252
+ /** Atomic projection commit. Only this verified path creates a receipt; failures leave current intact. */
253
+ export function commitTerminalProjection(
254
+ current: TerminalProjectionState | null,
255
+ envelope: TerminalProjectionEnvelope,
256
+ bytes: Uint8Array,
257
+ ): TerminalProjectionState {
258
+ const frame = TerminalProjectionEnvelopeSchemaZ.parse(envelope);
259
+ if (
260
+ bytes.length !== frame.representationBytes ||
261
+ hashTerminalDeliveryRepresentation(bytes) !== frame.representationHash
262
+ )
263
+ throw new TypeError("Projection representation mismatch");
264
+ const value = decodeTerminalProjection(bytes);
265
+ if (
266
+ value.frame !== frame.frame ||
267
+ value.revision !== frame.canonicalRevision ||
268
+ (value.frame === "seed"
269
+ ? frame.baseRevision !== null
270
+ : value.baseRevision !== frame.baseRevision)
271
+ )
272
+ throw new TypeError("Projection envelope mismatch");
273
+ if (current) {
274
+ for (const key of [
275
+ "workspaceName",
276
+ "semanticPaneId",
277
+ "generation",
278
+ "incarnation",
279
+ "deliveryNonce",
280
+ ] as const)
281
+ if (current.receipt[key] !== frame[key]) throw new TypeError("Projection lifetime mismatch");
282
+ if (
283
+ value.revision < current.receipt.canonicalRevision ||
284
+ (value.revision === current.receipt.canonicalRevision &&
285
+ (frame.canonicalStateHash !== current.receipt.canonicalStateHash ||
286
+ frame.projectionStateHash !== current.receipt.projectionStateHash))
287
+ )
288
+ throw new TypeError("Stale projection revision");
289
+ if (current.projection === null) throw new TypeError("Projection lifetime is closed");
290
+ }
291
+ if (
292
+ value.frame !== "seed" &&
293
+ (!current || current.receipt.canonicalRevision !== value.baseRevision)
294
+ )
295
+ throw new TypeError("Projection baseline mismatch");
296
+ const projection =
297
+ value.frame === "seed"
298
+ ? value.projection
299
+ : value.frame === "patch"
300
+ ? applyTerminalProjectionPatch(current!.projection!, value.patch)
301
+ : null;
302
+ const projectionHash = projection
303
+ ? hashTerminalProjection(projection)
304
+ : hashTerminalProjectionTombstone(value.frame === "tombstone" ? value.tombstone.reason : "");
305
+ const sourceHash = projection
306
+ ? hashSourceSnapshotFromProjection(projection)
307
+ : hashTerminalReplicaTombstone(value.frame === "tombstone" ? value.tombstone.reason : "");
308
+ if (projectionHash !== frame.projectionStateHash || sourceHash !== frame.canonicalStateHash)
309
+ throw new TypeError("Projection state proof mismatch");
310
+ const receipt = TerminalProjectionReceiptSchemaZ.parse({
311
+ type: "terminal.projection.receipt",
312
+ workspaceName: frame.workspaceName,
313
+ semanticPaneId: frame.semanticPaneId,
314
+ generation: frame.generation,
315
+ incarnation: frame.incarnation,
316
+ deliveryNonce: frame.deliveryNonce,
317
+ transactionId: frame.transactionId,
318
+ canonicalRevision: frame.canonicalRevision,
319
+ canonicalStateHash: frame.canonicalStateHash,
320
+ projectionStateHash: frame.projectionStateHash,
321
+ representationHash: frame.representationHash,
322
+ });
323
+ return Object.freeze({ projection, receipt: Object.freeze(receipt) });
324
+ }
325
+
326
+ function rowCheckpoints(
327
+ writer: BinaryTerminalWriter,
328
+ options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
329
+ ) {
330
+ let cells = 0,
331
+ bytes = writer.byteLength;
332
+ return async (length: number) => {
333
+ cells += length;
334
+ if (cells >= 256 || writer.byteLength - bytes >= 32 * 1024) {
335
+ cells = 0;
336
+ bytes = writer.byteLength;
337
+ await options.yieldControl();
338
+ options.signal?.throwIfAborted();
339
+ }
340
+ };
341
+ }
342
+
343
+ /** Task checkpoints bound cell validation and emission even for a single giant row. */
344
+ async function encodeProjectionCooperatively(
345
+ input: TerminalProjectionPayload,
346
+ options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
347
+ ): Promise<Uint8Array> {
348
+ options.signal?.throwIfAborted();
349
+ if (input.frame === "tombstone") return encodeTerminalProjection(input);
350
+ if (input.frame === "patch") {
351
+ const { patch, ...header } = input;
352
+ const parsedHeader = z
353
+ .object({
354
+ frame: z.literal("patch"),
355
+ revision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
356
+ baseRevision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
357
+ })
358
+ .strict()
359
+ .parse(header);
360
+ if (parsedHeader.revision <= parsedHeader.baseRevision)
361
+ throw new TypeError("Projection revision must advance");
362
+ const { rows, ...metadata } = patch;
363
+ const { rows: _rowSchema, ...shape } = TerminalProjectionPatchSchemaZ.shape;
364
+ void _rowSchema;
365
+ const parsed = z.object(shape).strict().parse(metadata);
366
+ if (
367
+ !Array.isArray(rows) ||
368
+ rows.length > 4096 ||
369
+ (parsed.dimensions && (parsed.dimensions.cols > 4096 || parsed.dimensions.rows > 4096))
370
+ )
371
+ throw new TypeError("Projection patch bound");
372
+ let cells = 0;
373
+ const indices = new Set<number>();
374
+ const writer = new BinaryTerminalWriter();
375
+ writer.header("patch", parsedHeader.revision, parsedHeader.baseRevision, "TCP1");
376
+ writer.array(7);
377
+ writer.value(parsed.dimensions ? [parsed.dimensions.cols, parsed.dimensions.rows] : null);
378
+ writer.array(rows.length);
379
+ const checkpoint = rowCheckpoints(writer, options);
380
+ for (const entry of rows) {
381
+ const parsedEntry = z
382
+ .object({ index: z.number().int().min(0).max(4095), row: z.unknown() })
383
+ .strict()
384
+ .parse(entry);
385
+ if (indices.has(parsedEntry.index)) throw new TypeError("Duplicate projection row");
386
+ indices.add(parsedEntry.index);
387
+ cells += entry.row.cells.length;
388
+ if (cells > 1_000_000) throw new TypeError("Projection cell bound");
389
+ writer.array(2);
390
+ writer.value(parsedEntry.index);
391
+ await writeTerminalProjectionRowCooperatively(
392
+ writer,
393
+ entry.row,
394
+ parsed.dimensions?.cols ?? null,
395
+ options,
396
+ );
397
+ await checkpoint(entry.row.cells.length);
398
+ }
399
+ const tuple = encodeTerminalProjectionPatchTuple({ ...parsed, rows: [] });
400
+ writer.value(tuple[2]);
401
+ writer.value(tuple[3]);
402
+ if (tuple[4] === null) writer.value(null);
403
+ else {
404
+ const placements = tuple[4] as readonly unknown[];
405
+ writer.array(placements.length);
406
+ for (const value of placements) {
407
+ writer.value(value);
408
+ await options.yieldControl();
409
+ options.signal?.throwIfAborted();
410
+ }
411
+ }
412
+ writer.value(tuple[5]);
413
+ writer.value([parsed.deferredHistory.rows, parsed.deferredHistory.digest]);
414
+ options.signal?.throwIfAborted();
415
+ return writer.finish();
416
+ }
417
+ const { projection, ...header } = input;
418
+ const parsedHeader = z
419
+ .object({
420
+ frame: z.literal("seed"),
421
+ revision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
422
+ })
423
+ .strict()
424
+ .parse(header);
425
+ const { grid, ...metadata } = projection;
426
+ const { grid: _gridSchema, ...metadataShape } = TerminalProjectionSnapshotSchemaZ.shape;
427
+ void _gridSchema;
428
+ const parsed = z.object(metadataShape).strict().parse(metadata);
429
+ if (
430
+ !Array.isArray(grid) ||
431
+ grid.length !== parsed.rows ||
432
+ parsed.cols * parsed.rows > 1_000_000 ||
433
+ parsed.cursor.x >= parsed.cols ||
434
+ parsed.cursor.y >= parsed.rows ||
435
+ parsed.placements.some(
436
+ (p) => p.row + p.rows > parsed.rows || p.column + p.columns > parsed.cols,
437
+ )
438
+ )
439
+ throw new TypeError("Invalid projection dimensions");
440
+ const writer = new BinaryTerminalWriter();
441
+ writer.header("seed", parsedHeader.revision, undefined, "TCP1");
442
+ writer.array(8);
443
+ writer.value(parsed.cols);
444
+ writer.value(parsed.rows);
445
+ writer.array(grid.length);
446
+ const checkpoint = rowCheckpoints(writer, options);
447
+ for (const row of grid) {
448
+ await writeTerminalProjectionRowCooperatively(writer, row, parsed.cols, options);
449
+ await checkpoint(row.cells.length);
450
+ }
451
+ const tuple = encodeTerminalProjectionMetadataTuple(parsed);
452
+ writer.value(tuple[0]);
453
+ writer.value(tuple[1]);
454
+ const placements = tuple[2] as readonly unknown[];
455
+ writer.array(placements.length);
456
+ for (const placement of placements) {
457
+ writer.value(placement);
458
+ await options.yieldControl();
459
+ options.signal?.throwIfAborted();
460
+ }
461
+ writer.value(tuple[3]);
462
+ writer.value([parsed.deferredHistory.rows, parsed.deferredHistory.digest]);
463
+ options.signal?.throwIfAborted();
464
+ return writer.finish();
465
+ }
466
+
467
+ export async function encodeTerminalProjectionCooperatively(
468
+ input: TerminalProjectionPayload,
469
+ options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
470
+ ): Promise<Uint8Array> {
471
+ try {
472
+ return await encodeProjectionCooperatively(input, options);
473
+ } catch (error) {
474
+ return projectionEncodingError(error);
475
+ }
476
+ }
@@ -704,6 +704,9 @@ function applyTerminalReplicaPatchProfiled(
704
704
  patch.placements === undefined &&
705
705
  patch.bootstrap === undefined;
706
706
  if (unchanged) return current;
707
+ Object.freeze(grid);
708
+ if (grid !== current.grid && cols === current.cols && rows === current.rows)
709
+ registerGridPatchHash(current.grid, grid, preparedRows, profile, instrumentation);
707
710
  let history: readonly TerminalReplicaRow[];
708
711
  if (patch.history) {
709
712
  history = Object.freeze(patch.history.map(freezeRow));
@@ -1048,7 +1051,7 @@ function hashTerminalReplicaRow(
1048
1051
  return hash;
1049
1052
  }
1050
1053
 
1051
- function hashTerminalReplicaRows(
1054
+ export function hashTerminalReplicaRows(
1052
1055
  rows: readonly TerminalReplicaRow[],
1053
1056
  profile?: MutableTerminalReplicaApplyProfile,
1054
1057
  instrumentation?: TerminalReplicaApplyOptions["instrumentation"],
@@ -1086,6 +1089,36 @@ export async function primeTerminalReplicaRowsHashCooperatively(
1086
1089
  ROW_ARRAY_HASH_CACHE.set(rows, { hash, length: rows.length });
1087
1090
  }
1088
1091
 
1092
+ function registerGridPatchHash(
1093
+ previous: readonly TerminalReplicaRow[],
1094
+ next: readonly TerminalReplicaRow[],
1095
+ changes: readonly { readonly index: number; readonly row: TerminalReplicaRow }[],
1096
+ profile?: MutableTerminalReplicaApplyProfile,
1097
+ instrumentation?: TerminalReplicaApplyOptions["instrumentation"],
1098
+ ): void {
1099
+ const prior = ROW_ARRAY_HASH_CACHE.get(previous);
1100
+ // A cached sequence proves that every retained row is deeply immutable.
1101
+ // Without that proof (including cold or external snapshots), hash normally.
1102
+ // Dense scrolling is cheaper as one linear fold than repeated polynomial
1103
+ // replacements; preserve that path when more than a quarter of rows change.
1104
+ if (!prior || changes.length > previous.length / 4) return;
1105
+ let hash = prior.hash;
1106
+ for (const { index } of changes) {
1107
+ const row = next[index]!;
1108
+ if (row === previous[index]) continue;
1109
+ if (!isTerminalReplicaRowDeeplyFrozen(row)) return;
1110
+ const before = BigInt(`0x${hashTerminalReplicaRow(previous[index]!)}`);
1111
+ const after = BigInt(`0x${hashTerminalReplicaRow(row, profile, instrumentation)}`);
1112
+ hash = BigInt.asUintN(
1113
+ 64,
1114
+ hash + (after - before) * pow64(ROW_SEQUENCE_BASE, prior.length - 1 - index),
1115
+ );
1116
+ }
1117
+ // applyTerminalReplicaPatch owns/froze this array and validates unique indices.
1118
+ // The polynomial is unchanged: replace only each changed row's contribution.
1119
+ ROW_ARRAY_HASH_CACHE.set(next, { hash, length: next.length });
1120
+ }
1121
+
1089
1122
  export function registerTerminalReplicaRowsDeltaHash(
1090
1123
  previous: readonly TerminalReplicaRow[],
1091
1124
  next: readonly TerminalReplicaRow[],
@@ -1,3 +1,4 @@
1
+ import { mountSavedMachineWatch } from "./saved-machine-watch.js";
1
2
  import { bodyLimit } from "hono/body-limit";
2
3
  import { z } from "zod";
3
4
  import { SavedMachineRegistrySchema } from "@tmux-ide/contracts";
@@ -13,6 +14,7 @@ const MutationRequest = z.strictObject({
13
14
  change: SavedMachineMutationSchema,
14
15
  });
15
16
  export function mountSavedMachineRoute(app, options) {
17
+ mountSavedMachineWatch(app, options);
16
18
  const authorize = ownerAuthorityGate(options.ownerToken, {
17
19
  whenOwnerless: "unavailable",
18
20
  unavailableMessage: "Machine registry unavailable",
@@ -0,0 +1,117 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { SavedMachineRegistrySchema, } from "@tmux-ide/contracts";
4
+ import { loadSavedMachines, watchSavedMachines } from "../../lib/saved-machines.js";
5
+ import { ownerAuthorityGate } from "../owner-authority.js";
6
+ export const SAVED_MACHINE_WATCH_WAIT_MS = 20_000;
7
+ const MAX_WATCHERS = 32;
8
+ const Query = z.strictObject({
9
+ expectedInstanceId: z.uuid(),
10
+ after: z
11
+ .string()
12
+ .regex(/^[a-f0-9]{64}$/u)
13
+ .optional(),
14
+ });
15
+ function snapshot(value) {
16
+ const registry = SavedMachineRegistrySchema.parse(value);
17
+ return {
18
+ registry,
19
+ revision: createHash("sha256").update(JSON.stringify(registry)).digest("hex"),
20
+ };
21
+ }
22
+ /** One bounded notification request. A new request always observes the latest registry. */
23
+ export function mountSavedMachineWatch(app, options) {
24
+ const authorize = ownerAuthorityGate(options.ownerToken, {
25
+ whenOwnerless: "unavailable",
26
+ unavailableMessage: "Machine registry unavailable",
27
+ mismatchMessage: "Machine registry requires owner authority",
28
+ });
29
+ const read = options.read ?? loadSavedMachines;
30
+ const watch = options.watch ?? watchSavedMachines;
31
+ let active = 0;
32
+ app.get("/api/resources/saved-machines/watch", async (c) => {
33
+ const gate = authorize(c);
34
+ if (gate)
35
+ return gate;
36
+ c.header("Cache-Control", "no-store");
37
+ const query = Query.safeParse(c.req.query());
38
+ if (!query.success)
39
+ return c.json({ error: "Invalid registry watch request" }, 400);
40
+ if (query.data.expectedInstanceId !== options.daemon.instanceId)
41
+ return c.json({ error: "Daemon generation changed" }, 409);
42
+ if (active >= MAX_WATCHERS)
43
+ return c.json({ error: "Registry watch capacity reached" }, 429);
44
+ active++;
45
+ try {
46
+ const initial = snapshot(read());
47
+ if (query.data.after !== initial.revision)
48
+ return c.json({ daemon: options.daemon, ...initial });
49
+ const signal = c.req.raw.signal;
50
+ const result = await new Promise((resolve) => {
51
+ let settled = false;
52
+ let stop = () => { };
53
+ const finish = (result) => {
54
+ if (settled)
55
+ return;
56
+ settled = true;
57
+ clearTimeout(timer);
58
+ signal.removeEventListener("abort", aborted);
59
+ try {
60
+ stop();
61
+ }
62
+ finally {
63
+ resolve(result);
64
+ }
65
+ };
66
+ const aborted = () => finish({ status: 408 });
67
+ const timer = setTimeout(() => finish({ status: 200, revision: initial.revision, registry: null }), SAVED_MACHINE_WATCH_WAIT_MS);
68
+ timer.unref();
69
+ const changed = (value) => {
70
+ if (settled)
71
+ return;
72
+ try {
73
+ const next = snapshot(value);
74
+ if (next.revision !== initial.revision)
75
+ finish({ status: 200, ...next });
76
+ }
77
+ catch {
78
+ finish({ status: 503 });
79
+ }
80
+ };
81
+ signal.addEventListener("abort", aborted, { once: true });
82
+ if (signal.aborted) {
83
+ aborted();
84
+ return;
85
+ }
86
+ try {
87
+ stop = watch(changed, () => finish({ status: 503 }));
88
+ // Watch implementations may synchronously report an initial error/change.
89
+ if (settled) {
90
+ stop();
91
+ return;
92
+ }
93
+ // Close the read-before-subscribe race independently of watcher behavior.
94
+ changed(read());
95
+ }
96
+ catch {
97
+ finish({ status: 503 });
98
+ }
99
+ });
100
+ if (result.status !== 200)
101
+ return c.json({
102
+ error: result.status === 408 ? "Registry watch cancelled" : "Machine registry unavailable",
103
+ }, result.status);
104
+ return c.json({
105
+ daemon: options.daemon,
106
+ revision: result.revision,
107
+ registry: result.registry,
108
+ });
109
+ }
110
+ catch {
111
+ return c.json({ error: "Machine registry unavailable" }, 503);
112
+ }
113
+ finally {
114
+ active--;
115
+ }
116
+ });
117
+ }
@@ -1,3 +1,4 @@
1
+ import { TERMINAL_SURFACE_CAPABILITY_VERSION } from "@tmux-ide/contracts";
1
2
  import { mountAutomationRoutes } from "./automation.js";
2
3
  import { mountTmuxServerRoutes } from "./tmux-servers.js";
3
4
  import { streamBoundedLogs } from "./log-stream.js";
@@ -422,6 +423,18 @@ export function createApp(options = {}) {
422
423
  status: "ok",
423
424
  daemon: daemonInstanceIdentity,
424
425
  capabilities: {
426
+ ...(c.req.query("nativeSurfaces") === "1"
427
+ ? {
428
+ nativeSurfaces: {
429
+ supported: !!options.paneStreamIssueBackend &&
430
+ options.nativeSurfaceProviderSupported?.() === true,
431
+ available: options.paneStreamIssueBackend !== undefined &&
432
+ options.paneStreamIssueBackend !== null &&
433
+ options.nativeSurfaceProviderAvailable?.() === true,
434
+ protocolVersion: TERMINAL_SURFACE_CAPABILITY_VERSION,
435
+ },
436
+ }
437
+ : {}),
425
438
  // Opt-in keeps the response compatible with older strict host schemas.
426
439
  ...(c.req.query("windowViewport") === "1"
427
440
  ? {