tmux-ide 2.9.12 → 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 (48) hide show
  1. package/bin/cli.js +3438 -2062
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +2 -0
  4. package/packages/contracts/src/pane-stream.ts +17 -0
  5. package/packages/contracts/src/terminal-delivery.ts +2 -1
  6. package/packages/contracts/src/terminal-graphics-compressed.ts +66 -0
  7. package/packages/contracts/src/terminal-graphics.ts +5 -0
  8. package/packages/contracts/src/terminal-surfaces.ts +137 -0
  9. package/packages/core/src/terminal-delivery.ts +8 -2
  10. package/packages/core/src/terminal-replica.ts +33 -0
  11. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +2 -0
  12. package/packages/daemon/dist/command-center/resources/saved-machine-watch.js +117 -0
  13. package/packages/daemon/dist/command-center/server.js +13 -0
  14. package/packages/daemon/dist/command-center/tmux-servers.js +9 -0
  15. package/packages/daemon/dist/lib/daemon-embed.js +32 -1
  16. package/packages/daemon/dist/lib/native-surface-context.js +96 -0
  17. package/packages/daemon/dist/lib/tmux-server-owner.js +9 -0
  18. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  19. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  20. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  21. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  22. package/packages/daemon/dist/server/native-surface-provider-upgrade.js +88 -0
  23. package/packages/daemon/dist/terminal/graphics/compress-chunk.js +40 -0
  24. package/packages/daemon/dist/terminal/graphics/native-surface-demand.js +99 -0
  25. package/packages/daemon/dist/terminal/graphics/native-surface-provider-channel.js +162 -0
  26. package/packages/daemon/dist/terminal/graphics/native-surface-provider.js +275 -0
  27. package/packages/daemon/dist/terminal/graphics/native-surface-viewer.js +146 -0
  28. package/packages/daemon/dist/terminal/mirror/mirror-service.js +3 -0
  29. package/packages/daemon/dist/terminal/mirror/session-channel.js +32 -9
  30. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +196 -3
  31. package/packages/daemon/dist/terminal/pane-stream/runtime.js +9 -0
  32. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +4 -0
  33. package/packages/daemon/src/command-center/resources/saved-machine-watch.ts +132 -0
  34. package/packages/daemon/src/command-center/server.ts +18 -0
  35. package/packages/daemon/src/command-center/tmux-servers.ts +14 -0
  36. package/packages/daemon/src/lib/daemon-embed.ts +43 -1
  37. package/packages/daemon/src/lib/native-surface-context.ts +123 -0
  38. package/packages/daemon/src/lib/tmux-server-owner.ts +9 -0
  39. package/packages/daemon/src/server/native-surface-provider-upgrade.ts +103 -0
  40. package/packages/daemon/src/terminal/graphics/compress-chunk.ts +49 -0
  41. package/packages/daemon/src/terminal/graphics/native-surface-demand.ts +88 -0
  42. package/packages/daemon/src/terminal/graphics/native-surface-provider-channel.ts +161 -0
  43. package/packages/daemon/src/terminal/graphics/native-surface-provider.ts +308 -0
  44. package/packages/daemon/src/terminal/graphics/native-surface-viewer.ts +154 -0
  45. package/packages/daemon/src/terminal/mirror/mirror-service.ts +5 -0
  46. package/packages/daemon/src/terminal/mirror/session-channel.ts +31 -10
  47. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +229 -2
  48. package/packages/daemon/src/terminal/pane-stream/runtime.ts +11 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.12",
3
+ "version": "2.9.13",
4
4
  "description": "The open-source workspace for coding agents: run Claude Code, Codex and other agents in tmux with live status",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,8 @@ export * from "./session-runtime.ts";
50
50
  export * from "./terminal-replica.ts";
51
51
  export * from "./terminal-delivery.ts";
52
52
  export * from "./terminal-graphics.ts";
53
+ export * from "./terminal-graphics-compressed.ts";
54
+ export * from "./terminal-surfaces.ts";
53
55
  export * from "./terminal-runtime-inventory.ts";
