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,8 +1,8 @@
1
+ import WebSocket from "ws";
2
+ import { createWorkspaceEventSupervisor, } from "@tmux-ide/daemon-client/workspace-event-supervisor";
1
3
  import { readApplicationDaemonInfo as readCanonicalDaemonInfo } from "./application-daemon-authority.js";
2
- import { WorkspaceCatalogResourceV3SchemaZ, } from "@tmux-ide/contracts";
4
+ import { TmuxServersResourceSchemaZ, TmuxServerSessionsResourceSchemaZ, } from "@tmux-ide/contracts";
3
5
  import { createPushResourceSession, } from "@tmux-ide/daemon-client/push-resource-session";
4
- import { createWorkspaceEventSupervisor, } from "@tmux-ide/daemon-client/workspace-event-supervisor";
5
- import WebSocket from "ws";
6
6
  import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.js";
7
7
  function sameDaemon(left, right) {
8
8
  return (left?.protocolVersion === right.protocolVersion &&
@@ -22,7 +22,7 @@ export function closeApplicationHomeCatalogTransport(subscription, supervisor) {
22
22
  subscription.close();
23
23
  supervisor.dispose();
24
24
  }
25
- function createApplicationHomeCatalogAdapter(onTransportRetired) {
25
+ function createApplicationHomeCatalogAdapter(_onTransportRetired) {
26
26
  return {
27
27
  validateTarget(value) {
28
28
  if (!value ||
@@ -42,10 +42,9 @@ function createApplicationHomeCatalogAdapter(onTransportRetired) {
42
42
  };
43
43
  },
44
44
  async fetch(target, _key, signal) {
45
- const url = canonicalDaemonUrl("http", target.daemon.bindHostname, target.daemon.port, "/api/resources/workspace-catalog?version=3");
46
- let response;
47
- try {
48
- response = await fetch(url, {
45
+ const base = canonicalDaemonUrl("http", target.daemon.bindHostname, target.daemon.port);
46
+ const read = async (path) => {
47
+ const response = await fetch(base + path, {
49
48
  headers: {
50
49
  accept: "application/json",
51
50
  ...(target.daemon.authToken
@@ -55,75 +54,129 @@ function createApplicationHomeCatalogAdapter(onTransportRetired) {
55
54
  credentials: "omit",
56
55
  cache: "no-store",
57
56
  redirect: "error",
58
- signal,
57
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3_000)]),
59
58
  });
60
- }
61
- catch {
62
- return {
63
- status: "failed",
64
- failure: catalogFailure("network", "Live session discovery failed.", true),
65
- };
66
- }
67
- if (!response.ok)
59
+ if (!response.ok)
60
+ throw new Error(`Discovery returned HTTP ${response.status}`);
61
+ return response.json();
62
+ };
63
+ try {
64
+ const inventory = TmuxServersResourceSchemaZ.parse(await read("/api/v1/tmux-servers"));
65
+ const sessions = await Promise.allSettled(inventory.servers.map(async (descriptor) => {
66
+ if (descriptor.state !== "online")
67
+ return [];
68
+ const server = { serverId: descriptor.serverId, generation: descriptor.generation };
69
+ const result = TmuxServerSessionsResourceSchemaZ.parse(await read(`/api/v1/tmux-servers/${server.serverId}/${server.generation}/sessions`));
70
+ if (result.server.serverId !== server.serverId ||
71
+ result.server.generation !== server.generation)
72
+ throw new Error("Server discovery identity changed");
73
+ return result.sessions.map((session) => ({
74
+ id: JSON.stringify([server.serverId, server.generation, session.liveSessionId]),
75
+ server,
76
+ serverLabel: descriptor.label,
77
+ liveSessionId: session.liveSessionId,
78
+ name: session.sessionName,
79
+ ...(session.workspaceName ? { workspaceName: session.workspaceName } : {}),
80
+ paneCount: session.paneCount,
81
+ }));
82
+ }));
68
83
  return {
69
- status: "failed",
70
- failure: catalogFailure("http", `Live session discovery returned HTTP ${response.status}.`, response.status >= 500),
84
+ status: "ok",
85
+ resource: {
86
+ kind: "live-catalog",
87
+ daemonInstanceId: target.daemon.instanceId,
88
+ servers: inventory.servers.map((server, index) => sessions[index]?.status === "rejected"
89
+ ? {
90
+ serverId: server.serverId,
91
+ label: server.label,
92
+ state: "offline",
93
+ generation: null,
94
+ }
95
+ : server),
96
+ scopedSessions: sessions.flatMap((result) => result.status === "fulfilled" ? result.value : []),
97
+ },
71
98
  };
72
- let body;
73
- try {
74
- body = await response.json();
75
99
  }
76
100
  catch {
77
101
  return {
78
102
  status: "failed",
79
- failure: catalogFailure("schema", "Live session discovery returned invalid JSON.", false),
103
+ failure: catalogFailure("network", "Server session discovery failed.", true),
80
104
  };
81
105
  }
82
- const parsed = WorkspaceCatalogResourceV3SchemaZ.safeParse(body);
83
- if (!parsed.success || !sameDaemon(target.daemon, parsed.data.daemon))
84
- return {
85
- status: "failed",
86
- failure: catalogFailure("schema", "Live session discovery failed daemon identity validation.", false),
87
- };
88
- return { status: "ok", resource: { kind: "live-catalog", value: parsed.data } };
89
106
  },
90
107
  async connect(target, _interests, handlers, signal) {
91
- const socketUrl = canonicalDaemonUrl("ws", target.daemon.bindHostname, target.daemon.port, "/ws/events?mode=semantic");
92
- const socket = new WebSocket(socketUrl, {
93
- headers: target.daemon.authToken
94
- ? { Authorization: `Bearer ${target.daemon.authToken}` }
95
- : undefined,
96
- });
97
- const supervisor = createWorkspaceEventSupervisor({
98
- socket,
99
- daemon: target.daemon,
100
- workspaceName: target.workspaceName,
101
- sessionName: target.workspaceName,
102
- fetchTerminalRuntimeInventory: () => Promise.reject(new Error("Home catalog owns no terminal inventory.")),
103
- onRetired: onTransportRetired,
104
- });
105
- const subscription = supervisor.connectWorkspaceCatalog(() => handlers.invalidate(["live-catalog"]), { terminalFirst: false });
108
+ // Root push is an advisory invalidation only: every refresh still resolves
109
+ // the full scoped inventory. The timer discovers changes on other owners.
106
110
  let closed = false;
107
- const close = () => {
108
- if (closed)
111
+ let pending = false;
112
+ let closePush = null;
113
+ const invalidate = () => {
114
+ if (!closed)
115
+ handlers.invalidate(["live-catalog"]);
116
+ };
117
+ const startPush = async () => {
118
+ if (closed || pending || closePush)
109
119
  return;
120
+ pending = true;
121
+ const socket = new WebSocket(canonicalDaemonUrl("ws", target.daemon.bindHostname, target.daemon.port, "/ws/events?mode=semantic"), {
122
+ headers: target.daemon.authToken
123
+ ? { Authorization: `Bearer ${target.daemon.authToken}` }
124
+ : undefined,
125
+ });
126
+ const supervisor = createWorkspaceEventSupervisor({
127
+ socket,
128
+ daemon: target.daemon,
129
+ workspaceName: target.workspaceName,
130
+ sessionName: target.workspaceName,
131
+ fetchTerminalRuntimeInventory: () => Promise.reject(new Error("Home catalog owns no terminal inventory.")),
132
+ onRetired: () => {
133
+ const cleanup = closePush;
134
+ closePush = null;
135
+ cleanup?.();
136
+ },
137
+ });
138
+ const subscription = supervisor.connectWorkspaceCatalog(invalidate, {
139
+ terminalFirst: false,
140
+ });
141
+ const cleanup = () => closeApplicationHomeCatalogTransport(subscription, supervisor);
142
+ closePush = cleanup;
143
+ try {
144
+ await subscription.ready;
145
+ }
146
+ catch {
147
+ if (closePush === cleanup)
148
+ closePush = null;
149
+ cleanup();
150
+ }
151
+ finally {
152
+ pending = false;
153
+ if (closed) {
154
+ cleanup();
155
+ closePush = null;
156
+ }
157
+ }
158
+ };
159
+ const timer = setInterval(() => {
160
+ invalidate();
161
+ void startPush().catch(() => {
162
+ pending = false;
163
+ });
164
+ }, 5_000);
165
+ const close = () => {
110
166
  closed = true;
167
+ clearInterval(timer);
111
168
  signal.removeEventListener("abort", close);
112
- // Release the logical observer before retiring its physical transport.
113
- closeApplicationHomeCatalogTransport(subscription, supervisor);
169
+ const cleanup = closePush;
170
+ closePush = null;
171
+ cleanup?.();
114
172
  };
115
173
  signal.addEventListener("abort", close, { once: true });
116
- try {
117
- await subscription.ready;
118
- }
119
- catch (error) {
174
+ if (signal.aborted)
120
175
  close();
121
- throw error;
122
- }
123
- if (signal.aborted) {
124
- close();
125
- throw signal.reason;
126
- }
176
+ else
177
+ void startPush().catch(() => {
178
+ pending = false;
179
+ });
127
180
  return { status: "connected", close };
128
181
  },
129
182
  rejectionFailure: () => catalogFailure("unavailable", "Live session discovery was rejected.", true),
@@ -145,29 +198,31 @@ const initialSnapshot = () => ({
145
198
  sessions: [],
146
199
  note: "Discovering live tmux sessions…",
147
200
  });
148
- function projectCatalog(resource) {
201
+ function projectCatalog(resource, scopedSessions) {
149
202
  return {
150
203
  phase: "live",
151
204
  daemonInstanceId: resource.daemon.instanceId,
152
- sessions: resource.liveSessions
153
- .map(({ liveSessionId, sessionName, paneCount }) => ({
154
- id: `${resource.daemon.instanceId}:${liveSessionId}`,
155
- liveSessionId,
156
- workspaceName: resource.intents.find((intent) => intent.sessionName === sessionName)
157
- ?.workspaceName,
158
- name: sessionName,
159
- paneCount,
160
- }))
161
- .sort((left, right) => left.name.localeCompare(right.name)),
162
- note: resource.liveSessions.length === 0
205
+ sessions: scopedSessions ??
206
+ resource.liveSessions
207
+ .map(({ liveSessionId, sessionName, paneCount }) => ({
208
+ id: `${resource.daemon.instanceId}:${liveSessionId}`,
209
+ liveSessionId,
210
+ workspaceName: resource.intents.find((intent) => intent.sessionName === sessionName)
211
+ ?.workspaceName,
212
+ name: sessionName,
213
+ paneCount,
214
+ }))
215
+ .sort((left, right) => left.name.localeCompare(right.name)),
216
+ note: (scopedSessions?.length ?? resource.liveSessions.length) === 0
163
217
  ? "No live tmux sessions yet. This list updates automatically."
164
218
  : null,
165
219
  };
166
220
  }
167
221
  /**
168
- * Daemon-authoritative Home catalog. Reads are push-invalidated while the
169
- * daemon generation is healthy; the only timer is a bounded failure-recovery
170
- * probe used when no canonical daemon or event transport is available.
222
+ * Daemon-authoritative fleet metadata. Root push preserves immediate updates,
223
+ * while a five-second refresh discovers independent
224
+ * server owners without opening pane streams. Failed reads use bounded backoff;
225
+ * each failed server is excluded without suppressing healthy siblings.
171
226
  */
172
227
  export function createApplicationHomeCatalog(overrides = {}) {
173
228
  const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
@@ -253,7 +308,17 @@ export function createApplicationHomeCatalog(overrides = {}) {
253
308
  if (slot?.status === "loaded" && !slot.refreshing && slot.resource.kind === "live-catalog") {
254
309
  clearRetry();
255
310
  retryAttempt = 0;
256
- publish(projectCatalog(slot.resource.value));
311
+ publish(slot.resource.value
312
+ ? projectCatalog(slot.resource.value)
313
+ : {
314
+ phase: "live",
315
+ daemonInstanceId: slot.resource.daemonInstanceId ?? null,
316
+ sessions: slot.resource.scopedSessions ?? [],
317
+ servers: slot.resource.servers,
318
+ note: slot.resource.scopedSessions?.length
319
+ ? null
320
+ : "No live tmux sessions yet. This list updates automatically.",
321
+ });
257
322
  return;
258
323
  }
259
324
  if (slot?.status === "error") {
@@ -0,0 +1,40 @@
1
+ import { createApplicationHomeFleetOwner } from "./application-home-fleet.js";
2
+ import { createApplicationHomeNavigationOwner } from "./application-home-agents-owner.js";
3
+ import { createApplicationGuidedTourIntegration } from "./application-guided-tour-integration.jsx";
4
+ /** Compose Home's fleet, navigation and learning experience without additional transports. */
5
+ export function createApplicationHomeExperience(options) {
6
+ const { machines, appearance } = options;
7
+ const paletteOpen = () => options.shell().semantic?.focus.palette.open ?? options.shell().localPaletteOpen;
8
+ const fleetHome = createApplicationHomeFleetOwner({
9
+ catalog: machines.catalog,
10
+ agents: machines.agents,
11
+ inputActive: () => options.activeSurface() === "home" &&
12
+ options.rendererFocused() &&
13
+ !options.paletteModalOpen() &&
14
+ !machines.switching() &&
15
+ !machines.adding() &&
16
+ !machines.focused() &&
17
+ !paletteOpen() &&
18
+ !appearance.pickerOpen(),
19
+ open: (_machineId, agent, source) => machines.openHomeAgent(agent, source),
20
+ });
21
+ const navigation = createApplicationHomeNavigationOwner({
22
+ ...options,
23
+ fleetHome: fleetHome.presentation,
24
+ fleetCommands: machines.paletteCommands,
25
+ openFleet: machines.openPalette,
26
+ });
27
+ const tour = createApplicationGuidedTourIntegration({
28
+ ...options,
29
+ sessionName: () => options.sessionOwner()?.sessionName() ?? null,
30
+ paletteOpen,
31
+ paletteCommands: navigation.paletteCommands,
32
+ blocked: () => options.paletteModalOpen() ||
33
+ !!navigation.paneRename.draft() ||
34
+ paletteOpen() ||
35
+ appearance.pickerOpen() ||
36
+ machines.switching() ||
37
+ machines.adding(),
38
+ });
39
+ return { ...navigation, tour };
40
+ }
@@ -0,0 +1,132 @@
1
+ import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
2
+ import { createHomeAgentSelectionOwner } from "./application-home-agent-selection.js";
3
+ export function projectHomeFleet(catalog, groups, filter) {
4
+ const machines = catalog.groups.filter((group) => filter.machineId === null || group.id === filter.machineId);
5
+ const rows = [];
6
+ const unavailable = new Set();
7
+ let observedSessions = 0, totalSessions = 0, loadingSessions = 0, unavailableSessions = 0, truncatedSessions = 0;
8
+ const panes = new Set();
9
+ for (const machine of machines) {
10
+ const group = groups.find((group) => group.machineId === machine.id);
11
+ totalSessions += machine.sessions.length;
12
+ const observation = machine.state === "ready" ? group?.observation : undefined;
13
+ observedSessions +=
14
+ observation?.observedSessions ?? (group?.available ? machine.sessions.length : 0);
15
+ loadingSessions +=
16
+ observation?.loadingSessions ??
17
+ (machine.state === "connecting" ? machine.sessions.length : 0);
18
+ unavailableSessions +=
19
+ observation?.unavailableSessions ??
20
+ (machine.state === "disconnected" ? machine.sessions.length : 0);
21
+ truncatedSessions += observation?.truncatedSessions ?? 0;
22
+ for (const key of observation?.unavailableSessionKeys ?? [])
23
+ unavailable.add(JSON.stringify([machine.id, key]));
24
+ for (const agent of [...(group?.agents ?? [])].sort((a, b) => Number(a.disabled) - Number(b.disabled) || a.id.localeCompare(b.id))) {
25
+ const sessionKey = JSON.stringify([machine.id, agent.sessionKey]);
26
+ if (agent.disabled)
27
+ unavailable.add(sessionKey);
28
+ if (filter.attentionOnly &&
29
+ !(agent.attention || agent.activity === "waiting" || agent.activity === "failed"))
30
+ continue;
31
+ const paneKey = agent.paneId
32
+ ? JSON.stringify([machine.id, agent.server?.serverId, agent.daemonInstanceId, agent.paneId])
33
+ : agent.id;
34
+ if (panes.has(paneKey))
35
+ continue;
36
+ panes.add(paneKey);
37
+ rows.push({ ...agent, key: agent.id, sessionKey, machineLabel: machine.label });
38
+ }
39
+ }
40
+ // Identity order never jumps when an activity signal changes.
41
+ rows.sort((a, b) => a.key.localeCompare(b.key));
42
+ const missing = machines.filter((machine) => machine.state !== "ready" ||
43
+ !groups.find((group) => group.machineId === machine.id)?.available);
44
+ return {
45
+ phase: missing.length
46
+ ? rows.length || observedSessions
47
+ ? "partial"
48
+ : machines.some((machine) => machine.state === "connecting")
49
+ ? "loading"
50
+ : "unavailable"
51
+ : "live",
52
+ rows,
53
+ observedSessions,
54
+ totalSessions,
55
+ loadingSessions,
56
+ unavailableSessions,
57
+ truncatedSessions,
58
+ refreshingSessionKeys: [],
59
+ unavailableSessionKeys: [...unavailable],
60
+ note: missing.length
61
+ ? `${missing.map((machine) => `${machine.label}: ${machine.state === "ready" ? "partial observations" : machine.state}`).join(" · ")} · unavailable rows show last observed activity`
62
+ : rows.length === 0 && filter.attentionOnly
63
+ ? "No agents need attention in this view."
64
+ : null,
65
+ };
66
+ }
67
+ /** Reuses resident fleet observations, adding no daemon connections or pane streams. */
68
+ export function createApplicationHomeFleetOwner(options) {
69
+ const [catalog, setCatalog] = createSignal(options.catalog.getSnapshot());
70
+ const [groups, setGroups] = createSignal(options.agents.getSnapshot());
71
+ const [filter, setFilter] = createSignal({
72
+ machineId: null,
73
+ attentionOnly: false,
74
+ });
75
+ const selection = createHomeAgentSelectionOwner();
76
+ const [selected, setSelected] = createSignal(selection.snapshot());
77
+ const snapshot = createMemo(() => projectHomeFleet(catalog(), groups(), filter()));
78
+ const stops = [
79
+ options.catalog.subscribe(setCatalog),
80
+ options.agents.subscribe(setGroups),
81
+ selection.subscribe(setSelected),
82
+ ];
83
+ createEffect(() => selection.setRows(snapshot().rows));
84
+ createEffect(() => {
85
+ const id = filter().machineId;
86
+ if (id && !catalog().groups.some((group) => group.id === id))
87
+ setFilter((value) => ({ ...value, machineId: null }));
88
+ });
89
+ onCleanup(() => {
90
+ for (const stop of stops)
91
+ stop();
92
+ selection.dispose();
93
+ });
94
+ const presentation = {
95
+ get agentRoster() {
96
+ return snapshot();
97
+ },
98
+ get agentSelection() {
99
+ return selected();
100
+ },
101
+ get agentInputActive() {
102
+ return options.inputActive();
103
+ },
104
+ get agentFilterLabel() {
105
+ return `${catalog().groups.find((group) => group.id === filter().machineId)?.label ?? "All machines"} · ${filter().attentionOnly ? "Needs attention" : "All agents"}`;
106
+ },
107
+ onCycleAgentMachine() {
108
+ const ids = [null, ...catalog().groups.map((group) => group.id)];
109
+ setFilter((value) => ({
110
+ ...value,
111
+ machineId: ids[(ids.indexOf(value.machineId) + 1) % ids.length],
112
+ }));
113
+ },
114
+ onToggleAgentAttention() {
115
+ setFilter((value) => ({ ...value, attentionOnly: !value.attentionOnly }));
116
+ },
117
+ onRetryAgents: () => options.agents.retry?.(),
118
+ onLoadMoreAgents: () => options.agents.loadMore?.(),
119
+ onSelectAgent: selection.select,
120
+ onMoveAgent: selection.move,
121
+ onAgentViewport: selection.setViewport,
122
+ onOpenAgent(row, source) {
123
+ if (!options.inputActive())
124
+ return;
125
+ const group = groups().find((group) => group.machineId === row.machineId);
126
+ const agent = group?.agents.find((agent) => agent.id === row.key);
127
+ if (agent && !agent.disabled && agent.paneId)
128
+ void options.open(agent.machineId, agent, source);
129
+ },
130
+ };
131
+ return { presentation };
132
+ }
@@ -34,6 +34,7 @@ export function createApplicationMachineAgents(options) {
34
34
  const current = !!entry && entry.binding === bindingFor(entry.handle);
35
35
  return Object.freeze({
36
36
  machineId: group.id,
37
+ observation: entry?.observer?.getSnapshot(),
37
38
  available: group.state === "ready" && current && entry?.observer?.getSnapshot().phase === "live",
38
39
  agents: Object.freeze((entry?.rows ?? []).map((row) => Object.freeze({
39
40
  ...row,
@@ -139,6 +140,14 @@ export function createApplicationMachineAgents(options) {
139
140
  entry.binding === bindingFor(entry.handle) &&
140
141
  !!entry.observer?.isCurrentTarget(row));
141
142
  },
143
+ retry() {
144
+ for (const entry of entries.values())
145
+ entry.observer?.retry();
146
+ },
147
+ loadMore() {
148
+ for (const entry of entries.values())
149
+ entry.observer?.loadMore();
150
+ },
142
151
  start() {
143
152
  if (started || disposed)
144
153
  return;
@@ -53,6 +53,7 @@ export function createApplicationMachineCatalog(options = {}) {
53
53
  note: diagnostic && diagnostic.phase !== "ready"
54
54
  ? fleetConnectionMessage(diagnostic)
55
55
  : (entry?.snapshot.note ?? null),
56
+ servers: entry?.snapshot.servers,
56
57
  sessions: (entry?.lastSessions ?? []).map((session) => ({
57
58
  ...session,
58
59
  sourceId: session.id,
@@ -86,7 +87,13 @@ export function createApplicationMachineCatalog(options = {}) {
86
87
  sessions: group.sessions.map((session) => ({
87
88
  ...session,
88
89
  id: session.liveSessionId && joined.environmentId && !joined.conflict
89
- ? JSON.stringify([joined.environmentId, "session", session.liveSessionId])
90
+ ? JSON.stringify([
91
+ joined.environmentId,
92
+ "session",
93
+ session.server?.serverId,
94
+ session.server?.generation,
95
+ session.liveSessionId,
96
+ ])
90
97
  : session.id,
91
98
  })),
92
99
  };
@@ -152,11 +159,14 @@ export function createApplicationMachineCatalog(options = {}) {
152
159
  environmentId: entry.environmentId,
153
160
  generation: entry.generation,
154
161
  seenAt: entry.seenAt,
155
- sessions: value.sessions.slice(0, 64).map(({ id, liveSessionId, name, paneCount }) => ({
162
+ sessions: value.sessions
163
+ .slice(0, 64)
164
+ .map(({ id, liveSessionId, name, paneCount, server, serverLabel }) => ({
156
165
  id,
157
166
  ...(liveSessionId ? { liveSessionId } : {}),
158
167
  name,
159
168
  paneCount,
169
+ ...(server ? { server, serverLabel } : {}),
160
170
  })),
161
171
  });
162
172
  }