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.
- package/bin/cli.js +6036 -3047
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +6 -0
- package/packages/contracts/src/pane-stream.ts +77 -0
- package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
- package/packages/contracts/src/terminal-delivery.ts +19 -3
- package/packages/contracts/src/terminal-graphics-compressed.ts +66 -0
- package/packages/contracts/src/terminal-graphics.ts +73 -0
- package/packages/contracts/src/terminal-projection.ts +153 -0
- package/packages/contracts/src/terminal-surfaces.ts +137 -0
- package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/terminal-delivery-binary.ts +160 -0
- package/packages/core/src/terminal-delivery.ts +377 -16
- package/packages/core/src/terminal-projection-vectors.mjs +82 -0
- package/packages/core/src/terminal-projection.ts +476 -0
- package/packages/core/src/terminal-replica.ts +34 -1
- package/packages/daemon/dist/command-center/resources/saved-machine-route.js +2 -0
- package/packages/daemon/dist/command-center/resources/saved-machine-watch.js +117 -0
- package/packages/daemon/dist/command-center/server.js +13 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +9 -0
- package/packages/daemon/dist/lib/daemon-embed.js +71 -4
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
- package/packages/daemon/dist/lib/native-surface-context.js +96 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +10 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/server/native-surface-provider-upgrade.js +88 -0
- package/packages/daemon/dist/terminal/graphics/compress-chunk.js +40 -0
- package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
- package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-demand.js +99 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-provider-channel.js +162 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-provider.js +275 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-viewer.js +146 -0
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +3 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +32 -9
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +369 -16
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +10 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
- package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
- package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
- package/packages/daemon/src/command-center/resources/saved-machine-route.ts +4 -0
- package/packages/daemon/src/command-center/resources/saved-machine-watch.ts +132 -0
- package/packages/daemon/src/command-center/server.ts +18 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +14 -0
- package/packages/daemon/src/lib/daemon-embed.ts +81 -3
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
- package/packages/daemon/src/lib/native-surface-context.ts +123 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +12 -0
- package/packages/daemon/src/server/native-surface-provider-upgrade.ts +103 -0
- package/packages/daemon/src/terminal/graphics/compress-chunk.ts +49 -0
- package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
- package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
- package/packages/daemon/src/terminal/graphics/native-surface-demand.ts +88 -0
- package/packages/daemon/src/terminal/graphics/native-surface-provider-channel.ts +161 -0
- package/packages/daemon/src/terminal/graphics/native-surface-provider.ts +308 -0
- package/packages/daemon/src/terminal/graphics/native-surface-viewer.ts +154 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +5 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +31 -10
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +446 -15
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +14 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
- package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
- 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
|
|
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([
|
|
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) => {
|