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
@@ -1,3 +1,9 @@
1
+ import {
2
+ listTmuxServers,
3
+ createTmuxServerClient,
4
+ } from "@tmux-ide/daemon-client/tmux-server-client";
5
+ import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.ts";
6
+ import type { TmuxServerScope } from "@tmux-ide/contracts";
1
7
  import { fetchCanonicalLiveWorkspaceRouting } from "../canonical-workspace-routing.ts";
2
8
  import type { ApplicationMachineAuthorityHandle } from "./application-machine-authority.ts";
3
9
  import { prepareOpenTuiApplicationShellConnection } from "../application-shell-daemon-connection.ts";
@@ -8,14 +14,100 @@ export function applicationRouteConnection(
8
14
  handle: ApplicationMachineAuthorityHandle,
9
15
  expectedLiveSessionId?: string,
10
16
  onDiagnostic?: (phase: string, details: Readonly<Record<string, unknown>>) => void,
17
+ server?: TmuxServerScope,
11
18
  ) {
19
+ let rootInstanceId = handle.read()?.instanceId;
20
+ let selectedServer = server;
21
+ let refresh: { root: string; promise: Promise<TmuxServerScope> } | null = null;
22
+ const refreshAfterDaemonReplacement = async (nextRoot: string): Promise<TmuxServerScope> => {
23
+ if (!selectedServer) throw new Error("Select the server session again");
24
+ if (rootInstanceId === nextRoot) return selectedServer;
25
+ if (!expectedLiveSessionId) throw new Error("Select the server session again");
26
+ if (refresh?.root === nextRoot) return refresh.promise;
27
+ const stableServerId = selectedServer.serverId;
28
+ const promise = (async () => {
29
+ const daemon = handle.read();
30
+ const epoch = handle.endpoint()?.epoch;
31
+ if (!daemon?.authToken || daemon.instanceId !== nextRoot)
32
+ throw new Error("Daemon changed during reconnect");
33
+ const options = {
34
+ baseUrl: canonicalDaemonUrl("http", daemon.bindHostname, daemon.port),
35
+ ownerToken: daemon.authToken,
36
+ hostClientId: `opentui-reconnect:${process.pid}`,
37
+ origin: "tmux-ide://opentui",
38
+ };
39
+ const catalog = await listTmuxServers(options);
40
+ const descriptor = catalog.servers.find((candidate) => candidate.serverId === stableServerId);
41
+ if (!descriptor || descriptor.state !== "online")
42
+ throw new Error("Selected tmux server is unavailable");
43
+ const scope = { serverId: descriptor.serverId, generation: descriptor.generation };
44
+ const client = createTmuxServerClient(options, scope);
45
+ try {
46
+ const sessions = await client.sessions();
47
+ if (!sessions.sessions.some((session) => session.liveSessionId === expectedLiveSessionId))
48
+ throw new Error("Selected tmux session was replaced");
49
+ } finally {
50
+ client.dispose();
51
+ }
52
+ if (handle.read()?.instanceId !== nextRoot || handle.endpoint()?.epoch !== epoch)
53
+ throw new Error("Daemon changed during reconnect");
54
+ rootInstanceId = nextRoot;
55
+ selectedServer = scope;
56
+ return scope;
57
+ })();
58
+ refresh = { root: nextRoot, promise };
59
+ try {
60
+ return await promise;
61
+ } finally {
62
+ if (refresh?.promise === promise) refresh = null;
63
+ }
64
+ };
12
65
  return {
13
66
  readDaemon: handle.read,
14
- observeCanonicalGeneration: handle.observe,
67
+ observeCanonicalGeneration: server
68
+ ? async (listener: (generation: string | null) => void) => {
69
+ let stopped = false;
70
+ let revision = 0;
71
+ const stop = await handle.observe((generation) => {
72
+ const token = ++revision;
73
+ if (!generation) {
74
+ listener(null);
75
+ return;
76
+ }
77
+ if (generation === rootInstanceId) {
78
+ listener(selectedServer!.generation);
79
+ return;
80
+ }
81
+ // Retire old input before reading the replacement daemon's registration.
82
+ listener(null);
83
+ void refreshAfterDaemonReplacement(generation).then(
84
+ (scope) => {
85
+ if (!stopped && token === revision && handle.read()?.instanceId === generation)
86
+ listener(scope.generation);
87
+ },
88
+ () => {
89
+ if (!stopped && token === revision) listener(null);
90
+ },
91
+ );
92
+ });
93
+ return () => {
94
+ stopped = true;
95
+ revision++;
96
+ stop();
97
+ };
98
+ }
99
+ : handle.observe,
15
100
  resolveConnection: async (sessionName: string) => {
101
+ if (selectedServer) {
102
+ const currentRoot = handle.read()?.instanceId;
103
+ if (!currentRoot) return null;
104
+ await refreshAfterDaemonReplacement(currentRoot);
105
+ }
16
106
  const connection = await prepareOpenTuiApplicationShellConnection(sessionName, {
17
107
  ...(onDiagnostic ? { onDiagnostic } : {}),
108
+ ...(selectedServer ? { server: selectedServer, expectedLiveSessionId } : {}),
18
109
  readCanonicalDaemonInfo: handle.read,
110
+ readDaemonEndpoint: handle.endpoint,
19
111
  isCanonicalDaemonAlive: handle.isAlive,
20
112
  ensureSessionWorkspace: (name) =>
21
113
  ensureOpenTuiSessionWorkspaceResult(name, {
@@ -71,6 +71,8 @@ export interface ApplicationCatalogShellProps {
71
71
  ) => void;
72
72
  readonly onCreateSession?: () => void;
73
73
  readonly onCycleTheme?: () => void;
74
+ readonly onOpenTutorial?: () => void;
75
+ readonly tutorialLabel?: string;
74
76
  }
75
77
 
76
78
  const CATALOG_VIEWS: readonly ShellChromeView[] = [
@@ -462,6 +464,8 @@ export function ApplicationCatalogShell(props: ApplicationCatalogShellProps): JS
462
464
  onOpenTerminals={() => props.onOpenSurface("terminals", "mouse")}
463
465
  onOpenCommands={() => props.onSetPaletteOpen(true, "mouse")}
464
466
  onCycleTheme={props.onCycleTheme}
467
+ onOpenTutorial={props.onOpenTutorial}
468
+ tutorialLabel={props.tutorialLabel}
465
469
  />
466
470
  </Show>
467
471
  </box>
@@ -35,7 +35,12 @@ export interface ApplicationHomeSurfaceProps {
35
35
  readonly theme: SemanticThemeSnapshot;
36
36
  readonly onOpenTerminals: () => void;
37
37
  readonly onOpenCommands: () => void;
38
+ readonly onOpenTutorial?: () => void;
39
+ readonly tutorialLabel?: string;
38
40
  readonly onCycleTheme?: () => void;
41
+ readonly agentFilterLabel?: string;
42
+ readonly onCycleAgentMachine?: () => void;
43
+ readonly onToggleAgentAttention?: () => void;
39
44
  readonly agentRoster?: HomeAgentSnapshot;
40
45
  readonly recentPaneActivity?: readonly InteractionReceipt[];
41
46
  readonly agentSelection?: HomeAgentSelectionSnapshot;
@@ -74,10 +79,11 @@ export function ApplicationHomeSurface(props: ApplicationHomeSurfaceProps): JSX.
74
79
  naturalButtonWidth("Open terminals", "F2") +
75
80
  naturalButtonWidth("Commands", "F5") +
76
81
  (props.onCycleTheme ? naturalButtonWidth(themeLabel()) + 2 : 0) +
82
+ (props.onOpenTutorial ? naturalButtonWidth(props.tutorialLabel ?? "Learn tmux-ide") + 2 : 0) +
77
83
  2;
78
84
  const reservedRows = () =>
79
85
  (spacious() ? 4 : 2) +
80
- (actionsInRow() ? 1 : props.onCycleTheme ? 3 : 2) +
86
+ (actionsInRow() ? 1 : 2 + (props.onCycleTheme ? 1 : 0) + (props.onOpenTutorial ? 1 : 0)) +
81
87
  (spacious() ? 1 : 0) +
82
88
  (props.note ? (spacious() ? 2 : 1) : 0);
83
89
  const recentActivity = () =>
@@ -156,6 +162,9 @@ export function ApplicationHomeSurface(props: ApplicationHomeSurfaceProps): JSX.
156
162
  >
157
163
  {(snapshot) => (
158
164
  <HomeAgentRoster
165
+ filterLabel={props.agentFilterLabel}
166
+ onCycleMachine={props.onCycleAgentMachine}
167
+ onToggleAttention={props.onToggleAgentAttention}
159
168
  theme={props.theme}
160
169
  width={bodyWidth()}
161
170
  height={rosterHeight()}
@@ -220,6 +229,16 @@ export function ApplicationHomeSurface(props: ApplicationHomeSurfaceProps): JSX.
220
229
  width={buttonWidth("Commands", "F5")}
221
230
  onPress={props.onOpenCommands}
222
231
  />
232
+ <Show when={props.onOpenTutorial}>
233
+ {(open) => (
234
+ <TuiButton
235
+ theme={props.theme}
236
+ label={props.tutorialLabel ?? "Learn tmux-ide"}
237
+ width={buttonWidth(props.tutorialLabel ?? "Learn tmux-ide")}
238
+ onPress={open()}
239
+ />
240
+ )}
241
+ </Show>
223
242
  <Show when={props.onCycleTheme}>
224
243
  {(onCycleTheme) => (
225
244
  <TuiButton
@@ -117,12 +117,16 @@ export interface ApplicationShellViewProps {
117
117
  readonly onCreateWindow?: () => void;
118
118
  readonly onCreateSession?: () => void;
119
119
  readonly onCycleTheme?: () => void;
120
+ readonly onOpenTutorial?: () => void;
121
+ readonly tutorialLabel?: string;
120
122
  readonly onBeginPaneRename?: (paneId: string, currentName: string) => void;
121
123
  readonly onCancelPaneRename?: () => void;
122
124
  readonly onSubmitPaneRename?: () => void;
123
125
  readonly onDismissNotification?: () => void;
124
126
  readonly paletteCloseArmed?: Accessor<boolean>;
125
127
  readonly onSelectPane: TerminalWorkspaceProps["onSelectPane"];
128
+ readonly onSelectWindowLink?: TerminalWorkspaceProps["onSelectWindowLink"];
129
+ readonly onUnlinkWindowLink?: TerminalWorkspaceProps["onUnlinkWindowLink"];
126
130
  readonly onResizePreview: TerminalWorkspaceProps["onResizePreview"];
127
131
  readonly onResizePane: TerminalWorkspaceProps["onResizePane"];
128
132
  readonly onCancelResize?: TerminalWorkspaceProps["onCancelResize"];
@@ -268,6 +272,8 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
268
272
  onPaletteActivate={props.onPaletteActivate}
269
273
  onCreateSession={props.onCreateSession}
270
274
  onCycleTheme={props.onCycleTheme}
275
+ onOpenTutorial={props.onOpenTutorial}
276
+ tutorialLabel={props.tutorialLabel}
271
277
  />
272
278
  }
273
279
  >
@@ -454,6 +460,8 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
454
460
  onOpenTerminals={() => props.onOpenSurface("terminals", "mouse")}
455
461
  onOpenCommands={() => props.onSetPaletteOpen(true, "mouse")}
456
462
  onCycleTheme={props.onCycleTheme}
463
+ onOpenTutorial={props.onOpenTutorial}
464
+ tutorialLabel={props.tutorialLabel}
457
465
  />
458
466
  </Show>
459
467
  <box
@@ -509,6 +517,8 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
509
517
  agentIndicators={agentIndicators}
510
518
  paneInteractions={props.paneInteractions}
511
519
  onSelectPane={props.onSelectPane}
520
+ onSelectWindowLink={props.onSelectWindowLink}
521
+ onUnlinkWindowLink={props.onUnlinkWindowLink}
512
522
  onCreateWindow={props.onCreateWindow}
513
523
  onPaneContextAction={(paneId, action, currentName) => {
514
524
  if (action === "rename-pane")
@@ -1,6 +1,8 @@
1
+ import { WorkspaceWindowLinkSelectResultSchemaZ } from "@tmux-ide/contracts";
2
+ import { windowLinkForPane, windowLinkTarget } from "./application-terminal-workspace-policy.ts";
1
3
  import { createHash, randomUUID } from "node:crypto";
2
4
  import type { Accessor, Setter } from "solid-js";
3
- import type { SessionRuntimeTerminalInput } from "@tmux-ide/contracts";
5
+ import type { WindowLinkTarget, SessionRuntimeTerminalInput } from "@tmux-ide/contracts";
4
6
  import {
5
7
  currentTuiPerformanceEventSink,
6
8
  type TuiTerminalInputOrigin,
@@ -79,6 +81,8 @@ export interface ApplicationTerminalInteractionController {
79
81
  adoptGeneration(snapshot: OpenTuiGenerationHostSnapshot | null): void;
80
82
  adoptLayout(snapshot: OpenTuiWorkspaceLayoutSnapshot): void;
81
83
  selectPane(paneId: string): void;
84
+ selectWindowLink(target: WindowLinkTarget): Promise<void>;
85
+ unlinkWindowLink(target: WindowLinkTarget): Promise<string>;
82
86
  sendInput(
83
87
  input: SessionRuntimeTerminalInput,
84
88
  parserOrigin?: Omit<
@@ -628,6 +632,41 @@ export function createApplicationTerminalInteractionController(
628
632
  }
629
633
  };
630
634
 
635
+ let linkSelection: {
636
+ target: WindowLinkTarget;
637
+ paneId: string;
638
+ layout: OpenTuiWorkspaceLayoutSnapshot;
639
+ received: boolean;
640
+ resolve: (selected: boolean) => void;
641
+ timer: ReturnType<typeof setTimeout>;
642
+ } | null = null;
643
+ const finishLinkSelection = (selected: boolean) => {
644
+ const pending = linkSelection;
645
+ if (!pending) return;
646
+ linkSelection = null;
647
+ clearTimeout(pending.timer);
648
+ pending.resolve(selected);
649
+ };
650
+ const reconcileLinkSelection = (snapshot: OpenTuiWorkspaceLayoutSnapshot) => {
651
+ const pending = linkSelection;
652
+ if (!pending) return;
653
+ pending.layout = snapshot;
654
+ const observed = windowLinkTarget(snapshot, pending.target.linkId);
655
+ if (
656
+ !observed ||
657
+ observed.linkRevision !== pending.target.linkRevision ||
658
+ observed.liveSessionId !== pending.target.liveSessionId ||
659
+ observed.expectedSemanticWindowId !== pending.target.expectedSemanticWindowId
660
+ ) {
661
+ finishLinkSelection(false);
662
+ return;
663
+ }
664
+ if (!pending.received || snapshot.windowLinks?.activeLinkId !== pending.target.linkId) return;
665
+ finishLinkSelection(
666
+ snapshot.current !== null && paneForWindow(snapshot.current) === pending.paneId,
667
+ );
668
+ };
669
+
631
670
  const paneInput = new TerminalPaneInputRouter<{
632
671
  readonly input: SessionRuntimeTerminalInput;
633
672
  readonly parserOrigin?: Omit<
@@ -636,6 +675,7 @@ export function createApplicationTerminalInteractionController(
636
675
  >;
637
676
  }>({
638
677
  select: async (paneId) => {
678
+ finishLinkSelection(false);
639
679
  const expected = liveSelectionTarget();
640
680
  const selecting = pendingWindowSwitch;
641
681
  const operationId = selecting?.traceId;
@@ -649,6 +689,7 @@ export function createApplicationTerminalInteractionController(
649
689
  (failure) => {
650
690
  selectionFailure.current = failure;
651
691
  },
692
+ windowLinkForPane(options.layout(), paneId) ?? undefined,
652
693
  )
653
694
  : null;
654
695
  if (selecting && pendingWindowSwitch === selecting) {
@@ -833,6 +874,7 @@ export function createApplicationTerminalInteractionController(
833
874
  next.fastLane !== inputAuthorityIdentity.fastLane ||
834
875
  next.adapter !== inputAuthorityIdentity.adapter);
835
876
  if (next === null || replaced) {
877
+ finishLinkSelection(false);
836
878
  paneInput.invalidateSelection();
837
879
  pendingWindowSwitch = null;
838
880
  pendingWindowRename = null;
@@ -846,6 +888,7 @@ export function createApplicationTerminalInteractionController(
846
888
  inputAuthorityIdentity = next;
847
889
  },
848
890
  adoptLayout(snapshot) {
891
+ reconcileLinkSelection(snapshot);
849
892
  paneInput.adoptCanonicalPane(snapshot.current ? paneForWindow(snapshot.current) : null);
850
893
  const currentWindow = snapshot.current?.semanticWindowId ?? snapshot.current?.windowName;
851
894
  const currentPane = snapshot.current ? paneForWindow(snapshot.current) : null;
@@ -933,7 +976,130 @@ export function createApplicationTerminalInteractionController(
933
976
  maybeRequestWindowSwitchFrame();
934
977
  maybeRequestPaneResizeFrame();
935
978
  },
936
- selectPane: (paneId) => paneInput.selectPane(paneId),
979
+ selectPane: (paneId) => {
980
+ finishLinkSelection(false);
981
+ paneInput.selectPane(paneId);
982
+ },
983
+ async unlinkWindowLink(target) {
984
+ const expected = liveSelectionTarget();
985
+ const observed = windowLinkTarget(options.layout(), target.linkId);
986
+ if (
987
+ !expected ||
988
+ !observed ||
989
+ observed.liveSessionId !== target.liveSessionId ||
990
+ observed.linkRevision !== target.linkRevision ||
991
+ observed.expectedSemanticWindowId !== target.expectedSemanticWindowId
992
+ )
993
+ return "Window link changed; reopen its menu";
994
+ try {
995
+ if (
996
+ !expected.client.ownsRuntimeAuthority?.("input") &&
997
+ !(await expected.client.requestAuthority("input"))
998
+ )
999
+ return "Window unlink requires input control";
1000
+ const current = liveSelectionTarget();
1001
+ if (
1002
+ current?.client !== expected.client ||
1003
+ current.daemonGeneration !== expected.daemonGeneration
1004
+ )
1005
+ return "Session changed; reopen the window menu";
1006
+ await expected.client.dispatch({
1007
+ kind: "semantic-intent",
1008
+ intent: {
1009
+ verb: "workspace.window.link.unlink",
1010
+ workspaceName: expected.workspaceName,
1011
+ target,
1012
+ },
1013
+ });
1014
+ return "Window link removed";
1015
+ } catch {
1016
+ return "Window link could not be removed; it may be the last link or have changed";
1017
+ }
1018
+ },
1019
+ async selectWindowLink(target) {
1020
+ const expected = liveSelectionTarget();
1021
+ const snapshot = options.layout();
1022
+ const observed = windowLinkTarget(snapshot, target.linkId);
1023
+ const backing = snapshot.windows.find(
1024
+ (window) => window.semanticWindowId === target.expectedSemanticWindowId,
1025
+ );
1026
+ const paneId = backing && paneForWindow(backing);
1027
+ if (
1028
+ !expected ||
1029
+ !paneId ||
1030
+ !observed ||
1031
+ observed.liveSessionId !== target.liveSessionId ||
1032
+ observed.linkRevision !== target.linkRevision ||
1033
+ observed.expectedSemanticWindowId !== target.expectedSemanticWindowId
1034
+ )
1035
+ return;
1036
+ finishLinkSelection(false);
1037
+ let resolve!: (selected: boolean) => void;
1038
+ const settled = new Promise<boolean>((done) => {
1039
+ resolve = done;
1040
+ });
1041
+ const pending = {
1042
+ target,
1043
+ paneId,
1044
+ layout: snapshot,
1045
+ received: false,
1046
+ resolve,
1047
+ timer: setTimeout(() => {
1048
+ if (linkSelection === pending) finishLinkSelection(false);
1049
+ }, 5_000),
1050
+ };
1051
+ pending.timer.unref?.();
1052
+ linkSelection = pending;
1053
+ // Register input ordering synchronously, before authority or command awaits.
1054
+ paneInput.selectPane(paneId, { presentOptimistically: false, selection: () => settled });
1055
+ try {
1056
+ if (
1057
+ !expected.client.ownsRuntimeAuthority?.("input") &&
1058
+ !(await expected.client.requestAuthority("input"))
1059
+ ) {
1060
+ if (linkSelection === pending) finishLinkSelection(false);
1061
+ return;
1062
+ }
1063
+ const current = liveSelectionTarget();
1064
+ if (
1065
+ linkSelection !== pending ||
1066
+ current?.client !== expected.client ||
1067
+ current.daemonGeneration !== expected.daemonGeneration
1068
+ ) {
1069
+ if (linkSelection === pending) finishLinkSelection(false);
1070
+ return;
1071
+ }
1072
+ const wrapper = (await expected.client.dispatch({
1073
+ kind: "semantic-intent",
1074
+ intent: {
1075
+ verb: "workspace.window.link.select",
1076
+ workspaceName: expected.workspaceName,
1077
+ target,
1078
+ },
1079
+ })) as { kind?: string; operationId?: string; result?: unknown };
1080
+ const result = WorkspaceWindowLinkSelectResultSchemaZ.safeParse(wrapper.result);
1081
+ if (linkSelection !== pending) return;
1082
+ if (
1083
+ wrapper.kind !== "semantic-intent" ||
1084
+ !result.success ||
1085
+ result.data.operationId !== wrapper.operationId ||
1086
+ result.data.daemonInstanceId !== expected.daemonGeneration ||
1087
+ result.data.workspaceName !== expected.workspaceName ||
1088
+ result.data.target.linkId !== target.linkId ||
1089
+ result.data.target.liveSessionId !== target.liveSessionId ||
1090
+ result.data.target.linkRevision !== target.linkRevision ||
1091
+ result.data.target.expectedSemanticWindowId !== target.expectedSemanticWindowId
1092
+ ) {
1093
+ finishLinkSelection(false);
1094
+ return;
1095
+ }
1096
+ pending.received = true;
1097
+ reconcileLinkSelection(pending.layout);
1098
+ await settled;
1099
+ } catch {
1100
+ if (linkSelection === pending) finishLinkSelection(false);
1101
+ }
1102
+ },
937
1103
  sendInput: async (input, parserOrigin) => {
938
1104
  await paneInput.sendInput({ input, parserOrigin });
939
1105
  },
@@ -1152,6 +1318,15 @@ export function createApplicationTerminalInteractionController(
1152
1318
  if (next) paneInput.selectPane(next);
1153
1319
  },
1154
1320
  cycleWindow() {
1321
+ const topology = options.layout().windowLinks;
1322
+ if (topology) {
1323
+ if (topology.links.length < 2) return;
1324
+ const current = topology.links.findIndex((link) => link.linkId === topology.activeLinkId);
1325
+ const next = topology.links[(current + 1) % topology.links.length]!;
1326
+ const target = windowLinkTarget(options.layout(), next.linkId);
1327
+ if (target) void this.selectWindowLink(target);
1328
+ return;
1329
+ }
1155
1330
  const windows = options.layout().windows;
1156
1331
  if (windows.length < 2) return;
1157
1332
  const current = windows.findIndex((window) => window.currentWindow);
@@ -1,3 +1,4 @@
1
+ import type { WindowLinkTarget } from "@tmux-ide/contracts";
1
2
  import type { AgentActivity } from "@tmux-ide/contracts";
2
3
 
3
4
  import type { OpenTuiWorkspaceLayoutSnapshot } from "../open-tui-workspace-runtime-port.ts";
@@ -309,3 +310,64 @@ export function terminalPaneResizePreview(
309
310
  }),
310
311
  });
311
312
  }
313
+
314
+ /** Resolve only live opaque observations; native indexes are display metadata. */
315
+ export function windowLinkTarget(
316
+ snapshot: OpenTuiWorkspaceLayoutSnapshot,
317
+ linkId: string,
318
+ ): WindowLinkTarget | null {
319
+ const topology = snapshot.windowLinks;
320
+ const link = topology?.links.find((candidate) => candidate.linkId === linkId);
321
+ if (!topology || !link) return null;
322
+ return {
323
+ liveSessionId: topology.liveSessionId,
324
+ linkRevision: topology.linkRevision,
325
+ linkId: link.linkId,
326
+ expectedSemanticWindowId: link.semanticWindowId,
327
+ };
328
+ }
329
+
330
+ /** Pane navigation within the active link must not silently choose a sibling link. */
331
+ export function windowLinkForPane(
332
+ snapshot: OpenTuiWorkspaceLayoutSnapshot,
333
+ paneId: string,
334
+ ): WindowLinkTarget | null {
335
+ const backing = snapshot.windows.find((window) =>
336
+ window.panes.some((pane) => pane.pane === paneId),
337
+ );
338
+ const topology = snapshot.windowLinks;
339
+ if (!backing || !topology) return null;
340
+ const links = topology.links.filter((link) => link.semanticWindowId === backing.semanticWindowId);
341
+ const link =
342
+ links.find((candidate) => candidate.linkId === topology.activeLinkId) ??
343
+ (links.length === 1 ? links[0] : undefined);
344
+ return link ? windowLinkTarget(snapshot, link.linkId) : null;
345
+ }
346
+
347
+ /** Root composition for backing zoom and link actions, all restoring host focus. */
348
+ export function terminalWindowActionCallbacks(
349
+ controller: Pick<
350
+ import("./application-terminal-interaction-controller.ts").ApplicationTerminalInteractionController,
351
+ "selectWindowLink" | "unlinkWindowLink" | "zoomPane" | "selectPane"
352
+ >,
353
+ notify: (message: string) => void,
354
+ recover: import("./application-host-focus-presentation.ts").ApplicationHostFocusRecovery,
355
+ cancelNavigation: () => void,
356
+ ) {
357
+ return {
358
+ onSelectPane: recover((paneId: string) => {
359
+ cancelNavigation();
360
+ controller.selectPane(paneId);
361
+ }),
362
+ onZoomPane: recover((paneId?: string) => {
363
+ void controller.zoomPane(paneId).then(notify);
364
+ }),
365
+ onSelectWindowLink: recover((target: WindowLinkTarget) => {
366
+ cancelNavigation();
367
+ void controller.selectWindowLink(target);
368
+ }),
369
+ onUnlinkWindowLink: recover((target: WindowLinkTarget) => {
370
+ void controller.unlinkWindowLink(target).then(notify);
371
+ }),
372
+ };
373
+ }