54
56
  export * from "./experience-shell.ts";
55
57
  export * from "./application-shell.ts";
@@ -1,3 +1,12 @@
1
+ import {
2
+ TerminalSurfaceWatchSchemaZ,
3
+ TerminalSurfaceReceiverSchemaZ,
4
+ TerminalSurfaceAvailableSchemaZ,
5
+ TerminalSurfacePeerSchemaZ,
6
+ TerminalSurfaceOfferSchemaZ,
7
+ TerminalSurfaceRetiredSchemaZ,
8
+ TerminalSurfaceFallbackSchemaZ,
9
+ } from "./terminal-surfaces.ts";
1
10
  import {
2
11
  TerminalProjectionEnvelopeSchemaZ,
3
12
  TerminalProjectionReceiptSchemaZ,
@@ -436,6 +445,10 @@ export const PaneStreamClientFrameSchemaZ = z.union([
436
445
  PaneStreamTerminalGraphicsRequestFrameSchemaZ,
437
446
  PaneStreamTerminalGraphicsLocalReleaseFrameSchemaZ,
438
447
  PaneStreamTerminalCellPixelsFrameSchemaZ,
448
+ TerminalSurfaceWatchSchemaZ,
449
+ TerminalSurfaceReceiverSchemaZ,
450
+ TerminalSurfaceRetiredSchemaZ,
451
+ TerminalSurfaceFallbackSchemaZ,
439
452
  ]);
440
453
  export type PaneStreamClientFrame = z.infer<typeof PaneStreamClientFrameSchemaZ>;
441
454
 
@@ -855,6 +868,10 @@ export const PaneStreamErrorFrameSchemaZ = z
855
868
  .strict();
856
869
 
857
870
  export const PaneStreamServerFrameSchemaZ = z.discriminatedUnion("type", [
871
+ TerminalSurfaceAvailableSchemaZ,
872
+ TerminalSurfacePeerSchemaZ,
873
+ TerminalSurfaceOfferSchemaZ,
874
+ TerminalSurfaceFallbackSchemaZ,
858
875
  PaneStreamReadyFrameSchemaZ,
859
876
  PaneStreamSeedBatchFrameSchemaZ,
860
877
  PaneStreamOutputFrameSchemaZ,
@@ -35,7 +35,8 @@ export type TerminalDeliveryEncoding = z.infer<typeof TerminalDeliveryEncodingSc
35
35
 
36
36
  export const TerminalDeliveryOfferSchemaZ = z
37
37
  .object({
38
- protocolVersions: z.array(z.number().int().positive()).min(1).max(8),
38
+ // Interactive binary/graphics offers use eight slots; native surfaces add one.
39
+ protocolVersions: z.array(z.number().int().positive()).min(1).max(9),
39
40
  encodings: z.array(TerminalDeliveryEncodingSchemaZ).min(1).max(4),
40
41
  richPlacements: z.boolean(),
41
42
  })
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ import {
3
+ TerminalGraphicsBinaryHeaderSchemaZ,
4
+ TERMINAL_GRAPHICS_BINARY_HEADER_BYTES,
5
+ TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES,
6
+ } from "./terminal-graphics.ts";
7
+
8
+ /** TGB2 uses an independent zlib stream per chunk. Digest/offset/total still
9
+ * describe the original image bytes. Decoders must require exact output length,
10
+ * stream termination and complete input consumption before hashing or painting. */
11
+ export const TerminalGraphicsCompressedHeaderSchemaZ = TerminalGraphicsBinaryHeaderSchemaZ.extend({
12
+ compression: z.literal("zlib"),
13
+ decodedBytes: z.number().int().min(1).max(TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES),
14
+ }).superRefine((header, context) => {
15
+ if (
16
+ header.blob.offset + header.decodedBytes > header.blob.total ||
17
+ (header.blob.format !== "rgb" && header.blob.format !== "rgba")
18
+ )
19
+ context.addIssue({ code: "custom", message: "Invalid compressed graphics range or format" });
20
+ });
21
+ export type TerminalGraphicsCompressedHeader = z.infer<
22
+ typeof TerminalGraphicsCompressedHeaderSchemaZ
23
+ >;
24
+ export function encodeTerminalGraphicsCompressed(
25
+ header: TerminalGraphicsCompressedHeader,
26
+ bytes: Uint8Array,
27
+ ): Uint8Array {
28
+ const parsed = TerminalGraphicsCompressedHeaderSchemaZ.parse(header);
29
+ const json = new TextEncoder().encode(JSON.stringify(parsed));
30
+ if (
31
+ json.length > TERMINAL_GRAPHICS_BINARY_HEADER_BYTES ||
32
+ bytes.length < 1 ||
33
+ bytes.length >= parsed.decodedBytes
34
+ )
35
+ throw new Error("Invalid compressed graphics bounds");
36
+ const result = new Uint8Array(8 + json.length + bytes.length);
37
+ result.set([84, 71, 66, 50]);
38
+ new DataView(result.buffer).setUint32(4, json.length, false);
39
+ result.set(json, 8);
40
+ result.set(bytes, 8 + json.length);
41
+ return result;
42
+ }
43
+ /** Only parses the envelope; inflation belongs on the receiving worker. */
44
+ export function decodeTerminalGraphicsCompressed(data: Uint8Array): {
45
+ header: TerminalGraphicsCompressedHeader;
46
+ bytes: Uint8Array;
47
+ } {
48
+ if (
49
+ data.length < 10 ||
50
+ data.length > 8 + TERMINAL_GRAPHICS_BINARY_HEADER_BYTES + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES ||
51
+ data[0] !== 84 ||
52
+ data[1] !== 71 ||
53
+ data[2] !== 66 ||
54
+ data[3] !== 50
55
+ )
56
+ throw new Error("Invalid compressed graphics frame");
57
+ const length = new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(4, false);
58
+ if (length < 1 || length > TERMINAL_GRAPHICS_BINARY_HEADER_BYTES || 8 + length >= data.length)
59
+ throw new Error("Invalid compressed graphics header");
60
+ const header = TerminalGraphicsCompressedHeaderSchemaZ.parse(
61
+ JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(8, 8 + length))),
62
+ );
63
+ const bytes = data.subarray(8 + length);
64
+ if (bytes.length >= header.decodedBytes) throw new Error("Invalid compressed graphics payload");
65
+ return { header, bytes };
66
+ }
@@ -18,6 +18,10 @@ import { TerminalAttachmentSemanticPaneIdSchemaZ } from "./semantic-identity.ts"
18
18
  export const TERMINAL_GRAPHICS_CAPABILITY_VERSION = 101;
19
19
  /** Raw binary blob frames; requires the graphics side channel (101). */
20
20
  export const TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION = 107;
21
+ /** Bundles 113/114 include projection 111/112 and bounded lossless raw-pixel chunks.
22
+ * Older daemons ignore these and retain the offered compact/text and JSON graphics fallback. */
23
+ export const TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION = 113;
24
+ export const TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION = 114;
21
25
  /** Verified same-host file leases, including binary-blob fallback (107).
22
26
  * Offer 108 instead of 107 to retain the existing eight-capability bound.
23
27
  * Never infer locality from a loopback URL. */
@@ -100,6 +104,7 @@ export const TerminalGraphicsStateSchemaZ = z
100
104
  cellPixels: z.literal(true).optional(),
101
105
  preciseCellPixels: z.literal(true).optional(),
102
106
  binaryBlobs: z.literal(true).optional(),
107
+ compressedBlobs: z.literal(true).optional(),
103
108
  localFiles: z.literal(true).optional(),
104
109
  })
