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
@@ -1,4 +1,7 @@
1
- import { TERMINAL_PROJECTION_CAPABILITY_VERSION, TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
1
+ import { TERMINAL_SURFACE_MAX_CONTROL_BYTES, TERMINAL_SURFACE_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
2
+ import { NativeSurfaceViewer } from "../graphics/native-surface-viewer.js";
3
+ import { compressGraphicsChunk } from "../graphics/compress-chunk.js";
4
+ import { TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION, TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION, TERMINAL_PROJECTION_CAPABILITY_VERSION, TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
2
5
  import { PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION, PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
3
6
  import { inflateSync } from "node:zlib";
4
7
  import { PaneStreamLayoutSnapshotFrameSchemaZ } from "@tmux-ide/contracts";
@@ -116,6 +119,7 @@ function errorFrameCode(error) {
116
119
  * ticket digests; a daemon restart makes every prior descriptor inert.
117
120
  */
118
121
  export class PaneStreamAdmissionCoordinator {
122
+ #nativeSurfaces;
119
123
  #localImages;
120
124
  #instanceId;
121
125
  #webSocketUrl;
@@ -147,6 +151,9 @@ export class PaneStreamAdmissionCoordinator {
147
151
  #shuttingDown = false;
148
152
  #shutdownPromise = null;
149
153
  constructor(options) {
154
+ this.#nativeSurfaces = options.nativeSurfaces;
155
+ if (this.#nativeSurfaces && this.#nativeSurfaces.generation !== options.daemonInstanceId)
156
+ throw new Error("Native surface provider generation mismatch");
150
157
  this.#localImages = options.localImages;
151
158
  this.#instanceId = BindingIdSchemaZ.parse(options.daemonInstanceId);
152
159
  this.#webSocketUrl = PaneStreamLoopbackWebSocketUrlSchemaZ.parse(options.webSocketUrl);
@@ -410,6 +417,7 @@ export class PaneStreamAdmissionCoordinator {
410
417
  let live;
411
418
  try {
412
419
  live = new PaneStreamLiveConnection({
420
+ nativeSurfaces: this.#nativeSurfaces,
413
421
  localImages: this.#localImages,
414
422
  clientId: `psc-${this.#clientCounter}`,
415
423
  socket,
@@ -618,6 +626,13 @@ export function rawGraphicsState(state) {
618
626
  }
619
627
  const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
620
628
  export class PaneStreamLiveConnection {
629
+ // One bounded native negotiation per redeemed socket; never reuse on reconnect.
630
+ #nativeSurfaces;
631
+ #surfaceViewer = null;
632
+ #surfaceDemands = new Map();
633
+ #surfaceNative = false;
634
+ #surfaceSession = null;
635
+ #surfaceReceiver = null;
621
636
  #localImages;
622
637
  #localLeases = new Map();
623
638
  #clientId;
@@ -707,6 +722,7 @@ export class PaneStreamLiveConnection {
707
722
  return elapsed;
708
723
  }
709
724
  constructor(options) {
725
+ this.#nativeSurfaces = options.nativeSurfaces;
710
726
  this.#localImages = options.localImages;
711
727
  this.#clientId = options.clientId;
712
728
  this.#socket = options.socket;
@@ -801,6 +817,8 @@ export class PaneStreamLiveConnection {
801
817
  }
802
818
  this.#stopAuthoritySnapshots =
803
819
  this.#sessionRuntimeBinding?.onAuthoritySnapshot?.((snapshot) => this.#usesExplicitAuthority ? this.#sendAuthoritySnapshot(snapshot) : undefined) ?? null;
820
+ if (!this.#descriptor.terminalDelivery)
821
+ this.#observeSurfaceDemand(this.#descriptor.panes);
804
822
  void this.#subscribeAll();
805
823
  }
806
824
  close(code = 1000, reason = "stream-closed") {
@@ -808,6 +826,13 @@ export class PaneStreamLiveConnection {
808
826
  return;
809
827
  this.#recordDiagnosticLifecycle("pane-stream-terminal", code, reason);
810
828
  this.#closed = true;
829
+ for (const demand of this.#surfaceDemands.values())
830
+ demand.close();
831
+ this.#surfaceDemands.clear();
832
+ this.#surfaceViewer?.close();
833
+ this.#surfaceViewer = null;
834
+ this.#surfaceSession?.close();
835
+ this.#surfaceSession = null;
811
836
  this.#cancelDrainTick?.();
812
837
  this.#cancelDrainTick = null;
813
838
  // Departure force-returns every ticket within this same tick.
@@ -839,6 +864,8 @@ export class PaneStreamLiveConnection {
839
864
  const delivery = channel.delivery;
840
865
  channel.delivery = null;
841
866
  channel.closed = true;
867
+ this.#surfaceDemands.get(channel.semanticPaneId)?.close();
868
+ this.#surfaceDemands.delete(channel.semanticPaneId);
842
869
  if (sub)
843
870
  closures.push(sub.close().catch(() => undefined));
844
871
  if (delivery)
@@ -1264,11 +1291,30 @@ export class PaneStreamLiveConnection {
1264
1291
  }
1265
1292
  await this.#awaitSemanticCredit(pane);
1266
1293
  }
1294
+ #observeSurfaceDemand(panes) {
1295
+ for (const pane of panes) {
1296
+ if (this.#surfaceDemands.has(pane))
1297
+ continue;
1298
+ const demand = this.#nativeSurfaces?.demand.viewer(this.#descriptor.sessionName, [pane]);
1299
+ if (!demand)
1300
+ return;
1301
+ demand.native(this.#surfaceNative);
1302
+ this.#surfaceDemands.set(pane, demand);
1303
+ }
1304
+ }
1305
+ #surfaceControl(type) {
1306
+ if (type === "terminal-surface-offer")
1307
+ return;
1308
+ this.#surfaceNative = type === "terminal-surface-peer";
1309
+ for (const demand of this.#surfaceDemands.values())
1310
+ demand.native(this.#surfaceNative);
1311
+ }
1267
1312
  /** Announce the pane's kitty graphics state; the client pulls bytes it lacks. */
1268
1313
  #startGraphics(channel) {
1269
1314
  const graphics = channel.delivery?.graphics;
1270
1315
  if (!graphics)
1271
1316
  return;
1317
+ this.#observeSurfaceDemand([channel.semanticPaneId]);
1272
1318
  // Renderers that cannot inflate see raw formats; their bytes are inflated
1273
1319
  // when served.
1274
1320
  const zlib = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION) === true;
@@ -1277,7 +1323,11 @@ export class PaneStreamLiveConnection {
1277
1323
  !!this.#localImages &&
1278
1324
  this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
1279
1325
  version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
1280
- version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION) === true;
1326
+ version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
1327
+ version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION) === true;
1328
+ channel.graphicsCompressed =
1329
+ this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
1330
+ version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION) === true;
1281
1331
  channel.graphicsInline = new Set();
1282
1332
  channel.graphicsBinary =
1283
1333
  this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION ||
@@ -1285,7 +1335,9 @@ export class PaneStreamLiveConnection {
1285
1335
  version === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
1286
1336
  version === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
1287
1337
  version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
1288
- version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION) === true;
1338
+ version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
1339
+ version === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
1340
+ version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION) === true;
1289
1341
  // Renderers that report cell pixels learn up front that this daemon
1290
1342
  // accepts them, before any image exists.
1291
1343
  const cellPixels = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION) === true;
@@ -1314,6 +1366,7 @@ export class PaneStreamLiveConnection {
1314
1366
  ...(cellPixels ? { cellPixels: true } : {}),
1315
1367
  ...(preciseCellPixels ? { preciseCellPixels: true } : {}),
1316
1368
  ...(channel.graphicsBinary ? { binaryBlobs: true } : {}),
1369
+ ...(channel.graphicsCompressed ? { compressedBlobs: true } : {}),
1317
1370
  ...(channel.graphicsLocal ? { localFiles: true } : {}),
1318
1371
  },
1319
1372
  });
@@ -1332,6 +1385,7 @@ export class PaneStreamLiveConnection {
1332
1385
  cellPixels: true,
1333
1386
  ...(preciseCellPixels ? { preciseCellPixels: true } : {}),
1334
1387
  ...(channel.graphicsBinary ? { binaryBlobs: true } : {}),
1388
+ ...(channel.graphicsCompressed ? { compressedBlobs: true } : {}),
1335
1389
  ...(channel.graphicsLocal ? { localFiles: true } : {}),
1336
1390
  },
1337
1391
  });
@@ -1452,6 +1506,29 @@ export class PaneStreamLiveConnection {
1452
1506
  break;
1453
1507
  const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
1454
1508
  if (channel.graphicsBinary) {
1509
+ const header = {
1510
+ pane: channel.semanticPaneId,
1511
+ blob: {
1512
+ digest,
1513
+ format,
1514
+ width: image.width,
1515
+ height: image.height,
1516
+ total,
1517
+ offset,
1518
+ },
1519
+ };
1520
+ const compressed = channel.graphicsCompressed
1521
+ ? await compressGraphicsChunk(header, data.subarray(offset, end))
1522
+ : null;
1523
+ // Compression yields: a pane may close or replace this image meanwhile.
1524
+ if (this.#closed || channel.closed)
1525
+ return;
1526
+ if (graphics.image(digest) !== image)
1527
+ break;
1528
+ if (compressed) {
1529
+ this.#sendEncoded(null, Buffer.from(compressed), true);
1530
+ continue;
1531
+ }
1455
1532
  this.#sendEncoded(null, Buffer.from(encodeTerminalGraphicsBinary({
1456
1533
  pane: channel.semanticPaneId,
1457
1534
  blob: {
@@ -1621,6 +1698,8 @@ export class PaneStreamLiveConnection {
1621
1698
  if (channel.closed)
1622
1699
  return;
1623
1700
  channel.closed = true;
1701
+ this.#surfaceDemands.get(channel.semanticPaneId)?.close();
1702
+ this.#surfaceDemands.delete(channel.semanticPaneId);
1624
1703
  const sub = channel.sub;
1625
1704
  channel.sub = null;
1626
1705
  if (sub)
@@ -2081,6 +2160,120 @@ export class PaneStreamLiveConnection {
2081
2160
  this.#acceptSemanticIntent(frame.operationId, frame.intent);
2082
2161
  return;
2083
2162
  }
2163
+ if (frame.type === "terminal-surface-watch") {
2164
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
2165
+ frame.generation !== this.#sessionRuntimeBinding?.generation ||
2166
+ !this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_SURFACE_CAPABILITY_VERSION) ||
2167
+ !this.#nativeSurfaces ||
2168
+ this.#surfaceReceiver)
2169
+ return this.#failProtocol("protocol-error");
2170
+ if (this.#surfaceViewer)
2171
+ return;
2172
+ this.#surfaceViewer = new NativeSurfaceViewer(this.#nativeSurfaces, this.#descriptor.panes, (control) => {
2173
+ if (this.#closed)
2174
+ return false;
2175
+ this.#surfaceControl(control.type);
2176
+ this.#sendFrame(null, control);
2177
+ return !this.#closed;
2178
+ }, (pane, generation) => {
2179
+ const channel = this.#panes.get(pane);
2180
+ const address = channel?.deliveryAddress;
2181
+ return !this.#closed &&
2182
+ channel &&
2183
+ !channel.closed &&
2184
+ channel.delivery &&
2185
+ address?.generation === generation
2186
+ ? address.deliveryNonce
2187
+ : null;
2188
+ });
2189
+ this.#surfaceViewer.start();
2190
+ return;
2191
+ }
2192
+ if (frame.type === "terminal-surface-receiver") {
2193
+ if (this.#surfaceViewer) {
2194
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.receive(frame))
2195
+ this.#failProtocol("protocol-error");
2196
+ return;
2197
+ }
2198
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
2199
+ !this.#descriptor.terminalDelivery ||
2200
+ frame.generation !== this.#sessionRuntimeBinding?.generation) {
2201
+ return this.#failProtocol("protocol-error");
2202
+ }
2203
+ if (this.#surfaceReceiver) {
2204
+ // Parsed schema order is deterministic. A duplicate cannot change the
2205
+ // challenge, kernel identity or endpoint, nor amplify fallback replies.
2206
+ if (JSON.stringify(frame) !== JSON.stringify(this.#surfaceReceiver)) {
2207
+ return this.#failProtocol("protocol-error");
2208
+ }
2209
+ return;
2210
+ }
2211
+ this.#surfaceReceiver = frame;
2212
+ if (this.#descriptor.terminalDelivery.protocolVersions.includes(TERMINAL_SURFACE_CAPABILITY_VERSION)) {
2213
+ const native = this.#nativeSurfaces?.open(frame, this.#descriptor.panes, (control) => {
2214
+ if (this.#closed)
2215
+ return false;
2216
+ this.#surfaceControl(control.type);
2217
+ this.#sendFrame(null, control);
2218
+ return !this.#closed;
2219
+ }, (pane, generation) => {
2220
+ const channel = this.#panes.get(pane);
2221
+ const address = channel?.deliveryAddress;
2222
+ if (this.#closed ||
2223
+ !channel ||
2224
+ channel.closed ||
2225
+ !channel.delivery ||
2226
+ !address ||
2227
+ address.generation !== generation)
2228
+ return null;
2229
+ return address.deliveryNonce;
2230
+ }) ?? null;
2231
+ if (this.#closed) {
2232
+ native?.close();
2233
+ return;
2234
+ }
2235
+ this.#surfaceSession = native;
2236
+ if (native)
2237
+ return;
2238
+ }
2239
+ // No provider/capability means no PID/slot lookup and no inferred locality.
2240
+ this.#sendFrame(null, {
2241
+ type: "terminal-surface-fallback",
2242
+ version: 1,
2243
+ generation: frame.generation,
2244
+ connectionNonce: frame.connectionNonce,
2245
+ reason: "unsupported",
2246
+ });
2247
+ return;
2248
+ }
2249
+ if (frame.type === "terminal-surface-fallback") {
2250
+ if (this.#surfaceViewer) {
2251
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.fallback(frame))
2252
+ this.#failProtocol("protocol-error");
2253
+ return;
2254
+ }
2255
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
2256
+ frame.generation !== this.#surfaceReceiver?.generation ||
2257
+ frame.connectionNonce !== this.#surfaceReceiver.connectionNonce) {
2258
+ return this.#failProtocol("protocol-error");
2259
+ }
2260
+ this.#surfaceSession?.fallback(frame.reason);
2261
+ return;
2262
+ }
2263
+ if (frame.type === "terminal-surface-retired") {
2264
+ if (this.#surfaceViewer) {
2265
+ if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.retire(frame))
2266
+ this.#failProtocol("protocol-error");
2267
+ return;
2268
+ }
2269
+ if (byteLength <= TERMINAL_SURFACE_MAX_CONTROL_BYTES &&
2270
+ frame.generation === this.#surfaceReceiver?.generation &&
2271
+ frame.connectionNonce === this.#surfaceReceiver.connectionNonce &&
2272
+ this.#surfaceSession?.retire(frame.sequence))
2273
+ return;
2274
+ // Unissued/replayed ACKs must not retire another socket's native storage.
2275
+ return this.#failProtocol("protocol-error");
2276
+ }
2084
2277
  if (frame.type === "terminal-graphics-request") {
2085
2278
  this.#requestGraphics(frame.pane, frame.digests, frame.preferInline === true);
2086
2279
  return;
@@ -1,3 +1,4 @@
1
+ import { NativeSurfaceProviderRegistry } from "../graphics/native-surface-provider.js";
1
2
  import { SessionRuntimeTransportBinder } from "../session-runtime/transport-binding.js";
2
3
  import { PaneStreamLeaseManager } from "./lease-manager.js";
3
4
  import { PaneStreamAdmissionCoordinator, } from "./pane-stream-websocket.js";
@@ -8,8 +9,14 @@ import { PaneStreamAdmissionCoordinator, } from "./pane-stream-websocket.js";
8
9
  */
9
10
  export class PaneStreamRuntime {
10
11
  coordinator;
12
+ nativeSurfaces;
11
13
  #disposePromise = null;
12
14
  constructor(options) {
15
+ this.nativeSurfaces =
16
+ options.nativeSurfaces ??
17
+ (process.platform === "darwin"
18
+ ? new NativeSurfaceProviderRegistry(options.daemonInstanceId)
19
+ : undefined);
13
20
  const leaseManager = new PaneStreamLeaseManager({
14
21
  daemonInstanceId: options.daemonInstanceId,
15
22
  });
@@ -17,6 +24,7 @@ export class PaneStreamRuntime {
17
24
  this.coordinator = new PaneStreamAdmissionCoordinator({
18
25
  ...options.admission,
19
26
  localImages: options.localImages,
27
+ nativeSurfaces: this.nativeSurfaces,
20
28
  daemonInstanceId: options.daemonInstanceId,
21
29
  webSocketUrl: options.webSocketUrl,
22
30
  leaseManager,
@@ -43,6 +51,7 @@ export class PaneStreamRuntime {
43
51
  dispose() {
44
52
  if (!this.#disposePromise) {
45
53
  this.#disposePromise = (async () => {
54
+ this.nativeSurfaces?.close();
46
55
  await this.coordinator.shutdown();
47
56
  })();
48
57
  }
@@ -1,3 +1,4 @@
1
+ import { mountSavedMachineWatch } from "./saved-machine-watch.ts";
1
2
  import type { Hono } from "hono";
2
3
  import { bodyLimit } from "hono/body-limit";
3
4
  import { z } from "zod";
@@ -7,6 +8,7 @@ import {
7
8
  loadSavedMachines,
8
9
  mergeSavedMachines,
9
10
  updateSavedMachines,
11
+ watchSavedMachines,
10
12
  } from "../../lib/saved-machines.ts";
11
13
  import { ownerAuthorityGate } from "../owner-authority.ts";
12
14
 
@@ -26,8 +28,10 @@ export function mountSavedMachineRoute(
26
28
  read?: typeof loadSavedMachines;
27
29
  merge?: typeof mergeSavedMachines;
28
30
  update?: typeof updateSavedMachines;
31
+ watch?: typeof watchSavedMachines;
29
32
  },
30
33
  ) {
34
+ mountSavedMachineWatch(app, options);
31
35
  const authorize = ownerAuthorityGate(options.ownerToken, {
32
36
  whenOwnerless: "unavailable",
33
37
  unavailableMessage: "Machine registry unavailable",
@@ -0,0 +1,132 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Hono } from "hono";
3
+ import { z } from "zod";
4
+ import {
5
+ SavedMachineRegistrySchema,
6
+ type SavedMachineRegistry,
7
+ type DaemonInstanceIdentity,
8
+ } from "@tmux-ide/contracts";
9
+ import { loadSavedMachines, watchSavedMachines } from "../../lib/saved-machines.ts";
10
+ import { ownerAuthorityGate } from "../owner-authority.ts";
11
+
12
+ export const SAVED_MACHINE_WATCH_WAIT_MS = 20_000;
13
+ const MAX_WATCHERS = 32;
14
+ const Query = z.strictObject({
15
+ expectedInstanceId: z.uuid(),
16
+ after: z
17
+ .string()
18
+ .regex(/^[a-f0-9]{64}$/u)
19
+ .optional(),
20
+ });
21
+ type Snapshot = { revision: string; registry: SavedMachineRegistry };
22
+ type Result =
23
+ | { status: 200; revision: string; registry: SavedMachineRegistry | null }
24
+ | { status: 408 | 503 };
25
+ function snapshot(value: SavedMachineRegistry): Snapshot {
26
+ const registry = SavedMachineRegistrySchema.parse(value);
27
+ return {
28
+ registry,
29
+ revision: createHash("sha256").update(JSON.stringify(registry)).digest("hex"),
30
+ };
31
+ }
32
+
33
+ /** One bounded notification request. A new request always observes the latest registry. */
34
+ export function mountSavedMachineWatch(
35
+ app: Hono,
36
+ options: {
37
+ daemon: DaemonInstanceIdentity;
38
+ ownerToken: string | null;
39
+ read?: typeof loadSavedMachines;
40
+ watch?: typeof watchSavedMachines;
41
+ },
42
+ ) {
43
+ const authorize = ownerAuthorityGate(options.ownerToken, {
44
+ whenOwnerless: "unavailable",
45
+ unavailableMessage: "Machine registry unavailable",
46
+ mismatchMessage: "Machine registry requires owner authority",
47
+ });
48
+ const read = options.read ?? loadSavedMachines;
49
+ const watch = options.watch ?? watchSavedMachines;
50
+ let active = 0;
51
+ app.get("/api/resources/saved-machines/watch", async (c) => {
52
+ const gate = authorize(c);
53
+ if (gate) return gate;
54
+ c.header("Cache-Control", "no-store");
55
+ const query = Query.safeParse(c.req.query());
56
+ if (!query.success) return c.json({ error: "Invalid registry watch request" }, 400);
57
+ if (query.data.expectedInstanceId !== options.daemon.instanceId)
58
+ return c.json({ error: "Daemon generation changed" }, 409);
59
+ if (active >= MAX_WATCHERS) return c.json({ error: "Registry watch capacity reached" }, 429);
60
+ active++;
61
+ try {
62
+ const initial = snapshot(read());
63
+ if (query.data.after !== initial.revision)
64
+ return c.json({ daemon: options.daemon, ...initial });
65
+ const signal = c.req.raw.signal;
66
+ const result = await new Promise<Result>((resolve) => {
67
+ let settled = false;
68
+ let stop = () => {};
69
+ const finish = (result: Result) => {
70
+ if (settled) return;
71
+ settled = true;
72
+ clearTimeout(timer);
73
+ signal.removeEventListener("abort", aborted);
74
+ try {
75
+ stop();
76
+ } finally {
77
+ resolve(result);
78
+ }
79
+ };
80
+ const aborted = () => finish({ status: 408 });
81
+ const timer = setTimeout(
82
+ () => finish({ status: 200, revision: initial.revision, registry: null }),
83
+ SAVED_MACHINE_WATCH_WAIT_MS,
84
+ );
85
+ timer.unref();
86
+ const changed = (value: SavedMachineRegistry) => {
87
+ if (settled) return;
88
+ try {
89
+ const next = snapshot(value);
90
+ if (next.revision !== initial.revision) finish({ status: 200, ...next });
91
+ } catch {
92
+ finish({ status: 503 });
93
+ }
94
+ };
95
+ signal.addEventListener("abort", aborted, { once: true });
96
+ if (signal.aborted) {
97
+ aborted();
98
+ return;
99
+ }
100
+ try {
101
+ stop = watch(changed, () => finish({ status: 503 }));
102
+ // Watch implementations may synchronously report an initial error/change.
103
+ if (settled) {
104
+ stop();
105
+ return;
106
+ }
107
+ // Close the read-before-subscribe race independently of watcher behavior.
108
+ changed(read());
109
+ } catch {
110
+ finish({ status: 503 });
111
+ }
112
+ });
113
+ if (result.status !== 200)
114
+ return c.json(
115
+ {
116
+ error:
117
+ result.status === 408 ? "Registry watch cancelled" : "Machine registry unavailable",
118
+ },
119
+ result.status,
120
+ );
121
+ return c.json({
122
+ daemon: options.daemon,
123
+ revision: result.revision,
124
+ registry: result.registry,
125
+ });
126
+ } catch {
127
+ return c.json({ error: "Machine registry unavailable" }, 503);
128
+ } finally {
129
+ active--;
130
+ }
131
+ });
132
+ }
@@ -1,3 +1,4 @@
1
+ import { TERMINAL_SURFACE_CAPABILITY_VERSION } from "@tmux-ide/contracts";
1
2
  import { mountAutomationRoutes } from "./automation.ts";
2
3
  import { mountTmuxServerRoutes } from "./tmux-servers.ts";
3
4
  import { streamBoundedLogs } from "./log-stream.ts";
@@ -197,6 +198,9 @@ export interface CreateAppOptions {
197
198
  /** Catalog + startup-barrier facts for the startup readiness ladder. */
198
199
  startupReadinessAttachmentBackend?: StartupReadinessAttachmentAuthority | null;
199
200
  paneStreamIssueBackend?: PaneStreamIssueBackend | null;
201
+ /** Current authenticated provider availability, never inferred from platform or PID. */
202
+ nativeSurfaceProviderAvailable?: () => boolean;
203
+ nativeSurfaceProviderSupported?: () => boolean;
200
204
  applicationShellInventoryBackend?: {
201
205
  discoverApplicationShellSession(
202
206
  requestedSessionName: string,
@@ -657,6 +661,20 @@ export function createApp(options: CreateAppOptions = {}): Hono {
657
661
  status: "ok" as const,
658
662
  daemon: daemonInstanceIdentity,
659
663
  capabilities: {
664
+ ...(c.req.query("nativeSurfaces") === "1"
665
+ ? {
666
+ nativeSurfaces: {
667
+ supported:
668
+ !!options.paneStreamIssueBackend &&
669
+ options.nativeSurfaceProviderSupported?.() === true,
670
+ available:
671
+ options.paneStreamIssueBackend !== undefined &&
672
+ options.paneStreamIssueBackend !== null &&
673
+ options.nativeSurfaceProviderAvailable?.() === true,
674
+ protocolVersion: TERMINAL_SURFACE_CAPABILITY_VERSION,
675
+ },
676
+ }
677
+ : {}),
660
678
  // Opt-in keeps the response compatible with older strict host schemas.
661
679
  ...(c.req.query("windowViewport") === "1"
662
680
  ? {
@@ -1,3 +1,4 @@
1
+ import { NativeSurfaceLocationSchema } from "../lib/native-surface-context.ts";
1
2
  import { mountTmuxServerHistoryAnchorRoute } from "./tmux-server-history-anchor.ts";
2
3
  import { mountTmuxServerDiagnosticsRoute } from "./tmux-server-diagnostics.ts";
3
4
  import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.ts";
@@ -277,6 +278,19 @@ export function mountTmuxServerRoutes(app: Hono, options: TmuxServerRoutesOption
277
278
  );
278
279
  }),
279
280
  );
281
+ app.post(
282
+ `${scoped}/native-surface-context`,
283
+ route(async (c) => {
284
+ const server = scope(c);
285
+ const target = NativeSurfaceLocationSchema.parse(await boundedJson(c.req.raw, 8192));
286
+ c.req.raw.signal.throwIfAborted();
287
+ const context = await options.owners.withOwner(server, (owner) =>
288
+ owner.resolveNativeSurfaceContext(target, c.req.raw.signal),
289
+ );
290
+ c.req.raw.signal.throwIfAborted();
291
+ return c.json({ version: 1, server, context });
292
+ }),
293
+ );
280
294
  app.get(
281
295
  `${scoped}/inventory/:workspaceName`,
282
296
  route(async (c) => {
@@ -1,3 +1,5 @@
1
+ import { createNativeSurfaceContextResolver } from "./native-surface-context.ts";
2
+ import { NativeSurfaceProviderRegistry } from "../terminal/graphics/native-surface-provider.ts";
1
3
  import { TmuxServerVersionSchemaZ } from "@tmux-ide/contracts";
2
4
  import {
3
5
  createNativeSplitCapabilityProbe,
@@ -139,6 +141,7 @@ import {
139
141
  attachTerminalAttachmentWebSocket,
140
142
  type TerminalAttachmentWebSocketBoundary,
141
143
  } from "../server/terminal-attachment-upgrade.ts";
144
+ import { attachNativeSurfaceProvider } from "../server/native-surface-provider-upgrade.ts";
142
145
  import {
143
146
  attachPaneStreamWebSocket,
144
147
  type PaneStreamWebSocketBoundary,
@@ -920,6 +923,10 @@ async function startHttpServer({
920
923
  issue: (request, context) => getTerminalAttachmentRuntime().admission.issue(request, context),
921
924
  },
922
925
  paneStreamIssueBackend: paneStreamRuntime.coordinator,
926
+ nativeSurfaceProviderSupported: () =>
927
+ serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces !== undefined,
928
+ nativeSurfaceProviderAvailable: () =>
929
+ serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces?.available === true,
923
930
  applicationShellInventoryBackend: terminalInventoryRuntime,
924
931
  startupReadinessAttachmentBackend: terminalInventoryRuntime,
925
932
  catalogLiveSessions,
@@ -970,12 +977,26 @@ async function startHttpServer({
970
977
  (create ? getTerminalAttachmentRuntime() : peekTerminalAttachmentRuntime())?.admission ??
971
978
  null,
972
979
  );
973
- const paneStreamBoundary = attachPaneStreamWebSocket(
980
+ const paneStreamSocketBoundary = attachPaneStreamWebSocket(
974
981
  server,
975
982
  paneStreamRuntime.coordinator,
976
983
  PANE_STREAM_REDEEM_PATH,
977
984
  () => serverOwners.isDefaultCurrent(),
978
985
  );
986
+ const nativeProviderBoundary = paneStreamRuntime.nativeSurfaces
987
+ ? attachNativeSurfaceProvider(
988
+ server,
989
+ paneStreamRuntime.nativeSurfaces,
990
+ localBypassToken ?? null,
991
+ () => serverOwners.isDefaultCurrent(),
992
+ )
993
+ : null;
994
+ const paneStreamBoundary: PaneStreamWebSocketBoundary = {
995
+ async close() {
996
+ nativeProviderBoundary?.close();
997
+ await paneStreamSocketBoundary.close();
998
+ },
999
+ };
979
1000
  serverOwners.attach(server);
980
1001
 
981
1002
  try {
@@ -1526,6 +1547,8 @@ async function startEmbeddedDaemonGeneration(
1526
1547
  externalInteractionObserver.setDiagnostics(diagnostics);
1527
1548
  setFleetFactsObserverDiagnostics(diagnostics);
1528
1549
  }
1550
+ const nativeSurfaces =
1551
+ process.platform === "darwin" ? new NativeSurfaceProviderRegistry(instanceId) : undefined;
1529
1552
  sessionRuntimeRegistry = new SessionRuntimeRegistry({
1530
1553
  generation: instanceId,
1531
1554
  ...(runtimeObservability ? { observability: runtimeObservability } : {}),
@@ -1561,6 +1584,8 @@ async function startEmbeddedDaemonGeneration(
1561
1584
  publishResourceChange: (change) => broadcastResourceChanged(change, instanceId),
1562
1585
  },
1563
1586
  mirror: {
1587
+ onNativeClientPresence: (session, present) =>
1588
+ nativeSurfaces?.demand.presence(session, present),
1564
1589
  splitLayoutEpoch: () => observationSelector?.nativeServerEpoch ?? null,
1565
1590
  splitLayoutPrepare: createNativeSplitReadiness({
1566
1591
  observation: () => observationSelector ?? null,
@@ -1762,6 +1787,7 @@ async function startEmbeddedDaemonGeneration(
1762
1787
  }
1763
1788
  }
1764
1789
  paneStreamRuntime = createPaneStreamRuntime({
1790
+ nativeSurfaces,
1765
1791
  localImages,
1766
1792
  daemonInstanceId: instanceId,
1767
1793
  webSocketUrl: paneStreamWebSocketUrl(bindHostname, port),
@@ -1880,6 +1906,22 @@ async function startEmbeddedDaemonGeneration(
1880
1906
  return grant ? interactionEvidence.captureSourceBinding(grant) : null;
1881
1907
  },
1882
1908
  interactionReceipts,
1909
+ resolveNativeSurfaceContext: createNativeSurfaceContextResolver({
1910
+ generation: instanceId,
1911
+ registry: workspaceRegistry,
1912
+ catalog: createNativeTmuxServerCatalog(workspaceRegistry, fleetFactsTmuxRunner),
1913
+ inventory: terminalInventoryRuntime,
1914
+ run: (args, signal) => fleetFactsTmuxRunner(args, signal),
1915
+ assertOpen: () => {
1916
+ if (
1917
+ defaultEditorContextRetired ||
1918
+ serverOwners?.defaultRetired ||
1919
+ !initialNativeServerIdentity ||
1920
+ terminalInventoryRuntime?.lifecycleState() !== "ready"
1921
+ )
1922
+ throw new Error("Default native surface context owner is unavailable");
1923
+ },
1924
+ }),
1883
1925
  resolvePaneEditorContext: createPaneEditorContextResolver({
1884
1926
  generation: instanceId,
1885
1927
  registry: workspaceRegistry,