tmux-ide 2.9.11 → 2.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/bin/cli.js +4158 -2545
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +4 -0
  4. package/packages/contracts/src/pane-stream.ts +60 -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.ts +1 -1
  16. package/packages/daemon/dist/lib/daemon-embed.js +39 -3
  17. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
  18. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -0
  19. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  20. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  21. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  22. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  23. package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
  24. package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
  25. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +176 -16
  26. package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
  27. package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
  28. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
  29. package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
  30. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
  31. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
  32. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
  33. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
  34. package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
  35. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
  36. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
  37. package/packages/daemon/src/lib/daemon-embed.ts +38 -2
  38. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
  39. package/packages/daemon/src/lib/tmux-server-owner.ts +3 -0
  40. package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
  41. package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
  42. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +219 -15
  43. package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
  44. package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
  45. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
  46. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
  47. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
  48. package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
  49. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
  50. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
  51. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
  52. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
  53. package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
  54. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
  55. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
  56. 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
+ }
@@ -1048,7 +1048,7 @@ function hashTerminalReplicaRow(
1048
1048
  return hash;
1049
1049
  }
1050
1050
 
1051
- function hashTerminalReplicaRows(
1051
+ export function hashTerminalReplicaRows(
1052
1052
  rows: readonly TerminalReplicaRow[],
1053
1053
  profile?: MutableTerminalReplicaApplyProfile,
1054
1054
  instrumentation?: TerminalReplicaApplyOptions["instrumentation"],
@@ -13,7 +13,8 @@ import { createGuardedNativeSplitResize } from "./guarded-native-split-resize.js
13
13
  import { WindowLinkTargetSchemaZ } from "@tmux-ide/contracts";
14
14
  import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.js";
15
15
  import { requireSupportedTmuxVersion } from "./tmux-version.js";
16
- import { join } from "node:path";
16
+ import { dirname, join } from "node:path";
17
+ import { LocalImageOwner, cleanupLocalImageOwner } from "../terminal/graphics/local-image-owner.js";
17
18
  import { createClaudeTeamMembershipReader } from "../terminal/attachments/claude-team-names.js";
18
19
  import { PaneSourceDiscovery } from "./pane-source-discovery.js";
19
20
  import { createBackgroundNativeCapture } from "./background-native-capture.js";
@@ -89,7 +90,7 @@ import { setActivationBackend } from "./active-projects.js";
89
90
  import { readOrMintEnvironmentId } from "./environment-identity.js";
90
91
  import { captureDaemonProvenance } from "./daemon-provenance.js";
91
92
  import { registerLogSecret, setLogIdentity } from "./log.js";
92
- import { canonicalDaemonUrl, clearCanonicalDaemonInfoIfOwned, clearCanonicalDaemonInfoIfUnchanged, inspectCanonicalDaemonInfo, isCanonicalDaemonAlive, isCanonicalDaemonRecordOwnerProvenDead, probeCanonicalDaemonHealth, probeCanonicalDaemonIdentity, releaseCanonicalDaemonClaim, tryAcquireCanonicalDaemonClaim, writeCanonicalDaemonInfo, matchesCanonicalDaemonPredecessor, } from "./canonical-daemon.js";
93
+ import { canonicalDaemonUrl, getCanonicalDaemonInfoPath, clearCanonicalDaemonInfoIfOwned, clearCanonicalDaemonInfoIfUnchanged, inspectCanonicalDaemonInfo, isCanonicalDaemonAlive, isCanonicalDaemonRecordOwnerProvenDead, probeCanonicalDaemonHealth, probeCanonicalDaemonIdentity, releaseCanonicalDaemonClaim, tryAcquireCanonicalDaemonClaim, writeCanonicalDaemonInfo, matchesCanonicalDaemonPredecessor, } from "./canonical-daemon.js";
93
94
  const requireFromHere = createRequire(import.meta.url);
94
95
  const DEFAULT_HOSTNAME = "127.0.0.1";
95
96
  const DEFAULT_GRACEFUL_MS = 2000;
@@ -955,6 +956,7 @@ async function startEmbeddedDaemonGeneration(opts) {
955
956
  };
956
957
  let terminalAttachmentRuntime = null;
957
958
  let paneStreamRuntime = null;
959
+ let localImages;
958
960
  let serverOwners = null;
959
961
  let defaultEditorContextRetired = false;
960
962
  const defaultDiagnosticsLifetime = new AbortController();
@@ -1219,7 +1221,35 @@ async function startEmbeddedDaemonGeneration(opts) {
1219
1221
  return terminalAttachmentRuntime;
1220
1222
  };
1221
1223
  const peekTerminalAttachmentRuntime = () => terminalAttachmentRuntime;
1224
+ // The held canonical claim has already proven and hardened this private root.
1225
+ // Local images are optional: unavailable storage retains inline graphics.
1226
+ if (process.platform !== "win32") {
1227
+ const root = dirname(getCanonicalDaemonInfoPath());
1228
+ try {
1229
+ if (existingCanonical.status === "valid") {
1230
+ await cleanupLocalImageOwner({
1231
+ root,
1232
+ owner: {
1233
+ generation: existingCanonical.info.instanceId,
1234
+ pid: existingCanonical.info.pid,
1235
+ startedAt: existingCanonical.info.startedAt,
1236
+ },
1237
+ proveDead: () => isCanonicalDaemonRecordOwnerProvenDead(existingCanonical),
1238
+ });
1239
+ }
1240
+ localImages = await LocalImageOwner.create({
1241
+ root,
1242
+ generation: instanceId,
1243
+ pid: process.pid,
1244
+ startedAt,
1245
+ });
1246
+ }
1247
+ catch {
1248
+ localImages = undefined;
1249
+ }
1250
+ }
1222
1251
  paneStreamRuntime = createPaneStreamRuntime({
1252
+ localImages,
1223
1253
  daemonInstanceId: instanceId,
1224
1254
  webSocketUrl: paneStreamWebSocketUrl(bindHostname, port),
1225
1255
  sessionRuntimeRegistry,
@@ -1239,6 +1269,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1239
1269
  });
1240
1270
  await externalInteractionObserver.start();
1241
1271
  serverOwners = await createEmbeddedTmuxServerOwners({
1272
+ localImages,
1242
1273
  environmentId,
1243
1274
  onDefaultScope: async (scope) => {
1244
1275
  interactionEvidence = new InteractionEvidenceAuthority(environmentId, scope);
@@ -1514,7 +1545,10 @@ async function startEmbeddedDaemonGeneration(opts) {
1514
1545
  // The pane-stream coordinator may still hold runtime consumers while it
1515
1546
  // drains. Preserve the normal shutdown order on startup rollback too:
1516
1547
  // transports first, then the one session/control authority.
1517
- await Promise.allSettled([sessionRuntimeRegistry?.dispose() ?? Promise.resolve()]);
1548
+ await Promise.allSettled([
1549
+ sessionRuntimeRegistry?.dispose() ?? Promise.resolve(),
1550
+ localImages?.dispose() ?? Promise.resolve(),
1551
+ ]);
1518
1552
  throw error;
1519
1553
  }
1520
1554
  const { server, sockets, closeClients, closeWsServers, terminalAttachmentBoundary, paneStreamBoundary, } = startedServer;
@@ -1535,6 +1569,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1535
1569
  ...(await retireTerminalAttachmentTransport(terminalAttachmentRuntime, terminalAttachmentBoundary)),
1536
1570
  ...(await retirePaneStreamTransport()),
1537
1571
  ];
1572
+ await localImages?.dispose();
1538
1573
  terminalInventoryRuntime.dispose();
1539
1574
  const paneDisposal = Promise.resolve().then(() => workspacePaneCreation.dispose());
1540
1575
  const workspaceOpenDisposal = Promise.resolve().then(() => workspaceOpen.dispose());
@@ -1785,6 +1820,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1785
1820
  // listener before touching the legacy WS or HTTP surfaces.
1786
1821
  failures.push(...(await retireTerminalAttachmentTransport(terminalAttachmentRuntime, terminalAttachmentBoundary)));
1787
1822
  failures.push(...(await retirePaneStreamTransport()));
1823
+ await capture(() => localImages?.dispose());
1788
1824
  await capture(() => terminalInventoryRuntime.dispose());
1789
1825
  await capture(() => workspacePaneCreation.dispose());
1790
1826
  await capture(() => workspaceOpen.dispose());
@@ -58,6 +58,7 @@ export async function createEmbeddedTmuxServerOwners(options) {
58
58
  await retireDefault();
59
59
  const route = tmuxServerPaneStreamPath(scope);
60
60
  const owner = await createNativeTmuxServerOwner({
61
+ localImages: options.localImages,
61
62
  environmentId: options.environmentId,
62
63
  ...scope,
63
64
  tmuxAuthority: observation.authority,
@@ -369,6 +369,7 @@ export async function createNativeTmuxServerOwner(options) {
369
369
  }),
370
370
  });
371
371
  const paneStreamRuntime = createPaneStreamRuntime({
372
+ localImages: options.localImages,
372
373
  daemonInstanceId: generation,
373
374
  webSocketUrl: options.webSocketUrl,
374
375
  sessionRuntimeRegistry,
@@ -21,9 +21,9 @@
21
21
  <key>CFBundlePackageType</key>
22
22
  <string>APPL</string>
23
23
  <key>CFBundleShortVersionString</key>
24
- <string>2.9.11</string>
24
+ <string>2.9.12</string>
25
25
  <key>CFBundleVersion</key>
26
- <string>2.9.11</string>
26
+ <string>2.9.12</string>
27
27
  <key>LSMinimumSystemVersion</key>
28
28
  <string>11.0</string>
29
29
  <key>LSUIElement</key>
@@ -10,7 +10,7 @@
10
10
  </data>
11
11
  <key>Resources/Assets.car</key>
12
12
  <data>
13
- nBfyKmj9/xOSU1E6fMrLP0j34to=
13
+ 4qUHjN0ukJdS6FxJvaGZD8XzSrI=
14
14
  </data>
15
15
  </dict>
16
16
  <key>files2</key>
@@ -26,7 +26,7 @@
26
26
  <dict>
27
27
  <key>hash2</key>
28
28
  <data>
29
- BH5ZBbufHUMGSzMRXcEqBgur+e94BeWVItaM9FDWrFA=
29
+ cnvGMeWWQfA0LC/wWkAWWggL5Acd/+3ysR+9aaOtMWg=
30
30
  </data>
31
31
  </dict>
32
32
  </dict>