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
@@ -109,14 +109,18 @@ function layoutKey(frame) {
109
109
  return null;
110
110
  return `panes:${panes.join("\u0000")}`;
111
111
  }
112
- function layoutSnapshot(windows, currentWindowKey = null) {
112
+ function layoutSnapshot(windows, currentWindowKey = null, windowLinks = null) {
113
113
  const selectedKey = currentWindowKey ??
114
114
  [...windows.entries()].find(([, frame]) => frame.currentWindow)?.[0] ??
115
115
  null;
116
116
  const frames = Object.freeze([...windows.entries()].map(([key, frame]) => frame.currentWindow === (key === selectedKey)
117
117
  ? frame
118
118
  : freezeLayout({ ...frame, currentWindow: key === selectedKey })));
119
+ const links = windowLinks?.links.map((link) => Object.freeze({ ...link }));
120
+ if (links)
121
+ Object.freeze(links);
119
122
  return Object.freeze({
123
+ windowLinks: windowLinks && links ? Object.freeze({ ...windowLinks, links }) : null,
120
124
  current: frames.find((frame) => frame.currentWindow) ?? null,
121
125
  windows: frames,
122
126
  });
@@ -144,7 +148,8 @@ function layoutFrameSemanticallyEqual(left, right) {
144
148
  }));
145
149
  }
