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.
Files changed (209) hide show
  1. package/bin/cli.js +46791 -43589
  2. package/package.json +2 -2
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
  4. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
  5. package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
  6. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
  7. package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
  8. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
  9. package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
  10. package/packages/contracts/src/actions-contract.ts +12 -0
  11. package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
  12. package/packages/contracts/src/daemon-wire.ts +10 -5
  13. package/packages/contracts/src/fleet-client-state.ts +3 -0
  14. package/packages/contracts/src/fleet-lifecycle.ts +5 -0
  15. package/packages/contracts/src/index.ts +2 -0
  16. package/packages/contracts/src/interaction-receipts.ts +39 -0
  17. package/packages/contracts/src/issue-error.ts +1 -1
  18. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  19. package/packages/contracts/src/pane-stream.ts +51 -8
  20. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  21. package/packages/contracts/src/window-links.ts +76 -0
  22. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  23. package/packages/core/src/interaction-receipts.ts +4 -0
  24. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  25. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  26. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  27. package/packages/daemon/dist/command-center/discovery.js +2 -9
  28. package/packages/daemon/dist/command-center/server.js +21 -1
  29. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  30. package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
  31. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  32. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  33. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  34. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  35. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
  36. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  37. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  38. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  39. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  40. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  41. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  42. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  43. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  44. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  46. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  47. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  48. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  49. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  50. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  51. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  52. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  53. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  54. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  55. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  56. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  57. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  58. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  59. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  60. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  61. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  62. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  63. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  64. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  65. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  66. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  67. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  68. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  70. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  71. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  72. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  73. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  74. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  75. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  76. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
  77. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
  78. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
  79. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
  80. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
  81. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
  82. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  83. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  84. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
  85. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
  86. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
  87. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  88. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
  89. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  90. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
  91. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  92. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  93. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
  94. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  95. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  96. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  97. package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
  98. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
  99. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
  100. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
  101. package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
  102. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  103. package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
  104. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  105. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
  106. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  107. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  108. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  109. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  110. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  111. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  112. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  113. package/packages/daemon/src/command-center/discovery.ts +2 -10
  114. package/packages/daemon/src/command-center/server.ts +29 -1
  115. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  116. package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
  117. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  118. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  119. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  120. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  121. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
  122. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  123. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  124. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  125. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  126. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  127. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  128. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  129. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  130. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  131. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  132. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  133. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  134. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  135. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  136. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  137. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  138. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  139. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  140. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  141. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  142. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  143. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  144. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  145. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  146. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  147. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  148. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  149. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  150. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  151. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  152. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  153. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  154. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  155. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  156. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  157. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  158. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  159. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  160. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  161. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  162. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  163. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  164. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
  165. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
  166. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
  167. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
  168. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
  169. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
  170. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  171. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  172. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
  173. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
  174. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
  175. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  176. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
  177. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  178. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  179. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  180. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
  181. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  182. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
  183. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  184. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
  185. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
  186. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
  187. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  188. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  189. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  190. package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
  191. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
  192. package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
  193. package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
  194. package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
  195. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  196. package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
  197. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  198. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
  199. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  200. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  201. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  202. package/packages/daemon-client/package.json +3 -1
  203. package/packages/daemon-client/src/index.ts +2 -0
  204. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  205. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  206. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  207. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  208. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  209. 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
- return await openClient({
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: 1,
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(layoutsByWindow, currentWindowKey);
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
- replacement.set(key, retained);
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 (closed || [...endpoints.values()].some((endpoint) => !endpoint.inputReady))
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
- (target.liveSessionId !== "" &&
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
- ? await ports().create(handle, text)
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
+ }