tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26
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 +46791 -43589
- package/package.json +2 -2
- 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/fleet-lifecycle.ts +5 -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 +351 -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 +31 -11
- 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-guided-tour-integration.jsx +119 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
- 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-home-experience.js +40 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
- 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-shell-home.jsx +6 -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/application-theme-repaint.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
- 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 +479 -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 +33 -9
- 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-guided-tour-integration.tsx +199 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
- 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-home-experience.ts +59 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -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-performance-log.ts +18 -6
- package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -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/application-theme-repaint.ts +19 -0
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
|
|
1
2
|
import { applicationDaemonEndpoint, readApplicationDaemonInfo as readCanonicalDaemonInfo, } from "./application-daemon-authority.js";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { dispatchOwnerAction } from "@tmux-ide/daemon-client/owner-action-client";
|
|
@@ -43,13 +44,31 @@ async function dispatchOnHandle(handle, name, input) {
|
|
|
43
44
|
? result
|
|
44
45
|
: null;
|
|
45
46
|
}
|
|
46
|
-
export function createFleetSession(input, name) {
|
|
47
|
+
export function createFleetSession(input, name, server, options) {
|
|
47
48
|
if ("read" in input) {
|
|
48
49
|
const daemon = input.read();
|
|
49
50
|
if (!daemon || !name)
|
|
50
51
|
return Promise.resolve(null);
|
|
52
|
+
if (server) {
|
|
53
|
+
if (!daemon.authToken || input.endpoint().state !== "ready")
|
|
54
|
+
return Promise.resolve(null);
|
|
55
|
+
const epoch = input.endpoint().epoch;
|
|
56
|
+
const baseUrl = canonicalDaemonUrl("http", daemon.bindHostname, daemon.port);
|
|
57
|
+
const client = createTmuxServerClient({ baseUrl, ownerToken: daemon.authToken, hostClientId: HOST_CLIENT_ID, origin: baseUrl }, server);
|
|
58
|
+
return client
|
|
59
|
+
.createSession(randomUUID(), {
|
|
60
|
+
displayName: name,
|
|
61
|
+
...(options?.includeLiveSessionId ? { includeLiveSessionId: true } : {}),
|
|
62
|
+
expectedDaemonInstanceId: server.generation,
|
|
63
|
+
})
|
|
64
|
+
.then((result) => input.endpoint().epoch === epoch && input.read()?.instanceId === daemon.instanceId
|
|
65
|
+
? result
|
|
66
|
+
: null)
|
|
67
|
+
.finally(() => client.dispose());
|
|
68
|
+
}
|
|
51
69
|
return dispatchOnHandle(input, "workspace.session.create", {
|
|
52
70
|
displayName: name,
|
|
71
|
+
...(options?.includeLiveSessionId ? { includeLiveSessionId: true } : {}),
|
|
53
72
|
expectedDaemonInstanceId: daemon.instanceId,
|
|
54
73
|
});
|
|
55
74
|
}
|
|
@@ -59,6 +78,30 @@ export function createFleetSession(input, name) {
|
|
|
59
78
|
export function closeFleetSession(handle, target) {
|
|
60
79
|
if (handle.read()?.instanceId !== target.daemonInstanceId)
|
|
61
80
|
return Promise.resolve(null);
|
|
81
|
+
if (target.server) {
|
|
82
|
+
const daemon = handle.read();
|
|
83
|
+
if (!daemon?.authToken || handle.endpoint().state !== "ready")
|
|
84
|
+
return Promise.resolve(null);
|
|
85
|
+
const epoch = handle.endpoint().epoch;
|
|
86
|
+
const baseUrl = canonicalDaemonUrl("http", daemon.bindHostname, daemon.port);
|
|
87
|
+
const client = createTmuxServerClient({ baseUrl, ownerToken: daemon.authToken, hostClientId: HOST_CLIENT_ID, origin: baseUrl }, target.server);
|
|
88
|
+
return client
|
|
89
|
+
.mutate(randomUUID(), {
|
|
90
|
+
verb: "workspace.session.kill",
|
|
91
|
+
workspaceName: target.workspaceName ?? target.sessionName,
|
|
92
|
+
fleetTarget: {
|
|
93
|
+
daemonInstanceId: target.server.generation,
|
|
94
|
+
liveSessionId: target.liveSessionId,
|
|
95
|
+
sessionName: target.sessionName,
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
.then((result) => handle.endpoint().epoch === epoch &&
|
|
99
|
+
handle.read()?.instanceId === target.daemonInstanceId &&
|
|
100
|
+
result.verb === "workspace.session.kill"
|
|
101
|
+
? result
|
|
102
|
+
: null)
|
|
103
|
+
.finally(() => client.dispose());
|
|
104
|
+
}
|
|
62
105
|
return dispatchOnHandle(handle, "workspace.session.kill", {
|
|
63
106
|
workspaceName: target.workspaceName ?? target.sessionName,
|
|
64
107
|
fleetTarget: {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { Show } from "solid-js";
|
|
3
|
+
import { TuiButton } from "../ui/button.jsx";
|
|
4
|
+
import { clipTerminal } from "../terminal-text.js";
|
|
5
|
+
import { GUIDED_TOUR_STEPS, guidedTourCopy, } from "./guided-tour.js";
|
|
6
|
+
/** Non-modal coach: terminal interaction stays available throughout the tour. */
|
|
7
|
+
export function GuidedTourCoach(props) {
|
|
8
|
+
const copy = () => guidedTourCopy(props.state.step, props.shortcuts);
|
|
9
|
+
const width = () => Math.max(1, props.width);
|
|
10
|
+
const primary = () => {
|
|
11
|
+
switch (props.state.step) {
|
|
12
|
+
case "welcome":
|
|
13
|
+
return { label: "Let's try it", run: props.onWelcomeRead };
|
|
14
|
+
case "practice":
|
|
15
|
+
return { label: "Create practice session", run: props.onCreatePractice };
|
|
16
|
+
case "split":
|
|
17
|
+
case "commands":
|
|
18
|
+
return { label: "Open commands", run: props.onOpenCommands };
|
|
19
|
+
case "theme":
|
|
20
|
+
return { label: "Choose theme", run: props.onOpenAppearance };
|
|
21
|
+
case "leave":
|
|
22
|
+
return { label: "Open Home", run: props.onOpenHome };
|
|
23
|
+
case "reopen":
|
|
24
|
+
return { label: "Reopen practice", run: props.onOpenPractice };
|
|
25
|
+
default:
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
return (<box id="guided-tour-coach" width={width()} height={props.height ?? 10} overflow="hidden" flexShrink={0} flexDirection="column" paddingX={1} backgroundColor={props.theme.roles.surfaces.panel}>
|
|
30
|
+
<text height={1} fg={props.theme.roles.text.primary}>
|
|
31
|
+
<strong>
|
|
32
|
+
{clipTerminal(`Learn tmux-ide · ${GUIDED_TOUR_STEPS.indexOf(props.state.step) + 1}/${GUIDED_TOUR_STEPS.length} · ${copy().title}`, Math.max(0, width() - 2))}
|
|
33
|
+
</strong>
|
|
34
|
+
</text>
|
|
35
|
+
<scrollbox flexGrow={1} width="100%">
|
|
36
|
+
<text fg={props.theme.roles.text.secondary}>{copy().body}</text>
|
|
37
|
+
</scrollbox>
|
|
38
|
+
<Show when={props.error}>
|
|
39
|
+
<text fg={props.theme.roles.text.primary}>{props.error}</text>
|
|
40
|
+
</Show>
|
|
41
|
+
<box flexDirection="row" flexWrap="wrap" flexShrink={0} gap={1}>
|
|
42
|
+
<Show when={props.state.practice && ["split", "focus", "resize"].includes(props.state.step)}>
|
|
43
|
+
<TuiButton theme={props.theme} label="Open practice" onPress={props.onOpenPractice} width={Math.min(Math.max(1, width() - 2), 17)}/>
|
|
44
|
+
</Show>
|
|
45
|
+
<Show when={props.onReplay && props.error}>
|
|
46
|
+
<TuiButton theme={props.theme} label="Restart tour" onPress={props.onReplay} width={Math.min(Math.max(1, width() - 2), 16)}/>
|
|
47
|
+
</Show>
|
|
48
|
+
<Show when={primary()}>
|
|
49
|
+
{(action) => (<TuiButton theme={props.theme} label={action().label} width={Math.min(Math.max(1, width() - 2), action().label.length + 4)} loading={props.busy} onPress={action().run}/>)}
|
|
50
|
+
</Show>
|
|
51
|
+
<TuiButton theme={props.theme} label={props.state.step === "complete" ? "Done" : "Pause tour"} width={Math.min(Math.max(1, width() - 2), 14)} variant="ghost" onPress={props.onPause}/>
|
|
52
|
+
</box>
|
|
53
|
+
</box>);
|
|
54
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { runtimeOwnedPath } from "../../../lib/runtime-namespace.js";
|
|
5
|
+
import { stateHome } from "../../../lib/state-home.js";
|
|
6
|
+
import { GUIDED_TOUR_STEPS, initialGuidedTourState } from "./guided-tour.js";
|
|
7
|
+
export function guidedTourStatePath() {
|
|
8
|
+
return runtimeOwnedPath(join(stateHome(), "guided-tour.json"));
|
|
9
|
+
}
|
|
10
|
+
export function parseGuidedTourState(value) {
|
|
11
|
+
if (!value || typeof value !== "object")
|
|
12
|
+
return initialGuidedTourState();
|
|
13
|
+
const raw = value;
|
|
14
|
+
if (raw.version !== 1 ||
|
|
15
|
+
!GUIDED_TOUR_STEPS.includes(raw.step) ||
|
|
16
|
+
typeof raw.active !== "boolean")
|
|
17
|
+
return initialGuidedTourState();
|
|
18
|
+
let practice = null;
|
|
19
|
+
if (raw.practice && typeof raw.practice === "object") {
|
|
20
|
+
const p = raw.practice;
|
|
21
|
+
if ([p.machineId, p.serverId, p.generation, p.sessionId, p.sessionName].every((v) => typeof v === "string" && v.length > 0)) {
|
|
22
|
+
practice = {
|
|
23
|
+
machineId: p.machineId,
|
|
24
|
+
serverId: p.serverId,
|
|
25
|
+
generation: p.generation,
|
|
26
|
+
sessionId: p.sessionId,
|
|
27
|
+
sessionName: p.sessionName,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const step = raw.step;
|
|
32
|
+
return {
|
|
33
|
+
version: 1,
|
|
34
|
+
step: !practice && !["welcome", "practice", "complete"].includes(step) ? "practice" : step,
|
|
35
|
+
active: raw.active,
|
|
36
|
+
practice,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function readGuidedTourState(path = guidedTourStatePath()) {
|
|
40
|
+
try {
|
|
41
|
+
return parseGuidedTourState(JSON.parse(readFileSync(path, "utf8")));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return initialGuidedTourState();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Atomic, best-effort persistence; failure must not prevent using terminals. */
|
|
48
|
+
export function writeGuidedTourState(state, path = guidedTourStatePath()) {
|
|
49
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
52
|
+
writeFileSync(temporary, JSON.stringify(state), { mode: 0o600 });
|
|
53
|
+
renameSync(temporary, path);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
try {
|
|
61
|
+
rmSync(temporary, { force: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* Best effort. */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** A small, action-driven tour. No timers and no mutations of terminal sessions. */
|
|
2
|
+
export const GUIDED_TOUR_STEPS = [
|
|
3
|
+
"welcome",
|
|
4
|
+
"practice",
|
|
5
|
+
"split",
|
|
6
|
+
"focus",
|
|
7
|
+
"resize",
|
|
8
|
+
"commands",
|
|
9
|
+
"theme",
|
|
10
|
+
"leave",
|
|
11
|
+
"reopen",
|
|
12
|
+
"complete",
|
|
13
|
+
];
|
|
14
|
+
export function initialGuidedTourState() {
|
|
15
|
+
return { version: 1, step: "welcome", active: false, practice: null };
|
|
16
|
+
}
|
|
17
|
+
export function sameGuidedTourPractice(a, b) {
|
|
18
|
+
return (a !== null &&
|
|
19
|
+
a.machineId === b.machineId &&
|
|
20
|
+
a.serverId === b.serverId &&
|
|
21
|
+
a.generation === b.generation &&
|
|
22
|
+
a.sessionId === b.sessionId);
|
|
23
|
+
}
|
|
24
|
+
/** Call confirmation events only after an observed change, never on request dispatch. */
|
|
25
|
+
export function reduceGuidedTour(state, event) {
|
|
26
|
+
if (event.type === "replay")
|
|
27
|
+
return { ...initialGuidedTourState(), active: true };
|
|
28
|
+
if (event.type === "pause")
|
|
29
|
+
return state.active ? { ...state, active: false } : state;
|
|
30
|
+
if (event.type === "start" || event.type === "resume") {
|
|
31
|
+
return state.step === "complete" ? state : { ...state, active: true };
|
|
32
|
+
}
|
|
33
|
+
if (event.type === "practice-created" && state.step === "practice") {
|
|
34
|
+
return { ...state, step: "split", practice: event.practice };
|
|
35
|
+
}
|
|
36
|
+
if (!state.active)
|
|
37
|
+
return state;
|
|
38
|
+
if (event.type === "practice-missing" && state.practice && state.step !== "complete") {
|
|
39
|
+
return { ...state, step: "practice", practice: null };
|
|
40
|
+
}
|
|
41
|
+
if (event.type === "welcome-read" && state.step === "welcome")
|
|
42
|
+
return { ...state, step: "practice" };
|
|
43
|
+
if ("practice" in event && !sameGuidedTourPractice(state.practice, event.practice))
|
|
44
|
+
return state;
|
|
45
|
+
const expected = {
|
|
46
|
+
split: "split-confirmed",
|
|
47
|
+
focus: "focus-confirmed",
|
|
48
|
+
resize: "resize-confirmed",
|
|
49
|
+
commands: "commands-opened",
|
|
50
|
+
theme: "theme-changed",
|
|
51
|
+
leave: "home-opened",
|
|
52
|
+
reopen: "reopened",
|
|
53
|
+
};
|
|
54
|
+
if (expected[state.step] !== event.type)
|
|
55
|
+
return state;
|
|
56
|
+
const step = GUIDED_TOUR_STEPS[GUIDED_TOUR_STEPS.indexOf(state.step) + 1];
|
|
57
|
+
return { ...state, step };
|
|
58
|
+
}
|
|
59
|
+
export function guidedTourCopy(step, keys = {}) {
|
|
60
|
+
const shortcut = (key) => (key ? ` (${key})` : "");
|
|
61
|
+
switch (step) {
|
|
62
|
+
case "welcome":
|
|
63
|
+
return {
|
|
64
|
+
title: "Meet tmux-ide",
|
|
65
|
+
body: "A machine can run several tmux servers. Each server has sessions; sessions contain windows, and windows contain terminal panes. Home brings agents across connected machines together.",
|
|
66
|
+
};
|
|
67
|
+
case "practice":
|
|
68
|
+
return {
|
|
69
|
+
title: "Your practice space",
|
|
70
|
+
body: "Create a separate local practice session. The tour leaves your existing sessions alone. You can pause now and resume from Home.",
|
|
71
|
+
};
|
|
72
|
+
case "split":
|
|
73
|
+
return {
|
|
74
|
+
title: "Make room",
|
|
75
|
+
body: `Split the practice pane${shortcut(keys.split)}. You can also find Split right in Commands${shortcut(keys.commands)}. The next step unlocks when the new pane appears.`,
|
|
76
|
+
};
|
|
77
|
+
case "focus":
|
|
78
|
+
return {
|
|
79
|
+
title: "Move between panes",
|
|
80
|
+
body: `Select the other practice pane by clicking it or using your pane navigation shortcut${shortcut(keys.focus)}. Each pane is an independent terminal.`,
|
|
81
|
+
};
|
|
82
|
+
case "resize":
|
|
83
|
+
return {
|
|
84
|
+
title: "Find a comfortable layout",
|
|
85
|
+
body: "Drag the divider between the practice panes. Continue working while tmux updates the layout; the tour advances after the changed dimensions arrive.",
|
|
86
|
+
};
|
|
87
|
+
case "commands":
|
|
88
|
+
return {
|
|
89
|
+
title: "Find an action",
|
|
90
|
+
body: `Open Commands${shortcut(keys.commands)} to discover actions and their current shortcuts. Close the palette when you are ready to continue.`,
|
|
91
|
+
};
|
|
92
|
+
case "theme":
|
|
93
|
+
return {
|
|
94
|
+
title: "Make it yours",
|
|
95
|
+
body: "Choose a different theme from Home. Automatic contrast correction improves hard-to-read terminal colors; you can turn it off in settings.",
|
|
96
|
+
};
|
|
97
|
+
case "leave":
|
|
98
|
+
return {
|
|
99
|
+
title: "Leave without losing work",
|
|
100
|
+
body: `Open Home${shortcut(keys.home)}. Your practice terminals keep running in tmux while you look at agents or other sessions.`,
|
|
101
|
+
};
|
|
102
|
+
case "reopen":
|
|
103
|
+
return {
|
|
104
|
+
title: "Pick up where you left off",
|
|
105
|
+
body: "Reopen your practice session. tmux keeps your terminals alive independently of which page or session you are viewing.",
|
|
106
|
+
};
|
|
107
|
+
case "complete":
|
|
108
|
+
return {
|
|
109
|
+
title: "Ready to explore",
|
|
110
|
+
body: "You have split, focused and resized panes, found commands, changed theme and returned to a running session. Your practice session remains available; close it when you are finished. Replay this tour from Home.",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -162,7 +162,7 @@ function buildProductionBundle(connection, callbacks, readDaemon = readCanonical
|
|
|
162
162
|
requestTerminalRuntimeInventoryRefresh: () => {
|
|
163
163
|
connection.transport.refreshTerminalRuntimeInventory();
|
|
164
164
|
},
|
|
165
|
-
actions: productionOwnerActions(readDaemon),
|
|
165
|
+
actions: connection.ownerActions ?? productionOwnerActions(readDaemon),
|
|
166
166
|
},
|
|
167
167
|
});
|
|
168
168
|
fastLane = createOpenTuiWorkspaceTerminalFastLane(client, OPEN_TUI_HOST_CLIENT_ID, causalCellLedger);
|
|
@@ -27,6 +27,10 @@ export async function resizeTerminalPane(expected, current, input, onFailure) {
|
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
29
|
try {
|
|
30
|
+
// Geometry acquisition is itself a mutation of the ownership lease. A stale
|
|
31
|
+
// gesture must not take it from another client even when resize is refused.
|
|
32
|
+
if (!isCurrent())
|
|
33
|
+
return fail({ stage: "pre-dispatch", reason: "generation-replaced" });
|
|
30
34
|
if (expected.client.ownsRuntimeAuthority?.("geometry") !== true) {
|
|
31
35
|
let lease;
|
|
32
36
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Acquire controller authority before issuing the canonical pane-select intent. */
|
|
2
|
-
export async function selectTerminalPane(expected, current, semanticPaneId, operationId, onFailure) {
|
|
2
|
+
export async function selectTerminalPane(expected, current, semanticPaneId, operationId, onFailure, windowLink) {
|
|
3
3
|
const fail = (failure) => {
|
|
4
4
|
try {
|
|
5
5
|
onFailure?.(failure);
|
|
@@ -57,6 +57,7 @@ export async function selectTerminalPane(expected, current, semanticPaneId, oper
|
|
|
57
57
|
verb: "workspace.pane.select",
|
|
58
58
|
workspaceName: expected.workspaceName,
|
|
59
59
|
semanticPaneId,
|
|
60
|
+
...(windowLink ? { windowLink } : {}),
|
|
60
61
|
},
|
|
61
62
|
});
|
|
62
63
|
}
|
|
@@ -8,7 +8,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
8
8
|
let applied = null;
|
|
9
9
|
let pending = null;
|
|
10
10
|
let desired = null;
|
|
11
|
-
let scopedFlight =
|
|
11
|
+
let scopedFlight = null;
|
|
12
12
|
let scopedEpoch = 0;
|
|
13
13
|
let authorityClient = null;
|
|
14
14
|
let stopAuthority = null;
|
|
@@ -21,7 +21,8 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
21
21
|
const drain = async () => {
|
|
22
22
|
if (scopedFlight || disposed || authoritySuspended)
|
|
23
23
|
return;
|
|
24
|
-
|
|
24
|
+
const flight = {};
|
|
25
|
+
scopedFlight = flight;
|
|
25
26
|
try {
|
|
26
27
|
while (desired && !disposed && !authoritySuspended) {
|
|
27
28
|
const owner = desired;
|
|
@@ -41,7 +42,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
41
42
|
if (disposed || !desired)
|
|
42
43
|
break;
|
|
43
44
|
if (epoch !== scopedEpoch)
|
|
44
|
-
|
|
45
|
+
break;
|
|
45
46
|
const stillDesired = desired.targets.some((entry) => targetKey(entry) === targetKey(target) && targetSize(entry) === targetSize(target));
|
|
46
47
|
if (outcome.status !== "applied") {
|
|
47
48
|
if (!stillDesired)
|
|
@@ -55,7 +56,8 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
finally {
|
|
58
|
-
scopedFlight
|
|
59
|
+
if (scopedFlight === flight)
|
|
60
|
+
scopedFlight = null;
|
|
59
61
|
}
|
|
60
62
|
};
|
|
61
63
|
const sameAuthority = (left, right) => left.lane === right.lane &&
|
|
@@ -83,6 +85,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
83
85
|
pending = null;
|
|
84
86
|
desired = null;
|
|
85
87
|
scopedEpoch += 1;
|
|
88
|
+
scopedFlight = null;
|
|
86
89
|
scopedApplied.clear();
|
|
87
90
|
return;
|
|
88
91
|
}
|
|
@@ -110,6 +113,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
110
113
|
pending = null;
|
|
111
114
|
scopedApplied.clear();
|
|
112
115
|
scopedEpoch += 1;
|
|
116
|
+
scopedFlight = null;
|
|
113
117
|
if (!authoritySuspended)
|
|
114
118
|
retry?.();
|
|
115
119
|
}) ?? null;
|
|
@@ -130,6 +134,8 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
130
134
|
pending = null;
|
|
131
135
|
if (!desired || !sameAuthority(desired.identity, target)) {
|
|
132
136
|
scopedEpoch += 1;
|
|
137
|
+
// A retired lane must not hold a replacement lane behind its receipt.
|
|
138
|
+
scopedFlight = null;
|
|
133
139
|
scopedApplied.clear();
|
|
134
140
|
}
|
|
135
141
|
const retainedWindows = new Set(["", ...windows.map((window) => window.semanticWindowId)]);
|
|
@@ -156,6 +162,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
156
162
|
if (desired) {
|
|
157
163
|
desired = null;
|
|
158
164
|
scopedEpoch += 1;
|
|
165
|
+
scopedFlight = null;
|
|
159
166
|
scopedApplied.clear();
|
|
160
167
|
applied = null;
|
|
161
168
|
pending = null;
|
|
@@ -186,6 +193,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
|
|
|
186
193
|
stopAuthority = null;
|
|
187
194
|
authorityClient = null;
|
|
188
195
|
desired = null;
|
|
196
|
+
scopedFlight = null;
|
|
189
197
|
scopedApplied.clear();
|
|
190
198
|
applied = null;
|
|
191
199
|
pending = null;
|
|
@@ -30,15 +30,18 @@ export class TerminalPaneInputRouter {
|
|
|
30
30
|
this.#pending = null;
|
|
31
31
|
}
|
|
32
32
|
selectPane(paneId, options = {}) {
|
|
33
|
-
if (this.#pending?.paneId === paneId)
|
|
33
|
+
if (!options.selection && this.#pending?.paneId === paneId)
|
|
34
34
|
return;
|
|
35
|
-
if (
|
|
35
|
+
if (!options.selection &&
|
|
36
|
+
this.#pending === null &&
|
|
37
|
+
this.#canonicalPane === paneId &&
|
|
38
|
+
this.#focusedPane === paneId)
|
|
36
39
|
return;
|
|
37
40
|
const token = ++this.#selectionToken;
|
|
38
41
|
const presentOptimistically = options.presentOptimistically !== false;
|
|
39
42
|
if (presentOptimistically)
|
|
40
43
|
this.#setFocusedPane(paneId);
|
|
41
|
-
const settled = this.#options.select(paneId);
|
|
44
|
+
const settled = options.selection ? options.selection() : this.#options.select(paneId);
|
|
42
45
|
this.#pending = { token, paneId, presentOptimistically, settled, selected: null };
|
|
43
46
|
void settled.then((selected) => {
|
|
44
47
|
if (this.#pending?.token !== token)
|
|
@@ -5,8 +5,14 @@ import { PANE_ACTION_MENU_ITEMS } from "./pane-action-menu-model.js";
|
|
|
5
5
|
export function applicationCommandDescription(command) {
|
|
6
6
|
if (typeof command === "object" && command.kind === "open-machine")
|
|
7
7
|
return {
|
|
8
|
-
id: JSON.stringify([
|
|
9
|
-
|
|
8
|
+
id: JSON.stringify([
|
|
9
|
+
command.kind,
|
|
10
|
+
command.fleet.machineId,
|
|
11
|
+
...(command.fleet.server
|
|
12
|
+
? [command.fleet.server.serverId, command.fleet.server.generation]
|
|
13
|
+
: []),
|
|
14
|
+
]),
|
|
15
|
+
label: `${command.fleet.server ? "Server" : "Machine"} · ${command.label}`,
|
|
10
16
|
detail: command.fleet.disabled ? "Unavailable" : "Browse sessions or create on this host",
|
|
11
17
|
};
|
|
12
18
|
if (typeof command === "object") {
|
|
@@ -16,6 +22,9 @@ export function applicationCommandDescription(command) {
|
|
|
16
22
|
? [
|
|
17
23
|
command.kind,
|
|
18
24
|
command.fleet.machineId,
|
|
25
|
+
...(command.fleet.server
|
|
26
|
+
? [command.fleet.server.serverId, command.fleet.server.generation]
|
|
27
|
+
: []),
|
|
19
28
|
command.fleet.liveSessionId,
|
|
20
29
|
session ? null : command.paneId,
|
|
21
30
|
]
|
|
@@ -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,
|