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,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(
@@ -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";
@@ -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
  });
@@ -417,11 +422,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
417
422
  useKeyboard((event) => {
418
423
  noteHostInteraction();
419
424
  const name = event.name.toLowerCase();
420
- if (paletteModalOpen()) {
421
- componentKeyboardRoutes.route(event);
422
- return;
423
- }
424
- if (machines.switching()) {
425
+ if (paletteModalOpen() || machines.switching()) {
425
426
  componentKeyboardRoutes.route(event);
426
427
  return;
427
428
  }
@@ -525,7 +526,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
525
526
  });
526
527
  onMount(() => {
527
528
  tuiPerfMark("solid-mounted");
528
- machines.start(config.target);
529
+ machines.start(config.target, config.serverId);
529
530
  });
530
531
  const resizeIngress = tuiPerfStream ? interaction.beginResizePointerIngress : undefined;
531
532
  const applicationMouseIngress = applicationMousePointerIngressCapability(
@@ -591,7 +592,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
591
592
  onOpenSession={recoverHostFocus((sessionName, source) => {
592
593
  machineAgentNavigator?.cancel();
593
594
  homeAgents?.cancel();
594
- void startGeneration(sessionName, false, source);
595
+ void machines.openSelectedSession(sessionName, source);
595
596
  })}
596
597
  onOpenAgent={recoverHostFocus((sessionName, paneId, source) => {
597
598
  machineAgentNavigator?.cancel();
@@ -600,9 +601,12 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
600
601
  })}
601
602
  onSetPaletteOpen={recoverHostFocus(paletteCommands.setOpen)}
602
603
  onPaletteActivate={recoverHostFocus(paletteCommands.activate)}
603
- onZoomPane={recoverHostFocus((paneId) => {
604
- void interaction.zoomPane(paneId).then(setTransientNote);
605
- })}
604
+ {...terminalWindowActionCallbacks(
605
+ interaction,
606
+ setTransientNote,
607
+ recoverHostFocus,
608
+ () => machineAgentNavigator?.cancel(),
609
+ )}
606
610
  onCreateWindow={recoverHostFocus(() =>
607
611
  paletteCommands.activate("new-window", "mouse"),
608
612
  )}
@@ -616,10 +620,6 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
616
620
  onCancelPaneRename={recoverHostFocus(paneRename.cancel)}
617
621
  onSubmitPaneRename={recoverHostFocus(paneRename.submit)}
618
622
  onDismissNotification={recoverHostFocus(() => setTransientNote(null))}
619
- onSelectPane={recoverHostFocus((paneId, source) => {
620
- machineAgentNavigator?.cancel();
621
- interaction.selectPane(paneId, source);
622
- })}
623
623
  onResizePreview={recoverHostFocus(interaction.previewPaneResize)}
624
624
  onResizePane={recoverHostFocus(interaction.resizePane)}
625
625
  onCancelResize={interaction.cancelPaneResize}
@@ -1,3 +1,9 @@
1
+ import {
2
+ listTmuxServers,
3
+ createTmuxServerClient,
4
+ } from "@tmux-ide/daemon-client/tmux-server-client";
5
+ import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.ts";
6
+ import type { TmuxServerScope } from "@tmux-ide/contracts";
1
7
  import { fetchCanonicalLiveWorkspaceRouting } from "../canonical-workspace-routing.ts";
2
8
  import type { ApplicationMachineAuthorityHandle } from "./application-machine-authority.ts";
3
9
  import { prepareOpenTuiApplicationShellConnection } from "../application-shell-daemon-connection.ts";
@@ -8,14 +14,100 @@ export function applicationRouteConnection(
8
14
  handle: ApplicationMachineAuthorityHandle,
9
15
  expectedLiveSessionId?: string,
10
16
  onDiagnostic?: (phase: string, details: Readonly<Record<string, unknown>>) => void,
17
+ server?: TmuxServerScope,
11
18
  ) {
19
+ let rootInstanceId = handle.read()?.instanceId;
20
+ let selectedServer = server;
21
+ let refresh: { root: string; promise: Promise<TmuxServerScope> } | null = null;
22
+ const refreshAfterDaemonReplacement = async (nextRoot: string): Promise<TmuxServerScope> => {
23
+ if (!selectedServer) throw new Error("Select the server session again");
24
+ if (rootInstanceId === nextRoot) return selectedServer;
25
+ if (!expectedLiveSessionId) throw new Error("Select the server session again");
26
+ if (refresh?.root === nextRoot) return refresh.promise;
27
+ const stableServerId = selectedServer.serverId;
28
+ const promise = (async () => {
29
+ const daemon = handle.read();
30
+ const epoch = handle.endpoint()?.epoch;
31
+ if (!daemon?.authToken || daemon.instanceId !== nextRoot)
32
+ throw new Error("Daemon changed during reconnect");
33
+ const options = {
34
+ baseUrl: canonicalDaemonUrl("http", daemon.bindHostname, daemon.port),
35
+ ownerToken: daemon.authToken,
36
+ hostClientId: `opentui-reconnect:${process.pid}`,
37
+ origin: "tmux-ide://opentui",
38
+ };
39
+ const catalog = await listTmuxServers(options);
40
+ const descriptor = catalog.servers.find((candidate) => candidate.serverId === stableServerId);
41
+ if (!descriptor || descriptor.state !== "online")
42
+ throw new Error("Selected tmux server is unavailable");
43
+ const scope = { serverId: descriptor.serverId, generation: descriptor.generation };
44
+ const client = createTmuxServerClient(options, scope);
45
+ try {
46
+ const sessions = await client.sessions();
47
+ if (!sessions.sessions.some((session) => session.liveSessionId === expectedLiveSessionId))
48
+ throw new Error("Selected tmux session was replaced");
49
+ } finally {
50
+ client.dispose();
51
+ }
52
+ if (handle.read()?.instanceId !== nextRoot || handle.endpoint()?.epoch !== epoch)
53
+ throw new Error("Daemon changed during reconnect");
54
+ rootInstanceId = nextRoot;
55
+ selectedServer = scope;
56
+ return scope;
57
+ })();
58
+ refresh = { root: nextRoot, promise };
59
+ try {
60
+ return await promise;
61
+ } finally {
62
+ if (refresh?.promise === promise) refresh = null;
63
+ }
64
+ };
12
65
  return {
13
66
  readDaemon: handle.read,
14
- observeCanonicalGeneration: handle.observe,
67
+ observeCanonicalGeneration: server
68
+ ? async (listener: (generation: string | null) => void) => {
69
+ let stopped = false;
70
+ let revision = 0;
71
+ const stop = await handle.observe((generation) => {
72
+ const token = ++revision;
73
+ if (!generation) {
74
+ listener(null);
75
+ return;
76
+ }
77
+ if (generation === rootInstanceId) {
78
+ listener(selectedServer!.generation);
79
+ return;
80
+ }
81
+ // Retire old input before reading the replacement daemon's registration.
82
+ listener(null);
83
+ void refreshAfterDaemonReplacement(generation).then(
84
+ (scope) => {
85
+ if (!stopped && token === revision && handle.read()?.instanceId === generation)
86
+ listener(scope.generation);
87
+ },
88
+ () => {
89
+ if (!stopped && token === revision) listener(null);
90
+ },
91
+ );
92
+ });
93
+ return () => {
94
+ stopped = true;
95
+ revision++;
96
+ stop();
97
+ };
98
+ }
99
+ : handle.observe,
15
100
  resolveConnection: async (sessionName: string) => {
101
+ if (selectedServer) {
102
+ const currentRoot = handle.read()?.instanceId;
103
+ if (!currentRoot) return null;
104
+ await refreshAfterDaemonReplacement(currentRoot);
105
+ }
16
106
  const connection = await prepareOpenTuiApplicationShellConnection(sessionName, {
17
107
  ...(onDiagnostic ? { onDiagnostic } : {}),
108
+ ...(selectedServer ? { server: selectedServer, expectedLiveSessionId } : {}),
18
109
  readCanonicalDaemonInfo: handle.read,
110
+ readDaemonEndpoint: handle.endpoint,
19
111
  isCanonicalDaemonAlive: handle.isAlive,
20
112
  ensureSessionWorkspace: (name) =>
21
113
  ensureOpenTuiSessionWorkspaceResult(name, {
@@ -123,6 +123,8 @@ export interface ApplicationShellViewProps {
123
123
  readonly onDismissNotification?: () => void;
124
124
  readonly paletteCloseArmed?: Accessor<boolean>;
125
125
  readonly onSelectPane: TerminalWorkspaceProps["onSelectPane"];
126
+ readonly onSelectWindowLink?: TerminalWorkspaceProps["onSelectWindowLink"];
127
+ readonly onUnlinkWindowLink?: TerminalWorkspaceProps["onUnlinkWindowLink"];
126
128
  readonly onResizePreview: TerminalWorkspaceProps["onResizePreview"];
127
129
  readonly onResizePane: TerminalWorkspaceProps["onResizePane"];
128
130
  readonly onCancelResize?: TerminalWorkspaceProps["onCancelResize"];
@@ -509,6 +511,8 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
509
511
  agentIndicators={agentIndicators}
510
512
  paneInteractions={props.paneInteractions}
511
513
  onSelectPane={props.onSelectPane}
514
+ onSelectWindowLink={props.onSelectWindowLink}
515
+ onUnlinkWindowLink={props.onUnlinkWindowLink}
512
516
  onCreateWindow={props.onCreateWindow}
513
517
  onPaneContextAction={(paneId, action, currentName) => {
514
518
  if (action === "rename-pane")