tmux-ide 2.9.0-beta.21 → 2.9.0-beta.22
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 +52253 -49070
- package/package.json +1 -1
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
- package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
- package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
- package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
- package/packages/contracts/src/actions-contract.ts +12 -0
- package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
- package/packages/contracts/src/daemon-wire.ts +10 -5
- package/packages/contracts/src/fleet-client-state.ts +3 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/interaction-receipts.ts +39 -0
- package/packages/contracts/src/issue-error.ts +1 -1
- package/packages/contracts/src/multiplexer-verbs.ts +25 -1
- package/packages/contracts/src/pane-stream.ts +51 -8
- package/packages/contracts/src/tmux-server-scope.ts +172 -0
- package/packages/contracts/src/window-links.ts +76 -0
- package/packages/contracts/src/workspace-multiplexer.ts +54 -0
- package/packages/core/src/interaction-receipts.ts +4 -0
- package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
- package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
- package/packages/daemon/dist/command-center/actions/registry.js +13 -1
- package/packages/daemon/dist/command-center/discovery.js +2 -9
- package/packages/daemon/dist/command-center/server.js +21 -1
- package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +345 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
- package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
- package/packages/daemon/dist/lib/daemon-embed.js +198 -30
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
- package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +16 -10
- package/packages/daemon/dist/lib/headless-daemon.js +10 -1
- package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
- package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
- package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
- package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
- package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
- package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
- package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
- package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
- package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
- package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
- package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
- package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
- package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
- package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
- package/packages/daemon/dist/lib/workspace-registry.js +4 -0
- package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
- package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
- package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
- package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
- package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
- package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
- package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
- package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
- package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
- package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
- package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +42 -1
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
- package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
- package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
- package/packages/daemon/src/command-center/actions/errors.ts +4 -0
- package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
- package/packages/daemon/src/command-center/actions/registry.ts +14 -0
- package/packages/daemon/src/command-center/discovery.ts +2 -10
- package/packages/daemon/src/command-center/server.ts +29 -1
- package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +468 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
- package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
- package/packages/daemon/src/lib/daemon-embed.ts +241 -28
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
- package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +16 -8
- package/packages/daemon/src/lib/headless-daemon.ts +18 -1
- package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
- package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
- package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
- package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
- package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
- package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
- package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
- package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
- package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
- package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
- package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
- package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
- package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
- package/packages/daemon/src/lib/workspace-registry.ts +5 -0
- package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
- package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
- package/packages/daemon/src/terminal/mirror/events.ts +2 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
- package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
- package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
- package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
- package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
- package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
- package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
- package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
- package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
- package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
- package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -36
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +52 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
- package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
- package/packages/daemon-client/package.json +3 -1
- package/packages/daemon-client/src/index.ts +2 -0
- package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
- package/packages/daemon-client/src/pane-stream-client.ts +26 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
- package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
- package/packages/daemon-client/src/tmux-server-client.ts +287 -0
|
@@ -214,9 +214,10 @@ function agentStatus(status) {
|
|
|
214
214
|
*/
|
|
215
215
|
export function TerminalWindowStrip(props) {
|
|
216
216
|
const tabs = () => (typeof props.tabs === "function" ? props.tabs() : props.tabs);
|
|
217
|
-
const tabId = (tab) => tab.semanticWindowId ?? `window:${tab.index}`;
|
|
217
|
+
const tabId = (tab) => tab.linkId ?? tab.semanticWindowId ?? `window:${tab.index}`;
|
|
218
218
|
const items = createMemo(() => tabs().map((tab) => ({
|
|
219
219
|
id: tabId(tab),
|
|
220
|
+
disabled: tab.disabled,
|
|
220
221
|
windowIndex: tab.index,
|
|
221
222
|
title: tab.name,
|
|
222
223
|
agentStatus: agentStatus(tab.status),
|
|
@@ -48,6 +48,12 @@ const ACTION_COMMAND_METADATA: Record<ActionName, ActionCommandMetadata> = {
|
|
|
48
48
|
"workspace.open.cancel": { label: "Cancel workspace handoff", category: "workspace" },
|
|
49
49
|
"workspace.promote": { label: "Promote live session to workspace", category: "workspace" },
|
|
50
50
|
"workspace.app-window.mutate": { label: "Mutate application window", category: "workspace" },
|
|
51
|
+
"workspace.window.link.select": { label: "Focus window link", category: "workspace" },
|
|
52
|
+
"workspace.window.link.unlink": {
|
|
53
|
+
label: "Unlink window",
|
|
54
|
+
category: "workspace",
|
|
55
|
+
dangerous: true,
|
|
56
|
+
},
|
|
51
57
|
"workspace.window.split": { label: "Split pane", category: "workspace" },
|
|
52
58
|
"workspace.window.kill": { label: "Close window", category: "workspace", dangerous: true },
|
|
53
59
|
"workspace.pane.kill": { label: "Close pane", category: "workspace", dangerous: true },
|
|
@@ -72,6 +72,10 @@ export type ActionErrorCode =
|
|
|
72
72
|
| "pane_not_found"
|
|
73
73
|
| "window_not_found"
|
|
74
74
|
| "ambiguous_target"
|
|
75
|
+
| "window_link_stale"
|
|
76
|
+
| "window_link_ambiguous"
|
|
77
|
+
| "window_link_session_mismatch"
|
|
78
|
+
| "window_link_backing_mismatch"
|
|
75
79
|
| "last_window_refused"
|
|
76
80
|
| "last_pane_refused"
|
|
77
81
|
| "mutation_failed"
|
|
@@ -84,6 +84,8 @@ function verbHandler<V extends WorkspaceMultiplexerVerb>(verb: V) {
|
|
|
84
84
|
): Promise<ActionResult<V>> => (await runVerb(verb, input, context, deps)) as ActionResult<V>;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export const workspaceWindowLinkSelectHandler = verbHandler("workspace.window.link.select");
|
|
88
|
+
export const workspaceWindowLinkUnlinkHandler = verbHandler("workspace.window.link.unlink");
|
|
87
89
|
export const workspaceWindowSplitHandler = verbHandler("workspace.window.split");
|
|
88
90
|
export const workspaceWindowKillHandler = verbHandler("workspace.window.kill");
|
|
89
91
|
export const workspacePaneKillHandler = verbHandler("workspace.pane.kill");
|
|
@@ -70,6 +70,8 @@ import {
|
|
|
70
70
|
workspaceRenameHandler,
|
|
71
71
|
workspaceSessionKillHandler,
|
|
72
72
|
workspaceWindowKillHandler,
|
|
73
|
+
workspaceWindowLinkSelectHandler,
|
|
74
|
+
workspaceWindowLinkUnlinkHandler,
|
|
73
75
|
workspaceWindowSplitHandler,
|
|
74
76
|
} from "./handlers/workspace-multiplexer.ts";
|
|
75
77
|
|
|
@@ -301,6 +303,18 @@ export const actionRegistry: RegistryShape = {
|
|
|
301
303
|
handler: (input) => workspacePaneZoomToggleHandler(input),
|
|
302
304
|
handlerWithContext: workspacePaneZoomToggleHandler,
|
|
303
305
|
},
|
|
306
|
+
"workspace.window.link.select": {
|
|
307
|
+
inputSchema: ActionContractsZ["workspace.window.link.select"].input,
|
|
308
|
+
resultSchema: ActionContractsZ["workspace.window.link.select"].result,
|
|
309
|
+
handler: (input) => workspaceWindowLinkSelectHandler(input),
|
|
310
|
+
handlerWithContext: workspaceWindowLinkSelectHandler,
|
|
311
|
+
},
|
|
312
|
+
"workspace.window.link.unlink": {
|
|
313
|
+
inputSchema: ActionContractsZ["workspace.window.link.unlink"].input,
|
|
314
|
+
resultSchema: ActionContractsZ["workspace.window.link.unlink"].result,
|
|
315
|
+
handler: (input) => workspaceWindowLinkUnlinkHandler(input),
|
|
316
|
+
handlerWithContext: workspaceWindowLinkUnlinkHandler,
|
|
317
|
+
},
|
|
304
318
|
"workspace.pane.select": {
|
|
305
319
|
inputSchema: ActionContractsZ["workspace.pane.select"].input,
|
|
306
320
|
resultSchema: ActionContractsZ["workspace.pane.select"].result,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.ts";
|
|
1
2
|
import { runtimeTmuxArgs } from "../lib/runtime-namespace.ts";
|
|
2
3
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
4
|
import { listSessionPanes } from "../widgets/lib/pane-comms.ts";
|
|
5
5
|
import type { PaneInfo } from "@tmux-ide/contracts";
|
|
6
6
|
import { getDefaultWorkspaceRegistry } from "../lib/workspace-registry.ts";
|
|
@@ -80,14 +80,6 @@ export interface LiveSessionSummary {
|
|
|
80
80
|
readonly paneCount: number;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function liveSessionId(serverPid: string, sessionId: string, sessionCreated: string) {
|
|
84
|
-
const digest = createHash("sha256")
|
|
85
|
-
.update(`${serverPid}\0${sessionId}\0${sessionCreated}`)
|
|
86
|
-
.digest("hex")
|
|
87
|
-
.slice(0, 20);
|
|
88
|
-
return `live-session.${digest}` as const;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
83
|
/**
|
|
92
84
|
* Enumerate observed tmux truth without consulting the workspace registry.
|
|
93
85
|
*
|
|
@@ -120,7 +112,7 @@ export function discoverLiveSessionSummaries(
|
|
|
120
112
|
if (!/^\d+$/u.test(serverPid) || !/^\$\d+$/u.test(sessionId) || !/^\d+$/u.test(sessionCreated))
|
|
121
113
|
continue;
|
|
122
114
|
if (!sessionName || !isVisibleFleetSession(sessionName)) continue;
|
|
123
|
-
const identity =
|
|
115
|
+
const identity = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
|
|
124
116
|
const previous = sessions.get(identity);
|
|
125
117
|
sessions.set(identity, {
|
|
126
118
|
liveSessionId: identity,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mountTmuxServerRoutes } from "./tmux-servers.ts";
|
|
1
2
|
import { streamBoundedLogs } from "./log-stream.ts";
|
|
2
3
|
import { mountWorkspaceAdmissionRoute } from "./resources/workspace-admission-route.ts";
|
|
3
4
|
import { mountFleetPreviewRoute } from "./resources/fleet-preview-route.ts";
|
|
@@ -162,6 +163,11 @@ import {
|
|
|
162
163
|
} from "./resources/startup-readiness-route.ts";
|
|
163
164
|
import { readWidgetAsset } from "../lib/widget-asset-store.ts";
|
|
164
165
|
export interface CreateAppOptions {
|
|
166
|
+
tmuxServerOwners?: import("../lib/tmux-server-owners.ts").TmuxServerOwners<
|
|
167
|
+
import("../lib/tmux-server-owner.ts").NativeTmuxServerOwner
|
|
168
|
+
>;
|
|
169
|
+
/** Fresh, credential-free proof of this owner's pinned tmux server. */
|
|
170
|
+
tmuxServerProof?: () => Promise<import("../lib/tmux-server-proof.ts").TmuxServerProof | null>;
|
|
165
171
|
authService?: AuthService;
|
|
166
172
|
authConfig?: AuthConfig;
|
|
167
173
|
remoteAccess?: {
|
|
@@ -533,6 +539,12 @@ export function createApp(options: CreateAppOptions = {}): Hono {
|
|
|
533
539
|
// Allow cross-origin (Next.js dashboard, Tailscale, etc.)
|
|
534
540
|
app.use("/*", cors());
|
|
535
541
|
|
|
542
|
+
if (options.tmuxServerOwners)
|
|
543
|
+
mountTmuxServerRoutes(app, {
|
|
544
|
+
ownerToken: options.remoteAccess?.ownerToken ?? null,
|
|
545
|
+
owners: options.tmuxServerOwners,
|
|
546
|
+
});
|
|
547
|
+
|
|
536
548
|
// Owner-only reads use the same early routing boundary as issuance below:
|
|
537
549
|
// remote/project credentials neither authorize nor block the owner bearer.
|
|
538
550
|
mountDiagnosticsRoute(app, {
|
|
@@ -743,10 +755,26 @@ export function createApp(options: CreateAppOptions = {}): Hono {
|
|
|
743
755
|
// Credential-free endpoint binding. A desktop host reads the nonce from the
|
|
744
756
|
// owner-only canonical record, probes this endpoint, and compares before it
|
|
745
757
|
// sends any remote-access or local-bypass credential.
|
|
746
|
-
|
|
758
|
+
// Coalesce concurrent probes, but never reuse a completed server proof.
|
|
759
|
+
let pendingTmuxProof: Promise<
|
|
760
|
+
import("../lib/tmux-server-proof.ts").TmuxServerProof | null
|
|
761
|
+
> | null = null;
|
|
762
|
+
app.get("/identity", async (c) => {
|
|
763
|
+
const wantsTmuxProof = c.req.query("tmuxServerProof") === "1";
|
|
764
|
+
let tmuxServerProof = null;
|
|
765
|
+
if (wantsTmuxProof && options.tmuxServerProof) {
|
|
766
|
+
pendingTmuxProof ??= Promise.resolve()
|
|
767
|
+
.then(options.tmuxServerProof)
|
|
768
|
+
.catch(() => null)
|
|
769
|
+
.finally(() => {
|
|
770
|
+
pendingTmuxProof = null;
|
|
771
|
+
});
|
|
772
|
+
tmuxServerProof = await pendingTmuxProof;
|
|
773
|
+
}
|
|
747
774
|
return c.json({
|
|
748
775
|
ok: true,
|
|
749
776
|
pid: process.pid,
|
|
777
|
+
...(wantsTmuxProof && options.tmuxServerProof ? { tmuxServerProof } : {}),
|
|
750
778
|
protocolVersion: DAEMON_WIRE_PROTOCOL_VERSION,
|
|
751
779
|
productVersion: daemonIdentity.productVersion,
|
|
752
780
|
instanceId: daemonIdentity.instanceId,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { TmuxServerScopeSchemaZ } from "@tmux-ide/contracts";
|
|
3
|
+
import { TmuxServerScopeError, type TmuxServerOwners } from "../lib/tmux-server-owners.ts";
|
|
4
|
+
import type { NativeTmuxServerOwner } from "../lib/tmux-server-owner.ts";
|
|
5
|
+
import { mountTerminalNativeBackingRoute } from "./resources/terminal-native-backing-route.ts";
|
|
6
|
+
|
|
7
|
+
/** Retain admission/coalescing limits for the lifetime of each independent owner. */
|
|
8
|
+
export function mountTmuxServerNativeBackingRoute(
|
|
9
|
+
app: Hono,
|
|
10
|
+
options: {
|
|
11
|
+
scopedPath: string;
|
|
12
|
+
ownerToken: string | null;
|
|
13
|
+
owners: TmuxServerOwners<NativeTmuxServerOwner>;
|
|
14
|
+
},
|
|
15
|
+
): void {
|
|
16
|
+
const routers = new WeakMap<NativeTmuxServerOwner, Hono>();
|
|
17
|
+
app.get(`${options.scopedPath}/native-backing/:workspaceName/:pane`, async (c) => {
|
|
18
|
+
const scope = TmuxServerScopeSchemaZ.safeParse({
|
|
19
|
+
serverId: c.req.param("serverId"),
|
|
20
|
+
generation: c.req.param("generation"),
|
|
21
|
+
});
|
|
22
|
+
if (!scope.success) return c.json({ status: "invalid" }, 400);
|
|
23
|
+
try {
|
|
24
|
+
return await options.owners.withOwner(scope.data, async (owner) => {
|
|
25
|
+
let router = routers.get(owner);
|
|
26
|
+
if (!router) {
|
|
27
|
+
router = new Hono();
|
|
28
|
+
mountTerminalNativeBackingRoute(router, {
|
|
29
|
+
ownerToken: options.ownerToken,
|
|
30
|
+
generation: owner.generation,
|
|
31
|
+
resolveSession: (workspace) =>
|
|
32
|
+
owner.workspaceRegistry.get(workspace)?.sessionName ?? null,
|
|
33
|
+
capture: (session, pane, expected) =>
|
|
34
|
+
owner.sessionRuntimeRegistry.captureNativeBacking(session, pane, expected),
|
|
35
|
+
});
|
|
36
|
+
routers.set(owner, router);
|
|
37
|
+
}
|
|
38
|
+
const url = new URL(c.req.url);
|
|
39
|
+
url.pathname = `/api/project/${encodeURIComponent(c.req.param("workspaceName"))}/terminal-native-backing/${encodeURIComponent(c.req.param("pane"))}`;
|
|
40
|
+
return await router.request(new Request(url, c.req.raw));
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return c.json(
|
|
44
|
+
{ status: error instanceof TmuxServerScopeError ? "retired" : "unavailable" },
|
|
45
|
+
error instanceof TmuxServerScopeError ? 409 : 503,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { mountTmuxServerNativeBackingRoute } from "./tmux-server-native-backing.ts";
|
|
2
|
+
import { Hono, type Context } from "hono";
|
|
3
|
+
import { streamSSE } from "hono/streaming";
|
|
4
|
+
import { projectApplicationShellResource } from "./resources/application-shell.ts";
|
|
5
|
+
import { fleetSessionIdForName } from "./resources/fleet-catalog.ts";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
TMUX_SERVERS_API_PATH,
|
|
9
|
+
PANE_STREAM_ISSUE_PATH,
|
|
10
|
+
TmuxServerIdSchemaZ,
|
|
11
|
+
TmuxServerScopeSchemaZ,
|
|
12
|
+
TmuxServersResourceSchemaZ,
|
|
13
|
+
TmuxServerSessionsResourceSchemaZ,
|
|
14
|
+
WorkspaceMultiplexerMutationRequestSchemaZ,
|
|
15
|
+
WorkspacePaneCreateMutationRequestSchemaZ,
|
|
16
|
+
WorkspaceSessionCreateArgumentsSchemaZ,
|
|
17
|
+
} from "@tmux-ide/contracts";
|
|
18
|
+
import {
|
|
19
|
+
TmuxServerOwners,
|
|
20
|
+
TmuxServerRegistrationSchemaZ,
|
|
21
|
+
TmuxServerScopeError,
|
|
22
|
+
} from "../lib/tmux-server-owners.ts";
|
|
23
|
+
import type { NativeTmuxServerOwner } from "../lib/tmux-server-owner.ts";
|
|
24
|
+
import { WorkspaceMultiplexerError } from "../lib/workspace-multiplexer-verbs.ts";
|
|
25
|
+
import { ownerBearerMatches } from "./owner-authority.ts";
|
|
26
|
+
import { mountPaneStreamIssueRoute } from "./pane-stream-issue.ts";
|
|
27
|
+
import { projectTerminalRuntimeInventory } from "./resources/terminal-runtime-inventory.ts";
|
|
28
|
+
|
|
29
|
+
export interface TmuxServerRoutesOptions {
|
|
30
|
+
readonly ownerToken: string | null;
|
|
31
|
+
readonly owners: TmuxServerOwners<NativeTmuxServerOwner>;
|
|
32
|
+
}
|
|
33
|
+
const registrationSchema = TmuxServerRegistrationSchemaZ.omit({ serverId: true }).strict();
|
|
34
|
+
const workspaceSchema = z
|
|
35
|
+
.string()
|
|
36
|
+
.min(1)
|
|
37
|
+
.max(160)
|
|
38
|
+
.refine((value) => !/[\0\r\n]/u.test(value));
|
|
39
|
+
async function boundedJson(request: Request): Promise<unknown> {
|
|
40
|
+
if (!request.body) throw new TypeError("Missing body");
|
|
41
|
+
const reader = request.body.getReader();
|
|
42
|
+
const chunks: Uint8Array[] = [];
|
|
43
|
+
let size = 0;
|
|
44
|
+
try {
|
|
45
|
+
for (;;) {
|
|
46
|
+
const next = await reader.read();
|
|
47
|
+
if (next.done) break;
|
|
48
|
+
size += next.value.byteLength;
|
|
49
|
+
if (size > 64 * 1024) {
|
|
50
|
+
void reader.cancel().catch(() => undefined);
|
|
51
|
+
throw new TypeError("Body too large");
|
|
52
|
+
}
|
|
53
|
+
chunks.push(next.value);
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
reader.releaseLock();
|
|
57
|
+
}
|
|
58
|
+
const bytes = new Uint8Array(size);
|
|
59
|
+
let offset = 0;
|
|
60
|
+
for (const chunk of chunks) {
|
|
61
|
+
bytes.set(chunk, offset);
|
|
62
|
+
offset += chunk.byteLength;
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
65
|
+
}
|
|
66
|
+
function failure(c: Context, error: unknown): Response {
|
|
67
|
+
if (error instanceof TmuxServerScopeError)
|
|
68
|
+
return c.json(
|
|
69
|
+
{ error: { code: error.code } },
|
|
70
|
+
error.code === "not-found" ? 404 : error.code === "capacity" ? 429 : 409,
|
|
71
|
+
);
|
|
72
|
+
if (error instanceof WorkspaceMultiplexerError)
|
|
73
|
+
return c.json({ error: { code: error.code } }, 409);
|
|
74
|
+
if (error instanceof z.ZodError || error instanceof SyntaxError || error instanceof TypeError)
|
|
75
|
+
return c.json({ error: { code: "invalid-request" } }, 400);
|
|
76
|
+
return c.json({ error: { code: "server-unavailable" } }, 503);
|
|
77
|
+
}
|
|
78
|
+
/** Owner capability is mandatory even when the ordinary local HTTP surface is open. */
|
|
79
|
+
export function mountTmuxServerRoutes(app: Hono, options: TmuxServerRoutesOptions): void {
|
|
80
|
+
const base = TMUX_SERVERS_API_PATH;
|
|
81
|
+
const authorize = async (c: Context, next: () => Promise<void>) => {
|
|
82
|
+
c.header("Cache-Control", "no-store");
|
|
83
|
+
if (!ownerBearerMatches(c.req.header("Authorization"), options.ownerToken))
|
|
84
|
+
return c.json({ error: { code: "owner-required" } }, 401);
|
|
85
|
+
await next();
|
|
86
|
+
};
|
|
87
|
+
app.use(base, authorize);
|
|
88
|
+
app.use(`${base}/*`, authorize);
|
|
89
|
+
const route = (work: (c: Context) => Promise<Response>) => async (c: Context) => {
|
|
90
|
+
try {
|
|
91
|
+
return await work(c);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return failure(c, error);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const scope = (c: Context) =>
|
|
97
|
+
TmuxServerScopeSchemaZ.parse({
|
|
98
|
+
serverId: c.req.param("serverId"),
|
|
99
|
+
generation: c.req.param("generation"),
|
|
100
|
+
});
|
|
101
|
+
app.get(
|
|
102
|
+
base,
|
|
103
|
+
route(async (c) =>
|
|
104
|
+
c.json(
|
|
105
|
+
TmuxServersResourceSchemaZ.parse({ version: 1, servers: await options.owners.refresh() }),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
app.post(
|
|
110
|
+
base,
|
|
111
|
+
route(async (c) =>
|
|
112
|
+
c.json(
|
|
113
|
+
await options.owners.register(registrationSchema.parse(await boundedJson(c.req.raw))),
|
|
114
|
+
201,
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
app.delete(
|
|
119
|
+
`${base}/:serverId`,
|
|
120
|
+
route(async (c) => {
|
|
121
|
+
await options.owners.remove(TmuxServerIdSchemaZ.parse(c.req.param("serverId")));
|
|
122
|
+
return c.json({ removed: true });
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
const scoped = `${base}/:serverId/:generation`;
|
|
126
|
+
mountTmuxServerNativeBackingRoute(app, {
|
|
127
|
+
scopedPath: scoped,
|
|
128
|
+
ownerToken: options.ownerToken,
|
|
129
|
+
owners: options.owners,
|
|
130
|
+
});
|
|
131
|
+
app.post(
|
|
132
|
+
`${scoped}/sessions/create`,
|
|
133
|
+
route(async (c) => {
|
|
134
|
+
const server = scope(c);
|
|
135
|
+
const request = z
|
|
136
|
+
.object({
|
|
137
|
+
operationId: z.uuid(),
|
|
138
|
+
expectedDaemonInstanceId: z.uuid(),
|
|
139
|
+
intent: WorkspaceSessionCreateArgumentsSchemaZ,
|
|
140
|
+
})
|
|
141
|
+
.strict()
|
|
142
|
+
.parse(await boundedJson(c.req.raw));
|
|
143
|
+
if (
|
|
144
|
+
request.expectedDaemonInstanceId !== server.generation ||
|
|
145
|
+
(request.intent.expectedDaemonInstanceId &&
|
|
146
|
+
request.intent.expectedDaemonInstanceId !== server.generation)
|
|
147
|
+
)
|
|
148
|
+
return c.json({ error: { code: "stale-generation" } }, 409);
|
|
149
|
+
return c.json(
|
|
150
|
+
await options.owners.withOwner(server, (owner) =>
|
|
151
|
+
owner.createSession(request.operationId, {
|
|
152
|
+
...request.intent,
|
|
153
|
+
expectedDaemonInstanceId: server.generation,
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
app.post(
|
|
160
|
+
`${scoped}/sessions/:liveSessionId/open`,
|
|
161
|
+
route(async (c) => {
|
|
162
|
+
const server = scope(c);
|
|
163
|
+
const result = await options.owners.withOwner(server, (owner) =>
|
|
164
|
+
owner.openSession(workspaceSchema.parse(c.req.param("liveSessionId"))),
|
|
165
|
+
);
|
|
166
|
+
return c.json({ version: 1, server, ...result });
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
app.get(
|
|
170
|
+
`${scoped}/sessions`,
|
|
171
|
+
route(async (c) => {
|
|
172
|
+
const server = scope(c);
|
|
173
|
+
return c.json(
|
|
174
|
+
TmuxServerSessionsResourceSchemaZ.parse({
|
|
175
|
+
version: 1,
|
|
176
|
+
server,
|
|
177
|
+
sessions: await options.owners.withOwner(server, async (owner) => {
|
|
178
|
+
const sessions = await owner.catalog();
|
|
179
|
+
const workspaces = owner.workspaceRegistry.list();
|
|
180
|
+
return sessions.map((session) => {
|
|
181
|
+
const aliases = workspaces.filter(
|
|
182
|
+
(workspace) => workspace.sessionName === session.sessionName,
|
|
183
|
+
);
|
|
184
|
+
return { ...session, workspaceName: aliases.length === 1 ? aliases[0]!.name : null };
|
|
185
|
+
});
|
|
186
|
+
}),
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
app.get(
|
|
192
|
+
`${scoped}/inventory/:workspaceName`,
|
|
193
|
+
route(async (c) => {
|
|
194
|
+
const server = scope(c);
|
|
195
|
+
const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
|
|
196
|
+
const resource = await options.owners.withOwner(server, async (owner) => {
|
|
197
|
+
const sessions = await owner.catalog();
|
|
198
|
+
const workspace = owner.workspaceRegistry.get(workspaceName);
|
|
199
|
+
if (!workspace) return null;
|
|
200
|
+
const expected = c.req.query("liveSessionId");
|
|
201
|
+
if (
|
|
202
|
+
expected &&
|
|
203
|
+
!sessions.some(
|
|
204
|
+
(row) => row.sessionName === workspace.sessionName && row.liveSessionId === expected,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
208
|
+
const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(
|
|
209
|
+
workspace.sessionName,
|
|
210
|
+
c.req.raw.signal,
|
|
211
|
+
);
|
|
212
|
+
return snapshot ? projectTerminalRuntimeInventory(snapshot, 0) : null;
|
|
213
|
+
});
|
|
214
|
+
return resource
|
|
215
|
+
? c.json({ version: 1, server, resource })
|
|
216
|
+
: c.json({ error: { code: "workspace-not-found" } }, 404);
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
app.get(
|
|
220
|
+
`${scoped}/catalog`,
|
|
221
|
+
route(async (c) => {
|
|
222
|
+
const server = scope(c);
|
|
223
|
+
const resource = await options.owners.withOwner(server, async (owner) => {
|
|
224
|
+
const sessions = await owner.catalog();
|
|
225
|
+
return {
|
|
226
|
+
intents: owner.workspaceRegistry.list().map((entry) => ({
|
|
227
|
+
workspaceName: entry.name,
|
|
228
|
+
sessionName: entry.sessionName,
|
|
229
|
+
source: "workspace",
|
|
230
|
+
availability: sessions.some((row) => row.sessionName === entry.sessionName)
|
|
231
|
+
? "live"
|
|
232
|
+
: "stopped",
|
|
233
|
+
})),
|
|
234
|
+
liveSessions: sessions.map((row) => ({
|
|
235
|
+
sessionName: row.sessionName,
|
|
236
|
+
fleetSessionId: fleetSessionIdForName(row.sessionName),
|
|
237
|
+
paneCount: row.paneCount,
|
|
238
|
+
})),
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
return c.json({ version: 1, server, resource });
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
app.get(
|
|
245
|
+
`${scoped}/application-shell/:workspaceName`,
|
|
246
|
+
route(async (c) => {
|
|
247
|
+
const server = scope(c);
|
|
248
|
+
const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
|
|
249
|
+
const resource = await options.owners.withOwner(server, async (owner) => {
|
|
250
|
+
const sessions = await owner.catalog();
|
|
251
|
+
const workspace = owner.workspaceRegistry.get(workspaceName);
|
|
252
|
+
if (!workspace) return null;
|
|
253
|
+
if (
|
|
254
|
+
!sessions.some(
|
|
255
|
+
(row) =>
|
|
256
|
+
row.sessionName === workspace.sessionName &&
|
|
257
|
+
row.liveSessionId === c.req.query("liveSessionId"),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
261
|
+
const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(
|
|
262
|
+
workspace.sessionName,
|
|
263
|
+
c.req.raw.signal,
|
|
264
|
+
);
|
|
265
|
+
return snapshot ? projectApplicationShellResource(snapshot) : null;
|
|
266
|
+
});
|
|
267
|
+
return resource
|
|
268
|
+
? c.json({ version: 1, server, resource })
|
|
269
|
+
: c.json({ error: { code: "workspace-not-found" } }, 404);
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
// Subscribe before publishing ready: a topology change during the subsequent HTTP read
|
|
273
|
+
// invalidates that read. This shares the owner's retained mirror, never a new tmux client.
|
|
274
|
+
app.get(
|
|
275
|
+
`${scoped}/session-events/:workspaceName`,
|
|
276
|
+
route(async (c) => {
|
|
277
|
+
const server = scope(c);
|
|
278
|
+
const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
|
|
279
|
+
const owner = await options.owners.withOwner(server, async (candidate) => {
|
|
280
|
+
const sessions = await candidate.catalog();
|
|
281
|
+
const workspace = candidate.workspaceRegistry.get(workspaceName);
|
|
282
|
+
if (
|
|
283
|
+
!workspace ||
|
|
284
|
+
!sessions.some(
|
|
285
|
+
(row) =>
|
|
286
|
+
row.sessionName === workspace.sessionName &&
|
|
287
|
+
row.liveSessionId === c.req.query("liveSessionId"),
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
291
|
+
return candidate;
|
|
292
|
+
});
|
|
293
|
+
const workspace = owner.workspaceRegistry.get(workspaceName)!;
|
|
294
|
+
return streamSSE(c, async (stream) => {
|
|
295
|
+
let stopped = false;
|
|
296
|
+
let wake: (() => void) | null = null;
|
|
297
|
+
let revision = 0;
|
|
298
|
+
const dirty = () => {
|
|
299
|
+
revision++;
|
|
300
|
+
wake?.();
|
|
301
|
+
};
|
|
302
|
+
stream.onAbort(() => {
|
|
303
|
+
stopped = true;
|
|
304
|
+
wake?.();
|
|
305
|
+
});
|
|
306
|
+
const identity = await owner.sessionRuntimeRegistry.describeSessionAuthority(
|
|
307
|
+
workspace.sessionName,
|
|
308
|
+
);
|
|
309
|
+
let topology: string | null = null;
|
|
310
|
+
let sessionReplaced = false;
|
|
311
|
+
const subscription = await owner.sessionRuntimeRegistry.subscribeLayout(
|
|
312
|
+
workspace.sessionName,
|
|
313
|
+
() => undefined,
|
|
314
|
+
{
|
|
315
|
+
expectedRuntimeSessionId: identity.runtimeSessionId,
|
|
316
|
+
expectedSemanticPaneIds: identity.description.panes.map((pane) => pane.semanticPaneId),
|
|
317
|
+
onAuthority(snapshot) {
|
|
318
|
+
if (snapshot.windowLinks.liveSessionId !== c.req.query("liveSessionId")) {
|
|
319
|
+
sessionReplaced = true;
|
|
320
|
+
dirty();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Full replacement observes removed windows and retains no historical keys.
|
|
324
|
+
// Geometry, focus and active-link changes stay entirely on the pane stream.
|
|
325
|
+
const signature = JSON.stringify(
|
|
326
|
+
snapshot.layouts
|
|
327
|
+
.map((layout) => [
|
|
328
|
+
layout.semanticWindowId,
|
|
329
|
+
layout.windowName,
|
|
330
|
+
layout.panes.map((pane) => pane.semanticPaneId).sort(),
|
|
331
|
+
])
|
|
332
|
+
.sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
|
|
333
|
+
);
|
|
334
|
+
if (topology === signature) return;
|
|
335
|
+
topology = signature;
|
|
336
|
+
dirty();
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
try {
|
|
341
|
+
options.owners.current(server);
|
|
342
|
+
if (sessionReplaced) throw new TmuxServerScopeError("stale-generation");
|
|
343
|
+
await stream.writeSSE({
|
|
344
|
+
data: JSON.stringify({ version: 1, server, type: "ready", revision }),
|
|
345
|
+
});
|
|
346
|
+
let sent = revision;
|
|
347
|
+
let nextIdentityCheck = Date.now() + 5000;
|
|
348
|
+
while (!stopped) {
|
|
349
|
+
await new Promise<void>((resolve) => {
|
|
350
|
+
const timer = setTimeout(resolve, 1000);
|
|
351
|
+
wake = () => {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
resolve();
|
|
354
|
+
};
|
|
355
|
+
if (stopped || revision !== sent) wake();
|
|
356
|
+
});
|
|
357
|
+
wake = null;
|
|
358
|
+
if (stopped) break;
|
|
359
|
+
if (sessionReplaced) throw new TmuxServerScopeError("stale-generation");
|
|
360
|
+
options.owners.current(server);
|
|
361
|
+
if (Date.now() >= nextIdentityCheck) {
|
|
362
|
+
const sessions = await owner.catalog();
|
|
363
|
+
options.owners.current(server);
|
|
364
|
+
if (
|
|
365
|
+
!sessions.some(
|
|
366
|
+
(row) =>
|
|
367
|
+
row.sessionName === workspace.sessionName &&
|
|
368
|
+
row.liveSessionId === c.req.query("liveSessionId"),
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
372
|
+
nextIdentityCheck = Date.now() + 5000;
|
|
373
|
+
}
|
|
374
|
+
if (revision !== sent) {
|
|
375
|
+
sent = revision;
|
|
376
|
+
await stream.writeSSE({
|
|
377
|
+
data: JSON.stringify({ version: 1, server, type: "invalidate", revision }),
|
|
378
|
+
});
|
|
379
|
+
} else
|
|
380
|
+
await stream.writeSSE({
|
|
381
|
+
data: JSON.stringify({ version: 1, server, type: "heartbeat", revision }),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
if (!stopped)
|
|
386
|
+
await stream
|
|
387
|
+
.writeSSE({ data: JSON.stringify({ version: 1, server, type: "retired", revision }) })
|
|
388
|
+
.catch(() => undefined);
|
|
389
|
+
} finally {
|
|
390
|
+
await subscription.close();
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
app.post(
|
|
396
|
+
`${scoped}/panes/create`,
|
|
397
|
+
route(async (c) => {
|
|
398
|
+
const server = scope(c);
|
|
399
|
+
const liveSessionId = workspaceSchema.parse(c.req.query("liveSessionId"));
|
|
400
|
+
const request = WorkspacePaneCreateMutationRequestSchemaZ.parse(await boundedJson(c.req.raw));
|
|
401
|
+
if (request.expectedDaemonInstanceId !== server.generation)
|
|
402
|
+
return c.json({ error: { code: "stale-generation" } }, 409);
|
|
403
|
+
return c.json(
|
|
404
|
+
await options.owners.withOwner(server, (owner) =>
|
|
405
|
+
owner.createSessionPane(liveSessionId, request),
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
app.post(
|
|
411
|
+
`${scoped}/mutations`,
|
|
412
|
+
route(async (c) => {
|
|
413
|
+
const server = scope(c);
|
|
414
|
+
const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(
|
|
415
|
+
await boundedJson(c.req.raw),
|
|
416
|
+
);
|
|
417
|
+
if (request.expectedDaemonInstanceId !== server.generation)
|
|
418
|
+
return c.json({ error: { code: "stale-generation" } }, 409);
|
|
419
|
+
const liveSessionId = c.req.query("liveSessionId");
|
|
420
|
+
const result = await options.owners.withOwner(server, (owner) =>
|
|
421
|
+
liveSessionId
|
|
422
|
+
? owner.mutateSession(liveSessionId, request, c.req.header("X-Tmux-Ide-Host-Client-Id"))
|
|
423
|
+
: owner.multiplexerBackend.mutate(
|
|
424
|
+
request,
|
|
425
|
+
c.req.header("X-Tmux-Ide-Host-Client-Id"),
|
|
426
|
+
undefined,
|
|
427
|
+
true,
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
return c.json(result);
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
app.post(
|
|
434
|
+
`${scoped}/pane-streams/issue`,
|
|
435
|
+
route(async (c) => {
|
|
436
|
+
const server = scope(c);
|
|
437
|
+
return options.owners.withOwner(server, async (owner) => {
|
|
438
|
+
const sessions = await owner.catalog();
|
|
439
|
+
const liveSessionId = c.req.query("liveSessionId");
|
|
440
|
+
if (liveSessionId) {
|
|
441
|
+
const body = (await boundedJson(c.req.raw.clone())) as {
|
|
442
|
+
stream?: { workspaceName?: string };
|
|
443
|
+
};
|
|
444
|
+
const workspace = owner.workspaceRegistry.get(body.stream?.workspaceName ?? "");
|
|
445
|
+
if (
|
|
446
|
+
!workspace ||
|
|
447
|
+
!sessions.some(
|
|
448
|
+
(row) =>
|
|
449
|
+
row.sessionName === workspace.sessionName && row.liveSessionId === liveSessionId,
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
453
|
+
}
|
|
454
|
+
const issueApp = new Hono();
|
|
455
|
+
mountPaneStreamIssueRoute(issueApp, {
|
|
456
|
+
daemonInstanceId: server.generation,
|
|
457
|
+
ownerToken: options.ownerToken,
|
|
458
|
+
workspaceRegistry: owner.workspaceRegistry,
|
|
459
|
+
backend: owner.paneStreamRuntime.coordinator,
|
|
460
|
+
});
|
|
461
|
+
const url = new URL(c.req.url);
|
|
462
|
+
url.pathname = PANE_STREAM_ISSUE_PATH;
|
|
463
|
+
url.search = "";
|
|
464
|
+
return await issueApp.request(new Request(url, c.req.raw));
|
|
465
|
+
});
|
|
466
|
+
}),
|
|
467
|
+
);
|
|
468
|
+
}
|