105
110
  .strict();
@@ -0,0 +1,137 @@
1
+ import { z } from "zod";
2
+ import { TerminalAttachmentSemanticPaneIdSchemaZ } from "./semantic-identity.ts";
3
+ import { SessionRuntimeGenerationSchemaZ } from "./session-runtime.ts";
4
+
5
+ /** Native surface control only: no pixel bytes, filesystem paths or raw Mach names.
6
+ * These schemas do not authenticate a peer or advertise support. They are valid
7
+ * for native delivery only after explicit negotiation and verified same-host
8
+ * authentication. A receiver request may instead get an unsupported fallback;
9
+ * that never authorizes endpoint lookup. SSH/unsupported peers retain the
10
+ * existing graphics transport.
11
+ */
12
+ export const TERMINAL_SURFACE_MAX_BYTES = 64 * 1024 * 1024;
13
+ /** Additive opt-in; does not replace binary/kitty fallback capability bundles. */
14
+ export const TERMINAL_SURFACE_CAPABILITY_VERSION = 116;
15
+ export const TERMINAL_SURFACE_MAX_OUTSTANDING = 8;
16
+ export const TERMINAL_SURFACE_MAX_CONTROL_BYTES = 16 * 1024;
17
+ const NonNilUuid = z.uuid().refine((value) => value !== "00000000-0000-0000-0000-000000000000");
18
+ const Sequence = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
19
+ /** Opaque full kernel audit identity, including incarnation. Values from control
20
+ * must still match the kernel trailer; numeric PID alone is never proof. */
21
+ export const TerminalSurfaceAuditTokenSchemaZ = z
22
+ .array(z.number().int().min(0).max(0xffff_ffff))
23
+ .length(8)
24
+ .refine((token) => token.some((word) => word !== 0));
25
+ const scope = {
26
+ version: z.literal(1),
27
+ generation: SessionRuntimeGenerationSchemaZ,
28
+ connectionNonce: NonNilUuid,
29
+ };
30
+
31
+ export const TerminalSurfaceWatchSchemaZ = z
32
+ .object({
33
+ type: z.literal("terminal-surface-watch"),
34
+ version: z.literal(1),
35
+ generation: SessionRuntimeGenerationSchemaZ,
36
+ })
37
+ .strict();
38
+ export type TerminalSurfaceWatch = z.infer<typeof TerminalSurfaceWatchSchemaZ>;
39
+
40
+ export const TerminalSurfaceAvailableSchemaZ = z
41
+ .object({
42
+ type: z.literal("terminal-surface-available"),
43
+ version: z.literal(1),
44
+ generation: SessionRuntimeGenerationSchemaZ,
45
+ connectionNonce: NonNilUuid.nullable(),
46
+ })
47
+ .strict();
48
+ export type TerminalSurfaceAvailable = z.infer<typeof TerminalSurfaceAvailableSchemaZ>;
49
+
50
+ export const TerminalSurfaceReceiverSchemaZ = z
51
+ .object({
52
+ type: z.literal("terminal-surface-receiver"),
53
+ ...scope,
54
+ pid: z.number().int().min(1).max(0x7fff_ffff),
55
+ slot: z.number().int().min(0).max(31),
56
+ auditToken: TerminalSurfaceAuditTokenSchemaZ,
57
+ challenge: NonNilUuid,
58
+ })
59
+ .strict();
60
+ export type TerminalSurfaceReceiver = z.infer<typeof TerminalSurfaceReceiverSchemaZ>;
61
+
62
+ export const TerminalSurfacePeerSchemaZ = z
63
+ .object({
64
+ type: z.literal("terminal-surface-peer"),
65
+ ...scope,
66
+ auditToken: TerminalSurfaceAuditTokenSchemaZ,
67
+ })
68
+ .strict();
69
+ export type TerminalSurfacePeer = z.infer<typeof TerminalSurfacePeerSchemaZ>;
70
+
71
+ /** Mirrors the Rust admission envelope. Admission additionally checks the exact
72
+ * retained committed pane identity before reservation and again before import.
73
+ * A storage ticket correlates a separately transferred capability, never a path.
74
+ */
75
+ /** Rectangle in the committed pane's visible cell grid. History and pane
76
+ * replacement never inherit this placement; the canonical identity fences it. */
77
+ export const TerminalSurfacePlacementSchemaZ = z
78
+ .object({
79
+ row: z.number().int().min(0).max(16383),
80
+ column: z.number().int().min(0).max(16383),
81
+ rows: z.number().int().min(1).max(16384),
82
+ columns: z.number().int().min(1).max(16384),
83
+ })
84
+ .strict()
85
+ .refine((rect) => rect.row + rect.rows <= 16384 && rect.column + rect.columns <= 16384);
86
+ export type TerminalSurfacePlacement = z.infer<typeof TerminalSurfacePlacementSchemaZ>;
87
+
88
+ export const TerminalSurfaceOfferSchemaZ = z
89
+ .object({
90
+ type: z.literal("terminal-surface-offer"),
91
+ version: z.literal(1),
92
+ pane: TerminalAttachmentSemanticPaneIdSchemaZ,
93
+ generation: SessionRuntimeGenerationSchemaZ,
94
+ incarnation: z.string().min(1).max(256),
95
+ deliveryNonce: NonNilUuid,
96
+ canonicalRevision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
97
+ canonicalStateHash: z.string().regex(/^[0-9a-f]{16}$/u),
98
+ sequence: Sequence,
99
+ surfaceId: NonNilUuid,
100
+ placement: TerminalSurfacePlacementSchemaZ,
101
+ storageTicket: NonNilUuid,
102
+ format: z.literal("bgra8-straight"),
103
+ width: z.number().int().min(1).max(16384),
104
+ height: z.number().int().min(1).max(16384),
105
+ stride: z.number().int().min(4).max(TERMINAL_SURFACE_MAX_BYTES),
106
+ allocationBytes: z.number().int().min(4).max(TERMINAL_SURFACE_MAX_BYTES),
107
+ })
108
+ .strict()
109
+ .refine(
110
+ (offer) =>
111
+ offer.stride % 4 === 0 &&
112
+ offer.stride >= offer.width * 4 &&
113
+ offer.stride * offer.height <= offer.allocationBytes,
114
+ );
115
+ export type TerminalSurfaceOffer = z.infer<typeof TerminalSurfaceOfferSchemaZ>;
116
+
117
+ /** Sent on the original connection only after final backing/GPU ownership drops.
118
+ * Receipt, import, CPU decode or submission to Metal is not retirement. */
119
+ export const TerminalSurfaceRetiredSchemaZ = z
120
+ .object({
121
+ type: z.literal("terminal-surface-retired"),
122
+ ...scope,
123
+ sequence: Sequence,
124
+ })
125
+ .strict();
126
+ export type TerminalSurfaceRetired = z.infer<typeof TerminalSurfaceRetiredSchemaZ>;
127
+
128
+ /** Ends this connection's native negotiation. A replacement connection starts
129
+ * fresh; no native storage is freed merely because fallback was requested. */
130
+ export const TerminalSurfaceFallbackSchemaZ = z
131
+ .object({
132
+ type: z.literal("terminal-surface-fallback"),
133
+ ...scope,
134
+ reason: z.enum(["unsupported", "timeout", "identity", "budget", "storage"]),
135
+ })
136
+ .strict();
137
+ export type TerminalSurfaceFallback = z.infer<typeof TerminalSurfaceFallbackSchemaZ>;
@@ -1,4 +1,6 @@
1
1
  import {
2
+ TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION,
3
+ TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
2
4
  TERMINAL_PROJECTION_CAPABILITY_VERSION,
3
5
  TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
4
6
  } from "@tmux-ide/contracts";
