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
@@ -109,14 +109,18 @@ function layoutKey(frame) {
109
109
  return null;
110
110
  return `panes:${panes.join("\u0000")}`;
111
111
  }
112
- function layoutSnapshot(windows, currentWindowKey = null) {
112
+ function layoutSnapshot(windows, currentWindowKey = null, windowLinks = null) {
113
113
  const selectedKey = currentWindowKey ??
114
114
  [...windows.entries()].find(([, frame]) => frame.currentWindow)?.[0] ??
115
115
  null;
116
116
  const frames = Object.freeze([...windows.entries()].map(([key, frame]) => frame.currentWindow === (key === selectedKey)
117
117
  ? frame
118
118
  : freezeLayout({ ...frame, currentWindow: key === selectedKey })));
119
+ const links = windowLinks?.links.map((link) => Object.freeze({ ...link }));
120
+ if (links)
121
+ Object.freeze(links);
119
122
  return Object.freeze({
123
+ windowLinks: windowLinks && links ? Object.freeze({ ...windowLinks, links }) : null,
120
124
  current: frames.find((frame) => frame.currentWindow) ?? null,
121
125
  windows: frames,
122
126
  });
@@ -144,7 +148,8 @@ function layoutFrameSemanticallyEqual(left, right) {
144
148
  }));
145
149
  }
