tmux-ide 2.9.0-beta.21 → 2.9.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/bin/cli.js +52253 -49070
  2. package/package.json +1 -1
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
  4. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
  5. package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
  6. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
  7. package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
  8. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
  9. package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
  10. package/packages/contracts/src/actions-contract.ts +12 -0
  11. package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
  12. package/packages/contracts/src/daemon-wire.ts +10 -5
  13. package/packages/contracts/src/fleet-client-state.ts +3 -0
  14. package/packages/contracts/src/index.ts +2 -0
  15. package/packages/contracts/src/interaction-receipts.ts +39 -0
  16. package/packages/contracts/src/issue-error.ts +1 -1
  17. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  18. package/packages/contracts/src/pane-stream.ts +51 -8
  19. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  20. package/packages/contracts/src/window-links.ts +76 -0
  21. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  22. package/packages/core/src/interaction-receipts.ts +4 -0
  23. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  24. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  25. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  26. package/packages/daemon/dist/command-center/discovery.js +2 -9
  27. package/packages/daemon/dist/command-center/server.js +21 -1
  28. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  29. package/packages/daemon/dist/command-center/tmux-servers.js +345 -0
  30. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  31. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  32. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  33. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  34. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +16 -10
  35. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  36. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  37. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  38. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  39. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  40. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  41. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  42. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  43. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  44. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  46. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  47. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  48. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  49. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  50. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  51. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  52. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  53. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  54. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  55. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  56. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  57. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  58. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  59. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  60. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  61. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  62. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  63. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  64. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  65. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  66. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  67. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  68. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  70. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  71. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  72. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  73. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  74. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  75. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  76. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  77. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  78. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
  79. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  80. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  81. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  82. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  83. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  84. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  85. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +42 -1
  86. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  87. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  88. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  89. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  90. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  91. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  92. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  93. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  94. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  95. package/packages/daemon/src/command-center/discovery.ts +2 -10
  96. package/packages/daemon/src/command-center/server.ts +29 -1
  97. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  98. package/packages/daemon/src/command-center/tmux-servers.ts +468 -0
  99. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  100. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  101. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  102. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  103. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +16 -8
  104. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  105. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  106. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  107. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  108. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  109. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  110. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  111. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  112. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  113. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  114. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  115. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  116. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  117. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  118. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  119. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  120. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  121. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  122. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  123. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  124. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  125. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  126. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  127. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  128. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  129. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  130. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  131. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  132. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  133. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  134. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  135. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  136. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  137. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  138. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  139. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  140. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  141. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  142. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  143. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  144. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  145. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  146. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  147. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  148. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  149. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -36
  150. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  151. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  152. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  153. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  154. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
  155. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  156. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -0
  157. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  158. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  159. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  160. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +52 -0
  161. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  162. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  163. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  164. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  165. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  166. package/packages/daemon-client/package.json +3 -1
  167. package/packages/daemon-client/src/index.ts +2 -0
  168. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  169. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  170. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  171. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  172. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  173. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -1,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") {
@@ -1,3 +1,4 @@
1
+ import type { TmuxServerDescriptor } from "@tmux-ide/contracts";
1
2
  import type { FleetCachedRoute } from "@tmux-ide/contracts/fleet-client-state";
2
3
  import { groupFleetEnvironments } from "@tmux-ide/daemon-client/fleet-environments";
3
4
  import {
@@ -27,6 +28,7 @@ export interface ApplicationMachineCatalogGroup {
27
28
  readonly label: string;
28
29
  readonly state: "ready" | "connecting" | "disconnected";
29
30
  readonly sessions: readonly ApplicationMachineCatalogSession[];
31
+ readonly servers?: readonly TmuxServerDescriptor[];
30
32
  readonly note: string | null;
31
33
  readonly diagnostic?: FleetConnectionStatus;
32
34
  readonly environmentId?: string | null;
@@ -114,6 +116,7 @@ export function createApplicationMachineCatalog(
114
116
  diagnostic && diagnostic.phase !== "ready"
115
117
  ? fleetConnectionMessage(diagnostic)
116
118
  : (entry?.snapshot.note ?? null),
119
+ servers: entry?.snapshot.servers,
117
120
  sessions: (entry?.lastSessions ?? []).map((session) => ({
118
121
  ...session,
119
122
  sourceId: session.id,
@@ -152,7 +155,13 @@ export function createApplicationMachineCatalog(
152
155
  ...session,
153
156
  id:
154
157
  session.liveSessionId && joined.environmentId && !joined.conflict
155
- ? JSON.stringify([joined.environmentId, "session", session.liveSessionId])
158
+ ? JSON.stringify([
159
+ joined.environmentId,
160
+ "session",
161
+ session.server?.serverId,
162
+ session.server?.generation,
163
+ session.liveSessionId,
164
+ ])
156
165
  : session.id,
157
166
  })),
158
167
  };
@@ -214,12 +223,15 @@ export function createApplicationMachineCatalog(
214
223
  environmentId: entry.environmentId,
215
224
  generation: entry.generation,
216
225
  seenAt: entry.seenAt,
217
- sessions: value.sessions.slice(0, 64).map(({ id, liveSessionId, name, paneCount }) => ({
218
- id,
219
- ...(liveSessionId ? { liveSessionId } : {}),
220
- name,
221
- paneCount,
222
- })),
226
+ sessions: value.sessions
227
+ .slice(0, 64)
228
+ .map(({ id, liveSessionId, name, paneCount, server, serverLabel }) => ({
229
+ id,
230
+ ...(liveSessionId ? { liveSessionId } : {}),
231
+ name,
232
+ paneCount,
233
+ ...(server ? { server, serverLabel } : {}),
234
+ })),
223
235
  });
224
236
  } catch {
225
237
  /* Cache failure must never prevent live catalog publication. */