tmux-ide 2.9.11 → 2.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/bin/cli.js +6036 -3047
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +6 -0
  4. package/packages/contracts/src/pane-stream.ts +77 -0
  5. package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
  6. package/packages/contracts/src/terminal-delivery.ts +19 -3
  7. package/packages/contracts/src/terminal-graphics-compressed.ts +66 -0
  8. package/packages/contracts/src/terminal-graphics.ts +73 -0
  9. package/packages/contracts/src/terminal-projection.ts +153 -0
  10. package/packages/contracts/src/terminal-surfaces.ts +137 -0
  11. package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
  12. package/packages/core/src/index.ts +2 -0
  13. package/packages/core/src/terminal-delivery-binary.ts +160 -0
  14. package/packages/core/src/terminal-delivery.ts +377 -16
  15. package/packages/core/src/terminal-projection-vectors.mjs +82 -0
  16. package/packages/core/src/terminal-projection.ts +476 -0
  17. package/packages/core/src/terminal-replica.ts +34 -1
  18. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +2 -0
  19. package/packages/daemon/dist/command-center/resources/saved-machine-watch.js +117 -0
  20. package/packages/daemon/dist/command-center/server.js +13 -0
  21. package/packages/daemon/dist/command-center/tmux-servers.js +9 -0
  22. package/packages/daemon/dist/lib/daemon-embed.js +71 -4
  23. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
  24. package/packages/daemon/dist/lib/native-surface-context.js +96 -0
  25. package/packages/daemon/dist/lib/tmux-server-owner.js +10 -0
  26. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  27. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  28. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  29. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  30. package/packages/daemon/dist/server/native-surface-provider-upgrade.js +88 -0
  31. package/packages/daemon/dist/terminal/graphics/compress-chunk.js +40 -0
  32. package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
  33. package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
  34. package/packages/daemon/dist/terminal/graphics/native-surface-demand.js +99 -0
  35. package/packages/daemon/dist/terminal/graphics/native-surface-provider-channel.js +162 -0
  36. package/packages/daemon/dist/terminal/graphics/native-surface-provider.js +275 -0
  37. package/packages/daemon/dist/terminal/graphics/native-surface-viewer.js +146 -0
  38. package/packages/daemon/dist/terminal/mirror/mirror-service.js +3 -0
  39. package/packages/daemon/dist/terminal/mirror/session-channel.js +32 -9
  40. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +369 -16
  41. package/packages/daemon/dist/terminal/pane-stream/runtime.js +10 -0
  42. package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
  43. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
  44. package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
  45. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
  46. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
  47. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
  48. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
  49. package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
  50. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
  51. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
  52. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +4 -0
  53. package/packages/daemon/src/command-center/resources/saved-machine-watch.ts +132 -0
  54. package/packages/daemon/src/command-center/server.ts +18 -0
  55. package/packages/daemon/src/command-center/tmux-servers.ts +14 -0
  56. package/packages/daemon/src/lib/daemon-embed.ts +81 -3
  57. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
  58. package/packages/daemon/src/lib/native-surface-context.ts +123 -0
  59. package/packages/daemon/src/lib/tmux-server-owner.ts +12 -0
  60. package/packages/daemon/src/server/native-surface-provider-upgrade.ts +103 -0
  61. package/packages/daemon/src/terminal/graphics/compress-chunk.ts +49 -0
  62. package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
  63. package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
  64. package/packages/daemon/src/terminal/graphics/native-surface-demand.ts +88 -0
  65. package/packages/daemon/src/terminal/graphics/native-surface-provider-channel.ts +161 -0
  66. package/packages/daemon/src/terminal/graphics/native-surface-provider.ts +308 -0
  67. package/packages/daemon/src/terminal/graphics/native-surface-viewer.ts +154 -0
  68. package/packages/daemon/src/terminal/mirror/mirror-service.ts +5 -0
  69. package/packages/daemon/src/terminal/mirror/session-channel.ts +31 -10
  70. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +446 -15
  71. package/packages/daemon/src/terminal/pane-stream/runtime.ts +14 -1
  72. package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
  73. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
  74. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
  75. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
  76. package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
  77. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
  78. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
  79. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
  80. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
  81. package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
  82. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
  83. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
  84. package/packages/daemon-client/src/pane-stream-client.ts +75 -1