146
150
  function layoutSnapshotsSemanticallyEqual(left, right) {
147
- return (left.windows.length === right.windows.length &&
151
+ return (JSON.stringify(left.windowLinks ?? null) === JSON.stringify(right.windowLinks ?? null) &&
152
+ left.windows.length === right.windows.length &&
148
153
  left.windows.every((window, index) => {
149
154
  const candidate = right.windows[index];
150
155
  return candidate !== undefined && layoutFrameSemanticallyEqual(window, candidate);
@@ -702,6 +707,8 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
702
707
  if (coherentSettled ||
703
708
  closed ||
704
709
  !physicalReady ||
710
+ (routing.liveSessionId !== undefined &&
711
+ latestLayoutSnapshot.windowLinks?.liveSessionId !== routing.liveSessionId) ||
705
712
  !layoutExactlyCoversPanes(latestLayoutSnapshot, panes) ||
706
713
  !latestLayoutSnapshot.current?.panes.every(({ pane }) => typeof pane === "string" && canonicalSeedPanes.has(pane))) {
707
714
  return;
@@ -866,7 +873,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
866
873
  : {}),
867
874
  signal: options.signal,
868
875
  stream: {
869
- protocolVersion: 1,
876
+ protocolVersion: 2,
870
877
  workspaceName: inventory.workspaceName,
871
878
  panes: [...panes],
872
879
  viewerMode: "interactive",
@@ -981,13 +988,16 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
981
988
  });
982
989
  return;
983
990
  }
991
+ if (latestLayoutSnapshot.windowLinks &&
992
+ !latestLayoutSnapshot.windowLinks.links.some((link) => link.semanticWindowId === retained.semanticWindowId))
993
+ return;
984
994
  layoutsByWindow.set(key, retained);
985
995
  // tmux reports a switch as incumbent=false followed by target=true.
986
996
  // Retain the incumbent through that pair and publish one unique current
987
997
  // window only when the positive target frame arrives.
988
- if (retained.currentWindow)
998
+ if (!latestLayoutSnapshot.windowLinks && retained.currentWindow)
989
999
  currentWindowKey = key;
990
- const candidate = layoutSnapshot(layoutsByWindow, currentWindowKey);
1000
+ const candidate = layoutSnapshot(layoutsByWindow, currentWindowKey, latestLayoutSnapshot.windowLinks ?? null);
991
1001
  if (!layoutExactlyCoversPanes(candidate, panes)) {
992
1002
  if (lastRejectedLayoutSnapshot &&
993
1003
  layoutSnapshotsSemanticallyEqual(candidate, lastRejectedLayoutSnapshot))
@@ -1023,6 +1033,9 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1023
1033
  onLayoutSnapshot: (snapshot) => {
1024
1034
  if (closed)
1025
1035
  return;
1036
+ if (routing.liveSessionId !== undefined &&
1037
+ snapshot.windowLinks.liveSessionId !== routing.liveSessionId)
1038
+ return failConnection(new Error("Selected live session was replaced before terminal activation"));
1026
1039
  const replacement = new Map();
1027
1040
  let replacementCurrent = null;
1028
1041
  for (const frame of snapshot.layouts) {
@@ -1030,14 +1043,15 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1030
1043
  const key = layoutKey(retained);
1031
1044
  if (key === null || replacement.has(key))
1032
1045
  return failConnection(new Error("invalid layout snapshot"));
1033
- replacement.set(key, retained);
1046
+ const previous = layoutsByWindow.get(key);
1047
+ replacement.set(key, previous && layoutFrameSemanticallyEqual(previous, retained) ? previous : retained);
1034
1048
  if (retained.currentWindow) {
1035
1049
  if (replacementCurrent !== null)
1036
1050
  return failConnection(new Error("invalid layout snapshot current window"));
1037
1051
  replacementCurrent = key;
1038
1052
  }
1039
1053
  }
1040
- const candidate = layoutSnapshot(replacement, replacementCurrent);
1054
+ const candidate = layoutSnapshot(replacement, replacementCurrent, snapshot.windowLinks);
1041
1055
  if (!layoutExactlyCoversPanes(candidate, panes))
1042
1056
  return failConnection(new Error("layout snapshot does not cover terminal inventory"));
1043
1057
  layoutsByWindow.clear();
@@ -1048,6 +1062,11 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1048
1062
  if (layoutSnapshotsSemanticallyEqual(candidate, latestLayoutSnapshot))
1049
1063
  return;
1050
1064
  latestLayoutSnapshot = candidate;
1065
+ options.onDiagnostic?.("layout", {
1066
+ windows: candidate.windows.length,
1067
+ current: candidate.current !== null,
1068
+ links: candidate.windowLinks?.links.length ?? 0,
1069
+ });
1051
1070
  settleCoherent();
1052
1071
  for (const listener of [...layoutListeners]) {
1053
1072
  try {
@@ -1198,7 +1217,9 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
1198
1217
  submitIntent: async (operationId, intent) => {
1199
1218
  // Structural commands can affect hidden panes. Do not defer user intent
1200
1219
  // until a different topology may be current; fail closed during seeding.
1201
- if (closed || [...endpoints.values()].some((endpoint) => !endpoint.inputReady))
1220
+ if (closed ||
1221
+ !latestLayoutSnapshot.windowLinks ||
1222
+ [...endpoints.values()].some((endpoint) => !endpoint.inputReady))
1202
1223
  throw new Error("Terminal inventory is still receiving canonical state");
1203
1224
  return (await opened.submitIntent(operationId, intent)) ?? undefined;
1204
1225
  },
@@ -56,8 +56,12 @@ export function createApplicationHomeCatalogOwner(options) {
56
56
  const currentSessions = next.sessions;
57
57
  if (automaticOpen && next.phase === "live") {
58
58
  automaticOpen = false;
59
- if (currentSessions.length === 1)
60
- void options.startGeneration(currentSessions[0].name);
59
+ if (currentSessions.length === 1) {
60
+ const session = currentSessions[0];
61
+ void (options.openCatalogSession
62
+ ? options.openCatalogSession(session)
63
+ : options.startGeneration(session.name));
64
+ }
61
65
  }
62
66
  if (currentSessions.length === 0) {
63
67
  if (current !== null)
@@ -93,9 +97,11 @@ export function createApplicationHomeCatalogOwner(options) {
93
97
  return true;
94
98
  }
95
99
  if (name === "return" || name === "enter") {
96
- const sessionName = sessions()[selectedSessionIndex()]?.name;
97
- if (sessionName)
98
- void options.startGeneration(sessionName);
100
+ const session = sessions()[selectedSessionIndex()];
101
+ if (session)
102
+ void (options.openCatalogSession
103
+ ? options.openCatalogSession(session)
104
+ : options.startGeneration(session.name));
99
105
  return true;
100
106
  }
101
107
  return false;
@@ -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") {
@@ -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
  }