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, TmuxServerScope } from "@tmux-ide/contracts";
1
2
  import { fleetHostColor, summarizeFleetActivity } from "./fleet-presentation.ts";
2
3
  import { TuiButton } from "../ui/button.tsx";
3
4
  /* @jsxImportSource @opentui/solid */
@@ -24,6 +25,7 @@ import { Surface } from "../ui/surface.tsx";
24
25
  import { useKeyboardRoute } from "../ui/keyboard-router.tsx";
25
26
 
26
27
  export interface ApplicationMachineAgent {
28
+ readonly server?: TmuxServerScope;
27
29
  readonly id: string;
28
30
  readonly name: string;
29
31
  readonly sessionName: string;
@@ -41,8 +43,11 @@ export interface ApplicationMachineGroup {
41
43
  readonly id: string;
42
44
  readonly label: string;
43
45
  readonly state: "ready" | "connecting" | "disconnected";
46
+ readonly servers?: readonly TmuxServerDescriptor[];
44
47
  readonly sessions: readonly {
45
48
  readonly id: string;
49
+ readonly server?: TmuxServerScope;
50
+ readonly serverLabel?: string;
46
51
  readonly name: string;
47
52
  readonly paneCount: number;
48
53
  readonly disabled?: boolean;
@@ -56,8 +61,15 @@ export interface ApplicationMachineSidebarModel {
56
61
  readonly onCollapse?: (key: string, enabled: boolean) => void;
57
62
  readonly activeMachineId: Accessor<string | null>;
58
63
  readonly activeSessionName: Accessor<string | null>;
64
+ readonly activeSessionKey?: Accessor<string | null>;
59
65
  readonly activePaneId?: Accessor<string | null>;
60
- readonly onOpen: (machineId: string, sessionName: string, source: "keyboard" | "mouse") => void;
66
+ readonly onOpen: (
67
+ machineId: string,
68
+ sessionName: string,
69
+ source: "keyboard" | "mouse",
70
+ sessionKey?: string,
71
+ ) => void;
72
+ readonly onSelectServer?: (machineId: string, server: TmuxServerScope) => void;
61
73
  readonly onSelectMachine: (machineId: string, source: "keyboard" | "mouse") => void;
62
74
  readonly onOpenAgent?: (
63
75
  machineId: string,
@@ -92,6 +104,8 @@ type Row = {
92
104
  session?: ApplicationMachineGroup["sessions"][number];
93
105
  agent?: ApplicationMachineAgent;
94
106
  agentHeading?: boolean;
107
+ serverHeading?: string;
108
+ server?: TmuxServerDescriptor;
95
109
  };
96
110
  /** Pure machine navigation. All connection and session authority stays with the caller. */
97
111
  export function ApplicationMachineSidebar(props: {
@@ -145,9 +159,18 @@ export function ApplicationMachineSidebar(props: {
145
159
  ? row.group.state === "ready" &&
146
160
  !row.agent.disabled &&
147
161
  row.agent.sessionName === props.model.activeSessionName() &&
162
+ (!props.model.activeSessionKey ||
163
+ row.group.sessions.some(
164
+ (session) =>
165
+ session.id === props.model.activeSessionKey?.() &&
166
+ session.server?.serverId === row.agent?.server?.serverId &&
167
+ session.server?.generation === row.agent?.server?.generation,
168
+ )) &&
148
169
  row.agent.paneId !== null &&
149
170
  row.agent.paneId === props.model.activePaneId?.()
150
- : row.session?.name === props.model.activeSessionName());
171
+ : props.model.activeSessionKey
172
+ ? row.session?.id === props.model.activeSessionKey()
173
+ : row.session?.name === props.model.activeSessionName());
151
174
  const preferenceKey = (group: ApplicationMachineGroup) => group.environmentId ?? group.id;
152
175
  const rows = createMemo<readonly Row[]>(() =>
153
176
  props.model.groups().flatMap((group) => [
@@ -158,13 +181,34 @@ export function ApplicationMachineSidebar(props: {
158
181
  !collapsed().has(preferenceKey(group)) ||
159
182
  props.model.favorites?.().includes(session.id) ||
160
183
  (group.id === props.model.activeMachineId() &&
161
- session.name === props.model.activeSessionName()),
184
+ (props.model.activeSessionKey
185
+ ? session.id === props.model.activeSessionKey()
186
+ : session.name === props.model.activeSessionName())),
162
187
  )
163
- .map((session) => ({
188
+ .map((session, index, sessions) => ({
189
+ serverHeading:
190
+ (group.servers?.length ??
191
+ new Set(group.sessions.map((row) => row.server?.serverId)).size) > 1 &&
192
+ session.server &&
193
+ (index === 0 || sessions[index - 1]?.server?.serverId !== session.server.serverId)
194
+ ? `${session.serverLabel ?? "Server"} · ${session.server.serverId.slice(-6)}`
195
+ : undefined,
164
196
  key: JSON.stringify([group.id, "session", session.id]),
165
197
  group,
166
198
  session,
167
199
  })),
200
+ ...(collapsed().has(preferenceKey(group))
201
+ ? []
202
+ : (group.servers ?? [])
203
+ .filter(
204
+ (server) =>
205
+ !group.sessions.some((session) => session.server?.serverId === server.serverId),
206
+ )
207
+ .map((server) => ({
208
+ key: JSON.stringify([group.id, "server", server.serverId]),
209
+ group,
210
+ server,
211
+ }))),
168
212
  ...(!collapsed().has(preferenceKey(group))
169
213
  ? (group.agents ?? []).map((agent, index) => ({
170
214
  key: JSON.stringify([group.id, "agent", agent.id]),
@@ -193,7 +237,11 @@ export function ApplicationMachineSidebar(props: {
193
237
  const activity = (row: Row) =>
194
238
  summarizeFleetActivity(
195
239
  (row.group.agents ?? []).filter(
196
- (agent) => !row.session || agent.sessionName === row.session.name,
240
+ (agent) =>
241
+ !row.session ||
242
+ (agent.sessionName === row.session.name &&
243
+ agent.server?.serverId === row.session.server?.serverId &&
244
+ agent.server?.generation === row.session.server?.generation),
197
245
  ),
198
246
  row.group.state === "ready" && row.group.agentsAvailable !== false && !row.session?.disabled,
199
247
  );
@@ -229,6 +277,14 @@ export function ApplicationMachineSidebar(props: {
229
277
  setSelectedKey(row.key);
230
278
  setLocalFocused(true);
231
279
  props.model.onFocus?.();
280
+ if (row.server) {
281
+ if (row.server.state === "online")
282
+ props.model.onSelectServer?.(row.group.id, {
283
+ serverId: row.server.serverId,
284
+ generation: row.server.generation,
285
+ });
286
+ return;
287
+ }
232
288
  if (row.agent) {
233
289
  if (
234
290
  row.group.state === "ready" &&
@@ -242,7 +298,7 @@ export function ApplicationMachineSidebar(props: {
242
298
  }
243
299
  } else if (row.session) {
244
300
  if (row.group.state === "ready" && !row.session.disabled)
245
- props.model.onOpen(row.group.id, row.session.name, source);
301
+ props.model.onOpen(row.group.id, row.session.name, source, row.session.id);
246
302
  } else if (
247
303
  row.group.state !== "ready" ||
248
304
  (row.group.sessions.length === 0 && !row.group.agents?.length)
@@ -252,7 +308,8 @@ export function ApplicationMachineSidebar(props: {
252
308
  };
253
309
  const revealFocusedRow = () => {
254
310
  if (!focused() || !scroll) return;
255
- const rowHeight = (row: Row) => (row.agent ? (row.agentHeading ? 3 : 2) : 1);
311
+ const rowHeight = (row: Row) =>
312
+ row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1;
256
313
  const y = rows()
257
314
  .slice(0, index())
258
315
  .reduce((sum, row) => sum + rowHeight(row), 0);
@@ -457,6 +514,12 @@ export function ApplicationMachineSidebar(props: {
457
514
  get agent() {
458
515
  return current().agent;
459
516
  },
517
+ get server() {
518
+ return current().server;
519
+ },
520
+ get serverHeading() {
521
+ return current().serverHeading;
522
+ },
460
523
  get agentHeading() {
461
524
  return current().agentHeading;
462
525
  },
@@ -464,10 +527,23 @@ export function ApplicationMachineSidebar(props: {
464
527
  return (
465
528
  <box
466
529
  width={Math.max(1, props.width - 1)}
467
- height={row.agent ? (row.agentHeading ? 3 : 2) : 1}
530
+ height={row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1}
468
531
  flexShrink={0}
469
532
  flexDirection="column"
470
533
  >
534
+ <Show when={row.serverHeading}>
535
+ <NavigationRow
536
+ theme={props.theme}
537
+ width={Math.max(1, props.width - 1)}
538
+ id={`server:${row.group.id}:${row.session?.server?.serverId}`}
539
+ label={row.serverHeading ?? "Server"}
540
+ marker=" ▾"
541
+ onActivate={() => {
542
+ if (row.session?.server)
543
+ props.model.onSelectServer?.(row.group.id, row.session.server);
544
+ }}
545
+ />
546
+ </Show>
471
547
  <Show when={row.agentHeading}>
472
548
  <text height={1} fg={props.theme.roles.text.secondary}>
473
549
  {" "}
@@ -480,43 +556,51 @@ export function ApplicationMachineSidebar(props: {
480
556
  width={Math.max(1, props.width - 1)}
481
557
  labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined}
482
558
  label={
483
- row.agent
484
- ? row.agent.name
485
- : row.session
486
- ? friendlySessionLabel(row.session.name)
487
- : row.group.label
559
+ row.server
560
+ ? ` ${row.server.label} · ${row.server.serverId.slice(-6)}`
561
+ : row.agent
562
+ ? row.agent.name
563
+ : row.session
564
+ ? friendlySessionLabel(row.session.name)
565
+ : row.group.label
488
566
  }
489
567
  marker={
490
- row.agent
491
- ? active(row)
492
- ? row.agent.attention
493
- ? "›!"
494
- : "›"
495
- : row.agent.attention
496
- ? "!"
497
- : "•"
498
- : row.session
499
- ? props.model.favorites?.().includes(row.session.id)
500
- ? " ★"
501
- : active(row)
502
- ? " ›"
503
- : " "
504
- : collapsed().has(preferenceKey(row.group))
505
- ? "▸"
506
- : "▾"
568
+ row.server
569
+ ? " "
570
+ : row.agent
571
+ ? active(row)
572
+ ? row.agent.attention
573
+ ? "›!"
574
+ : "›"
575
+ : row.agent.attention
576
+ ? "!"
577
+ : "•"
578
+ : row.session
579
+ ? props.model.favorites?.().includes(row.session.id)
580
+ ? " ★"
581
+ : active(row)
582
+ ? " ›"
583
+ : " "
584
+ : collapsed().has(preferenceKey(row.group))
585
+ ? "▸"
586
+ : "▾"
507
587
  }
508
588
  detail={
509
- row.agent
510
- ? row.group.state !== "ready" || row.agent.disabled
511
- ? "unavailable"
512
- : `[${terminalAgentStatusLabel(row.agent.activity)}]`
513
- : row.session
514
- ? row.group.state !== "ready" || row.session.disabled
589
+ row.server
590
+ ? row.server.state === "offline"
591
+ ? "offline"
592
+ : "empty"
593
+ : row.agent
594
+ ? row.group.state !== "ready" || row.agent.disabled
515
595
  ? "unavailable"
516
- : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
517
- : row.group.agents?.length && row.group.state === "ready"
518
- ? activity(row).label
519
- : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`
596
+ : `[${terminalAgentStatusLabel(row.agent.activity)}]`
597
+ : row.session
598
+ ? row.group.state !== "ready" || row.session.disabled
599
+ ? "unavailable"
600
+ : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
601
+ : row.group.agents?.length && row.group.state === "ready"
602
+ ? activity(row).label
603
+ : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`
520
604
  }
521
605
  selected={Boolean((row.session || row.agent) && active(row))}
522
606
  focused={Boolean(focused() && row.key === selectedKey())}
@@ -53,6 +53,8 @@ export function ApplicationPalettePreview(props: {
53
53
  const identity = () =>
54
54
  JSON.stringify([
55
55
  target()?.machineId,
56
+ target()?.server?.serverId,
57
+ target()?.server?.generation,
56
58
  target()?.liveSessionId,
57
59
  target()?.daemonInstanceId,
58
60
  paneId(),
@@ -77,6 +79,7 @@ export function ApplicationPalettePreview(props: {
77
79
  fleet &&
78
80
  Boolean(fleet.liveSessionId) &&
79
81
  !fleet.disabled &&
82
+ (!fleet.server || fleet.server.generation === fleet.daemonInstanceId) &&
80
83
  handle?.endpoint().state === "ready" &&
81
84
  handle.read()?.instanceId === fleet.daemonInstanceId;
82
85
  owner.select(
@@ -56,10 +56,13 @@ const LIFECYCLE_RUNTIME_PHASES = new Set([
56
56
  ]);
57
57
 
58
58
  const lifecycleRecords = new Map<string, string>();
59
+ let closing = false;
60
+ let closePromise: Promise<void> | null = null;
59
61
 
60
62
  function serializeMark(phase: string, details?: Readonly<Record<string, unknown>>): string | null {
61
63
  if (
62
64
  !stream ||
65
+ closing ||
63
66
  (!tuiPerfStream &&
64
67
  !LIFECYCLE_PHASES.has(phase) &&
65
68
  !(
@@ -160,10 +163,19 @@ async function flushTuiPerfMarks(): Promise<void> {
160
163
  });
161
164
  }
162
165
 
163
- export async function closeTuiPerfMarks(): Promise<void> {
164
- if (!stream) return;
165
- await flushTuiPerfMarks();
166
- await new Promise<void>((resolveClose) => stream.end(resolveClose));
167
- stream.off("error", fail);
168
- stream.off("drain", drain);
166
+ export function closeTuiPerfMarks(): Promise<void> {
167
+ if (closePromise) return closePromise;
168
+ // Renderer/runtime callbacks can still emit marks during asynchronous teardown.
169
+ // Stop accepting them before yielding, and let every caller await one close.
170
+ closing = true;
171
+ closePromise = (async () => {
172
+ if (!stream) return;
173
+ await flushTuiPerfMarks();
174
+ // No drain callback may enqueue another critical record after end().
175
+ stream.off("drain", drain);
176
+ await new Promise<void>((resolveClose) => stream.end(resolveClose));
177
+ // Keep the error listener through the underlying file descriptor teardown;
178
+ // an asynchronous close error must remain a logging failure, not a crash.
179
+ })();
180
+ return closePromise;
169
181
  }
@@ -1,3 +1,4 @@
1
+ import { TmuxServerIdSchemaZ } from "@tmux-ide/contracts";
1
2
  import { parseArgs } from "node:util";
2
3
 
3
4
  import { loadAppConfig, type AppConfig } from "../../../lib/app-config.ts";
@@ -6,11 +7,13 @@ import { tuiPerfMark } from "./application-performance-log.ts";
6
7
 
7
8
  export interface ApplicationArgs {
8
9
  readonly target: string | null;
10
+ readonly serverId?: string | null;
9
11
  }
10
12
 
11
13
  export interface ApplicationConfig {
12
14
  readonly app: AppConfig;
13
15
  readonly target: string | null;
16
+ readonly serverId?: string | null;
14
17
  }
15
18
 
16
19
  export interface StartApplicationRootOptions {
@@ -31,11 +34,17 @@ export const parseApplicationArgs = (argv: readonly string[]): ApplicationArgs =
31
34
  args: [...argv],
32
35
  allowPositionals: true,
33
36
  strict: false,
34
- options: { target: { type: "string" } },
37
+ options: { target: { type: "string" }, server: { type: "string" } },
35
38
  });
36
39
  const positional = parsed.positionals.find((value) => value !== "app") ?? null;
37
40
  const target = typeof parsed.values.target === "string" ? parsed.values.target : positional;
38
- return Object.freeze({ target: target && target !== "home" ? target : null });
41
+ return Object.freeze({
42
+ target: target && target !== "home" ? target : null,
43
+ serverId:
44
+ typeof parsed.values.server === "string"
45
+ ? TmuxServerIdSchemaZ.parse(parsed.values.server)
46
+ : null,
47
+ });
39
48
  };
40
49
 
41
50
  export async function loadApplicationConfig(args: ApplicationArgs): Promise<ApplicationConfig> {
@@ -43,5 +52,5 @@ export async function loadApplicationConfig(args: ApplicationArgs): Promise<Appl
43
52
  // Home discovery is daemon-authoritative and remains live after mount. The
44
53
  // immutable bootstrap config only carries an explicit user target.
45
54
  tuiPerfMark("config-load-end", { target: args.target });
46
- return Object.freeze({ app: loadAppConfig(), target: args.target });
55
+ return Object.freeze({ app: loadAppConfig(), target: args.target, serverId: args.serverId });
47
56
  }
@@ -1,9 +1,11 @@
1
+ import type { TmuxServerScope } from "@tmux-ide/contracts";
2
+ import { terminalWindowActionCallbacks } from "./application-terminal-workspace-policy.ts";
1
3
  import { applicationRouteConnection } from "./application-route-connection.ts";
2
4
  import { ApplicationMachineOverlays } from "./application-machine-overlays.tsx";
3
- import { handleFleetShortcut } from "./application-machine-navigation.ts";
4
5
  import { applicationMachineAuthorityManager } from "./application-machine-authority.ts";
5
6
  import { createApplicationMachineAgentNavigator } from "./application-machine-agent-navigation.ts";
6
7
  import { createApplicationMachineNavigation } from "./application-machine-navigation.ts";
8
+ import { handleFleetShortcut } from "./application-machine-navigation.ts";
7
9
  import { disposeApplicationDaemonAuthority } from "./application-daemon-authority.ts";
8
10
  import { createTerminalLinkOpener } from "./terminal-link-opener.ts";
9
11
  import { createApplicationPaneActivityOwner } from "./application-pane-activity-owner.ts";
@@ -29,14 +31,14 @@ import {
29
31
  type StartApplicationRootOptions,
30
32
  } from "./application-root-configuration.ts";
31
33
  import { createApplicationHomeCatalogOwner } from "./application-home-catalog-owner.ts";
32
- import { createApplicationHomeNavigationOwner } from "./application-home-agents-owner.ts";
34
+ import { createApplicationHomeExperience } from "./application-home-experience.ts";
33
35
  import { ApplicationShellView, applicationShellKeyAction } from "./application-shell-view.tsx";
34
36
  import {
35
37
  applicationGenerationNavigationKey,
36
38
  createApplicationGenerationStarter,
37
39
  } from "./application-generation-starter.ts";
38
40
  import { createApplicationInputReadiness } from "./application-input-readiness.ts";
39
- import { applyApplicationAppearanceToRenderer } from "./application-theme-repaint.ts";
41
+ import { createApplicationAppearanceRendererBinding } from "./application-theme-repaint.ts";
40
42
  import { createApplicationTerminalInteractionController } from "./application-terminal-interaction-controller.ts";
41
43
  import {
42
44
  createApplicationHostFocusRecovery,
@@ -174,6 +176,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
174
176
  const makeSessionOwner = (
175
177
  machineId = applicationMachineAuthorityManager.snapshot().selectedMachineId,
176
178
  expectedLiveSessionId?: string,
179
+ server?: TmuxServerScope,
177
180
  ) => {
178
181
  const ownedEpoch = ++sessionOwnerEpoch;
179
182
  connectionProgress.replaceOwner();
@@ -181,6 +184,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
181
184
  applicationMachineAuthorityManager.getMachine(machineId)!,
182
185
  expectedLiveSessionId,
183
186
  tuiLifecycleStream ? tuiPerfMark : undefined,
187
+ server,
184
188
  );
185
189
  return createOpenTuiSessionOwner({
186
190
  onStartupFailure: (sessionName, failure) => {
@@ -315,19 +319,18 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
315
319
  | ReturnType<typeof createApplicationMachineAgentNavigator>
316
320
  | undefined;
317
321
  const machines = createApplicationMachineNavigation({
318
- resetWorkspace(machineId, expectedLiveSessionId) {
319
- if (initialPreparation) {
320
- void initialPreparation.preparedConnection
321
- .then((connection) => connection?.dispose())
322
- .catch(() => undefined);
323
- initialPreparation = null;
324
- }
322
+ attachedGeneration: generation,
323
+ resetWorkspace(machineId, expectedLiveSessionId, server) {
324
+ void initialPreparation?.preparedConnection
325
+ .then((connection) => connection?.dispose())
326
+ .catch(() => undefined);
327
+ initialPreparation = null;
325
328
  interaction?.adoptGeneration(null);
326
329
  setGeneration(null);
327
330
  shellBinding.adoptGeneration(null);
328
331
  terminalHostFocus.adopt(null);
329
332
  void sessionOwner?.dispose().catch(() => undefined);
330
- sessionOwner = makeSessionOwner(machineId, expectedLiveSessionId);
333
+ sessionOwner = makeSessionOwner(machineId, expectedLiveSessionId, server);
331
334
  },
332
335
  cancelOpen: () => {
333
336
  machineAgentNavigator?.cancel();
@@ -361,9 +364,11 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
361
364
  onCleanup(() => machineAgentNavigator?.dispose());
362
365
  const homeCatalog = createApplicationHomeCatalogOwner({
363
366
  lifecycle,
364
- automaticOpen: config.target === null && machines.automaticOpen,
367
+ automaticOpen: config.target === null && !config.serverId && machines.automaticOpen,
365
368
  automaticOpenAllowed: machines.automaticOpenAllowed,
366
369
  catalog: machines.catalog.selectedCatalog,
370
+ openCatalogSession: (session) =>
371
+ machines.openSelectedSession(session.name, "keyboard", session.id),
367
372
  startGeneration,
368
373
  setNote: appearance.setNote,
369
374
  });
@@ -378,10 +383,16 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
378
383
  componentKeyboardRoutes.dispose();
379
384
  sessionFocusOwner?.dispose();
380
385
  });
381
- const { homeAgents, paneRename, paletteCommands, paletteCommandList, openAgent } =
382
- createApplicationHomeNavigationOwner({
383
- fleetCommands: machines.paletteCommands,
384
- openFleet: machines.openPalette,
386
+ const { homeAgents, paneRename, paletteCommands, paletteCommandList, openAgent, tour } =
387
+ createApplicationHomeExperience({
388
+ machines,
389
+ lifecycle,
390
+ generation,
391
+ generationMachineId,
392
+ layoutSnapshot,
393
+ appearance,
394
+ dimensions,
395
+ paletteModalOpen,
385
396
  focusedPane,
386
397
  catalog: homeCatalog,
387
398
  activeSurface,
@@ -398,16 +409,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
398
409
  setSurface,
399
410
  setNote: setTransientNote,
400
411
  });
401
- let paintedAppearanceGeneration: number | null = null;
402
- createEffect(() => {
403
- const nextAppearance = appearance.appearance();
404
- paintedAppearanceGeneration = applyApplicationAppearanceToRenderer(
405
- renderer,
406
- nextAppearance.theme,
407
- nextAppearance.generation,
408
- paintedAppearanceGeneration,
409
- );
410
- });
412
+ createApplicationAppearanceRendererBinding(renderer, appearance);
411
413
  createEffect(() => {
412
414
  const currentShell = shell();
413
415
  semanticViewportResize.adopt(dimensions(), currentShell.semantic, generation());
@@ -417,11 +419,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
417
419
  useKeyboard((event) => {
418
420
  noteHostInteraction();
419
421
  const name = event.name.toLowerCase();
420
- if (paletteModalOpen()) {
421
- componentKeyboardRoutes.route(event);
422
- return;
423
- }
424
- if (machines.switching()) {
422
+ if (paletteModalOpen() || machines.switching()) {
425
423
  componentKeyboardRoutes.route(event);
426
424
  return;
427
425
  }
@@ -525,7 +523,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
525
523
  });
526
524
  onMount(() => {
527
525
  tuiPerfMark("solid-mounted");
528
- machines.start(config.target);
526
+ machines.start(config.target, config.serverId);
529
527
  });
530
528
  const resizeIngress = tuiPerfStream ? interaction.beginResizePointerIngress : undefined;
531
529
  const applicationMouseIngress = applicationMousePointerIngressCapability(
@@ -541,6 +539,8 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
541
539
  machineSidebar={machines.sidebar}
542
540
  appearanceOwner={appearance}
543
541
  homeAgents={homeAgents.presentation}
542
+ onOpenTutorial={tour.open}
543
+ tutorialLabel={tour.label()}
544
544
  dimensions={dimensions}
545
545
  surface={activeSurface}
546
546
  semantic={() => shell().semantic}
@@ -591,7 +591,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
591
591
  onOpenSession={recoverHostFocus((sessionName, source) => {
592
592
  machineAgentNavigator?.cancel();
593
593
  homeAgents?.cancel();
594
- void startGeneration(sessionName, false, source);
594
+ void machines.openSelectedSession(sessionName, source);
595
595
  })}
596
596
  onOpenAgent={recoverHostFocus((sessionName, paneId, source) => {
597
597
  machineAgentNavigator?.cancel();
@@ -600,9 +600,12 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
600
600
  })}
601
601
  onSetPaletteOpen={recoverHostFocus(paletteCommands.setOpen)}
602
602
  onPaletteActivate={recoverHostFocus(paletteCommands.activate)}
603
- onZoomPane={recoverHostFocus((paneId) => {
604
- void interaction.zoomPane(paneId).then(setTransientNote);
605
- })}
603
+ {...terminalWindowActionCallbacks(
604
+ interaction,
605
+ setTransientNote,
606
+ recoverHostFocus,
607
+ () => machineAgentNavigator?.cancel(),
608
+ )}
606
609
  onCreateWindow={recoverHostFocus(() =>
607
610
  paletteCommands.activate("new-window", "mouse"),
608
611
  )}
@@ -616,10 +619,6 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
616
619
  onCancelPaneRename={recoverHostFocus(paneRename.cancel)}
617
620
  onSubmitPaneRename={recoverHostFocus(paneRename.submit)}
618
621
  onDismissNotification={recoverHostFocus(() => setTransientNote(null))}
619
- onSelectPane={recoverHostFocus((paneId, source) => {
620
- machineAgentNavigator?.cancel();
621
- interaction.selectPane(paneId, source);
622
- })}
623
622
  onResizePreview={recoverHostFocus(interaction.previewPaneResize)}
624
623
  onResizePane={recoverHostFocus(interaction.resizePane)}
625
624
  onCancelResize={interaction.cancelPaneResize}
@@ -636,6 +635,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
636
635
  onWindowPresented={tuiPerfStream ? interaction.observeWindowPresentation : undefined}
637
636
  onInteraction={() => noteHostInteraction()}
638
637
  />
638
+ <tour.Coach />
639
639
  <ApplicationMachineOverlays
640
640
  machines={machines}
641
641
  active={rendererFocused()}