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,6 +1,14 @@
1
+ import WebSocket from "ws";
2
+ import {
3
+ createWorkspaceEventSupervisor,
4
+ type WorkspaceEventSocket,
5
+ } from "@tmux-ide/daemon-client/workspace-event-supervisor";
1
6
  import { readApplicationDaemonInfo as readCanonicalDaemonInfo } from "./application-daemon-authority.ts";
2
7
  import {
3
- WorkspaceCatalogResourceV3SchemaZ,
8
+ TmuxServersResourceSchemaZ,
9
+ TmuxServerSessionsResourceSchemaZ,
10
+ type TmuxServerScope,
11
+ type TmuxServerDescriptor,
4
12
  type CanonicalDaemonInfo,
5
13
  type WorkspaceCatalogResourceV3,
6
14
  } from "@tmux-ide/contracts";
@@ -9,18 +17,16 @@ import {
9
17
  type PushResourceSessionAdapter,
10
18
  type PushResourceSessionOptions,
11
19
  } from "@tmux-ide/daemon-client/push-resource-session";
12
- import {
13
- createWorkspaceEventSupervisor,
14
- type WorkspaceEventSocket,
15
- } from "@tmux-ide/daemon-client/workspace-event-supervisor";
16
- import WebSocket from "ws";
17
20
 
18
21
  import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.ts";
19
22
 
20
23
  export type ApplicationHomeCatalogResourceKey = "live-catalog";
21
24
  export type ApplicationHomeCatalogResource = {
22
25
  readonly kind: "live-catalog";
23
- readonly value: WorkspaceCatalogResourceV3;
26
+ readonly value?: WorkspaceCatalogResourceV3;
27
+ readonly daemonInstanceId?: string;
28
+ readonly servers?: readonly TmuxServerDescriptor[];
29
+ readonly scopedSessions?: readonly ApplicationHomeCatalogSession[];
24
30
  };