146
150
  function layoutSnapshotsSemanticallyEqual(left, right) {
147
- return (left.windows.length === right.windows.length &&
151
+ return (JSON.stringify(left.windowLinks ?? null) === JSON.stringify(right.windowLinks ?? null) &&
152
+ left.windows.length === right.windows.length &&
148
153
  left.windows.every((window, index) => {
149
154
  const candidate = right.windows[index];
150
155
  return candidate !== undefined && layoutFrameSemanticallyEqual(window, candidate);
@@ -702,6 +707,8 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
702
707
  if (coherentSettled ||
703
708
  closed ||
704
709
  !physicalReady ||
710
+ (routing.liveSessionId !== undefined &&
711
+ latestLayoutSnapshot.windowLinks?.liveSessionId !== routing.liveSessionId) ||
705
712
  !layoutExactlyCoversPanes(latestLayoutSnapshot, panes) ||
706
713
  !latestLayoutSnapshot.current?.panes.every(({ pane }) => typeof pane === "string" && canonicalSeedPanes.has(pane))) {
707
714
  return;
@@ -866,7 +873,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
866
873
  : {}),
867
874
  signal: options.signal,
868
875
  stream: {
869
- protocolVersion: 1,
876
+ protocolVersion: 2,
870
877
  workspaceName: inventory.workspaceName,
871
878
  panes: [...panes],
872
879
  viewerMode: "interactive",
@@ -981,13 +988,16 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
981
988
  });
982
989
  return;
983
990
  }
991
+ if (latestLayoutSnapshot.windowLinks &&
992
+ !latestLayoutSnapshot.windowLinks.links.some((link) => link.semanticWindowId === retained.semanticWindowId))
993
+ return;
984
994
  layoutsByWindow.set(key, retained);
985
995
  // tmux reports a switch as incumbent=false followed by target=true.
986
996
  // Retain the incumbent through that pair and publish one unique current
987
997
  // window only when the positive target frame arrives.
988
- if (retained.currentWindow)
998
+ if (!latestLayoutSnapshot.windowLinks && retained.currentWindow)
989
999
  currentWindowKey = key;
990
- const candidate = layoutSnapshot(layoutsByWindow, currentWindowKey);
1000
+ const candidate = layoutSnapshot(layoutsByWindow, currentWindowKey, latestLayoutSnapshot.windowLinks ?? null);
991
1001
  if (!layoutExactlyCoversPanes(candidate, panes)) {
992
1002
  if (lastRejectedLayoutSnapshot &&
993
1003
  layoutSnapshotsSemanticallyEqual(candidate, lastRejectedLayoutSnapshot))
@@ -1023,6 +1033,9 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1023
1033
  onLayoutSnapshot: (snapshot) => {
1024
1034
  if (closed)
1025
1035
  return;
1036
+ if (routing.liveSessionId !== undefined &&
1037
+ snapshot.windowLinks.liveSessionId !== routing.liveSessionId)
1038
+ return failConnection(new Error("Selected live session was replaced before terminal activation"));
1026
1039
  const replacement = new Map();
1027
1040
  let replacementCurrent = null;
1028
1041
  for (const frame of snapshot.layouts) {
@@ -1030,14 +1043,15 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1030
1043
  const key = layoutKey(retained);
1031
1044
  if (key === null || replacement.has(key))
1032
1045
  return failConnection(new Error("invalid layout snapshot"));
1033
- replacement.set(key, retained);
1046
+ const previous = layoutsByWindow.get(key);
1047
+ replacement.set(key, previous && layoutFrameSemanticallyEqual(previous, retained) ? previous : retained);
1034
1048
  if (retained.currentWindow) {
1035
1049
  if (replacementCurrent !== null)
1036
1050
  return failConnection(new Error("invalid layout snapshot current window"));
1037
1051
  replacementCurrent = key;
1038
1052
  }
1039
1053
  }
1040
- const candidate = layoutSnapshot(replacement, replacementCurrent);
1054
+ const candidate = layoutSnapshot(replacement, replacementCurrent, snapshot.windowLinks);
1041
1055
  if (!layoutExactlyCoversPanes(candidate, panes))
1042
1056
  return failConnection(new Error("layout snapshot does not cover terminal inventory"));
1043
1057
  layoutsByWindow.clear();
@@ -1048,6 +1062,11 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1048
1062
  if (layoutSnapshotsSemanticallyEqual(candidate, latestLayoutSnapshot))
1049
1063
  return;
1050
1064
  latestLayoutSnapshot = candidate;
1065
+ options.onDiagnostic?.("layout", {
1066
+ windows: candidate.windows.length,
1067
+ current: candidate.current !== null,
1068
+ links: candidate.windowLinks?.links.length ?? 0,
1069
+ });
1051
1070
  settleCoherent();
1052
1071
  for (const listener of [...layoutListeners]) {
1053
1072
  try {
@@ -1198,7 +1217,9 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1198
1217
  submitIntent: async (operationId, intent) => {
1199
1218
  // Structural commands can affect hidden panes. Do not defer user intent
1200
1219
  // until a different topology may be current; fail closed during seeding.
1201
- if (closed || [...endpoints.values()].some((endpoint) => !endpoint.inputReady))
1220
+ if (closed ||
1221
+ !latestLayoutSnapshot.windowLinks ||
1222
+ [...endpoints.values()].some((endpoint) => !endpoint.inputReady))
1202
1223
  throw new Error("Terminal inventory is still receiving canonical state");
1203
1224
  return (await opened.submitIntent(operationId, intent)) ?? undefined;
1204
1225
  },
@@ -0,0 +1,119 @@
1
+ /* @jsxImportSource @opentui/solid */
2
+ import { randomUUID } from "node:crypto";
3
+ import { createEffect, createMemo, Show } from "solid-js";
4
+ import { createApplicationGuidedTourOwner } from "./application-guided-tour-owner.js";
5
+ import { GuidedTourCoach } from "./guided-tour-coach.jsx";
6
+ import { createFleetSession } from "./fleet-lifecycle-client.js";
7
+ import { applicationMachineAuthorityManager } from "./application-machine-authority.js";
8
+ export function createApplicationGuidedTourIntegration(options) {
9
+ const { machines, lifecycle, generation, generationMachineId, layoutSnapshot, sessionName, focusedPane, activeSurface, appearance, paletteOpen, paletteCommands, dimensions, blocked, } = options;
10
+ const practiceRows = () => machines.catalog
11
+ .getSnapshot()
12
+ .groups.filter((group) => group.id === "local" || group.routeIds?.includes("local"))
13
+ .flatMap((group) => group.sessions)
14
+ .filter((row) => !row.disabled);
15
+ const practiceIdentity = (row) => {
16
+ const daemon = applicationMachineAuthorityManager.getMachine("local")?.read();
17
+ if (!row.liveSessionId || !daemon)
18
+ return null;
19
+ return {
20
+ machineId: "local",
21
+ serverId: row.server?.serverId ?? "default",
22
+ generation: row.server?.generation ?? daemon.instanceId,
23
+ sessionId: row.liveSessionId,
24
+ sessionName: row.name,
25
+ };
26
+ };
27
+ const tour = createApplicationGuidedTourOwner({
28
+ async createPractice() {
29
+ const handle = applicationMachineAuthorityManager.getMachine("local");
30
+ if (!handle || handle.endpoint().state !== "ready")
31
+ throw new Error("Connect this machine before creating a practice session.");
32
+ const created = await createFleetSession(handle, `tmux-ide-practice-${randomUUID().slice(0, 8)}`, undefined, { includeLiveSessionId: true });
33
+ if (!created || created.outcome !== "created")
34
+ throw new Error("Could not create a fresh practice session. Try again.");
35
+ if (!created.liveSessionId)
36
+ throw new Error("Update the local tmux-ide daemon to use the guided practice session, then restart the tour.");
37
+ return await new Promise((resolve, reject) => {
38
+ let done = false;
39
+ let stop = () => { };
40
+ const abort = () => {
41
+ clearTimeout(timer);
42
+ stop();
43
+ reject(new Error("Tour closed."));
44
+ };
45
+ const timer = setTimeout(() => {
46
+ lifecycle.signal.removeEventListener("abort", abort);
47
+ stop();
48
+ reject(new Error("Practice session was created but has not appeared yet. Find it in the local session list."));
49
+ }, 10_000);
50
+ lifecycle.signal.addEventListener("abort", abort, { once: true });
51
+ const check = () => {
52
+ const rows = practiceRows().filter((row) => row.name === created.workspaceName && row.liveSessionId === created.liveSessionId);
53
+ const identity = rows.length === 1 ? practiceIdentity(rows[0]) : null;
54
+ if (!identity || identity.generation !== created.daemonInstanceId)
55
+ return;
56
+ done = true;
57
+ lifecycle.signal.removeEventListener("abort", abort);
58
+ clearTimeout(timer);
59
+ stop();
60
+ resolve(identity);
61
+ };
62
+ stop = machines.catalog.subscribe(check);
63
+ if (done)
64
+ stop();
65
+ else if (lifecycle.signal.aborted)
66
+ abort();
67
+ else
68
+ check();
69
+ });
70
+ },
71
+ openPractice(practice) {
72
+ if (practice.machineId !== "local")
73
+ throw new Error("Practice sessions must belong to this machine. Restart the tour.");
74
+ const row = practiceRows().find((row) => {
75
+ const identity = practiceIdentity(row);
76
+ return (identity &&
77
+ identity.serverId === practice.serverId &&
78
+ identity.generation === practice.generation &&
79
+ identity.sessionId === practice.sessionId);
80
+ });
81
+ if (!row)
82
+ throw new Error("That practice session is unavailable or has been replaced. Replay the tour to create a new one.");
83
+ machines.sidebar.onOpen("local", row.name, "mouse", row.id);
84
+ },
85
+ });
86
+ const committedTourTheme = createMemo((previous) => appearance.pickerOpen()
87
+ ? (previous ?? JSON.stringify(appearance.theme().canonical))
88
+ : JSON.stringify(appearance.theme().canonical));
89
+ createEffect(() => {
90
+ const current = generation();
91
+ const connection = current?.status === "live" ? current.connection : null;
92
+ const machineId = generationMachineId();
93
+ const practice = connection?.liveSessionId && current?.daemonGeneration && machineId
94
+ ? {
95
+ machineId,
96
+ serverId: connection.server?.serverId ?? "default",
97
+ generation: connection.server?.generation ?? current.daemonGeneration,
98
+ sessionId: connection.liveSessionId,
99
+ sessionName: sessionName() ?? "",
100
+ }
101
+ : null;
102
+ tour.observe({
103
+ practice,
104
+ panes: (layoutSnapshot().current?.panes ?? []).flatMap((pane) => pane.pane ? [{ id: pane.pane, width: pane.width, height: pane.height }] : []),
105
+ focusedPane: focusedPane(),
106
+ surface: activeSurface(),
107
+ paletteOpen: paletteOpen(),
108
+ theme: committedTourTheme(),
109
+ });
110
+ });
111
+ return {
112
+ ...tour,
113
+ Coach: () => (<Show when={tour.state().active && !blocked()}>
114
+ <box position="absolute" right={1} bottom={2} width={Math.max(1, Math.min(68, dimensions().width - 2))} zIndex={30}>
115
+ <GuidedTourCoach height={Math.max(4, Math.min(10, dimensions().height - 4))} state={tour.state()} theme={appearance.theme()} width={Math.max(1, Math.min(68, dimensions().width - 2))} shortcuts={{ commands: "F5", home: "F1" }} error={tour.error()} busy={tour.busy()} onPause={tour.pause} onReplay={tour.replay} onWelcomeRead={tour.welcomeRead} onCreatePractice={() => void tour.createPractice()} onOpenCommands={() => paletteCommands.setOpen(true, "mouse")} onOpenAppearance={appearance.openPicker} onOpenHome={() => paletteCommands.openSurface("home", "mouse")} onOpenPractice={() => void tour.openPractice()}/>
116
+ </box>
117
+ </Show>),
118
+ };
119
+ }
@@ -0,0 +1,132 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { initialGuidedTourState, reduceGuidedTour, sameGuidedTourPractice, } from "./guided-tour.js";
3
+ import { readGuidedTourState, writeGuidedTourState } from "./guided-tour-storage.js";
4
+ /** Compare confirmed snapshots; unrelated sessions and request attempts never count. */
5
+ export function observedGuidedTourEvent(state, previous, current) {
6
+ if (!state.active || !previous)
7
+ return null;
8
+ if (state.step === "commands" && current.paletteOpen && !previous.paletteOpen)
9
+ return { type: "commands-opened" };
10
+ if (state.step === "theme" && current.theme !== previous.theme)
11
+ return { type: "theme-changed" };
12
+ if (state.step === "leave" && current.surface === "home")
13
+ return { type: "home-opened" };
14
+ if (!current.practice ||
15
+ !sameGuidedTourPractice(state.practice, current.practice) ||
16
+ current.surface !== "terminals")
17
+ return null;
18
+ if (state.step === "reopen" &&
19
+ (previous.surface !== "terminals" ||
20
+ !sameGuidedTourPractice(previous.practice, current.practice)))
21
+ return { type: "reopened", practice: current.practice };
22
+ if (!previous.practice || !sameGuidedTourPractice(previous.practice, current.practice))
23
+ return null;
24
+ if (state.step === "split" &&
25
+ previous.panes.length > 0 &&
26
+ current.panes.length > previous.panes.length &&
27
+ previous.panes.every((old) => current.panes.some((p) => p.id === old.id)) &&
28
+ current.panes.some((p) => !previous.panes.some((old) => old.id === p.id)))
29
+ return { type: "split-confirmed", practice: current.practice };
30
+ if (state.step === "focus" &&
31
+ previous.focusedPane &&
32
+ current.focusedPane &&
33
+ previous.focusedPane !== current.focusedPane &&
34
+ current.panes.some((p) => p.id === current.focusedPane) &&
35
+ current.panes.some((p) => p.id === previous.focusedPane))
36
+ return { type: "focus-confirmed", practice: current.practice };
37
+ // A divider reallocates space between the same panes: one grows while
38
+ // another shrinks on the same axis. Whole-app growth/shrink is not practice.
39
+ const samePanes = current.panes.length >= 2 &&
40
+ current.panes.length === previous.panes.length &&
41
+ previous.panes.every((old) => current.panes.some((pane) => pane.id === old.id));
42
+ const reallocated = ["width", "height"].some((axis) => {
43
+ const changes = current.panes.map((pane) => pane[axis] - (previous.panes.find((old) => old.id === pane.id)?.[axis] ?? pane[axis]));
44
+ return changes.some((change) => change > 0) && changes.some((change) => change < 0);
45
+ });
46
+ if (state.step === "resize" && samePanes && reallocated)
47
+ return { type: "resize-confirmed", practice: current.practice };
48
+ return null;
49
+ }
50
+ export function createApplicationGuidedTourOwner(options) {
51
+ const [state, setState] = createSignal((options.read ?? readGuidedTourState)());
52
+ const [busy, setBusy] = createSignal(false);
53
+ const [error, setError] = createSignal(null);
54
+ let previous = null;
55
+ let disposed = false;
56
+ onCleanup(() => {
57
+ disposed = true;
58
+ });
59
+ const send = (event) => {
60
+ const next = reduceGuidedTour(state(), event);
61
+ if (next === state())
62
+ return;
63
+ setState(next);
64
+ if (!(options.write ?? writeGuidedTourState)(next))
65
+ setError("Progress could not be saved. You can still continue this tour.");
66
+ };
67
+ return {
68
+ state,
69
+ busy,
70
+ error,
71
+ label: () => state().step === "complete"
72
+ ? "Replay walkthrough"
73
+ : state().step === initialGuidedTourState().step
74
+ ? "Learn tmux-ide"
75
+ : "Resume walkthrough",
76
+ open() {
77
+ previous = null;
78
+ send({ type: state().step === "complete" ? "replay" : "resume" });
79
+ },
80
+ replay() {
81
+ previous = null;
82
+ send({ type: "replay" });
83
+ },
84
+ pause() {
85
+ send({ type: "pause" });
86
+ },
87
+ welcomeRead() {
88
+ send({ type: "welcome-read" });
89
+ },
90
+ observe(current) {
91
+ const event = observedGuidedTourEvent(state(), previous, current);
92
+ previous = current;
93
+ if (event)
94
+ send(event);
95
+ },
96
+ async createPractice() {
97
+ if (busy() || state().step !== "practice")
98
+ return;
99
+ setBusy(true);
100
+ setError(null);
101
+ try {
102
+ const practice = await options.createPractice();
103
+ if (disposed)
104
+ return;
105
+ send({ type: "practice-created", practice });
106
+ if (state().active)
107
+ await options.openPractice(practice);
108
+ }
109
+ catch (cause) {
110
+ if (!disposed)
111
+ setError(cause instanceof Error ? cause.message : "Could not create the practice session.");
112
+ }
113
+ finally {
114
+ if (!disposed)
115
+ setBusy(false);
116
+ }
117
+ },
118
+ async openPractice() {
119
+ const practice = state().practice;
120
+ if (!practice || busy())
121
+ return;
122
+ setError(null);
123
+ try {
124
+ if (state().active)
125
+ await options.openPractice(practice);
126
+ }
127
+ catch (cause) {
128
+ setError(cause instanceof Error ? cause.message : "Practice session is unavailable.");
129
+ }
130
+ },
131
+ };
132
+ }
@@ -23,7 +23,8 @@ export function HomeAgentRoster(props) {
23
23
  const [hovered, setHovered] = createSignal(null);
24
24
  const width = () => Math.max(0, Math.floor(props.width));
25
25
  const height = () => Math.max(0, Math.floor(props.height));
26
- const stale = (row) => (props.snapshot.refreshingSessionKeys ?? []).includes(row.sessionKey) ||
26
+ const stale = (row) => row.disabled ||
27
+ (props.snapshot.refreshingSessionKeys ?? []).includes(row.sessionKey) ||
27
28
  (props.snapshot.unavailableSessionKeys ?? []).includes(row.sessionKey);
28
29
  const showRecovery = () => Boolean(((props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
29
30
  props.onRetry) ||
@@ -50,8 +51,8 @@ export function HomeAgentRoster(props) {
50
51
  if (props.snapshot.phase === "live" && props.snapshot.rows.length === 0)
51
52
  return "No agents reported";
52
53
  const count = props.snapshot.rows.length;
53
- const attention = props.snapshot.rows.filter((row) => row.attention || row.activity === "waiting" || row.activity === "failed").length;
54
- const working = props.snapshot.rows.filter((row) => row.activity === "running").length;
54
+ const attention = props.snapshot.rows.filter((row) => !stale(row) && (row.attention || row.activity === "waiting" || row.activity === "failed")).length;
55
+ const working = props.snapshot.rows.filter((row) => !stale(row) && row.activity === "running").length;
55
56
  return `${count} observed ${count === 1 ? "agent" : "agents"} · ${attention} ${attention === 1 ? "needs" : "need"} attention · ${working} working`;
56
57
  };
57
58
  const coverage = () => `Scope: ${props.snapshot.observedSessions} of ${props.snapshot.totalSessions} sessions observed${props.snapshot.phase === "partial" ? " · partial" : ""}${props.snapshot.loadingSessions ? ` · ${props.snapshot.loadingSessions} loading` : ""}${props.snapshot.unavailableSessions ? ` · ${props.snapshot.unavailableSessions} unavailable` : ""}${props.snapshot.truncatedSessions ? ` · ${props.snapshot.truncatedSessions} not loaded` : ""}`;
@@ -69,15 +70,22 @@ export function HomeAgentRoster(props) {
69
70
  return "Enlarge the terminal to view agents.";
70
71
  const selected = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
71
72
  if (selected && stale(selected))
72
- return `${selected.sessionName} · last observed; waiting for fresh signals`;
73
- if (width() < 44 && selected)
74
- return `${selected.sessionName} · Enter open`;
73
+ return `${[selected.machineLabel, selected.serverLabel, selected.sessionName].filter(Boolean).join(" / ")} · last observed; waiting for fresh signals`;
74
+ if (selected?.machineLabel || (width() < 44 && selected))
75
+ return `${[selected?.machineLabel, selected?.serverLabel, selected?.sessionName].filter(Boolean).join(" / ")} · Enter open`;
75
76
  return `${offset() + 1}–${Math.min(props.snapshot.rows.length, offset() + visibleCount())} of ${props.snapshot.rows.length} · ↑↓ select · Enter open`;
76
77
  };
77
78
  useKeyboardRoute((event) => {
78
79
  if (!props.inputActive || event.eventType !== "press" || event.ctrl || event.meta)
79
80
  return false;
80
81
  const key = event.name.toLowerCase();
82
+ const filter = key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined;
83
+ if (filter) {
84
+ event.preventDefault();
85
+ event.stopPropagation();
86
+ filter();
87
+ return true;
88
+ }
81
89
  const retry = key === "r" &&
82
90
  (props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
83
91
  props.onRetry;
@@ -120,10 +128,12 @@ export function HomeAgentRoster(props) {
120
128
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
121
129
  {clipTerminal(coverage(), width())}
122
130
  </text>
123
- <box height={1} flexShrink={0}/>
131
+ <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
132
+ {clipTerminal(props.filterLabel ? `${props.filterLabel} · f machine · a attention` : "", width())}
133
+ </text>
124
134
  <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() + 1)} flexShrink={0}/>}>
125
135
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
126
- {clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells("SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
136
+ {clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells(props.filterLabel ? "MACHINE / SERVER / SESSION" : "SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
127
137
  </text>
128
138
  <box height={visibleCount()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
129
139
  <For each={keys()}>
@@ -131,7 +141,9 @@ export function HomeAgentRoster(props) {
131
141
  const row = () => byKey().get(key);
132
142
  return (<box height={1} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
133
143
  <NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={padCells(row().name, columns().agent) +
134
- padCells(row().sessionName, columns().session)} detail={padCells(`${row().attention ? "! " : ""}${homeAgentStatusLabel(row().activity)}${stale(row()) ? "*" : ""}`, columns().status)} detailAlign="end" marker={props.selection.selectedKey === key ? "›" : " "} selected={props.selection.selectedKey === key} focused={props.inputActive && props.selection.selectedKey === key} hovered={hovered() === key} attention={row().attention} status={statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
144
+ padCells([row().machineLabel, row().serverLabel, row().sessionName]
145
+ .filter(Boolean)
146
+ .join(" / "), columns().session)} detail={padCells(`${!stale(row()) && row().attention ? "! " : ""}${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity)}`, columns().status)} detailAlign="end" marker={props.selection.selectedKey === key ? "›" : " "} selected={props.selection.selectedKey === key} focused={props.inputActive && props.selection.selectedKey === key} hovered={hovered() === key} attention={!stale(row()) && row().attention} status={stale(row()) ? "unknown" : statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
135
147
  if (!props.inputActive || row().paneId === null || stale(row()))
136
148
  return;
137
149
  props.onSelect(key);
@@ -1,5 +1,5 @@
1
1
  import { readApplicationDaemonInfo as readCanonicalDaemonInfo } from "./application-daemon-authority.js";
2
- import { ApplicationShellResourceV2SchemaZ, DaemonEventClientFrameSchemaZ, DaemonEventServerFrameSchemaZ, WorkspaceCatalogResourceV3SchemaZ, } from "@tmux-ide/contracts";
2
+ import { ApplicationShellResourceV2SchemaZ, ApplicationShellProjectionInputV2SchemaZ, TmuxServerScopeSchemaZ, DaemonEventClientFrameSchemaZ, DaemonEventServerFrameSchemaZ, WorkspaceCatalogResourceV3SchemaZ, } from "@tmux-ide/contracts";
3
3
  import WebSocket from "ws";
4
4
  import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.js";
5
5
  export function sameHomeAgentDaemon(expected, actual) {
@@ -35,6 +35,21 @@ export function createApplicationHomeAgentTransport(options = {}) {
35
35
  throw new Error(`Agent observation returned HTTP ${response.status}.`);
36
36
  return response.json();
37
37
  };
38
+ if (session.server) {
39
+ const server = TmuxServerScopeSchemaZ.parse(session.server);
40
+ const base = `/api/v1/tmux-servers/${server.serverId}/${server.generation}`;
41
+ const raw = await get(`${base}/application-shell/${encodeURIComponent(session.workspaceName ?? session.name)}?liveSessionId=${encodeURIComponent(session.liveSessionId ?? "")}`);
42
+ const actual = TmuxServerScopeSchemaZ.parse(raw.server);
43
+ if (raw.version !== 1 ||
44
+ actual.serverId !== server.serverId ||
45
+ actual.generation !== server.generation)
46
+ throw new Error("Agent server incarnation changed.");
47
+ return {
48
+ version: 2,
49
+ daemon,
50
+ resource: ApplicationShellProjectionInputV2SchemaZ.parse(raw.resource),
51
+ };
52
+ }
38
53
  const shell = ApplicationShellResourceV2SchemaZ.parse(await get(`/api/project/${encodeURIComponent(session.name)}/application-shell?version=2`));
39
54
  if (!sameHomeAgentDaemon(daemon, shell.daemon))
40
55
  throw new Error("Agent daemon changed.");
@@ -48,6 +63,53 @@ export function createApplicationHomeAgentTransport(options = {}) {
48
63
  return shell;
49
64
  },
50
65
  connect(daemon, sessions, handlers) {
66
+ const scoped = sessions.filter((session) => session.server);
67
+ if (scoped.length > 0) {
68
+ // Scoped session events currently carry topology, not agent activity.
69
+ // Refresh metadata with a bounded cadence; never attach terminal streams.
70
+ // Keep legacy observations independent from scoped owner availability.
71
+ const pushed = sessions.filter((session) => !session.server || session.server.generation === daemon.instanceId);
72
+ let closed = false;
73
+ let pushHealthy = false;
74
+ const interval = setInterval(() => {
75
+ if (!closed)
76
+ for (const session of scoped) {
77
+ if (!pushHealthy || session.server?.generation !== daemon.instanceId)
78
+ handlers.invalidate(session.id);
79
+ }
80
+ }, 5_000);
81
+ interval.unref?.();
82
+ const connection = pushed.length
83
+ ? createApplicationHomeAgentTransport(options).connect(daemon, pushed.map((session) => ({ ...session, server: undefined })), {
84
+ ...handlers,
85
+ // The root event stream is advisory for scoped reads. Failure must
86
+ // not stall independent scoped owners or suppress their rows.
87
+ ready: (unavailable) => {
88
+ pushHealthy = unavailable.length === 0;
89
+ if (!closed)
90
+ for (const session of pushed)
91
+ handlers.invalidate(session.id);
92
+ },
93
+ unavailable: () => {
94
+ pushHealthy = false;
95
+ if (!closed)
96
+ for (const session of pushed)
97
+ handlers.invalidate(session.id);
98
+ },
99
+ })
100
+ : null;
101
+ queueMicrotask(() => {
102
+ if (!closed)
103
+ handlers.ready([]);
104
+ });
105
+ return {
106
+ close() {
107
+ closed = true;
108
+ clearInterval(interval);
109
+ connection?.close();
110
+ },
111
+ };
112
+ }
51
113
  let closed = false;
52
114
  let failed = false;
53
115
  const sockets = [];
@@ -159,7 +159,7 @@ export function createApplicationHomeNavigationOwner(options) {
159
159
  const paneRename = createApplicationPaneRenameOwner(options.interaction.renamePane, options.setNote);
160
160
  const homeAgents = createApplicationHomeAgentsOwner({
161
161
  catalog: options.catalog.snapshot,
162
- active: () => options.activeSurface() === "home",
162
+ active: () => !options.fleetHome && options.activeSurface() === "home",
163
163
  inputActive: () => options.activeSurface() === "home" &&
164
164
  !paneRename.draft() &&
165
165
  !options.appearanceOpen?.() &&
@@ -225,5 +225,11 @@ export function createApplicationHomeNavigationOwner(options) {
225
225
  onNavigationIntent: homeAgents.cancel,
226
226
  });
227
227
  const paletteCommandList = paletteCommands.commands;
228
- return { homeAgents, paneRename, paletteCommands, paletteCommandList, openAgent };
228
+ return {
229
+ homeAgents: options.fleetHome ? { ...homeAgents, presentation: options.fleetHome } : homeAgents,
230
+ paneRename,
231
+ paletteCommands,
232
+ paletteCommandList,
233
+ openAgent,
234
+ };
229
235
  }
@@ -14,11 +14,13 @@ export function projectHomeAgentRows(session, shell) {
14
14
  return shell.resource.workspace.sidebar.agents.map((agent) => ({
15
15
  key: `${session.id}\u0000${agent.id}`,
16
16
  sessionKey: session.id,
17
+ server: session.server,
18
+ serverLabel: session.serverLabel,
17
19
  sessionName: session.name,
18
20
  // Older catalog fixtures/callers retain a generation-qualified incarnation
19
21
  // key even when they do not expose the separately named wire field.
20
22
  liveSessionId: session.liveSessionId ?? session.id,
21
- daemonInstanceId: shell.daemon.instanceId,
23
+ daemonInstanceId: session.server?.generation ?? shell.daemon.instanceId,
22
24
  agentId: agent.id,
23
25
  paneId: agent.paneId,
24
26
  windowId: shell.resource.terminalInventory?.resources.find((resource) => resource.attachability.status === "available" &&
@@ -56,8 +56,12 @@ export function createApplicationHomeCatalogOwner(options) {
56
56
  const currentSessions = next.sessions;
57
57
  if (automaticOpen && next.phase === "live") {
58
58
  automaticOpen = false;
59
- if (currentSessions.length === 1)
60
- void options.startGeneration(currentSessions[0].name);
59
+ if (currentSessions.length === 1) {
60
+ const session = currentSessions[0];
61
+ void (options.openCatalogSession
62
+ ? options.openCatalogSession(session)
63
+ : options.startGeneration(session.name));
64
+ }
61
65
  }
62
66
  if (currentSessions.length === 0) {
63
67
  if (current !== null)
@@ -93,9 +97,11 @@ export function createApplicationHomeCatalogOwner(options) {
93
97
  return true;
94
98
  }
95
99
  if (name === "return" || name === "enter") {
96
- const sessionName = sessions()[selectedSessionIndex()]?.name;
97
- if (sessionName)
98
- void options.startGeneration(sessionName);
100
+ const session = sessions()[selectedSessionIndex()];
101
+ if (session)
102
+ void (options.openCatalogSession
103
+ ? options.openCatalogSession(session)
104
+ : options.startGeneration(session.name));
99
105
  return true;
100
106
  }
101
107
  return false;