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
@@ -7,6 +7,16 @@ import { createSignal, onCleanup } from "solid-js";
7
7
  import { applicationMachineAuthorityManager as manager } from "./application-machine-authority.js";
8
8
  import { createApplicationMachineCatalog } from "./application-machine-catalog.js";
9
9
  import { ephemeralMachineProfile } from "./application-machine-startup.js";
10
+ /** Exact attached authority; an unscoped default connection still proves its incarnation. */
11
+ function attachedMachineNavigationTarget(active) {
12
+ if (active?.status !== "live" || !active.connection?.liveSessionId || !active.daemonGeneration)
13
+ return null;
14
+ return {
15
+ liveSessionId: active.connection.liveSessionId,
16
+ daemonGeneration: active.daemonGeneration,
17
+ ...(active.connection.server ? { server: active.connection.server } : {}),
18
+ };
19
+ }
10
20
  export function createApplicationMachineNavigation(options) {
11
21
  const preferences = createApplicationFleetPreferences({
12
22
  onError: options.setNote,
@@ -31,10 +41,12 @@ export function createApplicationMachineNavigation(options) {
31
41
  const [snapshot, setSnapshot] = createSignal(catalog.getSnapshot());
32
42
  const [focused, setFocused] = createSignal(false);
33
43
  const [switching, setSwitching] = createSignal(false);
44
+ const [serverFocus, setServerFocus] = createSignal(null);
34
45
  const [attentionOnly, setAttentionOnly] = createSignal(false);
35
46
  const showSwitcher = (attention) => {
36
47
  navigation++;
37
48
  options.cancelOpen();
49
+ setServerFocus(null);
38
50
  setAttentionOnly(attention);
39
51
  setFocused(false);
40
52
  setSwitching(true);
@@ -42,39 +54,80 @@ export function createApplicationMachineNavigation(options) {
42
54
  const [adding, setAdding] = createSignal(false);
43
55
  const [alias, setAlias] = createSignal("");
44
56
  const [error, setError] = createSignal(null);
45
- const stop = catalog.subscribe(setSnapshot);
57
+ let attemptStartup = null;
58
+ const stop = catalog.subscribe((value) => {
59
+ setSnapshot(value);
60
+ attemptStartup?.();
61
+ });
46
62
  let navigation = 0;
63
+ const [activeSessionKey, setActiveSessionKey] = createSignal(null);
47
64
  const [tabRevision, setTabRevision] = createSignal(0);
48
65
  const history = [...saved().recent].reverse();
49
66
  let historyIndex = history.length - 1;
50
- const select = (id, expectedLiveSessionId) => {
67
+ const select = (id, expectedLiveSessionId, server) => {
51
68
  if (!manager.getMachine(id))
52
69
  return false;
53
70
  if (manager.snapshot().selectedMachineId !== id) {
54
71
  navigation++;
55
72
  options.cancelOpen();
56
73
  // Synchronously remove the old input/shell owner before changing the route.
57
- options.resetWorkspace(id, expectedLiveSessionId);
74
+ if (server)
75
+ options.resetWorkspace(id, expectedLiveSessionId, server);
76
+ else
77
+ options.resetWorkspace(id, expectedLiveSessionId);
58
78
  manager.select(id);
59
79
  }
60
80
  else if (expectedLiveSessionId) {
61
- options.resetWorkspace(id, expectedLiveSessionId);
81
+ const attached = options.attachedTarget?.() ??
82
+ attachedMachineNavigationTarget(options.attachedGeneration?.() ?? null);
83
+ const sameAuthority = attached?.liveSessionId === expectedLiveSessionId &&
84
+ (server
85
+ ? attached.server
86
+ ? attached.server.serverId === server.serverId &&
87
+ attached.server.generation === server.generation
88
+ : attached.daemonGeneration === server.generation &&
89
+ manager.getMachine(id)?.read()?.instanceId === server.generation
90
+ : !attached.server);
91
+ if (sameAuthority)
92
+ return true;
93
+ if (server)
94
+ options.resetWorkspace(id, expectedLiveSessionId, server);
95
+ else
96
+ options.resetWorkspace(id, expectedLiveSessionId);
62
97
  }
63
98
  return true;
64
99
  };
65
- const open = async (id, name, source, remember = true, expectedLiveSessionId) => {
100
+ const open = async (id, name, source, remember = true, expectedLiveSessionId, server, sessionKey) => {
66
101
  // A same-machine session click supersedes any pending exact-agent focus too.
67
102
  if (manager.snapshot().selectedMachineId === id)
68
103
  options.cancelOpen();
69
104
  // A sidebar selection can leave a tab whose route is pinned to a different
70
105
  // live incarnation on this same machine. Select a new exact owner before
71
106
  // preparation; never apply that old tab's identity fence to the new name.
72
- if (!expectedLiveSessionId && options.sessionName() !== name) {
73
- expectedLiveSessionId = snapshot()
74
- .groups.find((group) => group.id === id || group.routeIds?.includes(id))
75
- ?.sessions.find((row) => row.name === name && !row.disabled)?.liveSessionId;
107
+ if (manager.getMachine(id)?.endpoint().state !== "ready") {
108
+ select(id);
109
+ options.setNote("This machine is disconnected. Select the session after reconnecting.");
110
+ return false;
111
+ }
112
+ const candidates = snapshot()
113
+ .groups.find((group) => group.id === id || group.routeIds?.includes(id))
114
+ ?.sessions.filter((row) => row.name === name &&
115
+ !row.disabled &&
116
+ (!sessionKey || row.id === sessionKey) &&
117
+ (!expectedLiveSessionId || row.liveSessionId === expectedLiveSessionId) &&
118
+ (!server ||
119
+ (row.server?.serverId === server.serverId &&
120
+ row.server.generation === server.generation))) ?? [];
121
+ if (candidates.length !== 1) {
122
+ options.setNote("Select the session under its tmux server; this target is ambiguous or has changed.");
123
+ return false;
76
124
  }
77
- if (!select(id, expectedLiveSessionId))
125
+ const selected = candidates[0];
126
+ const sameSelection = activeSessionKey() === selected.id && options.sessionName() === name;
127
+ setActiveSessionKey(selected.id);
128
+ expectedLiveSessionId = selected.liveSessionId;
129
+ server = selected.server;
130
+ if (!select(id, sameSelection ? undefined : expectedLiveSessionId, server))
78
131
  return;
79
132
  const token = ++navigation;
80
133
  setFocused(false);
@@ -92,15 +145,21 @@ export function createApplicationMachineNavigation(options) {
92
145
  if (token === navigation) {
93
146
  const session = snapshot()
94
147
  .groups.find((group) => group.id === id || group.routeIds?.includes(id))
95
- ?.sessions.find((row) => row.name === name && !row.disabled);
148
+ ?.sessions.find((row) => row.id === selected.id && !row.disabled);
96
149
  if (session) {
97
150
  if (session.liveSessionId)
98
151
  tabs.remember({
99
- key: JSON.stringify([id, session.liveSessionId]),
152
+ key: JSON.stringify([
153
+ id,
154
+ session.server?.serverId,
155
+ session.server?.generation,
156
+ session.liveSessionId,
157
+ ]),
158
+ server: session.server,
100
159
  machineId: id,
101
160
  liveSessionId: session.liveSessionId,
102
161
  label: session.name,
103
- hostLabel: manager.getMachine(id)?.label ?? id,
162
+ hostLabel: `${manager.getMachine(id)?.label ?? id}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
104
163
  });
105
164
  preferences.change({ type: "visit", key: session.id });
106
165
  if (remember && history[historyIndex] !== session.id) {
@@ -118,26 +177,39 @@ export function createApplicationMachineNavigation(options) {
118
177
  if (manager.getMachine(target.machineId)?.endpoint().state !== "ready")
119
178
  return null;
120
179
  const group = snapshot().groups.find((g) => g.id === target.machineId || g.routeIds?.includes(target.machineId));
121
- const row = group?.sessions.find((s) => s.liveSessionId === target.liveSessionId && !s.disabled);
180
+ const row = group?.sessions.find((s) => s.liveSessionId === target.liveSessionId &&
181
+ !s.disabled &&
182
+ s.server?.serverId === target.server?.serverId &&
183
+ s.server?.generation === target.server?.generation);
122
184
  return row ? { ...target, label: row.name } : null;
123
185
  };
124
186
  const tabs = createFleetTabs({
125
187
  resolve: resolveTab,
126
188
  retireActive: () => {
189
+ setActiveSessionKey(null);
127
190
  navigation++;
128
191
  options.cancelOpen();
129
192
  options.resetWorkspace(manager.snapshot().selectedMachineId);
130
193
  options.setSurface("home");
131
194
  },
132
- open: async (target) => !!(await open(target.machineId, target.label, "keyboard", true, target.liveSessionId)),
195
+ open: async (target) => !!(await open(target.machineId, target.label, "keyboard", true, target.liveSessionId, target.server)),
133
196
  publish: () => setTabRevision((value) => value + 1),
134
197
  unavailable: () => options.setNote("That tab's session is unavailable or has been replaced."),
135
198
  });
199
+ const isDefaultSession = (machineId, session) => !session.server ||
200
+ session.server.generation === manager.getMachine(machineId)?.read()?.instanceId;
136
201
  const sidebar = {
137
202
  groups: () => snapshot().groups.map((group) => ({
138
203
  ...group,
139
- agentsAvailable: agentGroups().find((value) => value.machineId === group.id)?.available ?? false,
140
- agents: agentGroups().find((value) => value.machineId === group.id)?.agents ?? [],
204
+ agentsAvailable: group.sessions.some((session) => isDefaultSession(group.id, session)) &&
205
+ (agentGroups().find((value) => value.machineId === group.id)?.available ?? false),
206
+ agents: !group.sessions.some((session) => isDefaultSession(group.id, session))
207
+ ? []
208
+ : (agentGroups().find((value) => value.machineId === group.id)?.agents ?? []).map((agent) => ({
209
+ ...agent,
210
+ server: group.sessions.find((session) => session.liveSessionId === agent.liveSessionId &&
211
+ isDefaultSession(group.id, session))?.server,
212
+ })),
141
213
  })),
142
214
  tabs: () => {
143
215
  tabRevision();
@@ -158,6 +230,7 @@ export function createApplicationMachineNavigation(options) {
158
230
  onCollapse: (key, enabled) => preferences.change({ type: "collapse", key, enabled }),
159
231
  activeMachineId: () => snapshot().selectedMachineId,
160
232
  activeSessionName: options.sessionName,
233
+ activeSessionKey,
161
234
  activePaneId: options.activePaneId,
162
235
  focused,
163
236
  onFocus: () => {
@@ -166,8 +239,14 @@ export function createApplicationMachineNavigation(options) {
166
239
  setFocused(true);
167
240
  },
168
241
  onBlur: () => setFocused(false),
169
- onOpen: (id, name, source) => void open(id, name, source),
242
+ onOpen: (id, name, source, sessionKey) => void open(id, name, source, true, undefined, undefined, sessionKey),
170
243
  onOpenAgent: (id, sessionName, paneId, source) => {
244
+ const sessions = snapshot().groups.find((group) => group.id === id)?.sessions ?? [];
245
+ const selectedSession = sessions.find((session) => session.name === sessionName && isDefaultSession(id, session));
246
+ if (sessions.length && !selectedSession) {
247
+ options.setNote("Open this agent through its server session first.");
248
+ return;
249
+ }
171
250
  const row = agentGroups()
172
251
  .find((group) => group.machineId === id)
173
252
  ?.agents.find((agent) => agent.sessionName === sessionName && agent.paneId === paneId);
@@ -176,12 +255,18 @@ export function createApplicationMachineNavigation(options) {
176
255
  return;
177
256
  }
178
257
  options.cancelOpen();
179
- if (!select(id))
258
+ if (!select(id, selectedSession?.liveSessionId, selectedSession?.server))
180
259
  return;
260
+ if (selectedSession)
261
+ setActiveSessionKey(selectedSession.id);
181
262
  navigation++;
182
263
  setFocused(false);
183
264
  void options.openAgent?.(row, source);
184
265
  },
266
+ onSelectServer: (machineId, server) => {
267
+ showSwitcher(false);
268
+ setServerFocus({ machineId, server });
269
+ },
185
270
  onSelectMachine: (id) => {
186
271
  tabs.suspend();
187
272
  navigation++;
@@ -217,6 +302,7 @@ export function createApplicationMachineNavigation(options) {
217
302
  catalog,
218
303
  agents,
219
304
  selectedMachineId: () => manager.snapshot().selectedMachineId,
305
+ openSelectedSession: (name, source, key) => open(manager.snapshot().selectedMachineId, name, source, true, undefined, undefined, key),
220
306
  automaticOpen: manager.snapshot().machines.length === 1,
221
307
  automaticOpenAllowed: () => navigation === 0 && manager.snapshot().selectedMachineId === "local",
222
308
  sidebar,
@@ -235,7 +321,7 @@ export function createApplicationMachineNavigation(options) {
235
321
  options.setNote("That session is unavailable or has been replaced.");
236
322
  return;
237
323
  }
238
- if (await open(group.id, session.name, "keyboard", false))
324
+ if (await open(group.id, session.name, "keyboard", false, session.liveSessionId, session.server, session.id))
239
325
  historyIndex = next;
240
326
  },
241
327
  cycleTab(direction, close = false) {
@@ -253,10 +339,20 @@ export function createApplicationMachineNavigation(options) {
253
339
  void tabs.activate(target.key);
254
340
  },
255
341
  closeSwitcher: () => setSwitching(false),
342
+ switcherFilter: (command) => {
343
+ const focus = serverFocus();
344
+ return (!focus ||
345
+ (typeof command === "object" &&
346
+ command.fleet?.machineId === focus.machineId &&
347
+ command.fleet?.server?.serverId === focus.server.serverId &&
348
+ command.fleet?.server?.generation === focus.server.generation));
349
+ },
256
350
  paletteCommands() {
257
351
  const groupsById = new Map(agentGroups().map((group) => [group.machineId, group]));
258
352
  return snapshot().groups.flatMap((group) => {
259
- const agentGroup = groupsById.get(group.id);
353
+ const agentGroup = !group.sessions.some((session) => isDefaultSession(group.id, session))
354
+ ? undefined
355
+ : groupsById.get(group.id);
260
356
  const agentsBySession = new Map();
261
357
  for (const agent of agentGroup?.agents ?? []) {
262
358
  if (!agent.liveSessionId || !agent.paneId)
@@ -276,8 +372,9 @@ export function createApplicationMachineNavigation(options) {
276
372
  ? saved().recent.indexOf(session.id)
277
373
  : 1000,
278
374
  liveSessionId: session.liveSessionId,
279
- hostLabel: group.label,
280
- agentActivities: agentGroup?.available
375
+ server: session.server,
376
+ hostLabel: `${group.label}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
377
+ agentActivities: isDefaultSession(group.id, session) && agentGroup?.available
281
378
  ? (agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
282
379
  paneId: a.paneId,
283
380
  attention: a.attention,
@@ -289,7 +386,9 @@ export function createApplicationMachineNavigation(options) {
289
386
  };
290
387
  return [
291
388
  { kind: "open-session", sessionName: session.name, label: session.name, fleet },
292
- ...(agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
389
+ ...(isDefaultSession(group.id, session)
390
+ ? (agentsBySession.get(session.liveSessionId) ?? [])
391
+ : []).map((a) => ({
293
392
  kind: "jump-agent",
294
393
  sessionName: session.name,
295
394
  paneId: a.paneId,
@@ -311,6 +410,21 @@ export function createApplicationMachineNavigation(options) {
311
410
  disabled: group.state !== "ready",
312
411
  },
313
412
  },
413
+ ...(group.servers ?? []).map((server) => ({
414
+ kind: "open-machine",
415
+ sessionName: "",
416
+ label: `${server.label} · ${server.serverId.slice(-6)} · New session ^N`,
417
+ fleet: {
418
+ machineId: group.id,
419
+ hostLabel: `${group.label} / ${server.label}`,
420
+ liveSessionId: "",
421
+ daemonInstanceId: daemon?.instanceId ?? "",
422
+ disabled: group.state !== "ready" || server.state !== "online",
423
+ ...(server.state === "online"
424
+ ? { server: { serverId: server.serverId, generation: server.generation } }
425
+ : {}),
426
+ },
427
+ })),
314
428
  ...sessions,
315
429
  ];
316
430
  });
@@ -321,7 +435,9 @@ export function createApplicationMachineNavigation(options) {
321
435
  const target = command.fleet;
322
436
  const session = snapshot()
323
437
  .groups.find((g) => g.id === target.machineId)
324
- ?.sessions.find((s) => s.liveSessionId === target.liveSessionId);
438
+ ?.sessions.find((s) => s.liveSessionId === target.liveSessionId &&
439
+ s.server?.serverId === target.server?.serverId &&
440
+ s.server?.generation === target.server?.generation);
325
441
  if (session)
326
442
  preferences.change({
327
443
  type: "favorite",
@@ -335,6 +451,19 @@ export function createApplicationMachineNavigation(options) {
335
451
  return;
336
452
  const handle = manager.getMachine(target.machineId);
337
453
  if (command.kind === "open-machine") {
454
+ if (target.server) {
455
+ const descriptor = snapshot()
456
+ .groups.find((group) => group.id === target.machineId)
457
+ ?.servers?.find((server) => server.serverId === target.server?.serverId &&
458
+ server.generation === target.server.generation &&
459
+ server.state === "online");
460
+ if (!descriptor || handle?.read()?.instanceId !== target.daemonInstanceId) {
461
+ options.setNote("That server has changed. Select it again.");
462
+ return;
463
+ }
464
+ sidebar.onSelectServer?.(target.machineId, target.server);
465
+ return;
466
+ }
338
467
  if (handle?.endpoint().state === "ready" &&
339
468
  handle.read()?.instanceId === target.daemonInstanceId)
340
469
  sidebar.onSelectMachine(target.machineId, source);
@@ -343,7 +472,10 @@ export function createApplicationMachineNavigation(options) {
343
472
  return;
344
473
  }
345
474
  const group = snapshot().groups.find((g) => g.id === target.machineId || g.routeIds?.includes(target.machineId));
346
- const session = group?.sessions.find((s) => s.liveSessionId === target.liveSessionId && !s.disabled);
475
+ const session = group?.sessions.find((s) => s.liveSessionId === target.liveSessionId &&
476
+ !s.disabled &&
477
+ s.server?.serverId === target.server?.serverId &&
478
+ s.server?.generation === target.server?.generation);
347
479
  if (!session ||
348
480
  handle?.endpoint().state !== "ready" ||
349
481
  handle.read()?.instanceId !== target.daemonInstanceId) {
@@ -353,7 +485,7 @@ export function createApplicationMachineNavigation(options) {
353
485
  if (command.kind === "jump-agent")
354
486
  sidebar.onOpenAgent?.(target.machineId, session.name, command.paneId, source);
355
487
  else
356
- await open(target.machineId, session.name, source, true, target.liveSessionId);
488
+ await open(target.machineId, session.name, source, true, target.liveSessionId, target.server);
357
489
  },
358
490
  adding,
359
491
  alias,
@@ -402,17 +534,44 @@ export function createApplicationMachineNavigation(options) {
402
534
  setError(cause instanceof Error ? cause.message : "Could not add this machine.");
403
535
  }
404
536
  },
405
- start(target) {
537
+ start(target, serverId) {
406
538
  agents.start();
407
539
  catalog.start();
408
- if (!target)
540
+ if (!target && !serverId)
409
541
  return;
410
542
  const id = manager.snapshot().selectedMachineId;
411
543
  const token = navigation;
544
+ let machineReady = false;
545
+ const attempt = () => {
546
+ if (!machineReady)
547
+ return;
548
+ if (token !== navigation) {
549
+ attemptStartup = null;
550
+ return;
551
+ }
552
+ const group = snapshot().groups.find((group) => group.id === id);
553
+ if (group?.state !== "ready")
554
+ return;
555
+ const matches = group.sessions.filter((row) => !row.disabled &&
556
+ (!serverId || row.server?.serverId === serverId) &&
557
+ (!target || row.name === target));
558
+ if (matches.length !== 1) {
559
+ options.setNote(matches.length
560
+ ? "Select a session under the requested tmux server."
561
+ : "Requested server or session is unavailable. Select it again.");
562
+ attemptStartup = null;
563
+ return;
564
+ }
565
+ const row = matches[0];
566
+ attemptStartup = null;
567
+ void open(id, row.name, "keyboard", true, row.liveSessionId, row.server, row.id);
568
+ };
569
+ attemptStartup = attempt;
412
570
  void manager.getMachine(id)?.ready.then((ready) => {
413
- if (ready && token === navigation && manager.snapshot().selectedMachineId === id)
414
- void open(id, target, "keyboard");
571
+ machineReady = ready;
572
+ attemptStartup?.();
415
573
  });
574
+ attempt();
416
575
  },
417
576
  };
418
577
  }
@@ -43,9 +43,15 @@ export function ApplicationMachineSidebar(props) {
43
43
  ? row.group.state === "ready" &&
44
44
  !row.agent.disabled &&
45
45
  row.agent.sessionName === props.model.activeSessionName() &&
46
+ (!props.model.activeSessionKey ||
47
+ row.group.sessions.some((session) => session.id === props.model.activeSessionKey?.() &&
48
+ session.server?.serverId === row.agent?.server?.serverId &&
49
+ session.server?.generation === row.agent?.server?.generation)) &&
46
50
  row.agent.paneId !== null &&
47
51
  row.agent.paneId === props.model.activePaneId?.()
48
- : row.session?.name === props.model.activeSessionName());
52
+ : props.model.activeSessionKey
53
+ ? row.session?.id === props.model.activeSessionKey()
54
+ : row.session?.name === props.model.activeSessionName());
49
55
  const preferenceKey = (group) => group.environmentId ?? group.id;
50
56
  const rows = createMemo(() => props.model.groups().flatMap((group) => [
51
57
  { key: JSON.stringify([group.id]), group },
@@ -53,12 +59,29 @@ export function ApplicationMachineSidebar(props) {
53
59
  .filter((session) => !collapsed().has(preferenceKey(group)) ||
54
60
  props.model.favorites?.().includes(session.id) ||
55
61
  (group.id === props.model.activeMachineId() &&
56
- session.name === props.model.activeSessionName()))
57
- .map((session) => ({
62
+ (props.model.activeSessionKey
63
+ ? session.id === props.model.activeSessionKey()
64
+ : session.name === props.model.activeSessionName())))
65
+ .map((session, index, sessions) => ({
66
+ serverHeading: (group.servers?.length ??
67
+ new Set(group.sessions.map((row) => row.server?.serverId)).size) > 1 &&
68
+ session.server &&
69
+ (index === 0 || sessions[index - 1]?.server?.serverId !== session.server.serverId)
70
+ ? `${session.serverLabel ?? "Server"} · ${session.server.serverId.slice(-6)}`
71
+ : undefined,
58
72
  key: JSON.stringify([group.id, "session", session.id]),
59
73
  group,
60
74
  session,
61
75
  })),
76
+ ...(collapsed().has(preferenceKey(group))
77
+ ? []
78
+ : (group.servers ?? [])
79
+ .filter((server) => !group.sessions.some((session) => session.server?.serverId === server.serverId))
80
+ .map((server) => ({
81
+ key: JSON.stringify([group.id, "server", server.serverId]),
82
+ group,
83
+ server,
84
+ }))),
62
85
  ...(!collapsed().has(preferenceKey(group))
63
86
  ? (group.agents ?? []).map((agent, index) => ({
64
87
  key: JSON.stringify([group.id, "agent", agent.id]),
@@ -79,7 +102,10 @@ export function ApplicationMachineSidebar(props) {
79
102
  ? group
80
103
  : null;
81
104
  };
82
- const activity = (row) => summarizeFleetActivity((row.group.agents ?? []).filter((agent) => !row.session || agent.sessionName === row.session.name), row.group.state === "ready" && row.group.agentsAvailable !== false && !row.session?.disabled);
105
+ const activity = (row) => summarizeFleetActivity((row.group.agents ?? []).filter((agent) => !row.session ||
106
+ (agent.sessionName === row.session.name &&
107
+ agent.server?.serverId === row.session.server?.serverId &&
108
+ agent.server?.generation === row.session.server?.generation)), row.group.state === "ready" && row.group.agentsAvailable !== false && !row.session?.disabled);
83
109
  const connectionDetail = (group) => {
84
110
  const diagnostic = group.diagnostic;
85
111
  if (!diagnostic)
@@ -111,6 +137,14 @@ export function ApplicationMachineSidebar(props) {
111
137
  setSelectedKey(row.key);
112
138
  setLocalFocused(true);
113
139
  props.model.onFocus?.();
140
+ if (row.server) {
141
+ if (row.server.state === "online")
142
+ props.model.onSelectServer?.(row.group.id, {
143
+ serverId: row.server.serverId,
144
+ generation: row.server.generation,
145
+ });
146
+ return;
147
+ }
114
148
  if (row.agent) {
115
149
  if (row.group.state === "ready" &&
116
150
  !row.agent.disabled &&
@@ -123,7 +157,7 @@ export function ApplicationMachineSidebar(props) {
123
157
  }
124
158
  else if (row.session) {
125
159
  if (row.group.state === "ready" && !row.session.disabled)
126
- props.model.onOpen(row.group.id, row.session.name, source);
160
+ props.model.onOpen(row.group.id, row.session.name, source, row.session.id);
127
161
  }
128
162
  else if (row.group.state !== "ready" ||
129
163
  (row.group.sessions.length === 0 && !row.group.agents?.length))
@@ -134,7 +168,7 @@ export function ApplicationMachineSidebar(props) {
134
168
  const revealFocusedRow = () => {
135
169
  if (!focused() || !scroll)
136
170
  return;
137
- const rowHeight = (row) => (row.agent ? (row.agentHeading ? 3 : 2) : 1);
171
+ const rowHeight = (row) => row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1;
138
172
  const y = rows()
139
173
  .slice(0, index())
140
174
  .reduce((sum, row) => sum + rowHeight(row), 0);
@@ -298,48 +332,68 @@ export function ApplicationMachineSidebar(props) {
298
332
  get agent() {
299
333
  return current().agent;
300
334
  },
335
+ get server() {
336
+ return current().server;
337
+ },
338
+ get serverHeading() {
339
+ return current().serverHeading;
340
+ },
301
341
  get agentHeading() {
302
342
  return current().agentHeading;
303
343
  },
304
344
  };
305
- return (<box width={Math.max(1, props.width - 1)} height={row.agent ? (row.agentHeading ? 3 : 2) : 1} flexShrink={0} flexDirection="column">
345
+ return (<box width={Math.max(1, props.width - 1)} height={row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1} flexShrink={0} flexDirection="column">
346
+ <Show when={row.serverHeading}>
347
+ <NavigationRow theme={props.theme} width={Math.max(1, props.width - 1)} id={`server:${row.group.id}:${row.session?.server?.serverId}`} label={row.serverHeading ?? "Server"} marker=" ▾" onActivate={() => {
348
+ if (row.session?.server)
349
+ props.model.onSelectServer?.(row.group.id, row.session.server);
350
+ }}/>
351
+ </Show>
306
352
  <Show when={row.agentHeading}>
307
353
  <text height={1} fg={props.theme.roles.text.secondary}>
308
354
  {" "}
309
355
  Agents
310
356
  </text>
311
357
  </Show>
312
- <NavigationRow theme={props.theme} id={`machine:${row.key}`} width={Math.max(1, props.width - 1)} labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined} label={row.agent
313
- ? row.agent.name
314
- : row.session
315
- ? friendlySessionLabel(row.session.name)
316
- : row.group.label} marker={row.agent
317
- ? active(row)
318
- ? row.agent.attention
319
- ? "›!"
320
- : "›"
321
- : row.agent.attention
322
- ? "!"
323
- : "•"
324
- : row.session
325
- ? props.model.favorites?.().includes(row.session.id)
326
- ? " ★"
327
- : active(row)
328
- ? " ›"
329
- : " "
330
- : collapsed().has(preferenceKey(row.group))
331
- ? "▸"
332
- : "▾"} detail={row.agent
333
- ? row.group.state !== "ready" || row.agent.disabled
334
- ? "unavailable"
335
- : `[${terminalAgentStatusLabel(row.agent.activity)}]`
336
- : row.session
337
- ? row.group.state !== "ready" || row.session.disabled
358
+ <NavigationRow theme={props.theme} id={`machine:${row.key}`} width={Math.max(1, props.width - 1)} labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined} label={row.server
359
+ ? ` ${row.server.label} · ${row.server.serverId.slice(-6)}`
360
+ : row.agent
361
+ ? row.agent.name
362
+ : row.session
363
+ ? friendlySessionLabel(row.session.name)
364
+ : row.group.label} marker={row.server
365
+ ? " "
366
+ : row.agent
367
+ ? active(row)
368
+ ? row.agent.attention
369
+ ? "›!"
370
+ : "›"
371
+ : row.agent.attention
372
+ ? "!"
373
+ : "•"
374
+ : row.session
375
+ ? props.model.favorites?.().includes(row.session.id)
376
+ ? " ★"
377
+ : active(row)
378
+ ? " ›"
379
+ : " "
380
+ : collapsed().has(preferenceKey(row.group))
381
+ ? "▸"
382
+ : "▾"} detail={row.server
383
+ ? row.server.state === "offline"
384
+ ? "offline"
385
+ : "empty"
386
+ : row.agent
387
+ ? row.group.state !== "ready" || row.agent.disabled
338
388
  ? "unavailable"
339
- : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
340
- : row.group.agents?.length && row.group.state === "ready"
341
- ? activity(row).label
342
- : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && row.key === selectedKey())} attention={row.agent?.attention ?? activity(row).kind === "attention"} onActivate={(source) => activate(row, source)}/>
389
+ : `[${terminalAgentStatusLabel(row.agent.activity)}]`
390
+ : row.session
391
+ ? row.group.state !== "ready" || row.session.disabled
392
+ ? "unavailable"
393
+ : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
394
+ : row.group.agents?.length && row.group.state === "ready"
395
+ ? activity(row).label
396
+ : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && row.key === selectedKey())} attention={row.agent?.attention ?? activity(row).kind === "attention"} onActivate={(source) => activate(row, source)}/>
343
397
  <Show when={row.agent}>
344
398
  {(agent) => (<text height={1} fg={props.theme.roles.text.muted} content={clipTerminal(` ${friendlySessionLabel(agent().sessionName)}`, Math.max(1, props.width - 1))}/>)}
345
399
  </Show>
@@ -1,3 +1,4 @@
1
+ import { TmuxServerIdSchemaZ } from "@tmux-ide/contracts";
1
2
  import { parseArgs } from "node:util";
2
3
  import { loadAppConfig } from "../../../lib/app-config.js";
3
4
  import { tuiPerfMark } from "./application-performance-log.js";
@@ -6,16 +7,21 @@ export const parseApplicationArgs = (argv) => {
6
7
  args: [...argv],
7
8
  allowPositionals: true,
8
9
  strict: false,
9
- options: { target: { type: "string" } },
10
+ options: { target: { type: "string" }, server: { type: "string" } },
10
11
  });
11
12
  const positional = parsed.positionals.find((value) => value !== "app") ?? null;
12
13
  const target = typeof parsed.values.target === "string" ? parsed.values.target : positional;
13
- return Object.freeze({ target: target && target !== "home" ? target : null });
14
+ return Object.freeze({
15
+ target: target && target !== "home" ? target : null,
16
+ serverId: typeof parsed.values.server === "string"
17
+ ? TmuxServerIdSchemaZ.parse(parsed.values.server)
18
+ : null,
19
+ });
14
20
  };
15
21
  export async function loadApplicationConfig(args) {
16
22
  tuiPerfMark("config-load-start", { requestedTarget: args.target });
17
23
  // Home discovery is daemon-authoritative and remains live after mount. The
18
24
  // immutable bootstrap config only carries an explicit user target.
19
25
  tuiPerfMark("config-load-end", { target: args.target });
20
- return Object.freeze({ app: loadAppConfig(), target: args.target });
26
+ return Object.freeze({ app: loadAppConfig(), target: args.target, serverId: args.serverId });
21
27
  }