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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { DesktopApplicationShellTarget } from "@tmux-ide/contracts";
|
|
2
|
+
import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
|
|
3
|
+
import {
|
|
4
|
+
createScopedTmuxServerTransport,
|
|
5
|
+
createScopedTmuxServerCatalogPort,
|
|
6
|
+
createScopedTmuxServerOwnerActions,
|
|
7
|
+
} from "@tmux-ide/daemon-client/scoped-tmux-server-transport";
|
|
8
|
+
import { canonicalDaemonUrl } from "../../lib/canonical-daemon.ts";
|
|
9
|
+
import type {
|
|
10
|
+
OpenTuiApplicationShellConnection,
|
|
11
|
+
OpenTuiApplicationShellConnectionDependencies,
|
|
12
|
+
} from "./application-shell-daemon-connection.ts";
|
|
13
|
+
import { createOpenTuiVerifiedRoutingContext } from "./open-tui-verified-routing.ts";
|
|
14
|
+
import { OPEN_TUI_HOST_CLIENT_ID } from "./open-tui-workspace-runtime-port.ts";
|
|
15
|
+
import { OpenTuiStartupError } from "./startup-failure.ts";
|
|
16
|
+
|
|
17
|
+
/** A selected registration never falls through to a name-only/default-server route. */
|
|
18
|
+
export async function prepareScopedOpenTuiConnection(
|
|
19
|
+
sessionName: string,
|
|
20
|
+
dependencies: OpenTuiApplicationShellConnectionDependencies,
|
|
21
|
+
prepareDefault: () => Promise<OpenTuiApplicationShellConnection | null>,
|
|
22
|
+
): Promise<OpenTuiApplicationShellConnection | null> {
|
|
23
|
+
const scope = dependencies.server!;
|
|
24
|
+
const daemon = dependencies.readCanonicalDaemonInfo();
|
|
25
|
+
if (!daemon || !daemon.authToken || !(await dependencies.isCanonicalDaemonAlive(daemon)))
|
|
26
|
+
throw new OpenTuiStartupError({ reason: "daemon-unavailable" });
|
|
27
|
+
const clientOptions = {
|
|
28
|
+
baseUrl: canonicalDaemonUrl("http", daemon.bindHostname, daemon.port),
|
|
29
|
+
ownerToken: daemon.authToken,
|
|
30
|
+
hostClientId: OPEN_TUI_HOST_CLIENT_ID,
|
|
31
|
+
origin: "tmux-ide://opentui",
|
|
32
|
+
};
|
|
33
|
+
const client = createTmuxServerClient(clientOptions, scope);
|
|
34
|
+
try {
|
|
35
|
+
const catalog = await client.sessions();
|
|
36
|
+
const candidates = catalog.sessions.filter(
|
|
37
|
+
(session) =>
|
|
38
|
+
session.sessionName === sessionName &&
|
|
39
|
+
(!dependencies.expectedLiveSessionId ||
|
|
40
|
+
session.liveSessionId === dependencies.expectedLiveSessionId),
|
|
41
|
+
);
|
|
42
|
+
if (candidates.length !== 1) throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
43
|
+
const liveSessionId = candidates[0]!.liveSessionId;
|
|
44
|
+
// The borrowed default owner has the daemon's original generation. Its existing
|
|
45
|
+
// transport preserves the full app surface while the scoped read proves routing.
|
|
46
|
+
if (scope.generation === daemon.instanceId) {
|
|
47
|
+
const connection = await prepareDefault();
|
|
48
|
+
if (!connection || connection.liveSessionId !== liveSessionId) {
|
|
49
|
+
connection?.dispose();
|
|
50
|
+
throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const current = await client.sessions();
|
|
54
|
+
if (
|
|
55
|
+
!current.sessions.some(
|
|
56
|
+
(session) =>
|
|
57
|
+
session.liveSessionId === liveSessionId && session.sessionName === sessionName,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
61
|
+
const options = {
|
|
62
|
+
clientOptions,
|
|
63
|
+
scope,
|
|
64
|
+
target: connection.target,
|
|
65
|
+
sessionName,
|
|
66
|
+
liveSessionId,
|
|
67
|
+
};
|
|
68
|
+
const routing = createOpenTuiVerifiedRoutingContext(
|
|
69
|
+
daemon,
|
|
70
|
+
connection.workspaceName,
|
|
71
|
+
sessionName,
|
|
72
|
+
undefined,
|
|
73
|
+
dependencies.readDaemonEndpoint,
|
|
74
|
+
scope,
|
|
75
|
+
liveSessionId,
|
|
76
|
+
);
|
|
77
|
+
connection.routing?.retire();
|
|
78
|
+
return {
|
|
79
|
+
...connection,
|
|
80
|
+
server: scope,
|
|
81
|
+
routing,
|
|
82
|
+
ownerActions: createScopedTmuxServerOwnerActions(options, connection.transport),
|
|
83
|
+
dispose() {
|
|
84
|
+
routing?.retire();
|
|
85
|
+
connection.dispose();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
connection.dispose();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const opened = await client.openSession(liveSessionId);
|
|
94
|
+
if (opened.liveSessionId !== liveSessionId)
|
|
95
|
+
throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
96
|
+
const workspaceName = opened.workspaceName;
|
|
97
|
+
const target: DesktopApplicationShellTarget = {
|
|
98
|
+
daemon: {
|
|
99
|
+
protocolVersion: daemon.protocolVersion,
|
|
100
|
+
productVersion: daemon.productVersion,
|
|
101
|
+
instanceId: scope.generation,
|
|
102
|
+
startedAt: daemon.startedAt,
|
|
103
|
+
...(daemon.environmentId ? { environmentId: daemon.environmentId } : {}),
|
|
104
|
+
},
|
|
105
|
+
workspaceName,
|
|
106
|
+
};
|
|
107
|
+
const options = { clientOptions, scope, target, sessionName, liveSessionId };
|
|
108
|
+
const transport = createScopedTmuxServerTransport(options);
|
|
109
|
+
const routing = createOpenTuiVerifiedRoutingContext(
|
|
110
|
+
daemon,
|
|
111
|
+
workspaceName,
|
|
112
|
+
sessionName,
|
|
113
|
+
undefined,
|
|
114
|
+
dependencies.readDaemonEndpoint,
|
|
115
|
+
scope,
|
|
116
|
+
liveSessionId,
|
|
117
|
+
);
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
let preparation: ReturnType<
|
|
120
|
+
OpenTuiApplicationShellConnection["prepareTerminalRuntimeInventory"]
|
|
121
|
+
> | null = null;
|
|
122
|
+
return {
|
|
123
|
+
workspaceName,
|
|
124
|
+
liveSessionId,
|
|
125
|
+
target,
|
|
126
|
+
server: scope,
|
|
127
|
+
transport,
|
|
128
|
+
routing,
|
|
129
|
+
catalog: createScopedTmuxServerCatalogPort(options, transport),
|
|
130
|
+
ownerActions: createScopedTmuxServerOwnerActions(options, transport),
|
|
131
|
+
prepareTerminalRuntimeInventory() {
|
|
132
|
+
preparation ??= transport
|
|
133
|
+
.prepareTerminalRuntimeInventory(
|
|
134
|
+
target,
|
|
135
|
+
AbortSignal.any([controller.signal, AbortSignal.timeout(5_000)]),
|
|
136
|
+
)
|
|
137
|
+
.catch(() => {
|
|
138
|
+
// Fallback remains inside this owner; never use the legacy default resource.
|
|
139
|
+
transport.selectApplicationShellFallback("preparation-rejected");
|
|
140
|
+
return null;
|
|
141
|
+
});
|
|
142
|
+
return preparation;
|
|
143
|
+
},
|
|
144
|
+
dispose() {
|
|
145
|
+
controller.abort(new DOMException("Selected server connection retired", "AbortError"));
|
|
146
|
+
void preparation?.then((prepared) => prepared?.dispose()).catch(() => undefined);
|
|
147
|
+
transport.disposeEventSupervisor();
|
|
148
|
+
routing?.retire();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
client.dispose();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -72,7 +72,7 @@ export const MENU_ITEMS: Record<MenuRegion, MenuItem[]> = {
|
|
|
72
72
|
window: [
|
|
73
73
|
{ id: "new", label: "New window" },
|
|
74
74
|
{ id: "rename", label: "Rename window", input: "rename to" },
|
|
75
|
-
{ id: "kill", label: "Kill window", danger: true },
|
|
75
|
+
{ id: "kill", label: "Kill window and panes in all links", danger: true },
|
|
76
76
|
],
|
|
77
77
|
};
|
|
78
78
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { CanonicalDaemonInfo } from "@tmux-ide/contracts";
|
|
1
|
+
import type { CanonicalDaemonInfo, TmuxServerScope } from "@tmux-ide/contracts";
|
|
2
2
|
import {
|
|
3
3
|
openPaneStreamRuntimeClient,
|
|
4
|
+
connectIssuedPaneStreamRuntimeClient,
|
|
4
5
|
type OpenPaneStreamClientOptions,
|
|
5
6
|
type PaneStreamRuntimeClient,
|
|
6
7
|
} from "@tmux-ide/daemon-client/pane-stream-client";
|
|
7
8
|
|
|
9
|
+
import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
|
|
10
|
+
|
|
8
11
|
import { canonicalDaemonUrl } from "../../lib/canonical-daemon.ts";
|
|
9
12
|
import {
|
|
10
13
|
readNativeBacking,
|
|
@@ -34,6 +37,7 @@ export type OpenTuiVerifiedPaneStreamOptions = Omit<
|
|
|
34
37
|
* downstream render/runtime code can use the route but cannot inspect its token.
|
|
35
38
|
*/
|
|
36
39
|
export interface OpenTuiVerifiedRoutingContext extends OpenTuiVerifiedRoutingIdentity {
|
|
40
|
+
readonly liveSessionId?: string;
|
|
37
41
|
readNativeBacking?: ReadNativeBacking;
|
|
38
42
|
assertCurrent(expected: OpenTuiVerifiedRoutingIdentity): void;
|
|
39
43
|
openPaneStream(
|
|
@@ -49,6 +53,8 @@ export function createOpenTuiVerifiedRoutingContext(
|
|
|
49
53
|
sessionName: string,
|
|
50
54
|
openClient: typeof openPaneStreamRuntimeClient = openPaneStreamRuntimeClient,
|
|
51
55
|
readEndpoint: () => ApplicationDaemonEndpoint = applicationDaemonEndpoint,
|
|
56
|
+
server?: TmuxServerScope,
|
|
57
|
+
liveSessionId?: string,
|
|
52
58
|
): OpenTuiVerifiedRoutingContext | null {
|
|
53
59
|
if (!daemon.authToken) return null;
|
|
54
60
|
const ownerToken = daemon.authToken;
|
|
@@ -69,7 +75,7 @@ export function createOpenTuiVerifiedRoutingContext(
|
|
|
69
75
|
const localSocketOrigin = new URL(baseUrl);
|
|
70
76
|
localSocketOrigin.protocol = "ws:";
|
|
71
77
|
const identity = Object.freeze({
|
|
72
|
-
daemonInstanceId: daemon.instanceId,
|
|
78
|
+
daemonInstanceId: server?.generation ?? daemon.instanceId,
|
|
73
79
|
workspaceName,
|
|
74
80
|
sessionName,
|
|
75
81
|
});
|
|
@@ -96,6 +102,7 @@ export function createOpenTuiVerifiedRoutingContext(
|
|
|
96
102
|
};
|
|
97
103
|
return Object.freeze({
|
|
98
104
|
...identity,
|
|
105
|
+
...(liveSessionId ? { liveSessionId } : {}),
|
|
99
106
|
assertCurrent,
|
|
100
107
|
readNativeBacking: async (
|
|
101
108
|
paneId: string,
|
|
@@ -105,6 +112,11 @@ export function createOpenTuiVerifiedRoutingContext(
|
|
|
105
112
|
assertCurrent({ ...identity, daemonInstanceId: expected.generation });
|
|
106
113
|
const result = await readNativeBacking({
|
|
107
114
|
baseUrl,
|
|
115
|
+
...(server
|
|
116
|
+
? {
|
|
117
|
+
resourcePath: `/api/v1/tmux-servers/${server.serverId}/${server.generation}/native-backing/${encodeURIComponent(workspaceName)}/${encodeURIComponent(paneId)}`,
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
108
120
|
ownerToken,
|
|
109
121
|
workspaceName,
|
|
110
122
|
paneId,
|
|
@@ -122,7 +134,33 @@ export function createOpenTuiVerifiedRoutingContext(
|
|
|
122
134
|
if (options.stream.workspaceName !== identity.workspaceName) {
|
|
123
135
|
throw new Error("Pane-stream request escaped its verified workspace route");
|
|
124
136
|
}
|
|
125
|
-
|
|
137
|
+
const openScoped: typeof openClient = async (scopedOptions) => {
|
|
138
|
+
const client = createTmuxServerClient(
|
|
139
|
+
{
|
|
140
|
+
baseUrl,
|
|
141
|
+
ownerToken,
|
|
142
|
+
hostClientId: scopedOptions.hostClientId,
|
|
143
|
+
origin: scopedOptions.origin,
|
|
144
|
+
fetch: scopedOptions.fetch,
|
|
145
|
+
},
|
|
146
|
+
server!,
|
|
147
|
+
);
|
|
148
|
+
try {
|
|
149
|
+
if (scopedOptions.signal?.aborted) throw scopedOptions.signal.reason;
|
|
150
|
+
const issued = await client.issuePaneStream(
|
|
151
|
+
scopedOptions.requestId,
|
|
152
|
+
scopedOptions.stream,
|
|
153
|
+
liveSessionId,
|
|
154
|
+
);
|
|
155
|
+
assertCurrent(expected);
|
|
156
|
+
if (scopedOptions.signal?.aborted) throw scopedOptions.signal.reason;
|
|
157
|
+
if (issued.status === "error") throw new Error(issued.error.reason);
|
|
158
|
+
return await connectIssuedPaneStreamRuntimeClient(scopedOptions, issued.descriptor);
|
|
159
|
+
} finally {
|
|
160
|
+
client.dispose();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return await (server ? openScoped : openClient)({
|
|
126
164
|
...options,
|
|
127
165
|
baseUrl,
|
|
128
166
|
ownerToken,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type {
|
|
3
|
+
WindowLinkTopology,
|
|
3
4
|
CanonicalTerminalReplicaUpdate,
|
|
4
5
|
InteractionReceipt,
|
|
5
6
|
PaneStreamServerFrame,
|
|
@@ -142,6 +143,8 @@ export const OPEN_TUI_HOST_CLIENT_ID = `opentui:${process.pid}`;
|
|
|
142
143
|
export type OpenTuiWorkspaceLayout = Extract<PaneStreamServerFrame, { type: "layout" }>;
|
|
143
144
|
|
|
144
145
|
export interface OpenTuiWorkspaceLayoutSnapshot {
|
|
146
|
+
/** Live link authority; absent until an atomic stream snapshot arrives. */
|
|
147
|
+
readonly windowLinks?: WindowLinkTopology | null;
|
|
145
148
|
readonly current: OpenTuiWorkspaceLayout | null;
|
|
146
149
|
readonly windows: readonly OpenTuiWorkspaceLayout[];
|
|
147
150
|
}
|
|
@@ -233,6 +236,7 @@ function layoutKey(frame: OpenTuiWorkspaceLayout): string | null {
|
|
|
233
236
|
function layoutSnapshot(
|
|
234
237
|
windows: ReadonlyMap<string, OpenTuiWorkspaceLayout>,
|
|
235
238
|
currentWindowKey: string | null = null,
|
|
239
|
+
windowLinks: WindowLinkTopology | null = null,
|
|
236
240
|
): OpenTuiWorkspaceLayoutSnapshot {
|
|
237
241
|
const selectedKey =
|
|
238
242
|
currentWindowKey ??
|
|
@@ -245,7 +249,10 @@ function layoutSnapshot(
|
|
|
245
249
|
: freezeLayout({ ...frame, currentWindow: key === selectedKey }),
|
|
246
250
|
),
|
|
247
251
|
);
|
|
252
|
+
const links = windowLinks?.links.map((link) => Object.freeze({ ...link }));
|
|
253
|
+
if (links) Object.freeze(links);
|
|
248
254
|
return Object.freeze({
|
|
255
|
+
windowLinks: windowLinks && links ? Object.freeze({ ...windowLinks, links }) : null,
|
|
249
256
|
current: frames.find((frame) => frame.currentWindow) ?? null,
|
|
250
257
|
windows: frames,
|
|
251
258
|
});
|
|
@@ -286,6 +293,7 @@ function layoutSnapshotsSemanticallyEqual(
|
|
|
286
293
|
right: OpenTuiWorkspaceLayoutSnapshot,
|
|
287
294
|
): boolean {
|
|
288
295
|
return (
|
|
296
|
+
JSON.stringify(left.windowLinks ?? null) === JSON.stringify(right.windowLinks ?? null) &&
|
|
289
297
|
left.windows.length === right.windows.length &&
|
|
290
298
|
left.windows.every((window, index) => {
|
|
291
299
|
const candidate = right.windows[index];
|
|
@@ -972,6 +980,8 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
972
980
|
coherentSettled ||
|
|
973
981
|
closed ||
|
|
974
982
|
!physicalReady ||
|
|
983
|
+
(routing.liveSessionId !== undefined &&
|
|
984
|
+
latestLayoutSnapshot.windowLinks?.liveSessionId !== routing.liveSessionId) ||
|
|
975
985
|
!layoutExactlyCoversPanes(latestLayoutSnapshot, panes) ||
|
|
976
986
|
!latestLayoutSnapshot.current?.panes.every(
|
|
977
987
|
({ pane }) => typeof pane === "string" && canonicalSeedPanes.has(pane),
|
|
@@ -1147,7 +1157,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1147
1157
|
: {}),
|
|
1148
1158
|
signal: options.signal,
|
|
1149
1159
|
stream: {
|
|
1150
|
-
protocolVersion:
|
|
1160
|
+
protocolVersion: 2,
|
|
1151
1161
|
workspaceName: inventory.workspaceName,
|
|
1152
1162
|
panes: [...panes],
|
|
1153
1163
|
viewerMode: "interactive",
|
|
@@ -1268,12 +1278,23 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1268
1278
|
});
|
|
1269
1279
|
return;
|
|
1270
1280
|
}
|
|
1281
|
+
if (
|
|
1282
|
+
latestLayoutSnapshot.windowLinks &&
|
|
1283
|
+
!latestLayoutSnapshot.windowLinks.links.some(
|
|
1284
|
+
(link) => link.semanticWindowId === retained.semanticWindowId,
|
|
1285
|
+
)
|
|
1286
|
+
)
|
|
1287
|
+
return;
|
|
1271
1288
|
layoutsByWindow.set(key, retained);
|
|
1272
1289
|
// tmux reports a switch as incumbent=false followed by target=true.
|
|
1273
1290
|
// Retain the incumbent through that pair and publish one unique current
|
|
1274
1291
|
// window only when the positive target frame arrives.
|
|
1275
|
-
if (retained.currentWindow) currentWindowKey = key;
|
|
1276
|
-
const candidate = layoutSnapshot(
|
|
1292
|
+
if (!latestLayoutSnapshot.windowLinks && retained.currentWindow) currentWindowKey = key;
|
|
1293
|
+
const candidate = layoutSnapshot(
|
|
1294
|
+
layoutsByWindow,
|
|
1295
|
+
currentWindowKey,
|
|
1296
|
+
latestLayoutSnapshot.windowLinks ?? null,
|
|
1297
|
+
);
|
|
1277
1298
|
if (!layoutExactlyCoversPanes(candidate, panes)) {
|
|
1278
1299
|
if (
|
|
1279
1300
|
lastRejectedLayoutSnapshot &&
|
|
@@ -1308,6 +1329,13 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1308
1329
|
},
|
|
1309
1330
|
onLayoutSnapshot: (snapshot) => {
|
|
1310
1331
|
if (closed) return;
|
|
1332
|
+
if (
|
|
1333
|
+
routing.liveSessionId !== undefined &&
|
|
1334
|
+
snapshot.windowLinks.liveSessionId !== routing.liveSessionId
|
|
1335
|
+
)
|
|
1336
|
+
return failConnection(
|
|
1337
|
+
new Error("Selected live session was replaced before terminal activation"),
|
|
1338
|
+
);
|
|
1311
1339
|
const replacement = new Map<string, ReturnType<typeof freezeLayout>>();
|
|
1312
1340
|
let replacementCurrent: string | null = null;
|
|
1313
1341
|
for (const frame of snapshot.layouts) {
|
|
@@ -1315,14 +1343,18 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1315
1343
|
const key = layoutKey(retained);
|
|
1316
1344
|
if (key === null || replacement.has(key))
|
|
1317
1345
|
return failConnection(new Error("invalid layout snapshot"));
|
|
1318
|
-
|
|
1346
|
+
const previous = layoutsByWindow.get(key);
|
|
1347
|
+
replacement.set(
|
|
1348
|
+
key,
|
|
1349
|
+
previous && layoutFrameSemanticallyEqual(previous, retained) ? previous : retained,
|
|
1350
|
+
);
|
|
1319
1351
|
if (retained.currentWindow) {
|
|
1320
1352
|
if (replacementCurrent !== null)
|
|
1321
1353
|
return failConnection(new Error("invalid layout snapshot current window"));
|
|
1322
1354
|
replacementCurrent = key;
|
|
1323
1355
|
}
|
|
1324
1356
|
}
|
|
1325
|
-
const candidate = layoutSnapshot(replacement, replacementCurrent);
|
|
1357
|
+
const candidate = layoutSnapshot(replacement, replacementCurrent, snapshot.windowLinks);
|
|
1326
1358
|
if (!layoutExactlyCoversPanes(candidate, panes))
|
|
1327
1359
|
return failConnection(new Error("layout snapshot does not cover terminal inventory"));
|
|
1328
1360
|
layoutsByWindow.clear();
|
|
@@ -1331,6 +1363,11 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1331
1363
|
lastRejectedLayoutSnapshot = null;
|
|
1332
1364
|
if (layoutSnapshotsSemanticallyEqual(candidate, latestLayoutSnapshot)) return;
|
|
1333
1365
|
latestLayoutSnapshot = candidate;
|
|
1366
|
+
options.onDiagnostic?.("layout", {
|
|
1367
|
+
windows: candidate.windows.length,
|
|
1368
|
+
current: candidate.current !== null,
|
|
1369
|
+
links: candidate.windowLinks?.links.length ?? 0,
|
|
1370
|
+
});
|
|
1334
1371
|
settleCoherent();
|
|
1335
1372
|
for (const listener of [...layoutListeners]) {
|
|
1336
1373
|
try {
|
|
@@ -1485,7 +1522,11 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1485
1522
|
submitIntent: async (operationId, intent) => {
|
|
1486
1523
|
// Structural commands can affect hidden panes. Do not defer user intent
|
|
1487
1524
|
// until a different topology may be current; fail closed during seeding.
|
|
1488
|
-
if (
|
|
1525
|
+
if (
|
|
1526
|
+
closed ||
|
|
1527
|
+
!latestLayoutSnapshot.windowLinks ||
|
|
1528
|
+
[...endpoints.values()].some((endpoint) => !endpoint.inputReady)
|
|
1529
|
+
)
|
|
1489
1530
|
throw new Error("Terminal inventory is still receiving canonical state");
|
|
1490
1531
|
return (await opened.submitIntent(operationId, intent)) ?? undefined;
|
|
1491
1532
|
},
|
|
@@ -68,7 +68,7 @@ export function explicitApplicationTarget(argv: readonly string[]): string | nul
|
|
|
68
68
|
args: [...argv],
|
|
69
69
|
allowPositionals: true,
|
|
70
70
|
strict: false,
|
|
71
|
-
options: { target: { type: "string" } },
|
|
71
|
+
options: { target: { type: "string" }, server: { type: "string" } },
|
|
72
72
|
});
|
|
73
73
|
const positional = parsed.positionals.find((value) => value !== "app") ?? null;
|
|
74
74
|
const target = typeof parsed.values.target === "string" ? parsed.values.target : positional;
|
|
@@ -79,6 +79,7 @@ export function prepareExplicitApplicationTarget<Value>(
|
|
|
79
79
|
argv: readonly string[],
|
|
80
80
|
prepare: (sessionName: string) => Promise<Value>,
|
|
81
81
|
): { readonly sessionName: string; readonly prepared: Promise<Value> } | null {
|
|
82
|
+
if (argv.some((arg) => arg === "--server" || arg.startsWith("--server="))) return null;
|
|
82
83
|
const sessionName = explicitApplicationTarget(argv);
|
|
83
84
|
if (!sessionName) return null;
|
|
84
85
|
const prepared = prepare(sessionName);
|
|
@@ -43,6 +43,8 @@ export function ApplicationFleetSessionActions(props: {
|
|
|
43
43
|
return command
|
|
44
44
|
? JSON.stringify([
|
|
45
45
|
command.fleet!.machineId,
|
|
46
|
+
command.fleet!.server?.serverId,
|
|
47
|
+
command.fleet!.server?.generation,
|
|
46
48
|
command.fleet!.daemonInstanceId,
|
|
47
49
|
command.fleet!.liveSessionId,
|
|
48
50
|
command.sessionName,
|
|
@@ -117,12 +119,13 @@ export function ApplicationFleetSessionActions(props: {
|
|
|
117
119
|
)
|
|
118
120
|
throw new Error("The selected host is unavailable or has restarted.");
|
|
119
121
|
const epoch = handle.endpoint().epoch;
|
|
120
|
-
const routing = await ports().routing(daemon, undefined, signal);
|
|
122
|
+
const routing = target.server ? null : await ports().routing(daemon, undefined, signal);
|
|
121
123
|
if (!current()) return;
|
|
122
124
|
if (
|
|
123
125
|
handle.endpoint().epoch !== epoch ||
|
|
124
126
|
handle.read()?.instanceId !== target.daemonInstanceId ||
|
|
125
|
-
(
|
|
127
|
+
(routing &&
|
|
128
|
+
target.liveSessionId !== "" &&
|
|
126
129
|
!routing.liveSessions.some(
|
|
127
130
|
(session) =>
|
|
128
131
|
session.liveSessionId === target.liveSessionId &&
|
|
@@ -132,11 +135,14 @@ export function ApplicationFleetSessionActions(props: {
|
|
|
132
135
|
throw new Error("The selected session has changed. Select it again.");
|
|
133
136
|
const result =
|
|
134
137
|
action === "create"
|
|
135
|
-
?
|
|
138
|
+
? target.server
|
|
139
|
+
? await ports().create(handle, text, target.server)
|
|
140
|
+
: await ports().create(handle, text)
|
|
136
141
|
: await ports().close(handle, {
|
|
137
142
|
daemonInstanceId: target.daemonInstanceId,
|
|
138
143
|
liveSessionId: target.liveSessionId,
|
|
139
144
|
sessionName: command.sessionName,
|
|
145
|
+
...(target.server ? { server: target.server } : {}),
|
|
140
146
|
});
|
|
141
147
|
if (!current()) return;
|
|
142
148
|
if (!result)
|
|
@@ -180,7 +186,7 @@ export function ApplicationFleetSessionActions(props: {
|
|
|
180
186
|
<TuiButton
|
|
181
187
|
theme={props.theme}
|
|
182
188
|
size="compact"
|
|
183
|
-
label="New on host ^N"
|
|
189
|
+
label={selected()?.fleet?.server ? "New on server ^N" : "New on host ^N"}
|
|
184
190
|
disabled={!props.active || selected()?.fleet?.disabled || busy()}
|
|
185
191
|
onPress={() => begin("create")}
|
|
186
192
|
/>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { TmuxServerScope } from "@tmux-ide/contracts";
|
|
1
2
|
export interface FleetTabTarget {
|
|
2
3
|
readonly key: string;
|
|
3
4
|
readonly machineId: string;
|
|
4
5
|
readonly liveSessionId: string;
|
|
6
|
+
readonly server?: TmuxServerScope;
|
|
5
7
|
readonly label: string;
|
|
6
8
|
readonly hostLabel: string;
|
|
7
9
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createEffect, createMemo, Show, type Accessor } from "solid-js";
|
|
4
|
+
import { createApplicationGuidedTourOwner } from "./application-guided-tour-owner.ts";
|
|
5
|
+
import { GuidedTourCoach } from "./guided-tour-coach.tsx";
|
|
6
|
+
import type { GuidedTourPractice } from "./guided-tour.ts";
|
|
7
|
+
import { createFleetSession } from "./fleet-lifecycle-client.ts";
|
|
8
|
+
import { applicationMachineAuthorityManager } from "./application-machine-authority.ts";
|
|
9
|
+
import type { createApplicationMachineNavigation } from "./application-machine-navigation.ts";
|
|
10
|
+
import type { ApplicationAppearanceOwner } from "./application-appearance-owner.ts";
|
|
11
|
+
import type { createApplicationHomeNavigationOwner } from "./application-home-agents-owner.ts";
|
|
12
|
+
import type { OpenTuiGenerationHostSnapshot } from "./open-tui-generation-host.ts";
|
|
13
|
+
import type { OpenTuiWorkspaceLayoutSnapshot } from "../open-tui-workspace-runtime-port.ts";
|
|
14
|
+
|
|
15
|
+
export function createApplicationGuidedTourIntegration(options: {
|
|
16
|
+
machines: ReturnType<typeof createApplicationMachineNavigation>;
|
|
17
|
+
lifecycle: { signal: AbortSignal };
|
|
18
|
+
generation: Accessor<OpenTuiGenerationHostSnapshot | null>;
|
|
19
|
+
generationMachineId: Accessor<string | null>;
|
|
20
|
+
layoutSnapshot: Accessor<OpenTuiWorkspaceLayoutSnapshot>;
|
|
21
|
+
sessionName: () => string | null;
|
|
22
|
+
focusedPane: Accessor<string | null>;
|
|
23
|
+
activeSurface: Accessor<"home" | "terminals">;
|
|
24
|
+
appearance: ApplicationAppearanceOwner;
|
|
25
|
+
paletteOpen: Accessor<boolean>;
|
|
26
|
+
paletteCommands: ReturnType<typeof createApplicationHomeNavigationOwner>["paletteCommands"];
|
|
27
|
+
dimensions: Accessor<{ width: number; height: number }>;
|
|
28
|
+
blocked: Accessor<boolean>;
|
|
29
|
+
}) {
|
|
30
|
+
const {
|
|
31
|
+
machines,
|
|
32
|
+
lifecycle,
|
|
33
|
+
generation,
|
|
34
|
+
generationMachineId,
|
|
35
|
+
layoutSnapshot,
|
|
36
|
+
sessionName,
|
|
37
|
+
focusedPane,
|
|
38
|
+
activeSurface,
|
|
39
|
+
appearance,
|
|
40
|
+
paletteOpen,
|
|
41
|
+
paletteCommands,
|
|
42
|
+
dimensions,
|
|
43
|
+
blocked,
|
|
44
|
+
} = options;
|
|
45
|
+
const practiceRows = () =>
|
|
46
|
+
machines.catalog
|
|
47
|
+
.getSnapshot()
|
|
48
|
+
.groups.filter((group) => group.id === "local" || group.routeIds?.includes("local"))
|
|
49
|
+
.flatMap((group) => group.sessions)
|
|
50
|
+
.filter((row) => !row.disabled);
|
|
51
|
+
const practiceIdentity = (
|
|
52
|
+
row: ReturnType<typeof practiceRows>[number],
|
|
53
|
+
): GuidedTourPractice | null => {
|
|
54
|
+
const daemon = applicationMachineAuthorityManager.getMachine("local")?.read();
|
|
55
|
+
if (!row.liveSessionId || !daemon) return null;
|
|
56
|
+
return {
|
|
57
|
+
machineId: "local",
|
|
58
|
+
serverId: row.server?.serverId ?? "default",
|
|
59
|
+
generation: row.server?.generation ?? daemon.instanceId,
|
|
60
|
+
sessionId: row.liveSessionId,
|
|
61
|
+
sessionName: row.name,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const tour = createApplicationGuidedTourOwner({
|
|
65
|
+
async createPractice() {
|
|
66
|
+
const handle = applicationMachineAuthorityManager.getMachine("local");
|
|
67
|
+
if (!handle || handle.endpoint().state !== "ready")
|
|
68
|
+
throw new Error("Connect this machine before creating a practice session.");
|
|
69
|
+
const created = await createFleetSession(
|
|
70
|
+
handle,
|
|
71
|
+
`tmux-ide-practice-${randomUUID().slice(0, 8)}`,
|
|
72
|
+
undefined,
|
|
73
|
+
{ includeLiveSessionId: true },
|
|
74
|
+
);
|
|
75
|
+
if (!created || created.outcome !== "created")
|
|
76
|
+
throw new Error("Could not create a fresh practice session. Try again.");
|
|
77
|
+
if (!created.liveSessionId)
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Update the local tmux-ide daemon to use the guided practice session, then restart the tour.",
|
|
80
|
+
);
|
|
81
|
+
return await new Promise<GuidedTourPractice>((resolve, reject) => {
|
|
82
|
+
let done = false;
|
|
83
|
+
let stop = () => {};
|
|
84
|
+
const abort = () => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
stop();
|
|
87
|
+
reject(new Error("Tour closed."));
|
|
88
|
+
};
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
lifecycle.signal.removeEventListener("abort", abort);
|
|
91
|
+
stop();
|
|
92
|
+
reject(
|
|
93
|
+
new Error(
|
|
94
|
+
"Practice session was created but has not appeared yet. Find it in the local session list.",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}, 10_000);
|
|
98
|
+
lifecycle.signal.addEventListener("abort", abort, { once: true });
|
|
99
|
+
const check = () => {
|
|
100
|
+
const rows = practiceRows().filter(
|
|
101
|
+
(row) =>
|
|
102
|
+
row.name === created.workspaceName && row.liveSessionId === created.liveSessionId,
|
|
103
|
+
);
|
|
104
|
+
const identity = rows.length === 1 ? practiceIdentity(rows[0]!) : null;
|
|
105
|
+
if (!identity || identity.generation !== created.daemonInstanceId) return;
|
|
106
|
+
done = true;
|
|
107
|
+
lifecycle.signal.removeEventListener("abort", abort);
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
stop();
|
|
110
|
+
resolve(identity);
|
|
111
|
+
};
|
|
112
|
+
stop = machines.catalog.subscribe(check);
|
|
113
|
+
if (done) stop();
|
|
114
|
+
else if (lifecycle.signal.aborted) abort();
|
|
115
|
+
else check();
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
openPractice(practice) {
|
|
119
|
+
if (practice.machineId !== "local")
|
|
120
|
+
throw new Error("Practice sessions must belong to this machine. Restart the tour.");
|
|
121
|
+
const row = practiceRows().find((row) => {
|
|
122
|
+
const identity = practiceIdentity(row);
|
|
123
|
+
return (
|
|
124
|
+
identity &&
|
|
125
|
+
identity.serverId === practice.serverId &&
|
|
126
|
+
identity.generation === practice.generation &&
|
|
127
|
+
identity.sessionId === practice.sessionId
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
if (!row)
|
|
131
|
+
throw new Error(
|
|
132
|
+
"That practice session is unavailable or has been replaced. Replay the tour to create a new one.",
|
|
133
|
+
);
|
|
134
|
+
machines.sidebar.onOpen("local", row.name, "mouse", row.id);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const committedTourTheme = createMemo<string>((previous) =>
|
|
138
|
+
appearance.pickerOpen()
|
|
139
|
+
? (previous ?? JSON.stringify(appearance.theme().canonical))
|
|
140
|
+
: JSON.stringify(appearance.theme().canonical),
|
|
141
|
+
);
|
|
142
|
+
createEffect(() => {
|
|
143
|
+
const current = generation();
|
|
144
|
+
const connection = current?.status === "live" ? current.connection : null;
|
|
145
|
+
const machineId = generationMachineId();
|
|
146
|
+
const practice =
|
|
147
|
+
connection?.liveSessionId && current?.daemonGeneration && machineId
|
|
148
|
+
? {
|
|
149
|
+
machineId,
|
|
150
|
+
serverId: connection.server?.serverId ?? "default",
|
|
151
|
+
generation: connection.server?.generation ?? current.daemonGeneration,
|
|
152
|
+
sessionId: connection.liveSessionId,
|
|
153
|
+
sessionName: sessionName() ?? "",
|
|
154
|
+
}
|
|
155
|
+
: null;
|
|
156
|
+
tour.observe({
|
|
157
|
+
practice,
|
|
158
|
+
panes: (layoutSnapshot().current?.panes ?? []).flatMap((pane) =>
|
|
159
|
+
pane.pane ? [{ id: pane.pane, width: pane.width, height: pane.height }] : [],
|
|
160
|
+
),
|
|
161
|
+
focusedPane: focusedPane(),
|
|
162
|
+
surface: activeSurface(),
|
|
163
|
+
paletteOpen: paletteOpen(),
|
|
164
|
+
theme: committedTourTheme(),
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
...tour,
|
|
169
|
+
Coach: () => (
|
|
170
|
+
<Show when={tour.state().active && !blocked()}>
|
|
171
|
+
<box
|
|
172
|
+
position="absolute"
|
|
173
|
+
right={1}
|
|
174
|
+
bottom={2}
|
|
175
|
+
width={Math.max(1, Math.min(68, dimensions().width - 2))}
|
|
176
|
+
zIndex={30}
|
|
177
|
+
>
|
|
178
|
+
<GuidedTourCoach
|
|
179
|
+
height={Math.max(4, Math.min(10, dimensions().height - 4))}
|
|
180
|
+
state={tour.state()}
|
|
181
|
+
theme={appearance.theme()}
|
|
182
|
+
width={Math.max(1, Math.min(68, dimensions().width - 2))}
|
|
183
|
+
shortcuts={{ commands: "F5", home: "F1" }}
|
|
184
|
+
error={tour.error()}
|
|
185
|
+
busy={tour.busy()}
|
|
186
|
+
onPause={tour.pause}
|
|
187
|
+
onReplay={tour.replay}
|
|
188
|
+
onWelcomeRead={tour.welcomeRead}
|
|
189
|
+
onCreatePractice={() => void tour.createPractice()}
|
|
190
|
+
onOpenCommands={() => paletteCommands.setOpen(true, "mouse")}
|
|
191
|
+
onOpenAppearance={appearance.openPicker}
|
|
192
|
+
onOpenHome={() => paletteCommands.openSurface("home", "mouse")}
|
|
193
|
+
onOpenPractice={() => void tour.openPractice()}
|
|
194
|
+
/>
|
|
195
|
+
</box>
|
|
196
|
+
</Show>
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
}
|