@@ -95,6 +95,10 @@ export class SemanticPaneReplica {
95
95
  : [...this.#dirtyRows].sort((a, b) => a - b);
96
96
  }
97
97
  accept(message) {
98
+ if (message.type === "terminal.projection") {
99
+ this.#fail("protocol-violation", message.transactionId);
100
+ return;
101
+ }
98
102
  if (message.type === "terminal.delivery.fault") {
99
103
  this.#assembler = null;
100
104
  this.#snapshot = null;
@@ -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,
@@ -8,7 +10,8 @@ import { createGuardedNativeSplitResize } from "./guarded-native-split-resize.ts
8
10
  import { WindowLinkTargetSchemaZ } from "@tmux-ide/contracts";
9
11
  import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.ts";
10
12
  import { requireSupportedTmuxVersion } from "./tmux-version.ts";
11
- import { join } from "node:path";
13
+ import { dirname, join } from "node:path";
14
+ import { LocalImageOwner, cleanupLocalImageOwner } from "../terminal/graphics/local-image-owner.ts";
12
15
  import { createClaudeTeamMembershipReader } from "../terminal/attachments/claude-team-names.ts";
13
16
  import { PaneSourceDiscovery } from "./pane-source-discovery.ts";
14
17
  import { createBackgroundNativeCapture } from "./background-native-capture.ts";
@@ -138,6 +141,7 @@ import {
138
141
  attachTerminalAttachmentWebSocket,
139
142
  type TerminalAttachmentWebSocketBoundary,
140
143
  } from "../server/terminal-attachment-upgrade.ts";
144
+ import { attachNativeSurfaceProvider } from "../server/native-surface-provider-upgrade.ts";
141
145
  import {
142
146
  attachPaneStreamWebSocket,
143
147
  type PaneStreamWebSocketBoundary,
@@ -159,6 +163,7 @@ import { captureDaemonProvenance } from "./daemon-provenance.ts";
159
163
  import { registerLogSecret, setLogIdentity } from "./log.ts";
160
164
  import {
161
165
  canonicalDaemonUrl,
166
+ getCanonicalDaemonInfoPath,
162
167
  clearCanonicalDaemonInfoIfOwned,
163
168
  clearCanonicalDaemonInfoIfUnchanged,
164
169
  inspectCanonicalDaemonInfo,
@@ -918,6 +923,10 @@ async function startHttpServer({
918
923
  issue: (request, context) => getTerminalAttachmentRuntime().admission.issue(request, context),
919
924
  },
920
925
  paneStreamIssueBackend: paneStreamRuntime.coordinator,
926
+ nativeSurfaceProviderSupported: () =>
927
+ serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces !== undefined,
928
+ nativeSurfaceProviderAvailable: () =>
929
+ serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces?.available === true,
921
930
  applicationShellInventoryBackend: terminalInventoryRuntime,
922
931
  startupReadinessAttachmentBackend: terminalInventoryRuntime,
923
932
  catalogLiveSessions,
@@ -968,12 +977,26 @@ async function startHttpServer({
968
977
  (create ? getTerminalAttachmentRuntime() : peekTerminalAttachmentRuntime())?.admission ??
969
978
  null,
970
979
  );
971
- const paneStreamBoundary = attachPaneStreamWebSocket(
980
+ const paneStreamSocketBoundary = attachPaneStreamWebSocket(
972
981
  server,
973
982
  paneStreamRuntime.coordinator,
974
983
  PANE_STREAM_REDEEM_PATH,
975
984
  () => serverOwners.isDefaultCurrent(),
976
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
+ };
977
1000
  serverOwners.attach(server);
978
1001
 
979
1002
  try {
@@ -1399,6 +1422,7 @@ async function startEmbeddedDaemonGeneration(
1399
1422
  };
1400
1423
  let terminalAttachmentRuntime: NativeTerminalAttachmentRuntime | null = null;
1401
1424
  let paneStreamRuntime: PaneStreamRuntime | null = null;
1425
+ let localImages: LocalImageOwner | undefined;
1402
1426
  let serverOwners: Awaited<ReturnType<typeof createEmbeddedTmuxServerOwners>> | null = null;
1403
1427
  let defaultEditorContextRetired = false;
1404
1428
  const defaultDiagnosticsLifetime = new AbortController();
@@ -1523,6 +1547,8 @@ async function startEmbeddedDaemonGeneration(
1523
1547
  externalInteractionObserver.setDiagnostics(diagnostics);
1524
1548
  setFleetFactsObserverDiagnostics(diagnostics);
1525
1549
  }
1550
+ const nativeSurfaces =
1551
+ process.platform === "darwin" ? new NativeSurfaceProviderRegistry(instanceId) : undefined;
1526
1552
  sessionRuntimeRegistry = new SessionRuntimeRegistry({
1527
1553
  generation: instanceId,
1528
1554
  ...(runtimeObservability ? { observability: runtimeObservability } : {}),
@@ -1558,6 +1584,8 @@ async function startEmbeddedDaemonGeneration(
1558
1584
  publishResourceChange: (change) => broadcastResourceChanged(change, instanceId),
1559
1585
  },
1560
1586
  mirror: {
1587
+ onNativeClientPresence: (session, present) =>
1588
+ nativeSurfaces?.demand.presence(session, present),
1561
1589
  splitLayoutEpoch: () => observationSelector?.nativeServerEpoch ?? null,
1562
1590
  splitLayoutPrepare: createNativeSplitReadiness({
1563
1591
  observation: () => observationSelector ?? null,
@@ -1732,7 +1760,35 @@ async function startEmbeddedDaemonGeneration(
1732
1760
  };
1733
1761
  const peekTerminalAttachmentRuntime = (): NativeTerminalAttachmentRuntime | null =>
1734
1762
  terminalAttachmentRuntime;
1763
+ // The held canonical claim has already proven and hardened this private root.
1764
+ // Local images are optional: unavailable storage retains inline graphics.
1765
+ if (process.platform !== "win32") {
1766
+ const root = dirname(getCanonicalDaemonInfoPath());
1767
+ try {
1768
+ if (existingCanonical.status === "valid") {
1769
+ await cleanupLocalImageOwner({
1770
+ root,
1771
+ owner: {
1772
+ generation: existingCanonical.info.instanceId,
1773
+ pid: existingCanonical.info.pid,
1774
+ startedAt: existingCanonical.info.startedAt,
1775
+ },
1776
+ proveDead: () => isCanonicalDaemonRecordOwnerProvenDead(existingCanonical),
1777
+ });
1778
+ }
1779
+ localImages = await LocalImageOwner.create({
1780
+ root,
1781
+ generation: instanceId,
1782
+ pid: process.pid,
1783
+ startedAt,
1784
+ });
1785
+ } catch {
1786
+ localImages = undefined;
1787
+ }
1788
+ }
1735
1789
  paneStreamRuntime = createPaneStreamRuntime({
1790
+ nativeSurfaces,
1791
+ localImages,
1736
1792
  daemonInstanceId: instanceId,
1737
1793
  webSocketUrl: paneStreamWebSocketUrl(bindHostname, port),
1738
1794
  sessionRuntimeRegistry,
@@ -1758,6 +1814,7 @@ async function startEmbeddedDaemonGeneration(
1758
1814
  });
1759
1815
  await externalInteractionObserver.start();
1760
1816
  serverOwners = await createEmbeddedTmuxServerOwners({
1817
+ localImages,
1761
1818
  environmentId,
1762
1819
  onDefaultScope: async (scope) => {
1763
1820
  interactionEvidence = new InteractionEvidenceAuthority(environmentId, scope);
@@ -1849,6 +1906,22 @@ async function startEmbeddedDaemonGeneration(
1849
1906
  return grant ? interactionEvidence.captureSourceBinding(grant) : null;
1850
1907
  },
1851
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
+ }),
1852
1925
  resolvePaneEditorContext: createPaneEditorContextResolver({
1853
1926
  generation: instanceId,
1854
1927
  registry: workspaceRegistry,
@@ -2075,7 +2148,10 @@ async function startEmbeddedDaemonGeneration(
2075
2148
  // The pane-stream coordinator may still hold runtime consumers while it
2076
2149
  // drains. Preserve the normal shutdown order on startup rollback too:
2077
2150
  // transports first, then the one session/control authority.
2078
- await Promise.allSettled([sessionRuntimeRegistry?.dispose() ?? Promise.resolve()]);
2151
+ await Promise.allSettled([
2152
+ sessionRuntimeRegistry?.dispose() ?? Promise.resolve(),
2153
+ localImages?.dispose() ?? Promise.resolve(),
2154
+ ]);
2079
2155
  throw error;
2080
2156
  }
2081
2157
  const {
@@ -2108,6 +2184,7 @@ async function startEmbeddedDaemonGeneration(
2108
2184
  )),
2109
2185
  ...(await retirePaneStreamTransport()),
2110
2186
  ];
2187
+ await localImages?.dispose();
2111
2188
  terminalInventoryRuntime.dispose();
2112
2189
  const paneDisposal = Promise.resolve().then(() => workspacePaneCreation.dispose());
2113
2190
  const workspaceOpenDisposal = Promise.resolve().then(() => workspaceOpen.dispose());
@@ -2375,6 +2452,7 @@ async function startEmbeddedDaemonGeneration(
2375
2452
  )),
2376
2453
  );
2377
2454
  failures.push(...(await retirePaneStreamTransport()));
2455
+ await capture(() => localImages?.dispose());
2378
2456
  await capture(() => terminalInventoryRuntime.dispose());
2379
2457
  await capture(() => workspacePaneCreation.dispose());
2380
2458
  await capture(() => workspaceOpen.dispose());
@@ -1,3 +1,4 @@
1
+ import type { LocalImageOwner } from "../terminal/graphics/local-image-owner.ts";
1
2
  import type { Server, IncomingMessage } from "node:http";
2
3
  import type { Socket } from "node:net";
3
4
  import { randomUUID } from "node:crypto";
@@ -22,6 +23,7 @@ import {
22
23
 
23
24
  /** Adapts the established default owner without duplicating its control clients. */
24
25
  export async function createEmbeddedTmuxServerOwners(options: {
26
+ readonly localImages?: LocalImageOwner;
25
27
  readonly environmentId: string;
26
28
  readonly onDefaultScope?: (scope: TmuxServerScope) => Promise<void>;
27
29
  readonly defaultAuthority: WorkspacePaneTmuxAuthority;
@@ -89,6 +91,7 @@ export async function createEmbeddedTmuxServerOwners(options: {
89
91
  if (registration.serverId === defaultRegistration.serverId) await retireDefault();
90
92
  const route = tmuxServerPaneStreamPath(scope);
91
93
  const owner = await createNativeTmuxServerOwner({
94
+ localImages: options.localImages,
92
95
  environmentId: options.environmentId,
93
96
  ...scope,
94
97
  tmuxAuthority: observation.authority,
@@ -0,0 +1,123 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { z } from "zod";
3
+ import {
4
+ TerminalAttachmentSemanticPaneIdSchemaZ,
5
+ WorkspaceCatalogLiveSessionIdSchemaZ,
6
+ } from "@tmux-ide/contracts";
7
+ import type { LiveSessionSummary } from "../command-center/discovery.ts";
8
+ import type { WorkspaceTerminalInventoryRuntime } from "../terminal/attachments/native-runtime.ts";
9
+ import type { WorkspaceRegistry } from "./workspace-registry.ts";
10
+
11
+ export const NativeSurfaceLocationSchema = z
12
+ .object({
13
+ socketPath: z
14
+ .string()
15
+ .min(1)
16
+ .max(4096)
17
+ .refine((p) => isAbsolute(p) && !/[\0\r\n\t]/u.test(p)),
18
+ serverPid: z
19
+ .string()
20
+ .max(16)
21
+ .regex(/^[1-9][0-9]*$/u),
22
+ sessionId: z
23
+ .string()
24
+ .max(32)
25
+ .regex(/^\$(?:0|[1-9][0-9]*)$/u),
26
+ runtimePaneId: z
27
+ .string()
28
+ .max(32)
29
+ .regex(/^%(?:0|[1-9][0-9]*)$/u),
30
+ })
31
+ .strict();
32
+ export type NativeSurfaceLocation = z.infer<typeof NativeSurfaceLocationSchema>;
33
+
34
+ interface Options {
35
+ generation: string;
36
+ assertOpen(): void;
37
+ run(args: readonly string[], signal: AbortSignal): Promise<string>;
38
+ registry: Pick<WorkspaceRegistry, "get">;
39
+ catalog(): Promise<LiveSessionSummary[]>;
40
+ inventory: Pick<WorkspaceTerminalInventoryRuntime, "discoverTerminalInventory">;
41
+ }
42
+
43
+ /** Resolve inherited tmux location only through the current generation's pinned owner. */
44
+ export function createNativeSurfaceContextResolver(options: Options) {
45
+ const generation = z.uuid().parse(options.generation);
46
+ return async (input: NativeSurfaceLocation, cancellation?: AbortSignal) => {
47
+ const target = NativeSurfaceLocationSchema.parse(input);
48
+ const signal = AbortSignal.any([
49
+ AbortSignal.timeout(5000),
50
+ ...(cancellation ? [cancellation] : []),
51
+ ]);
52
+ const assertCurrent = () => {
53
+ options.assertOpen();
54
+ signal.throwIfAborted();
55
+ };
56
+ const unavailable = (): never => {
57
+ throw new Error("Native surface source unavailable");
58
+ };
59
+ const observe = async () => {
60
+ assertCurrent();
61
+ const proof = await options.run(
62
+ [
63
+ "display-message",
64
+ "-p",
65
+ "-t",
66
+ target.runtimePaneId,
67
+ "#{socket_path}\t#{pid}\t#{session_id}\t#{pane_id}",
68
+ ],
69
+ signal,
70
+ );
71
+ assertCurrent();
72
+ if (
73
+ proof.trimEnd() !==
74
+ [target.socketPath, target.serverPid, target.sessionId, target.runtimePaneId].join("\t")
75
+ )
76
+ unavailable();
77
+ const snapshot = await options.inventory.discoverTerminalInventory(signal);
78
+ assertCurrent();
79
+ if (
80
+ snapshot.catalog.invalidRuntimeProof ||
81
+ snapshot.catalog.duplicateSemanticStamp ||
82
+ snapshot.catalog.duplicateRuntimePaneBinding
83
+ )
84
+ unavailable();
85
+ const matches = snapshot.panes.filter(
86
+ (pane) =>
87
+ pane.runtimePaneId === target.runtimePaneId && pane.sessionId === target.sessionId,
88
+ );
89
+ if (matches.length !== 1) return unavailable();
90
+ const pane = { ...matches[0]! };
91
+ const semantic = TerminalAttachmentSemanticPaneIdSchemaZ.parse(pane.semanticPaneId);
92
+ const workspace = options.registry.get(pane.workspaceName);
93
+ if (!workspace || workspace.sessionName !== pane.sessionName) return unavailable();
94
+ const sessions = (await options.catalog()).filter(
95
+ (row) => row.sessionName === pane.sessionName,
96
+ );
97
+ assertCurrent();
98
+ if (sessions.length !== 1 || options.registry.get(pane.workspaceName) !== workspace)
99
+ return unavailable();
100
+ const live = WorkspaceCatalogLiveSessionIdSchemaZ.parse(sessions[0]!.liveSessionId);
101
+ return {
102
+ context: {
103
+ generation,
104
+ workspaceName: pane.workspaceName,
105
+ liveSessionId: live,
106
+ pane: semantic,
107
+ },
108
+ identity: [
109
+ pane.sessionId,
110
+ pane.windowId,
111
+ pane.runtimePaneId,
112
+ pane.windowStamp ?? null,
113
+ pane.nativePaneBirthId ?? null,
114
+ ],
115
+ };
116
+ };
117
+ const before = await observe();
118
+ const after = await observe();
119
+ if (JSON.stringify(before) !== JSON.stringify(after)) return unavailable();
120
+ assertCurrent();
121
+ return Object.freeze(after.context);
122
+ };
123
+ }
@@ -1,3 +1,5 @@
1
+ import { createNativeSurfaceContextResolver } from "./native-surface-context.ts";
2
+ import type { LocalImageOwner } from "../terminal/graphics/local-image-owner.ts";
1
3
  import { shellEscape } from "./shell.ts";
2
4
  import {
3
5
  createNativeSplitCapabilityProbe,
@@ -68,6 +70,7 @@ import {
68
70
  } from "./tmux-server-generation-runner.ts";
69
71
 
70
72
  export interface NativeTmuxServerOwnerOptions {
73
+ readonly localImages?: LocalImageOwner;
71
74
  readonly environmentId: string;
72
75
  readonly serverId: string;
73
76
  readonly generation: string;
@@ -485,6 +488,7 @@ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwner
485
488
  }),
486
489
  });
487
490
  const paneStreamRuntime = createPaneStreamRuntime({
491
+ localImages: options.localImages,
488
492
  daemonInstanceId: generation,
489
493
  webSocketUrl: options.webSocketUrl,
490
494
  sessionRuntimeRegistry,
@@ -606,6 +610,14 @@ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwner
606
610
  },
607
611
  serverId: options.serverId,
608
612
  generation,
613
+ resolveNativeSurfaceContext: createNativeSurfaceContextResolver({
614
+ generation,
615
+ assertOpen,
616
+ registry: workspaceRegistry,
617
+ catalog,
618
+ inventory: terminalInventoryRuntime,
619
+ run: (args, signal) => generationRunAsync(args, signal),
620
+ }),
609
621
  resolvePaneEditorContext,
610
622
  catalog,
611
623
  readWindowSplitLayout: async (workspaceName: string, input: WindowLinkTarget) => {