@@ -88,7 +90,9 @@ export function negotiateTerminalDelivery(
88
90
  v === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
89
91
  v === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
90
92
  v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
91
- v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
93
+ v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
94
+ v === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
95
+ v === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
92
96
  ) && offer.encodings.includes("semantic-compact-v1");
93
97
  const encoding = binary
94
98
  ? ("semantic-binary-v1" as const)
@@ -112,7 +116,9 @@ export function negotiateTerminalDelivery(
112
116
  offer.protocolVersions.some(
113
117
  (v) =>
114
118
  v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
115
- v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
119
+ v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
120
+ v === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
121
+ v === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
116
122
  )
117
123
  ? { alternateScreenProjection: "deferred-history-v1" as const }
118
124
  : {}),
@@ -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));
@@ -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
  ? {
@@ -1,3 +1,4 @@
1
+ import { NativeSurfaceLocationSchema } from "../lib/native-surface-context.js";
1
2
  import { mountTmuxServerHistoryAnchorRoute } from "./tmux-server-history-anchor.js";
2
3
  import { mountTmuxServerDiagnosticsRoute } from "./tmux-server-diagnostics.js";
3
4
  import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.js";
@@ -191,6 +192,14 @@ export function mountTmuxServerRoutes(app, options) {
191
192
  }),