25
31
  export interface ApplicationHomeCatalogTarget {
26
32
  readonly daemon: CanonicalDaemonInfo;
@@ -35,6 +41,8 @@ export interface ApplicationHomeCatalogFailure {
35
41
 
36
42
  export interface ApplicationHomeCatalogSession {
37
43
  readonly id: string;
44
+ readonly server?: TmuxServerScope;
45
+ readonly serverLabel?: string;
38
46
  readonly liveSessionId?: string;
39
47
  readonly workspaceName?: string;
40
48
  readonly name: string;
@@ -45,6 +53,7 @@ export interface ApplicationHomeCatalogSnapshot {
45
53
  readonly phase: "loading" | "live" | "unavailable";
46
54
  readonly daemonInstanceId: string | null;
47
55
  readonly sessions: readonly ApplicationHomeCatalogSession[];
56
+ readonly servers?: readonly TmuxServerDescriptor[];
48
57
  readonly note: string | null;
49
58
  }
50
59
 
@@ -117,7 +126,7 @@ export function closeApplicationHomeCatalogTransport(
117
126
  supervisor.dispose();
118
127
  }
119
128
 
120
- function createApplicationHomeCatalogAdapter(onTransportRetired: () => void): HomeCatalogAdapter {
129
+ function createApplicationHomeCatalogAdapter(_onTransportRetired: () => void): HomeCatalogAdapter {
121
130
  return {
122
131
  validateTarget(value) {
123
132
  if (
@@ -139,15 +148,9 @@ function createApplicationHomeCatalogAdapter(onTransportRetired: () => void): Ho
139
148
  };
140
149
  },
141
150
  async fetch(target, _key, signal) {
142
- const url = canonicalDaemonUrl(
143
- "http",
144
- target.daemon.bindHostname,
145
- target.daemon.port,
146
- "/api/resources/workspace-catalog?version=3",
147
- );
148
- let response: Response;
149
- try {
150
- response = await fetch(url, {
151
+ const base = canonicalDaemonUrl("http", target.daemon.bindHostname, target.daemon.port);
152
+ const read = async (path: string) => {
153
+ const response = await fetch(base + path, {
151
154
  headers: {
152
155
  accept: "application/json",
153
156
  ...(target.daemon.authToken
@@ -157,88 +160,139 @@ function createApplicationHomeCatalogAdapter(onTransportRetired: () => void): Ho
157
160
  credentials: "omit",
158
161
  cache: "no-store",
159
162
  redirect: "error",
160
- signal,
163
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3_000)]),
161
164
  });
162
- } catch {
163
- return {
164
- status: "failed",
165
- failure: catalogFailure("network", "Live session discovery failed.", true),
166
- };
167
- }
168
- if (!response.ok)
165
+ if (!response.ok) throw new Error(`Discovery returned HTTP ${response.status}`);
166
+ return response.json();
167
+ };
168
+ try {
169
+ const inventory = TmuxServersResourceSchemaZ.parse(await read("/api/v1/tmux-servers"));
170
+ const sessions = await Promise.allSettled(
171
+ inventory.servers.map(async (descriptor) => {
172
+ if (descriptor.state !== "online") return [];
173
+ const server = { serverId: descriptor.serverId, generation: descriptor.generation };
174
+ const result = TmuxServerSessionsResourceSchemaZ.parse(
175
+ await read(`/api/v1/tmux-servers/${server.serverId}/${server.generation}/sessions`),
176
+ );
177
+ if (
178
+ result.server.serverId !== server.serverId ||
179
+ result.server.generation !== server.generation
180
+ )
181
+ throw new Error("Server discovery identity changed");
182
+ return result.sessions.map((session) => ({
183
+ id: JSON.stringify([server.serverId, server.generation, session.liveSessionId]),
184
+ server,
185
+ serverLabel: descriptor.label,
186
+ liveSessionId: session.liveSessionId,
187
+ name: session.sessionName,
188
+ ...(session.workspaceName ? { workspaceName: session.workspaceName } : {}),
189
+ paneCount: session.paneCount,
190
+ }));
191
+ }),
192
+ );
169
193
  return {
170
- status: "failed",
171
- failure: catalogFailure(
172
- "http",
173
- `Live session discovery returned HTTP ${response.status}.`,
174
- response.status >= 500,
175
- ),
194
+ status: "ok",
195
+ resource: {
196
+ kind: "live-catalog",
197
+ daemonInstanceId: target.daemon.instanceId,
198
+ servers: inventory.servers.map((server, index) =>
199
+ sessions[index]?.status === "rejected"
200
+ ? {
201
+ serverId: server.serverId,
202
+ label: server.label,
203
+ state: "offline" as const,
204
+ generation: null,
205
+ }
206
+ : server,
207
+ ),
208
+ scopedSessions: sessions.flatMap((result) =>
209
+ result.status === "fulfilled" ? result.value : [],
210
+ ),
211
+ },
176
212
  };
177
- let body: unknown;
178
- try {
179
- body = await response.json();
180
213
  } catch {
181
214
  return {
182
215
  status: "failed",
183
- failure: catalogFailure("schema", "Live session discovery returned invalid JSON.", false),
216
+ failure: catalogFailure("network", "Server session discovery failed.", true),
184
217
  };
185
218
  }
186
- const parsed = WorkspaceCatalogResourceV3SchemaZ.safeParse(body);
187
- if (!parsed.success || !sameDaemon(target.daemon, parsed.data.daemon))
188
- return {
189
- status: "failed",
190
- failure: catalogFailure(
191
- "schema",
192
- "Live session discovery failed daemon identity validation.",
193
- false,
194
- ),
195
- };
196
- return { status: "ok", resource: { kind: "live-catalog", value: parsed.data } };
197
219
  },
198
220
  async connect(target, _interests, handlers, signal) {
199
- const socketUrl = canonicalDaemonUrl(
200
- "ws",
201
- target.daemon.bindHostname,
202
- target.daemon.port,
203
- "/ws/events?mode=semantic",
204
- );
205
- const socket = new WebSocket(socketUrl, {
206
- headers: target.daemon.authToken
207
- ? { Authorization: `Bearer ${target.daemon.authToken}` }
208
- : undefined,
209
- }) as unknown as WorkspaceEventSocket;
210
- const supervisor = createWorkspaceEventSupervisor({
211
- socket,
212
- daemon: target.daemon,
213
- workspaceName: target.workspaceName,
214
- sessionName: target.workspaceName,
215
- fetchTerminalRuntimeInventory: () =>
216
- Promise.reject(new Error("Home catalog owns no terminal inventory.")),
217
- onRetired: onTransportRetired,
218
- });
219
- const subscription = supervisor.connectWorkspaceCatalog(
220
- () => handlers.invalidate(["live-catalog"]),
221
- { terminalFirst: false },
222
- );
221
+ // Root push is an advisory invalidation only: every refresh still resolves
222
+ // the full scoped inventory. The timer discovers changes on other owners.
223
223
  let closed = false;
224
- const close = (): void => {
225
- if (closed) return;
224
+ let pending = false;
225
+ let closePush: (() => void) | null = null;
226
+ const invalidate = () => {
227
+ if (!closed) handlers.invalidate(["live-catalog"]);
228
+ };
229
+ const startPush = async () => {
230
+ if (closed || pending || closePush) return;
231
+ pending = true;
232
+ const socket = new WebSocket(
233
+ canonicalDaemonUrl(
234
+ "ws",
235
+ target.daemon.bindHostname,
236
+ target.daemon.port,
237
+ "/ws/events?mode=semantic",
238
+ ),
239
+ {
240
+ headers: target.daemon.authToken
241
+ ? { Authorization: `Bearer ${target.daemon.authToken}` }
242
+ : undefined,
243
+ },
244
+ ) as unknown as WorkspaceEventSocket;
245
+ const supervisor = createWorkspaceEventSupervisor({
246
+ socket,
247
+ daemon: target.daemon,
248
+ workspaceName: target.workspaceName,
249
+ sessionName: target.workspaceName,
250
+ fetchTerminalRuntimeInventory: () =>
251
+ Promise.reject(new Error("Home catalog owns no terminal inventory.")),
252
+ onRetired: () => {
253
+ const cleanup = closePush;
254
+ closePush = null;
255
+ cleanup?.();
256
+ },
257
+ });
258
+ const subscription = supervisor.connectWorkspaceCatalog(invalidate, {
259
+ terminalFirst: false,
260
+ });
261
+ const cleanup = () => closeApplicationHomeCatalogTransport(subscription, supervisor);
262
+ closePush = cleanup;
263
+ try {
264
+ await subscription.ready;
265
+ } catch {
266
+ if (closePush === cleanup) closePush = null;
267
+ cleanup();
268
+ } finally {
269
+ pending = false;
270
+ if (closed) {
271
+ cleanup();
272
+ closePush = null;
273
+ }
274
+ }
275
+ };
276
+ const timer = setInterval(() => {
277
+ invalidate();
278
+ void startPush().catch(() => {
279
+ pending = false;
280
+ });
281
+ }, 5_000);
282
+ const close = () => {
226
283
  closed = true;
284
+ clearInterval(timer);
227
285
  signal.removeEventListener("abort", close);
228
- // Release the logical observer before retiring its physical transport.
229
- closeApplicationHomeCatalogTransport(subscription, supervisor);
286
+ const cleanup = closePush;
287
+ closePush = null;
288
+ cleanup?.();
230
289
  };
231
290
  signal.addEventListener("abort", close, { once: true });
232
- try {
233
- await subscription.ready;
234
- } catch (error) {
235
- close();
236
- throw error;
237
- }
238
- if (signal.aborted) {
239
- close();
240
- throw signal.reason;
241
- }
291
+ if (signal.aborted) close();
292
+ else
293
+ void startPush().catch(() => {
294
+ pending = false;
295
+ });
242
296
  return { status: "connected", close };
243
297
  },
244
298
  rejectionFailure: () =>
@@ -264,31 +318,37 @@ const initialSnapshot = (): ApplicationHomeCatalogSnapshot => ({
264
318
  note: "Discovering live tmux sessions…",
265
319
  });
266
320
 
267
- function projectCatalog(resource: WorkspaceCatalogResourceV3): ApplicationHomeCatalogSnapshot {
321
+ function projectCatalog(
322
+ resource: WorkspaceCatalogResourceV3,
323
+ scopedSessions?: readonly ApplicationHomeCatalogSession[],
324
+ ): ApplicationHomeCatalogSnapshot {
268
325
  return {
269
326
  phase: "live",
270
327
  daemonInstanceId: resource.daemon.instanceId,
271
- sessions: resource.liveSessions
272
- .map(({ liveSessionId, sessionName, paneCount }) => ({
273
- id: `${resource.daemon.instanceId}:${liveSessionId}`,
274
- liveSessionId,
275
- workspaceName: resource.intents.find((intent) => intent.sessionName === sessionName)
276
- ?.workspaceName,
277
- name: sessionName,
278
- paneCount,
279
- }))
280
- .sort((left, right) => left.name.localeCompare(right.name)),
328
+ sessions:
329
+ scopedSessions ??
330
+ resource.liveSessions
331
+ .map(({ liveSessionId, sessionName, paneCount }) => ({
332
+ id: `${resource.daemon.instanceId}:${liveSessionId}`,
333
+ liveSessionId,
334
+ workspaceName: resource.intents.find((intent) => intent.sessionName === sessionName)
335
+ ?.workspaceName,
336
+ name: sessionName,
337
+ paneCount,
338
+ }))
339
+ .sort((left, right) => left.name.localeCompare(right.name)),
281
340
  note:
282
- resource.liveSessions.length === 0
341
+ (scopedSessions?.length ?? resource.liveSessions.length) === 0
283
342
  ? "No live tmux sessions yet. This list updates automatically."
284
343
  : null,
285
344
  };
286
345
  }
287
346
 
288
347
  /**
289
- * Daemon-authoritative Home catalog. Reads are push-invalidated while the
290
- * daemon generation is healthy; the only timer is a bounded failure-recovery
291
- * probe used when no canonical daemon or event transport is available.
348
+ * Daemon-authoritative fleet metadata. Root push preserves immediate updates,
349
+ * while a five-second refresh discovers independent
350
+ * server owners without opening pane streams. Failed reads use bounded backoff;
351
+ * each failed server is excluded without suppressing healthy siblings.
292
352
  */
293
353
  export function createApplicationHomeCatalog(
294
354
  overrides: Partial<ApplicationHomeCatalogDependencies> = {},
@@ -373,7 +433,19 @@ export function createApplicationHomeCatalog(
373
433
  if (slot?.status === "loaded" && !slot.refreshing && slot.resource.kind === "live-catalog") {
374
434
  clearRetry();
375
435
  retryAttempt = 0;
376
- publish(projectCatalog(slot.resource.value));
436
+ publish(
437
+ slot.resource.value
438
+ ? projectCatalog(slot.resource.value)
439
+ : {
440
+ phase: "live",
441
+ daemonInstanceId: slot.resource.daemonInstanceId ?? null,
442
+ sessions: slot.resource.scopedSessions ?? [],
443
+ servers: slot.resource.servers,
444
+ note: slot.resource.scopedSessions?.length
445
+ ? null
446
+ : "No live tmux sessions yet. This list updates automatically.",
447
+ },
448
+ );
377
449
  return;
378
450
  }
379
451
  if (slot?.status === "error") {
@@ -0,0 +1,59 @@
1
+ import type { Accessor } from "solid-js";
2
+ import { createApplicationHomeFleetOwner } from "./application-home-fleet.ts";
3
+ import { createApplicationHomeNavigationOwner } from "./application-home-agents-owner.ts";
4
+ import { createApplicationGuidedTourIntegration } from "./application-guided-tour-integration.tsx";
5
+
6
+ type NavigationOptions = Parameters<typeof createApplicationHomeNavigationOwner>[0];
7
+ type TourOptions = Parameters<typeof createApplicationGuidedTourIntegration>[0];
8
+ /** Compose Home's fleet, navigation and learning experience without additional transports. */
9
+ export function createApplicationHomeExperience(
10
+ options: Omit<NavigationOptions, "fleetHome" | "fleetCommands" | "openFleet"> &
11
+ Pick<
12
+ TourOptions,
13
+ | "machines"
14
+ | "lifecycle"
15
+ | "generation"
16
+ | "generationMachineId"
17
+ | "layoutSnapshot"
18
+ | "appearance"
19
+ | "dimensions"
20
+ > & { paletteModalOpen: Accessor<boolean> },
21
+ ) {
22
+ const { machines, appearance } = options;
23
+ const paletteOpen = () =>
24
+ options.shell().semantic?.focus.palette.open ?? options.shell().localPaletteOpen;
25
+ const fleetHome = createApplicationHomeFleetOwner({
26
+ catalog: machines.catalog,
27
+ agents: machines.agents,
28
+ inputActive: () =>
29
+ options.activeSurface() === "home" &&
30
+ options.rendererFocused() &&
31
+ !options.paletteModalOpen() &&
32
+ !machines.switching() &&
33
+ !machines.adding() &&
34
+ !machines.focused() &&
35
+ !paletteOpen() &&
36
+ !appearance.pickerOpen(),
37
+ open: (_machineId, agent, source) => machines.openHomeAgent(agent, source),
38
+ });
39
+ const navigation = createApplicationHomeNavigationOwner({
40
+ ...options,
41
+ fleetHome: fleetHome.presentation,
42
+ fleetCommands: machines.paletteCommands,
43
+ openFleet: machines.openPalette,
44
+ });
45
+ const tour = createApplicationGuidedTourIntegration({
46
+ ...options,
47
+ sessionName: () => options.sessionOwner()?.sessionName() ?? null,
48
+ paletteOpen,
49
+ paletteCommands: navigation.paletteCommands,
50
+ blocked: () =>
51
+ options.paletteModalOpen() ||
52
+ !!navigation.paneRename.draft() ||
53
+ paletteOpen() ||
54
+ appearance.pickerOpen() ||
55
+ machines.switching() ||
56
+ machines.adding(),
57
+ });
58
+ return { ...navigation, tour };
59
+ }
@@ -0,0 +1,175 @@
1
+ import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
2
+ import type {
3
+ ApplicationMachineAgent,
4
+ ApplicationMachineAgentGroup,
5
+ } from "./application-machine-agents.ts";
6
+ import type { ApplicationMachineCatalogSnapshot } from "./application-machine-catalog.ts";
7
+ import type { HomeAgentRow, HomeAgentSnapshot } from "./application-home-agents.ts";
8
+ import { createHomeAgentSelectionOwner } from "./application-home-agent-selection.ts";
9
+ import type { ApplicationHomeAgentPresentation } from "./application-home-agents-owner.ts";
10
+
11
+ export interface HomeFleetFilter {
12
+ readonly machineId: string | null;
13
+ readonly attentionOnly: boolean;
14
+ }
15
+ export function projectHomeFleet(
16
+ catalog: ApplicationMachineCatalogSnapshot,
17
+ groups: readonly ApplicationMachineAgentGroup[],
18
+ filter: HomeFleetFilter,
19
+ ): HomeAgentSnapshot {
20
+ const machines = catalog.groups.filter(
21
+ (group) => filter.machineId === null || group.id === filter.machineId,
22
+ );
23
+ const rows: HomeAgentRow[] = [];
24
+ const unavailable = new Set<string>();
25
+ let observedSessions = 0,
26
+ totalSessions = 0,
27
+ loadingSessions = 0,
28
+ unavailableSessions = 0,
29
+ truncatedSessions = 0;
30
+ const panes = new Set<string>();
31
+ for (const machine of machines) {
32
+ const group = groups.find((group) => group.machineId === machine.id);
33
+ totalSessions += machine.sessions.length;
34
+ const observation = machine.state === "ready" ? group?.observation : undefined;
35
+ observedSessions +=
36
+ observation?.observedSessions ?? (group?.available ? machine.sessions.length : 0);
37
+ loadingSessions +=
38
+ observation?.loadingSessions ??
39
+ (machine.state === "connecting" ? machine.sessions.length : 0);
40
+ unavailableSessions +=
41
+ observation?.unavailableSessions ??
42
+ (machine.state === "disconnected" ? machine.sessions.length : 0);
43
+ truncatedSessions += observation?.truncatedSessions ?? 0;
44
+ for (const key of observation?.unavailableSessionKeys ?? [])
45
+ unavailable.add(JSON.stringify([machine.id, key]));
46
+ for (const agent of [...(group?.agents ?? [])].sort(
47
+ (a, b) => Number(a.disabled) - Number(b.disabled) || a.id.localeCompare(b.id),
48
+ )) {
49
+ const sessionKey = JSON.stringify([machine.id, agent.sessionKey]);
50
+ if (agent.disabled) unavailable.add(sessionKey);
51
+ if (
52
+ filter.attentionOnly &&
53
+ !(agent.attention || agent.activity === "waiting" || agent.activity === "failed")
54
+ )
55
+ continue;
56
+ const paneKey = agent.paneId
57
+ ? JSON.stringify([machine.id, agent.server?.serverId, agent.daemonInstanceId, agent.paneId])
58
+ : agent.id;
59
+ if (panes.has(paneKey)) continue;
60
+ panes.add(paneKey);
61
+ rows.push({ ...agent, key: agent.id, sessionKey, machineLabel: machine.label });
62
+ }
63
+ }
64
+ // Identity order never jumps when an activity signal changes.
65
+ rows.sort((a, b) => a.key.localeCompare(b.key));
66
+ const missing = machines.filter(
67
+ (machine) =>
68
+ machine.state !== "ready" ||
69
+ !groups.find((group) => group.machineId === machine.id)?.available,
70
+ );
71
+ return {
72
+ phase: missing.length
73
+ ? rows.length || observedSessions
74
+ ? "partial"
75
+ : machines.some((machine) => machine.state === "connecting")
76
+ ? "loading"
77
+ : "unavailable"
78
+ : "live",
79
+ rows,
80
+ observedSessions,
81
+ totalSessions,
82
+ loadingSessions,
83
+ unavailableSessions,
84
+ truncatedSessions,
85
+ refreshingSessionKeys: [],
86
+ unavailableSessionKeys: [...unavailable],
87
+ note: missing.length
88
+ ? `${missing.map((machine) => `${machine.label}: ${machine.state === "ready" ? "partial observations" : machine.state}`).join(" · ")} · unavailable rows show last observed activity`
89
+ : rows.length === 0 && filter.attentionOnly
90
+ ? "No agents need attention in this view."
91
+ : null,
92
+ };
93
+ }
94
+
95
+ /** Reuses resident fleet observations, adding no daemon connections or pane streams. */
96
+ export function createApplicationHomeFleetOwner(options: {
97
+ readonly catalog: {
98
+ getSnapshot(): ApplicationMachineCatalogSnapshot;
99
+ subscribe(listener: (value: ApplicationMachineCatalogSnapshot) => void): () => void;
100
+ };
101
+ readonly agents: {
102
+ retry?(): void;
103
+ loadMore?(): void;
104
+ getSnapshot(): readonly ApplicationMachineAgentGroup[];
105
+ subscribe(listener: (value: readonly ApplicationMachineAgentGroup[]) => void): () => void;
106
+ };
107
+ readonly inputActive: () => boolean;
108
+ readonly open: (
109
+ machineId: string,
110
+ agent: ApplicationMachineAgent,
111
+ source: "keyboard" | "mouse",
112
+ ) => Promise<unknown> | void;
113
+ }) {
114
+ const [catalog, setCatalog] = createSignal(options.catalog.getSnapshot());
115
+ const [groups, setGroups] = createSignal(options.agents.getSnapshot());
116
+ const [filter, setFilter] = createSignal<HomeFleetFilter>({
117
+ machineId: null,
118
+ attentionOnly: false,
119
+ });
120
+ const selection = createHomeAgentSelectionOwner();
121
+ const [selected, setSelected] = createSignal(selection.snapshot());
122
+ const snapshot = createMemo(() => projectHomeFleet(catalog(), groups(), filter()));
123
+ const stops = [
124
+ options.catalog.subscribe(setCatalog),
125
+ options.agents.subscribe(setGroups),
126
+ selection.subscribe(setSelected),
127
+ ];
128
+ createEffect(() => selection.setRows(snapshot().rows));
129
+ createEffect(() => {
130
+ const id = filter().machineId;
131
+ if (id && !catalog().groups.some((group) => group.id === id))
132
+ setFilter((value) => ({ ...value, machineId: null }));
133
+ });
134
+ onCleanup(() => {
135
+ for (const stop of stops) stop();
136
+ selection.dispose();
137
+ });
138
+ const presentation: ApplicationHomeAgentPresentation = {
139
+ get agentRoster() {
140
+ return snapshot();
141
+ },
142
+ get agentSelection() {
143
+ return selected();
144
+ },
145
+ get agentInputActive() {
146
+ return options.inputActive();
147
+ },
148
+ get agentFilterLabel() {
149
+ return `${catalog().groups.find((group) => group.id === filter().machineId)?.label ?? "All machines"} · ${filter().attentionOnly ? "Needs attention" : "All agents"}`;
150
+ },
151
+ onCycleAgentMachine() {
152
+ const ids = [null, ...catalog().groups.map((group) => group.id)];
153
+ setFilter((value) => ({
154
+ ...value,
155
+ machineId: ids[(ids.indexOf(value.machineId) + 1) % ids.length]!,
156
+ }));
157
+ },
158
+ onToggleAgentAttention() {
159
+ setFilter((value) => ({ ...value, attentionOnly: !value.attentionOnly }));
160
+ },
161
+ onRetryAgents: () => options.agents.retry?.(),
162
+ onLoadMoreAgents: () => options.agents.loadMore?.(),
163
+ onSelectAgent: selection.select,
164
+ onMoveAgent: selection.move,
165
+ onAgentViewport: selection.setViewport,
166
+ onOpenAgent(row, source) {
167
+ if (!options.inputActive()) return;
168
+ const group = groups().find((group) => group.machineId === row.machineId);
169
+ const agent = group?.agents.find((agent) => agent.id === row.key);
170
+ if (agent && !agent.disabled && agent.paneId)
171
+ void options.open(agent.machineId, agent, source);
172
+ },
173
+ };
174
+ return { presentation };
175
+ }
@@ -10,7 +10,7 @@ import {
10
10
  type ApplicationHomeAgentObserver,
11
11
  } from "./application-home-agent-observer.ts";
12
12
  import { createApplicationHomeAgentTransport } from "./application-home-agent-transport.ts";
13
- import type { HomeAgentRow } from "./application-home-agents.ts";
13
+ import type { HomeAgentRow, HomeAgentSnapshot } from "./application-home-agents.ts";
14
14
 
15
15
  export interface ApplicationMachineAgent extends HomeAgentRow {
16
16
  readonly id: string;
@@ -19,6 +19,7 @@ export interface ApplicationMachineAgent extends HomeAgentRow {
19
19
  }
20
20
  export interface ApplicationMachineAgentGroup {
21
21
  readonly available?: boolean;
22
+ readonly observation?: HomeAgentSnapshot;
22
23
  readonly machineId: string;
23
24
  readonly agents: readonly ApplicationMachineAgent[];
24
25
  }
@@ -74,6 +75,7 @@ export function createApplicationMachineAgents(options: {
74
75
  const current = !!entry && entry.binding === bindingFor(entry.handle);
75
76
  return Object.freeze({
76
77
  machineId: group.id,
78
+ observation: entry?.observer?.getSnapshot(),
77
79
  available:
78
80
  group.state === "ready" && current && entry?.observer?.getSnapshot().phase === "live",
79
81
  agents: Object.freeze(
@@ -185,6 +187,12 @@ export function createApplicationMachineAgents(options: {
185
187
  !!entry.observer?.isCurrentTarget(row)
186
188
  );
187
189
  },
190
+ retry() {
191
+ for (const entry of entries.values()) entry.observer?.retry();
192
+ },
193
+ loadMore() {
194
+ for (const entry of entries.values()) entry.observer?.loadMore();
195
+ },
188
196
  start() {
189
197
  if (started || disposed) return;
190
198
  started = true;