tmux-ide 2.9.0-beta.21 → 2.9.0-beta.22

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 (173) hide show
  1. package/bin/cli.js +52253 -49070
  2. package/package.json +1 -1
  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/index.ts +2 -0
  15. package/packages/contracts/src/interaction-receipts.ts +39 -0
  16. package/packages/contracts/src/issue-error.ts +1 -1
  17. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  18. package/packages/contracts/src/pane-stream.ts +51 -8
  19. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  20. package/packages/contracts/src/window-links.ts +76 -0
  21. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  22. package/packages/core/src/interaction-receipts.ts +4 -0
  23. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  24. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  25. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  26. package/packages/daemon/dist/command-center/discovery.js +2 -9
  27. package/packages/daemon/dist/command-center/server.js +21 -1
  28. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  29. package/packages/daemon/dist/command-center/tmux-servers.js +345 -0
  30. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  31. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  32. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  33. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  34. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +16 -10
  35. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  36. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  37. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  38. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  39. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  40. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  41. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  42. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  43. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  44. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  46. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  47. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  48. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  49. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  50. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  51. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  52. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  53. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  54. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  55. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  56. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  57. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  58. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  59. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  60. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  61. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  62. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  63. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  64. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  65. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  66. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  67. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  68. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  70. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  71. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  72. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  73. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  74. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  75. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  76. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  77. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  78. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
  79. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  80. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  81. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  82. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  83. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  84. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  85. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +42 -1
  86. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  87. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  88. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  89. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  90. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  91. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  92. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  93. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  94. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  95. package/packages/daemon/src/command-center/discovery.ts +2 -10
  96. package/packages/daemon/src/command-center/server.ts +29 -1
  97. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  98. package/packages/daemon/src/command-center/tmux-servers.ts +468 -0
  99. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  100. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  101. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  102. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  103. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +16 -8
  104. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  105. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  106. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  107. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  108. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  109. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  110. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  111. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  112. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  113. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  114. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  115. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  116. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  117. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  118. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  119. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  120. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  121. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  122. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  123. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  124. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  125. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  126. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  127. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  128. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  129. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  130. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  131. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  132. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  133. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  134. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  135. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  136. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  137. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  138. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  139. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  140. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  141. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  142. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  143. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  144. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  145. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  146. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  147. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  148. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  149. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -36
  150. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  151. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  152. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  153. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  154. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
  155. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  156. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -0
  157. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  158. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  159. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  160. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +52 -0
  161. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  162. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  163. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  164. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  165. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  166. package/packages/daemon-client/package.json +3 -1
  167. package/packages/daemon-client/src/index.ts +2 -0
  168. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  169. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  170. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  171. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  172. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  173. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -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,8 @@ export function createApplicationMachineNavigation(options: {
242
360
  catalog,
243
361
  agents,
244
362
  selectedMachineId: () => manager.snapshot().selectedMachineId,
363
+ openSelectedSession: (name: string, source: "keyboard" | "mouse", key?: string) =>
364
+ open(manager.snapshot().selectedMachineId, name, source, true, undefined, undefined, key),
245
365
  automaticOpen: manager.snapshot().machines.length === 1,
246
366
  automaticOpenAllowed: () =>
247
367
  navigation === 0 && manager.snapshot().selectedMachineId === "local",
@@ -262,7 +382,18 @@ export function createApplicationMachineNavigation(options: {
262
382
  options.setNote("That session is unavailable or has been replaced.");
263
383
  return;
264
384
  }
265
- if (await open(group.id, session.name, "keyboard", false)) historyIndex = next;
385
+ if (
386
+ await open(
387
+ group.id,
388
+ session.name,
389
+ "keyboard",
390
+ false,
391
+ session.liveSessionId,
392
+ session.server,
393
+ session.id,
394
+ )
395
+ )
396
+ historyIndex = next;
266
397
  },
267
398
  cycleTab(direction: -1 | 1, close = false) {
268
399
  const value = tabs.snapshot();
@@ -276,10 +407,22 @@ export function createApplicationMachineNavigation(options: {
276
407
  if (target) void tabs.activate(target.key);
277
408
  },
278
409
  closeSwitcher: () => setSwitching(false),
410
+ switcherFilter: (command: ApplicationPaletteCommand) => {
411
+ const focus = serverFocus();
412
+ return (
413
+ !focus ||
414
+ (typeof command === "object" &&
415
+ command.fleet?.machineId === focus.machineId &&
416
+ command.fleet?.server?.serverId === focus.server.serverId &&
417
+ command.fleet?.server?.generation === focus.server.generation)
418
+ );
419
+ },
279
420
  paletteCommands(): readonly ApplicationPaletteCommand[] {
280
421
  const groupsById = new Map(agentGroups().map((group) => [group.machineId, group]));
281
422
  return snapshot().groups.flatMap((group) => {
282
- const agentGroup = groupsById.get(group.id);
423
+ const agentGroup = !group.sessions.some((session) => isDefaultSession(group.id, session))
424
+ ? undefined
425
+ : groupsById.get(group.id);
283
426
  const agentsBySession = new Map<string, ApplicationMachineAgent[]>();
284
427
  for (const agent of agentGroup?.agents ?? []) {
285
428
  if (!agent.liveSessionId || !agent.paneId) continue;
@@ -297,20 +440,25 @@ export function createApplicationMachineNavigation(options: {
297
440
  ? saved().recent.indexOf(session.id)
298
441
  : 1000,
299
442
  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,
443
+ server: session.server,
444
+ hostLabel: `${group.label}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
445
+ agentActivities:
446
+ isDefaultSession(group.id, session) && agentGroup?.available
447
+ ? (agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
448
+ paneId: a.paneId!,
449
+ attention: a.attention,
450
+ activity: a.activity,
451
+ }))
452
+ : undefined,
308
453
  daemonInstanceId: daemon?.instanceId ?? "",
309
454
  disabled: session.disabled || group.state !== "ready",
310
455
  };
311
456
  return [
312
457
  { kind: "open-session", sessionName: session.name, label: session.name, fleet },
313
- ...(agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
458
+ ...(isDefaultSession(group.id, session)
459
+ ? (agentsBySession.get(session.liveSessionId) ?? [])
460
+ : []
461
+ ).map((a) => ({
314
462
  kind: "jump-agent" as const,
315
463
  sessionName: session.name,
316
464
  paneId: a.paneId!,
@@ -332,6 +480,21 @@ export function createApplicationMachineNavigation(options: {
332
480
  disabled: group.state !== "ready",
333
481
  },
334
482
  },
483
+ ...(group.servers ?? []).map((server) => ({
484
+ kind: "open-machine" as const,
485
+ sessionName: "" as const,
486
+ label: `${server.label} · ${server.serverId.slice(-6)} · New session ^N`,
487
+ fleet: {
488
+ machineId: group.id,
489
+ hostLabel: `${group.label} / ${server.label}`,
490
+ liveSessionId: "",
491
+ daemonInstanceId: daemon?.instanceId ?? "",
492
+ disabled: group.state !== "ready" || server.state !== "online",
493
+ ...(server.state === "online"
494
+ ? { server: { serverId: server.serverId, generation: server.generation } }
495
+ : {}),
496
+ },
497
+ })),
335
498
  ...sessions,
336
499
  ];
337
500
  });
@@ -341,7 +504,12 @@ export function createApplicationMachineNavigation(options: {
341
504
  const target = command.fleet;
342
505
  const session = snapshot()
343
506
  .groups.find((g) => g.id === target.machineId)
344
- ?.sessions.find((s) => s.liveSessionId === target.liveSessionId);
507
+ ?.sessions.find(
508
+ (s) =>
509
+ s.liveSessionId === target.liveSessionId &&
510
+ s.server?.serverId === target.server?.serverId &&
511
+ s.server?.generation === target.server?.generation,
512
+ );
345
513
  if (session)
346
514
  preferences.change({
347
515
  type: "favorite",
@@ -357,6 +525,22 @@ export function createApplicationMachineNavigation(options: {
357
525
  if (!target) return;
358
526
  const handle = manager.getMachine(target.machineId);
359
527
  if (command.kind === "open-machine") {
528
+ if (target.server) {
529
+ const descriptor = snapshot()
530
+ .groups.find((group) => group.id === target.machineId)
531
+ ?.servers?.find(
532
+ (server) =>
533
+ server.serverId === target.server?.serverId &&
534
+ server.generation === target.server.generation &&
535
+ server.state === "online",
536
+ );
537
+ if (!descriptor || handle?.read()?.instanceId !== target.daemonInstanceId) {
538
+ options.setNote("That server has changed. Select it again.");
539
+ return;
540
+ }
541
+ sidebar.onSelectServer?.(target.machineId, target.server);
542
+ return;
543
+ }
360
544
  if (
361
545
  handle?.endpoint().state === "ready" &&
362
546
  handle.read()?.instanceId === target.daemonInstanceId
@@ -369,7 +553,11 @@ export function createApplicationMachineNavigation(options: {
369
553
  (g) => g.id === target.machineId || g.routeIds?.includes(target.machineId),
370
554
  );
371
555
  const session = group?.sessions.find(
372
- (s) => s.liveSessionId === target.liveSessionId && !s.disabled,
556
+ (s) =>
557
+ s.liveSessionId === target.liveSessionId &&
558
+ !s.disabled &&
559
+ s.server?.serverId === target.server?.serverId &&
560
+ s.server?.generation === target.server?.generation,
373
561
  );
374
562
  if (
375
563
  !session ||
@@ -381,7 +569,15 @@ export function createApplicationMachineNavigation(options: {
381
569
  }
382
570
  if (command.kind === "jump-agent")
383
571
  sidebar.onOpenAgent?.(target.machineId, session.name, command.paneId, source);
384
- else await open(target.machineId, session.name, source, true, target.liveSessionId);
572
+ else
573
+ await open(
574
+ target.machineId,
575
+ session.name,
576
+ source,
577
+ true,
578
+ target.liveSessionId,
579
+ target.server,
580
+ );
385
581
  },
386
582
  adding,
387
583
  alias,
@@ -433,16 +629,47 @@ export function createApplicationMachineNavigation(options: {
433
629
  setError(cause instanceof Error ? cause.message : "Could not add this machine.");
434
630
  }
435
631
  },
436
- start(target: string | null) {
632
+ start(target: string | null, serverId?: string | null) {
437
633
  agents.start();
438
634
  catalog.start();
439
- if (!target) return;
635
+ if (!target && !serverId) return;
440
636
  const id = manager.snapshot().selectedMachineId;
441
637
  const token = navigation;
638
+
639
+ let machineReady = false;
640
+ const attempt = () => {
641
+ if (!machineReady) return;
642
+ if (token !== navigation) {
643
+ attemptStartup = null;
644
+ return;
645
+ }
646
+ const group = snapshot().groups.find((group) => group.id === id);
647
+ if (group?.state !== "ready") return;
648
+ const matches = group.sessions.filter(
649
+ (row) =>
650
+ !row.disabled &&
651
+ (!serverId || row.server?.serverId === serverId) &&
652
+ (!target || row.name === target),
653
+ );
654
+ if (matches.length !== 1) {
655
+ options.setNote(
656
+ matches.length
657
+ ? "Select a session under the requested tmux server."
658
+ : "Requested server or session is unavailable. Select it again.",
659
+ );
660
+ attemptStartup = null;
661
+ return;
662
+ }
663
+ const row = matches[0]!;
664
+ attemptStartup = null;
665
+ void open(id, row.name, "keyboard", true, row.liveSessionId, row.server, row.id);
666
+ };
667
+ attemptStartup = attempt;
442
668
  void manager.getMachine(id)?.ready.then((ready) => {
443
- if (ready && token === navigation && manager.snapshot().selectedMachineId === id)
444
- void open(id, target, "keyboard");
669
+ machineReady = ready;
670
+ attemptStartup?.();
445
671
  });
672
+ attempt();
446
673
  },
447
674
  };
448
675
  }
@@ -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
  }}