192
193
  }));
193
194
  }));
195
+ app.post(`${scoped}/native-surface-context`, route(async (c) => {
196
+ const server = scope(c);
197
+ const target = NativeSurfaceLocationSchema.parse(await boundedJson(c.req.raw, 8192));
198
+ c.req.raw.signal.throwIfAborted();
199
+ const context = await options.owners.withOwner(server, (owner) => owner.resolveNativeSurfaceContext(target, c.req.raw.signal));
200
+ c.req.raw.signal.throwIfAborted();
201
+ return c.json({ version: 1, server, context });
202
+ }));
194
203
  app.get(`${scoped}/inventory/:workspaceName`, route(async (c) => {
195
204
  const server = scope(c);
196
205
  const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
@@ -6,6 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ import { createNativeSurfaceContextResolver } from "./native-surface-context.js";
10
+ import { NativeSurfaceProviderRegistry } from "../terminal/graphics/native-surface-provider.js";
9
11
  import { TmuxServerVersionSchemaZ } from "@tmux-ide/contracts";
10
12
  import { createNativeSplitCapabilityProbe, createNativeSplitReadiness, } from "./native-split-capability.js";
11
13
  import { executeCanonicalSplitMutation } from "./canonical-split-mutation.js";
@@ -80,6 +82,7 @@ import { InteractionEvidenceAuthority } from "./interaction-evidence-authority.j
80
82
  import { createNativeTerminalAttachmentRuntime, WorkspaceTerminalInventoryRuntime, } from "../terminal/attachments/native-runtime.js";
81
83
  import { createTmuxAgentStatusProbe } from "../terminal/attachments/agent-status-probe.js";
82
84
  import { attachTerminalAttachmentWebSocket, } from "../server/terminal-attachment-upgrade.js";
85
+ import { attachNativeSurfaceProvider } from "../server/native-surface-provider-upgrade.js";
83
86
  import { attachPaneStreamWebSocket, } from "../server/pane-stream-upgrade.js";
84
87
  import { createPaneStreamRuntime, } from "../terminal/pane-stream/runtime.js";
85
88
  import { SessionRuntimeRegistry } from "../terminal/session-runtime/registry.js";
@@ -543,6 +546,8 @@ async function startHttpServer({ sessionName, requestedPort, bindHostname, dir,
543
546
  issue: (request, context) => getTerminalAttachmentRuntime().admission.issue(request, context),
544
547
  },
545
548
  paneStreamIssueBackend: paneStreamRuntime.coordinator,
549
+ nativeSurfaceProviderSupported: () => serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces !== undefined,
550
+ nativeSurfaceProviderAvailable: () => serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces?.available === true,
546
551
  applicationShellInventoryBackend: terminalInventoryRuntime,
547
552
  startupReadinessAttachmentBackend: terminalInventoryRuntime,
548
553
  catalogLiveSessions,
@@ -586,7 +591,16 @@ async function startHttpServer({ sessionName, requestedPort, bindHostname, dir,
586
591
  });
587
592
  const terminalAttachmentBoundary = attachTerminalAttachmentWebSocket(server, (create) => (create ? getTerminalAttachmentRuntime() : peekTerminalAttachmentRuntime())?.admission ??
588
593
  null);
589
- const paneStreamBoundary = attachPaneStreamWebSocket(server, paneStreamRuntime.coordinator, PANE_STREAM_REDEEM_PATH, () => serverOwners.isDefaultCurrent());
594
+ const paneStreamSocketBoundary = attachPaneStreamWebSocket(server, paneStreamRuntime.coordinator, PANE_STREAM_REDEEM_PATH, () => serverOwners.isDefaultCurrent());
595
+ const nativeProviderBoundary = paneStreamRuntime.nativeSurfaces
596
+ ? attachNativeSurfaceProvider(server, paneStreamRuntime.nativeSurfaces, localBypassToken ?? null, () => serverOwners.isDefaultCurrent())
597
+ : null;
598
+ const paneStreamBoundary = {
599
+ async close() {
600
+ nativeProviderBoundary?.close();
601
+ await paneStreamSocketBoundary.close();
602
+ },
603
+ };
590
604
  serverOwners.attach(server);
591
605
  try {
592
606
  await new Promise((resolve, reject) => {
@@ -1059,6 +1073,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1059
1073
  externalInteractionObserver.setDiagnostics(diagnostics);
1060
1074
  setFleetFactsObserverDiagnostics(diagnostics);
1061
1075
  }
1076
+ const nativeSurfaces = process.platform === "darwin" ? new NativeSurfaceProviderRegistry(instanceId) : undefined;
1062
1077
  sessionRuntimeRegistry = new SessionRuntimeRegistry({
1063
1078
  generation: instanceId,
1064
1079
  ...(runtimeObservability ? { observability: runtimeObservability } : {}),
@@ -1085,6 +1100,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1085
1100
  publishResourceChange: (change) => broadcastResourceChanged(change, instanceId),
1086
1101
  },
1087
1102
  mirror: {
1103
+ onNativeClientPresence: (session, present) => nativeSurfaces?.demand.presence(session, present),
1088
1104
  splitLayoutEpoch: () => observationSelector?.nativeServerEpoch ?? null,
1089
1105
  splitLayoutPrepare: createNativeSplitReadiness({
1090
1106
  observation: () => observationSelector ?? null,
@@ -1249,6 +1265,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1249
1265
  }
1250
1266
  }
1251
1267
  paneStreamRuntime = createPaneStreamRuntime({
1268
+ nativeSurfaces,
1252
1269
  localImages,
1253
1270
  daemonInstanceId: instanceId,
1254
1271
  webSocketUrl: paneStreamWebSocketUrl(bindHostname, port),
@@ -1353,6 +1370,20 @@ async function startEmbeddedDaemonGeneration(opts) {
1353
1370
  return grant ? interactionEvidence.captureSourceBinding(grant) : null;
1354
1371
  },
1355
1372
  interactionReceipts,
1373
+ resolveNativeSurfaceContext: createNativeSurfaceContextResolver({
1374
+ generation: instanceId,
1375
+ registry: workspaceRegistry,
1376
+ catalog: createNativeTmuxServerCatalog(workspaceRegistry, fleetFactsTmuxRunner),
1377
+ inventory: terminalInventoryRuntime,
1378
+ run: (args, signal) => fleetFactsTmuxRunner(args, signal),
1379
+ assertOpen: () => {
1380
+ if (defaultEditorContextRetired ||
1381
+ serverOwners?.defaultRetired ||
1382
+ !initialNativeServerIdentity ||
1383
+ terminalInventoryRuntime?.lifecycleState() !== "ready")
1384
+ throw new Error("Default native surface context owner is unavailable");
1385
+ },
1386
+ }),
1356
1387
  resolvePaneEditorContext: createPaneEditorContextResolver({
1357
1388
  generation: instanceId,
1358
1389
  registry: workspaceRegistry,