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,4 @@
1
+ import type { TmuxServerDescriptor } from "@tmux-ide/contracts";
1
2
  import type { FleetCachedRoute } from "@tmux-ide/contracts/fleet-client-state";
2
3
  import { groupFleetEnvironments } from "@tmux-ide/daemon-client/fleet-environments";
3
4
  import {
@@ -27,6 +28,7 @@ export interface ApplicationMachineCatalogGroup {
27
28
  readonly label: string;
28
29
  readonly state: "ready" | "connecting" | "disconnected";
29
30
  readonly sessions: readonly ApplicationMachineCatalogSession[];
31
+ readonly servers?: readonly TmuxServerDescriptor[];
30
32
  readonly note: string | null;
31
33
  readonly diagnostic?: FleetConnectionStatus;
32
34
  readonly environmentId?: string | null;
@@ -114,6 +116,7 @@ export function createApplicationMachineCatalog(
114
116
  diagnostic && diagnostic.phase !== "ready"
115
117
  ? fleetConnectionMessage(diagnostic)
116
118
  : (entry?.snapshot.note ?? null),
119
+ servers: entry?.snapshot.servers,
117
120
  sessions: (entry?.lastSessions ?? []).map((session) => ({
118
121
  ...session,
119
122
  sourceId: session.id,
@@ -152,7 +155,13 @@ export function createApplicationMachineCatalog(
152
155
  ...session,
153
156
  id:
154
157
  session.liveSessionId && joined.environmentId && !joined.conflict
155
- ? JSON.stringify([joined.environmentId, "session", session.liveSessionId])
158
+ ? JSON.stringify([
159
+ joined.environmentId,
160
+ "session",
161
+ session.server?.serverId,
162
+ session.server?.generation,
163
+ session.liveSessionId,
164
+ ])
156
165
  : session.id,
157
166
  })),
158
167
  };
@@ -214,12 +223,15 @@ export function createApplicationMachineCatalog(
214
223
  environmentId: entry.environmentId,
215
224
  generation: entry.generation,
216
225
  seenAt: entry.seenAt,
217
- sessions: value.sessions.slice(0, 64).map(({ id, liveSessionId, name, paneCount }) => ({
218
- id,
219
- ...(liveSessionId ? { liveSessionId } : {}),
220
- name,
221
- paneCount,
222
- })),
226
+ sessions: value.sessions
227
+ .slice(0, 64)
228
+ .map(({ id, liveSessionId, name, paneCount, server, serverLabel }) => ({
229
+ id,
230
+ ...(liveSessionId ? { liveSessionId } : {}),
231
+ name,
232
+ paneCount,
233
+ ...(server ? { server, serverLabel } : {}),
234
+ })),
223
235
  });
224
236
  } catch {
225
237
  /* Cache failure must never prevent live catalog publication. */
@@ -1,3 +1,5 @@
1
+ import type { OpenTuiGenerationHostSnapshot } from "./open-tui-generation-host.ts";
2
+ import type { TmuxServerScope } from "@tmux-ide/contracts";
1
3
  import { fleetHostColor } from "./fleet-presentation.ts";
2
4
  import type { ApplicationPaletteCommand } from "./application-palette-input.ts";
3
5
  import { createFleetTabs, type FleetTabTarget } from "./application-fleet-tabs.ts";
@@ -13,13 +15,30 @@ import { createApplicationMachineCatalog } from "./application-machine-catalog.t
13
15
  import { ephemeralMachineProfile } from "./application-machine-startup.ts";
14
16
  import type { ApplicationMachineSidebarModel } from "./application-machine-sidebar.tsx";
15
17
 
18
+ /** Exact attached authority; an unscoped default connection still proves its incarnation. */
19
+ function attachedMachineNavigationTarget(active: OpenTuiGenerationHostSnapshot | null) {
20
+ if (active?.status !== "live" || !active.connection?.liveSessionId || !active.daemonGeneration)
21
+ return null;
22
+ return {
23
+ liveSessionId: active.connection.liveSessionId,
24
+ daemonGeneration: active.daemonGeneration,
25
+ ...(active.connection.server ? { server: active.connection.server } : {}),
26
+ };
27
+ }
28
+
16
29
  export function createApplicationMachineNavigation(options: {
17
- resetWorkspace(machineId: string, expectedLiveSessionId?: string): void;
30
+ resetWorkspace(machineId: string, expectedLiveSessionId?: string, server?: TmuxServerScope): void;
18
31
  openAgent?(agent: ApplicationMachineAgent, source: "keyboard" | "mouse"): Promise<unknown> | void;
19
32
  cancelOpen(): void;
20
33
  openSession(name: string, source: "keyboard" | "mouse"): Promise<unknown>;
21
34
  sessionName(): string | null;
22
35
  activePaneId?(): string | null;
36
+ attachedGeneration?(): OpenTuiGenerationHostSnapshot | null;
37
+ attachedTarget?(): {
38
+ liveSessionId: string;
39
+ daemonGeneration: string;
40
+ server?: TmuxServerScope;
41
+ } | null;
23
42
  setSurface(value: "home" | "terminals"): void;
24
43
  setNote(value: string | null | ((current: string | null) => string | null)): void;
25
44
  }) {
@@ -45,10 +64,15 @@ export function createApplicationMachineNavigation(options: {
45
64
  const [snapshot, setSnapshot] = createSignal(catalog.getSnapshot());
46
65
  const [focused, setFocused] = createSignal(false);
47
66
  const [switching, setSwitching] = createSignal(false);
67
+ const [serverFocus, setServerFocus] = createSignal<{
68
+ machineId: string;
69
+ server: TmuxServerScope;
70
+ } | null>(null);
48
71
  const [attentionOnly, setAttentionOnly] = createSignal(false);
49
72
  const showSwitcher = (attention: boolean) => {
50
73
  navigation++;
51
74
  options.cancelOpen();
75
+ setServerFocus(null);
52
76
  setAttentionOnly(attention);
53
77
  setFocused(false);
54
78
  setSwitching(true);
@@ -56,21 +80,41 @@ export function createApplicationMachineNavigation(options: {
56
80
  const [adding, setAdding] = createSignal(false);
57
81
  const [alias, setAlias] = createSignal("");
58
82
  const [error, setError] = createSignal<string | null>(null);
59
- const stop = catalog.subscribe(setSnapshot);
83
+ let attemptStartup: (() => void) | null = null;
84
+ const stop = catalog.subscribe((value) => {
85
+ setSnapshot(value);
86
+ attemptStartup?.();
87
+ });
60
88
  let navigation = 0;
89
+ const [activeSessionKey, setActiveSessionKey] = createSignal<string | null>(null);
61
90
  const [tabRevision, setTabRevision] = createSignal(0);
62
91
  const history: string[] = [...saved().recent].reverse();
63
92
  let historyIndex = history.length - 1;
64
- const select = (id: string, expectedLiveSessionId?: string) => {
93
+ const select = (id: string, expectedLiveSessionId?: string, server?: TmuxServerScope) => {
65
94
  if (!manager.getMachine(id)) return false;
66
95
  if (manager.snapshot().selectedMachineId !== id) {
67
96
  navigation++;
68
97
  options.cancelOpen();
69
98
  // Synchronously remove the old input/shell owner before changing the route.
70
- options.resetWorkspace(id, expectedLiveSessionId);
99
+ if (server) options.resetWorkspace(id, expectedLiveSessionId, server);
100
+ else options.resetWorkspace(id, expectedLiveSessionId);
71
101
  manager.select(id);
72
102
  } else if (expectedLiveSessionId) {
73
- options.resetWorkspace(id, expectedLiveSessionId);
103
+ const attached =
104
+ options.attachedTarget?.() ??
105
+ attachedMachineNavigationTarget(options.attachedGeneration?.() ?? null);
106
+ const sameAuthority =
107
+ attached?.liveSessionId === expectedLiveSessionId &&
108
+ (server
109
+ ? attached.server
110
+ ? attached.server.serverId === server.serverId &&
111
+ attached.server.generation === server.generation
112
+ : attached.daemonGeneration === server.generation &&
113
+ manager.getMachine(id)?.read()?.instanceId === server.generation
114
+ : !attached.server);
115
+ if (sameAuthority) return true;
116
+ if (server) options.resetWorkspace(id, expectedLiveSessionId, server);
117
+ else options.resetWorkspace(id, expectedLiveSessionId);
74
118
  }
75
119
  return true;
76
120
  };
@@ -80,18 +124,44 @@ export function createApplicationMachineNavigation(options: {
80
124
  source: "keyboard" | "mouse",
81
125
  remember = true,
82
126
  expectedLiveSessionId?: string,
127
+ server?: TmuxServerScope,
128
+ sessionKey?: string,
83
129
  ) => {
84
130
  // A same-machine session click supersedes any pending exact-agent focus too.
85
131
  if (manager.snapshot().selectedMachineId === id) options.cancelOpen();
86
132
  // A sidebar selection can leave a tab whose route is pinned to a different
87
133
  // live incarnation on this same machine. Select a new exact owner before
88
134
  // preparation; never apply that old tab's identity fence to the new name.
89
- if (!expectedLiveSessionId && options.sessionName() !== name) {
90
- expectedLiveSessionId = snapshot()
135
+ if (manager.getMachine(id)?.endpoint().state !== "ready") {
136
+ select(id);
137
+ options.setNote("This machine is disconnected. Select the session after reconnecting.");
138
+ return false;
139
+ }
140
+ const candidates =
141
+ snapshot()
91
142
  .groups.find((group) => group.id === id || group.routeIds?.includes(id))
92
- ?.sessions.find((row) => row.name === name && !row.disabled)?.liveSessionId;
143
+ ?.sessions.filter(
144
+ (row) =>
145
+ row.name === name &&
146
+ !row.disabled &&
147
+ (!sessionKey || row.id === sessionKey) &&
148
+ (!expectedLiveSessionId || row.liveSessionId === expectedLiveSessionId) &&
149
+ (!server ||
150
+ (row.server?.serverId === server.serverId &&
151
+ row.server.generation === server.generation)),
152
+ ) ?? [];
153
+ if (candidates.length !== 1) {
154
+ options.setNote(
155
+ "Select the session under its tmux server; this target is ambiguous or has changed.",
156
+ );
157
+ return false;
93
158
  }
94
- if (!select(id, expectedLiveSessionId)) return;
159
+ const selected = candidates[0]!;
160
+ const sameSelection = activeSessionKey() === selected.id && options.sessionName() === name;
161
+ setActiveSessionKey(selected.id);
162
+ expectedLiveSessionId = selected.liveSessionId;
163
+ server = selected.server;
164
+ if (!select(id, sameSelection ? undefined : expectedLiveSessionId, server)) return;
95
165
  const token = ++navigation;
96
166
  setFocused(false);
97
167
  options.setSurface("terminals");
@@ -111,15 +181,21 @@ export function createApplicationMachineNavigation(options: {
111
181
  if (token === navigation) {
112
182
  const session = snapshot()
113
183
  .groups.find((group) => group.id === id || group.routeIds?.includes(id))
114
- ?.sessions.find((row) => row.name === name && !row.disabled);
184
+ ?.sessions.find((row) => row.id === selected.id && !row.disabled);
115
185
  if (session) {
116
186
  if (session.liveSessionId)
117
187
  tabs.remember({
118
- key: JSON.stringify([id, session.liveSessionId]),
188
+ key: JSON.stringify([
189
+ id,
190
+ session.server?.serverId,
191
+ session.server?.generation,
192
+ session.liveSessionId,
193
+ ]),
194
+ server: session.server,
119
195
  machineId: id,
120
196
  liveSessionId: session.liveSessionId,
121
197
  label: session.name,
122
- hostLabel: manager.getMachine(id)?.label ?? id,
198
+ hostLabel: `${manager.getMachine(id)?.label ?? id}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
123
199
  });
124
200
  preferences.change({ type: "visit", key: session.id });
125
201
  if (remember && history[historyIndex] !== session.id) {
@@ -138,30 +214,57 @@ export function createApplicationMachineNavigation(options: {
138
214
  (g) => g.id === target.machineId || g.routeIds?.includes(target.machineId),
139
215
  );
140
216
  const row = group?.sessions.find(
141
- (s) => s.liveSessionId === target.liveSessionId && !s.disabled,
217
+ (s) =>
218
+ s.liveSessionId === target.liveSessionId &&
219
+ !s.disabled &&
220
+ s.server?.serverId === target.server?.serverId &&
221
+ s.server?.generation === target.server?.generation,
142
222
  );
143
223
  return row ? { ...target, label: row.name } : null;
144
224
  };
145
225
  const tabs = createFleetTabs({
146
226
  resolve: resolveTab,
147
227
  retireActive: () => {
228
+ setActiveSessionKey(null);
148
229
  navigation++;
149
230
  options.cancelOpen();
150
231
  options.resetWorkspace(manager.snapshot().selectedMachineId);
151
232
  options.setSurface("home");
152
233
  },
153
234
  open: async (target) =>
154
- !!(await open(target.machineId, target.label, "keyboard", true, target.liveSessionId)),
235
+ !!(await open(
236
+ target.machineId,
237
+ target.label,
238
+ "keyboard",
239
+ true,
240
+ target.liveSessionId,
241
+ target.server,
242
+ )),
155
243
  publish: () => setTabRevision((value) => value + 1),
156
244
  unavailable: () => options.setNote("That tab's session is unavailable or has been replaced."),
157
245
  });
246
+ const isDefaultSession = (machineId: string, session: { server?: TmuxServerScope }) =>
247
+ !session.server ||
248
+ session.server.generation === manager.getMachine(machineId)?.read()?.instanceId;
158
249
  const sidebar: ApplicationMachineSidebarModel = {
159
250
  groups: () =>
160
251
  snapshot().groups.map((group) => ({
161
252
  ...group,
162
253
  agentsAvailable:
163
- agentGroups().find((value) => value.machineId === group.id)?.available ?? false,
164
- agents: agentGroups().find((value) => value.machineId === group.id)?.agents ?? [],
254
+ group.sessions.some((session) => isDefaultSession(group.id, session)) &&
255
+ (agentGroups().find((value) => value.machineId === group.id)?.available ?? false),
256
+ agents: !group.sessions.some((session) => isDefaultSession(group.id, session))
257
+ ? []
258
+ : (agentGroups().find((value) => value.machineId === group.id)?.agents ?? []).map(
259
+ (agent) => ({
260
+ ...agent,
261
+ server: group.sessions.find(
262
+ (session) =>
263
+ session.liveSessionId === agent.liveSessionId &&
264
+ isDefaultSession(group.id, session),
265
+ )?.server,
266
+ }),
267
+ ),
165
268
  })),
166
269
  tabs: () => {
167
270
  tabRevision();
@@ -182,6 +285,7 @@ export function createApplicationMachineNavigation(options: {
182
285
  onCollapse: (key, enabled) => preferences.change({ type: "collapse", key, enabled }),
183
286
  activeMachineId: () => snapshot().selectedMachineId,
184
287
  activeSessionName: options.sessionName,
288
+ activeSessionKey,
185
289
  activePaneId: options.activePaneId,
186
290
  focused,
187
291
  onFocus: () => {
@@ -190,8 +294,17 @@ export function createApplicationMachineNavigation(options: {
190
294
  setFocused(true);
191
295
  },
192
296
  onBlur: () => setFocused(false),
193
- onOpen: (id, name, source) => void open(id, name, source),
297
+ onOpen: (id, name, source, sessionKey) =>
298
+ void open(id, name, source, true, undefined, undefined, sessionKey),
194
299
  onOpenAgent: (id, sessionName, paneId, source) => {
300
+ const sessions = snapshot().groups.find((group) => group.id === id)?.sessions ?? [];
301
+ const selectedSession = sessions.find(
302
+ (session) => session.name === sessionName && isDefaultSession(id, session),
303
+ );
304
+ if (sessions.length && !selectedSession) {
305
+ options.setNote("Open this agent through its server session first.");
306
+ return;
307
+ }
195
308
  const row = agentGroups()
196
309
  .find((group) => group.machineId === id)
197
310
  ?.agents.find((agent) => agent.sessionName === sessionName && agent.paneId === paneId);
@@ -202,11 +315,16 @@ export function createApplicationMachineNavigation(options: {
202
315
  return;
203
316
  }
204
317
  options.cancelOpen();
205
- if (!select(id)) return;
318
+ if (!select(id, selectedSession?.liveSessionId, selectedSession?.server)) return;
319
+ if (selectedSession) setActiveSessionKey(selectedSession.id);
206
320
  navigation++;
207
321
  setFocused(false);
208
322
  void options.openAgent?.(row, source);
209
323
  },
324
+ onSelectServer: (machineId, server) => {
325
+ showSwitcher(false);
326
+ setServerFocus({ machineId, server });
327
+ },
210
328
  onSelectMachine: (id) => {
211
329
  tabs.suspend();
212
330
  navigation++;
@@ -242,6 +360,30 @@ export function createApplicationMachineNavigation(options: {
242
360
  catalog,
243
361
  agents,
244
362
  selectedMachineId: () => manager.snapshot().selectedMachineId,
363
+ openHomeAgent: (row: ApplicationMachineAgent, source: "keyboard" | "mouse") => {
364
+ if (!agents.isCurrentTarget(row.machineId, row) || row.disabled) return;
365
+ const session = snapshot()
366
+ .groups.find((group) => group.id === row.machineId)
367
+ ?.sessions.find(
368
+ (session) =>
369
+ session.liveSessionId === row.liveSessionId &&
370
+ !session.disabled &&
371
+ session.server?.serverId === row.server?.serverId &&
372
+ session.server?.generation === row.server?.generation,
373
+ );
374
+ if (!session) {
375
+ options.setNote("That agent session has changed. Select it again.");
376
+ return;
377
+ }
378
+ options.cancelOpen();
379
+ if (!select(row.machineId, row.liveSessionId, row.server)) return;
380
+ setActiveSessionKey(session.id);
381
+ navigation++;
382
+ setFocused(false);
383
+ return options.openAgent?.(row, source);
384
+ },
385
+ openSelectedSession: (name: string, source: "keyboard" | "mouse", key?: string) =>
386
+ open(manager.snapshot().selectedMachineId, name, source, true, undefined, undefined, key),
245
387
  automaticOpen: manager.snapshot().machines.length === 1,
246
388
  automaticOpenAllowed: () =>
247
389
  navigation === 0 && manager.snapshot().selectedMachineId === "local",
@@ -262,7 +404,18 @@ export function createApplicationMachineNavigation(options: {
262
404
  options.setNote("That session is unavailable or has been replaced.");
263
405
  return;
264
406
  }
265
- if (await open(group.id, session.name, "keyboard", false)) historyIndex = next;
407
+ if (
408
+ await open(
409
+ group.id,
410
+ session.name,
411
+ "keyboard",
412
+ false,
413
+ session.liveSessionId,
414
+ session.server,
415
+ session.id,
416
+ )
417
+ )
418
+ historyIndex = next;
266
419
  },
267
420
  cycleTab(direction: -1 | 1, close = false) {
268
421
  const value = tabs.snapshot();
@@ -276,10 +429,22 @@ export function createApplicationMachineNavigation(options: {
276
429
  if (target) void tabs.activate(target.key);
277
430
  },
278
431
  closeSwitcher: () => setSwitching(false),
432
+ switcherFilter: (command: ApplicationPaletteCommand) => {
433
+ const focus = serverFocus();
434
+ return (
435
+ !focus ||
436
+ (typeof command === "object" &&
437
+ command.fleet?.machineId === focus.machineId &&
438
+ command.fleet?.server?.serverId === focus.server.serverId &&
439
+ command.fleet?.server?.generation === focus.server.generation)
440
+ );
441
+ },
279
442
  paletteCommands(): readonly ApplicationPaletteCommand[] {
280
443
  const groupsById = new Map(agentGroups().map((group) => [group.machineId, group]));
281
444
  return snapshot().groups.flatMap((group) => {
282
- const agentGroup = groupsById.get(group.id);
445
+ const agentGroup = !group.sessions.some((session) => isDefaultSession(group.id, session))
446
+ ? undefined
447
+ : groupsById.get(group.id);
283
448
  const agentsBySession = new Map<string, ApplicationMachineAgent[]>();
284
449
  for (const agent of agentGroup?.agents ?? []) {
285
450
  if (!agent.liveSessionId || !agent.paneId) continue;
@@ -297,20 +462,25 @@ export function createApplicationMachineNavigation(options: {
297
462
  ? saved().recent.indexOf(session.id)
298
463
  : 1000,
299
464
  liveSessionId: session.liveSessionId,
300
- hostLabel: group.label,
301
- agentActivities: agentGroup?.available
302
- ? (agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
303
- paneId: a.paneId!,
304
- attention: a.attention,
305
- activity: a.activity,
306
- }))
307
- : undefined,
465
+ server: session.server,
466
+ hostLabel: `${group.label}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
467
+ agentActivities:
468
+ isDefaultSession(group.id, session) && agentGroup?.available
469
+ ? (agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
470
+ paneId: a.paneId!,
471
+ attention: a.attention,
472
+ activity: a.activity,
473
+ }))
474
+ : undefined,
308
475
  daemonInstanceId: daemon?.instanceId ?? "",
309
476
  disabled: session.disabled || group.state !== "ready",
310
477
  };
311
478
  return [
312
479
  { kind: "open-session", sessionName: session.name, label: session.name, fleet },
313
- ...(agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
480
+ ...(isDefaultSession(group.id, session)
481
+ ? (agentsBySession.get(session.liveSessionId) ?? [])
482
+ : []
483
+ ).map((a) => ({
314
484
  kind: "jump-agent" as const,
315
485
  sessionName: session.name,
316
486
  paneId: a.paneId!,
@@ -332,6 +502,21 @@ export function createApplicationMachineNavigation(options: {
332
502
  disabled: group.state !== "ready",
333
503
  },
334
504
  },
505
+ ...(group.servers ?? []).map((server) => ({
506
+ kind: "open-machine" as const,
507
+ sessionName: "" as const,
508
+ label: `${server.label} · ${server.serverId.slice(-6)} · New session ^N`,
509
+ fleet: {
510
+ machineId: group.id,
511
+ hostLabel: `${group.label} / ${server.label}`,
512
+ liveSessionId: "",
513
+ daemonInstanceId: daemon?.instanceId ?? "",
514
+ disabled: group.state !== "ready" || server.state !== "online",
515
+ ...(server.state === "online"
516
+ ? { server: { serverId: server.serverId, generation: server.generation } }
517
+ : {}),
518
+ },
519
+ })),
335
520
  ...sessions,
336
521
  ];
337
522
  });
@@ -341,7 +526,12 @@ export function createApplicationMachineNavigation(options: {
341
526
  const target = command.fleet;
342
527
  const session = snapshot()
343
528
  .groups.find((g) => g.id === target.machineId)
344
- ?.sessions.find((s) => s.liveSessionId === target.liveSessionId);
529
+ ?.sessions.find(
530
+ (s) =>
531
+ s.liveSessionId === target.liveSessionId &&
532
+ s.server?.serverId === target.server?.serverId &&
533
+ s.server?.generation === target.server?.generation,
534
+ );
345
535
  if (session)
346
536
  preferences.change({
347
537
  type: "favorite",
@@ -357,6 +547,22 @@ export function createApplicationMachineNavigation(options: {
357
547
  if (!target) return;
358
548
  const handle = manager.getMachine(target.machineId);
359
549
  if (command.kind === "open-machine") {
550
+ if (target.server) {
551
+ const descriptor = snapshot()
552
+ .groups.find((group) => group.id === target.machineId)
553
+ ?.servers?.find(
554
+ (server) =>
555
+ server.serverId === target.server?.serverId &&
556
+ server.generation === target.server.generation &&
557
+ server.state === "online",
558
+ );
559
+ if (!descriptor || handle?.read()?.instanceId !== target.daemonInstanceId) {
560
+ options.setNote("That server has changed. Select it again.");
561
+ return;
562
+ }
563
+ sidebar.onSelectServer?.(target.machineId, target.server);
564
+ return;
565
+ }
360
566
  if (
361
567
  handle?.endpoint().state === "ready" &&
362
568
  handle.read()?.instanceId === target.daemonInstanceId
@@ -369,7 +575,11 @@ export function createApplicationMachineNavigation(options: {
369
575
  (g) => g.id === target.machineId || g.routeIds?.includes(target.machineId),
370
576
  );
371
577
  const session = group?.sessions.find(
372
- (s) => s.liveSessionId === target.liveSessionId && !s.disabled,
578
+ (s) =>
579
+ s.liveSessionId === target.liveSessionId &&
580
+ !s.disabled &&
581
+ s.server?.serverId === target.server?.serverId &&
582
+ s.server?.generation === target.server?.generation,
373
583
  );
374
584
  if (
375
585
  !session ||
@@ -381,7 +591,15 @@ export function createApplicationMachineNavigation(options: {
381
591
  }
382
592
  if (command.kind === "jump-agent")
383
593
  sidebar.onOpenAgent?.(target.machineId, session.name, command.paneId, source);
384
- else await open(target.machineId, session.name, source, true, target.liveSessionId);
594
+ else
595
+ await open(
596
+ target.machineId,
597
+ session.name,
598
+ source,
599
+ true,
600
+ target.liveSessionId,
601
+ target.server,
602
+ );
385
603
  },
386
604
  adding,
387
605
  alias,
@@ -433,16 +651,47 @@ export function createApplicationMachineNavigation(options: {
433
651
  setError(cause instanceof Error ? cause.message : "Could not add this machine.");
434
652
  }
435
653
  },
436
- start(target: string | null) {
654
+ start(target: string | null, serverId?: string | null) {
437
655
  agents.start();
438
656
  catalog.start();
439
- if (!target) return;
657
+ if (!target && !serverId) return;
440
658
  const id = manager.snapshot().selectedMachineId;
441
659
  const token = navigation;
660
+
661
+ let machineReady = false;
662
+ const attempt = () => {
663
+ if (!machineReady) return;
664
+ if (token !== navigation) {
665
+ attemptStartup = null;
666
+ return;
667
+ }
668
+ const group = snapshot().groups.find((group) => group.id === id);
669
+ if (group?.state !== "ready") return;
670
+ const matches = group.sessions.filter(
671
+ (row) =>
672
+ !row.disabled &&
673
+ (!serverId || row.server?.serverId === serverId) &&
674
+ (!target || row.name === target),
675
+ );
676
+ if (matches.length !== 1) {
677
+ options.setNote(
678
+ matches.length
679
+ ? "Select a session under the requested tmux server."
680
+ : "Requested server or session is unavailable. Select it again.",
681
+ );
682
+ attemptStartup = null;
683
+ return;
684
+ }
685
+ const row = matches[0]!;
686
+ attemptStartup = null;
687
+ void open(id, row.name, "keyboard", true, row.liveSessionId, row.server, row.id);
688
+ };
689
+ attemptStartup = attempt;
442
690
  void manager.getMachine(id)?.ready.then((ready) => {
443
- if (ready && token === navigation && manager.snapshot().selectedMachineId === id)
444
- void open(id, target, "keyboard");
691
+ machineReady = ready;
692
+ attemptStartup?.();
445
693
  });
694
+ attempt();
446
695
  },
447
696
  };
448
697
  }
@@ -17,7 +17,7 @@ export function ApplicationMachineOverlays(props: {
17
17
  open={true}
18
18
  attentionOnly={props.machines.attentionOnly()}
19
19
  rows={[]}
20
- commands={props.machines.paletteCommands()}
20
+ commands={props.machines.paletteCommands().filter(props.machines.switcherFilter)}
21
21
  onActivate={(c) => {
22
22
  if (typeof c === "object") void props.machines.openPalette(c, "keyboard");
23
23
  }}