tmux-ide 2.9.2 → 2.9.4

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 (267) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -140
  3. package/bin/cli.js +2457 -727
  4. package/package.json +23 -8
  5. package/packages/contracts/src/desktop-host.ts +3 -0
  6. package/packages/contracts/src/saved-machines.ts +19 -0
  7. package/packages/contracts/src/terminal-replica.ts +1 -1
  8. package/packages/core/package.json +3 -2
  9. package/packages/core/src/terminal-delivery.ts +51 -3
  10. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  11. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  12. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  13. package/packages/core/src/terminal-replica.ts +13 -28
  14. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  15. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  16. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  17. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  18. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  19. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  20. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  21. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  22. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  23. package/packages/daemon/dist/lib/install-origin.js +2 -0
  24. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  25. package/packages/daemon/dist/lib/log.js +6 -10
  26. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  27. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  28. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  29. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  30. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  31. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  32. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  33. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  34. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  35. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  36. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  37. package/packages/daemon/dist/lib/update.js +18 -0
  38. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  39. package/packages/daemon/dist/machines.js +40 -4
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  43. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  44. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  45. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  46. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  47. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  48. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  49. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  50. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  51. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  52. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  53. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  54. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  55. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  56. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  57. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  61. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  62. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  63. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  64. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  65. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  66. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  67. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  68. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  69. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  70. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  71. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  72. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  73. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  74. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  75. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  76. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  78. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  79. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  80. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  81. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  82. package/packages/daemon/src/lib/install-origin.ts +5 -0
  83. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  84. package/packages/daemon/src/lib/log.ts +13 -9
  85. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  86. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  87. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  88. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  89. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  90. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  91. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  92. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  93. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  94. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  95. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  96. package/packages/daemon/src/lib/update.ts +24 -0
  97. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  98. package/packages/daemon/src/machines.ts +53 -5
  99. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  100. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  101. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  102. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  103. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  104. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  105. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  106. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  107. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  108. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  109. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  110. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  111. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  112. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  115. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  116. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  117. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  118. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  119. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  120. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  121. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  122. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  123. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  124. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  125. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  126. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  127. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  128. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  129. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  130. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  131. package/packages/daemon-client/src/workspace-client.ts +4 -1
  132. package/skill/SKILL.md +30 -30
  133. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  134. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  135. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  136. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  137. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  138. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  139. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  140. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  141. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  142. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  143. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  144. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  145. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  146. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  147. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  148. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  149. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  150. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  151. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  152. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  153. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  154. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  155. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  156. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  157. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  158. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  159. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  160. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  161. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  162. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  163. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  164. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  165. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  166. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  167. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  168. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  169. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  170. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  171. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  172. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  173. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  174. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  175. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  176. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  177. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  178. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  179. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  180. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  181. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  182. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  183. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  184. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  185. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  186. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  187. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  188. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  189. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  190. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  191. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  192. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  193. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  194. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  195. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  196. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  197. package/packages/core/src/agent-provisioning.test.ts +0 -42
  198. package/packages/core/src/application-shell-session.test.ts +0 -105
  199. package/packages/core/src/interaction-evidence.test.ts +0 -134
  200. package/packages/core/src/interaction-flow.test.ts +0 -66
  201. package/packages/core/src/interaction-receipts.test.ts +0 -671
  202. package/packages/core/src/navigator.test.ts +0 -34
  203. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  204. package/packages/core/src/optimistic-projection.test.ts +0 -118
  205. package/packages/core/src/performance-metrics.test.ts +0 -121
  206. package/packages/core/src/performance-qualification.test.ts +0 -283
  207. package/packages/core/src/saved-machines.test.ts +0 -80
  208. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  209. package/packages/core/src/terminal-conformance.test.ts +0 -48
  210. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  211. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  212. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  213. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  214. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  215. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  216. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  217. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  218. package/packages/core/src/terminal-replica.test.ts +0 -347
  219. package/packages/core/src/workspace-selection.test.ts +0 -63
  220. package/packages/daemon/dist/__tests__/support.js +0 -33
  221. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  222. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  223. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  224. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  225. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  226. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  227. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  228. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  229. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  230. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  231. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  232. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  236. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  237. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  238. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  239. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  240. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  241. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  242. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  243. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  244. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  245. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  246. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  247. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  248. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  249. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  250. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  251. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  252. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  253. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  254. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  255. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  256. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  257. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  258. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  259. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  260. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  261. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  262. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  263. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  264. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  265. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  266. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  267. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -137,6 +137,11 @@ export class SessionRuntimeRegistry {
137
137
  this.#mirror = new MirrorService({
138
138
  ...options.mirror,
139
139
  ...diagnosticMirrorOptions,
140
+ onSharedWindowConflict: (session, conflict) => {
141
+ options.mirror?.onSharedWindowConflict?.(session, conflict);
142
+ if (conflict)
143
+ this.#sessions.get(session)?.invalidateSharedWindowAuthority();
144
+ },
140
145
  onNativeClientActivity: (session) => {
141
146
  options.mirror?.onNativeClientActivity?.(session);
142
147
  this.#sessions.get(session)?.noteNativeGeometryActivity();
@@ -171,6 +176,7 @@ export class SessionRuntimeRegistry {
171
176
  signal?.throwIfAborted();
172
177
  const runtime = this.#runtime(session);
173
178
  await abortable(runtime.whenReady(), signal);
179
+ await abortable(this.#mirror.verifyRegisteredWindows(), signal);
174
180
  if (this.#sessions.get(session) !== runtime) {
175
181
  throw new Error(`SessionRuntime ${session} was retired while prewarming`);
176
182
  }
@@ -213,6 +219,7 @@ export class SessionRuntimeRegistry {
213
219
  throw new Error(`SessionRuntime ${session} is attached to a different tmux identity`);
214
220
  }
215
221
  signal?.throwIfAborted();
222
+ await abortable(this.#mirror.verifyRegisteredWindows(), signal);
216
223
  runtime.qualifyTrustedInventory(runtimeSessionId);
217
224
  }
218
225
  finally {
@@ -235,24 +242,28 @@ export class SessionRuntimeRegistry {
235
242
  if (!runtime?.trustedInventoryQualified()) {
236
243
  throw new Error(`SessionRuntime ${session} has no proof-qualified inventory authority`);
237
244
  }
245
+ const epoch = runtime.trustedInventoryEpoch;
238
246
  const inventory = await this.#observeTerminalAttempt("terminal-trusted-inventory-attempt", abortable(runtime.describeTrustedInventory(), signal));
239
- if (this.#sessions.get(session) !== runtime || !runtime.trustedInventoryQualified()) {
247
+ if (this.#sessions.get(session) !== runtime ||
248
+ !runtime.trustedInventoryQualified() ||
249
+ runtime.trustedInventoryEpoch !== epoch) {
240
250
  throw new Error(`SessionRuntime ${session} changed during trusted inventory discovery`);
241
251
  }
242
252
  if (!inventory) {
243
253
  throw new Error(`SessionRuntime ${session} lost its retained inventory authority`);
244
254
  }
245
255
  const token = Object.freeze({});
246
- this.#trustedInventoryTokens.set(token, runtime);
256
+ this.#trustedInventoryTokens.set(token, { runtime, epoch });
247
257
  return Object.freeze({ inventory, token });
248
258
  }
249
259
  isTrustedSessionInventoryCandidateCurrent(session, token) {
250
260
  if (this.#disposed)
251
261
  return false;
252
- const runtime = this.#trustedInventoryTokens.get(token);
253
- return (runtime !== undefined &&
254
- this.#sessions.get(session) === runtime &&
255
- runtime.trustedInventoryQualified());
262
+ const candidate = this.#trustedInventoryTokens.get(token);
263
+ return (candidate !== undefined &&
264
+ this.#sessions.get(session) === candidate.runtime &&
265
+ candidate.runtime.trustedInventoryEpoch === candidate.epoch &&
266
+ candidate.runtime.trustedInventoryQualified());
256
267
  }
257
268
  hasProofQualifiedInventory(session) {
258
269
  return ((this.#sessions.get(session)?.trustedInventoryQualified() ?? false) &&
@@ -269,6 +280,14 @@ export class SessionRuntimeRegistry {
269
280
  return Promise.reject(new Error("Session runtime disposed"));
270
281
  return this.#mirror.executeWindowLinkAction(session, request);
271
282
  }
283
+ /** Resolve pending ownership before an asynchronous owner-authorized mutation. */
284
+ async verifyWindowOwnership() {
285
+ await this.#mirror.verifyRegisteredWindows();
286
+ }
287
+ /** Install the bounded registered-session membership reader. */
288
+ setRegisteredWindowReader(reader) {
289
+ return this.#mirror.setRegisteredWindowReader(reader);
290
+ }
272
291
  /** Retire one no-longer-registered session without disturbing siblings. */
273
292
  async retireSession(session) {
274
293
  const runtime = this.#sessions.get(session);
@@ -665,6 +684,10 @@ class SessionRuntime {
665
684
  #releasedLeases = new Set();
666
685
  #disposed = false;
667
686
  #trustedInventoryRuntimeSessionId = null;
687
+ #trustedInventoryEpoch = 0;
688
+ get trustedInventoryEpoch() {
689
+ return this.#trustedInventoryEpoch;
690
+ }
668
691
  #activeCausalCellProbes = 0;
669
692
  constructor(generation, session, mirror, createControllerToken, scheduler, observability, createTraceCorrelator, nativeGeometryHysteresisMs, submitAuthorized) {
670
693
  this.generation = generation;
@@ -725,6 +748,7 @@ class SessionRuntime {
725
748
  this.#publishAuthority();
726
749
  }
727
750
  acquireAuthority(clientId, authority) {
751
+ this.assertNoSharedWindow();
728
752
  if (authority === "input" && this.#controllerClientId !== clientId) {
729
753
  // Input's executable proof is the compatibility controller lease. The
730
754
  // transport synchronization seam must establish/handoff it first.
@@ -776,6 +800,7 @@ class SessionRuntime {
776
800
  });
777
801
  }
778
802
  acquireController(clientId) {
803
+ this.assertNoSharedWindow();
779
804
  this.#assertConnected(clientId);
780
805
  if (this.#controllerClientId === clientId) {
781
806
  this.#authority.updatePresence(clientId, "foreground");
@@ -836,6 +861,7 @@ class SessionRuntime {
836
861
  this.#authority.release(callerClientId, "geometry");
837
862
  }
838
863
  assertController(lease, callerClientId) {
864
+ this.assertNoSharedWindow();
839
865
  const parsedLease = this.#validatedLease(lease);
840
866
  if (parsedLease.generation !== this.generation ||
841
867
  parsedLease.session !== this.session ||
@@ -973,6 +999,7 @@ class SessionRuntime {
973
999
  this.#mirror.fitWindowViewport(this.session, semanticWindowId, cols, rows);
974
1000
  }
975
1001
  fitViewportWithAuthority(clientId, lease, cols, rows, semanticWindowId) {
1002
+ this.assertNoSharedWindow();
976
1003
  const parsed = SessionRuntimeAuthorityLeaseSchemaZ.parse(lease);
977
1004
  let exact;
978
1005
  try {
@@ -1016,13 +1043,16 @@ class SessionRuntime {
1016
1043
  return await this.#mirror.describeSession(this.session);
1017
1044
  }
1018
1045
  qualifyTrustedInventory(runtimeSessionId) {
1046
+ this.assertNoSharedWindow();
1019
1047
  if (this.#disposed || !this.#retention) {
1020
1048
  throw new Error(`SessionRuntime ${this.session} is not retained`);
1021
1049
  }
1022
1050
  this.#trustedInventoryRuntimeSessionId = runtimeSessionId;
1023
1051
  }
1024
1052
  trustedInventoryQualified() {
1025
- return !this.#disposed && this.#trustedInventoryRuntimeSessionId !== null;
1053
+ return (!this.#disposed &&
1054
+ this.#trustedInventoryRuntimeSessionId !== null &&
1055
+ !this.#mirror.hasSharedWindowConflict(this.session));
1026
1056
  }
1027
1057
  async describeTrustedInventory() {
1028
1058
  if (!this.trustedInventoryQualified()) {
@@ -1176,8 +1206,26 @@ class SessionRuntime {
1176
1206
  this.#authority.disconnect(consumer.clientId);
1177
1207
  this.#publishAuthority();
1178
1208
  }
1209
+ assertNoSharedWindow() {
1210
+ const message = this.#mirror.windowOwnershipMessage(this.session);
1211
+ if (message) {
1212
+ throw new SessionRuntimeControllerLeaseError("controller-conflict", message);
1213
+ }
1214
+ }
1215
+ invalidateSharedWindowAuthority() {
1216
+ this.#trustedInventoryRuntimeSessionId = null;
1217
+ this.#trustedInventoryEpoch += 1;
1218
+ this.#clearController();
1219
+ for (const clientId of this.#consumersByClientId.keys()) {
1220
+ for (const authority of ["input", "focus", "geometry"])
1221
+ this.#authority.release(clientId, authority);
1222
+ }
1223
+ this.#completedHandoffs.clear();
1224
+ this.#publishAuthority();
1225
+ }
1179
1226
  noteControlExit() {
1180
1227
  this.#trustedInventoryRuntimeSessionId = null;
1228
+ this.#trustedInventoryEpoch += 1;
1181
1229
  const retention = this.#retention;
1182
1230
  this.#retention = null;
1183
1231
  this.#startPromise = null;
@@ -1198,6 +1246,7 @@ class SessionRuntime {
1198
1246
  return;
1199
1247
  this.#disposed = true;
1200
1248
  this.#trustedInventoryRuntimeSessionId = null;
1249
+ this.#trustedInventoryEpoch += 1;
1201
1250
  const consumers = [...this.#consumers];
1202
1251
  await Promise.allSettled(consumers.map((consumer) => consumer.close()));
1203
1252
  this.#clearController();
@@ -276,7 +276,9 @@ export class SessionSemanticMutationExecutor {
276
276
  let trace = null;
277
277
  if (this.#observability.enabled && this.#options.traceAuthority) {
278
278
  try {
279
- trace = this.#observability.beginTrace(intent.verb === "workspace.pane.select" ? "window-switch" : "semantic-mutation", this.#options.traceAuthority, operationId);
279
+ trace = this.#observability.beginTrace(intent.verb === "workspace.pane.select" || intent.verb === "workspace.window.link.select"
280
+ ? "window-switch"
281
+ : "semantic-mutation", this.#options.traceAuthority, operationId);
280
282
  }
281
283
  catch {
282
284
  trace = null;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Claude Code agent teams — opt-out enablement in the user's Claude settings.
3
+ *
4
+ * Agent teams let one Claude Code session spawn teammates; with
5
+ * `teammateMode: "auto"` Claude puts them in split panes when it runs inside
6
+ * tmux (in-process elsewhere), which is exactly what tmux-ide shows and
7
+ * routes. The installer enables this by default and `tmux-ide integration
8
+ * agent-teams enable|disable|status` manages it explicitly.
9
+ *
10
+ * Rules (pure, in {@link mergeAgentTeams}):
11
+ * - `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is set to "1" only when absent.
12
+ * An explicit off value ("0", "false", false, 0…) is the user's opt-out: the
13
+ * installer never overrides it and touches nothing else. Only an explicit
14
+ * `tmux-ide integration agent-teams enable` (overrideOptOut) turns it back on.
15
+ * - `teammateMode: "auto"` is set only when `teammateMode` is absent.
16
+ * - Every other key is preserved. Nothing changed → nothing written.
17
+ *
18
+ * Writes (io) reuse the hooks integration's settings path resolution, including
19
+ * the `TMUX_IDE_CLAUDE_SETTINGS` override: one-time timestamped backup before
20
+ * the first modification, then an atomic temp-file + rename. Invalid JSON is
21
+ * reported and never rewritten.
22
+ */
23
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, copyFileSync, unlinkSync, writeFileSync, accessSync, constants, } from "node:fs";
24
+ import { basename, delimiter, dirname, join } from "node:path";
25
+ import { resolveRuntimeNamespace, runtimeOwnedPath } from "../../lib/runtime-namespace.js";
26
+ export const AGENT_TEAMS_ENV = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS";
27
+ export const DEFAULT_TEAMMATE_MODE = "auto";
28
+ const BACKUP_INFIX = ".tmux-ide-backup-";
29
+ /** PURE — an explicit off value is the user's opt-out. */
30
+ export function isOptOut(value) {
31
+ if (value === false || value === 0)
32
+ return true;
33
+ return typeof value === "string" && /^\s*(0|false|off|no)\s*$/iu.test(value);
34
+ }
35
+ function envOf(settings) {
36
+ const env = settings.env;
37
+ if (env === undefined)
38
+ return undefined;
39
+ if (!env || typeof env !== "object" || Array.isArray(env))
40
+ throw new Error("settings env must be an object");
41
+ return env;
42
+ }
43
+ /** PURE — how the settings currently stand for agent teams. */
44
+ export function agentTeamsSetting(settings) {
45
+ const value = envOf(settings)?.[AGENT_TEAMS_ENV];
46
+ if (value === undefined)
47
+ return "not-configured";
48
+ return isOptOut(value) ? "disabled-by-user" : "enabled";
49
+ }
50
+ /**
51
+ * PURE — enable agent teams without overriding anything the user chose, unless
52
+ * `overrideOptOut` (the user explicitly asked to enable) replaces their "0".
53
+ */
54
+ export function mergeAgentTeams(settings, { overrideOptOut = false } = {}) {
55
+ const setting = agentTeamsSetting(settings);
56
+ if (setting === "disabled-by-user" && !overrideOptOut)
57
+ return { settings, changes: [], setting };
58
+ const changes = [];
59
+ let next = settings;
60
+ if (setting !== "enabled") {
61
+ next = { ...next, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "1" } };
62
+ changes.push(`env.${AGENT_TEAMS_ENV}=1`);
63
+ }
64
+ if (!("teammateMode" in settings)) {
65
+ next = { ...next, teammateMode: DEFAULT_TEAMMATE_MODE };
66
+ changes.push(`teammateMode=${DEFAULT_TEAMMATE_MODE}`);
67
+ }
68
+ return { settings: next, changes, setting: "enabled" };
69
+ }
70
+ /** PURE — record an explicit opt-out ("0"), which later enables respect. */
71
+ export function disableAgentTeams(settings) {
72
+ if (agentTeamsSetting(settings) === "disabled-by-user")
73
+ return { settings, changes: [], setting: "disabled-by-user" };
74
+ return {
75
+ settings: { ...settings, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "0" } },
76
+ changes: [`env.${AGENT_TEAMS_ENV}=0`],
77
+ setting: "disabled-by-user",
78
+ };
79
+ }
80
+ export function agentTeamsPaths() {
81
+ const namespace = resolveRuntimeNamespace();
82
+ return { settingsPath: namespace.claudeSettingsPath, claudeDir: namespace.claudeDir };
83
+ }
84
+ /** `claude` on PATH, without spawning anything. */
85
+ export function claudeOnPath(path = process.env.PATH ?? "") {
86
+ return path
87
+ .split(delimiter)
88
+ .filter(Boolean)
89
+ .some((dir) => {
90
+ try {
91
+ accessSync(join(dir, "claude"), constants.X_OK);
92
+ return statSync(join(dir, "claude")).isFile();
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ });
98
+ }
99
+ export class InvalidClaudeSettingsError extends Error {
100
+ }
101
+ function readSettings(path) {
102
+ if (!existsSync(path))
103
+ return { settings: {}, exists: false };
104
+ try {
105
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
106
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
107
+ throw new Error("not an object");
108
+ envOf(parsed);
109
+ return { settings: parsed, exists: true };
110
+ }
111
+ catch {
112
+ throw new InvalidClaudeSettingsError(`${path} is not valid settings JSON — fix or move it, then retry`);
113
+ }
114
+ }
115
+ /** Existing one-time backup, if tmux-ide already made one for this file. */
116
+ function existingBackup(settingsPath) {
117
+ const dir = dirname(settingsPath);
118
+ const prefix = `${basename(settingsPath)}${BACKUP_INFIX}`;
119
+ try {
120
+ const found = readdirSync(dir).find((name) => name.startsWith(prefix));
121
+ return found ? join(dir, found) : null;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ function writeSettings(path, settings, exists) {
128
+ mkdirSync(dirname(path), { recursive: true });
129
+ let backup = null;
130
+ if (exists) {
131
+ backup = existingBackup(path);
132
+ if (!backup) {
133
+ const stamp = new Date()
134
+ .toISOString()
135
+ .replace(/[-:]/gu, "")
136
+ .replace(/\.\d+Z$/u, "Z");
137
+ backup = `${path}${BACKUP_INFIX}${stamp}`;
138
+ copyFileSync(path, backup);
139
+ }
140
+ }
141
+ const temporary = join(dirname(path), `.${basename(path)}.tmux-ide-${process.pid}.tmp`);
142
+ try {
143
+ writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
144
+ if (exists)
145
+ chmodSync(temporary, statSync(path).mode & 0o777);
146
+ renameSync(temporary, path);
147
+ }
148
+ catch (error) {
149
+ try {
150
+ unlinkSync(temporary);
151
+ }
152
+ catch {
153
+ /* already renamed or never written */
154
+ }
155
+ throw error;
156
+ }
157
+ return backup;
158
+ }
159
+ function detected(paths, onPath) {
160
+ return existsSync(paths.claudeDir) || onPath();
161
+ }
162
+ function guarded(paths) {
163
+ if (!paths && resolveRuntimeNamespace().development)
164
+ throw new Error("Development agent-teams changes require explicit fixture paths");
165
+ const resolved = paths ?? agentTeamsPaths();
166
+ runtimeOwnedPath(resolved.settingsPath);
167
+ return resolved;
168
+ }
169
+ /** Enable agent teams when Claude Code is installed and the user hasn't opted out. */
170
+ export function enableClaudeAgentTeams(paths, onPath = () => claudeOnPath(), { overrideOptOut = false } = {}) {
171
+ const resolved = guarded(paths);
172
+ const { settingsPath } = resolved;
173
+ const base = { settingsPath, changes: [], backup: null };
174
+ if (!detected(resolved, onPath))
175
+ return {
176
+ ...base,
177
+ action: "skipped",
178
+ claudeDetected: false,
179
+ message: `Claude Code not found (no ${resolved.claudeDir}, no claude on PATH); agent teams not configured.`,
180
+ };
181
+ let read;
182
+ try {
183
+ read = readSettings(settingsPath);
184
+ }
185
+ catch (error) {
186
+ return { ...base, action: "invalid", claudeDetected: true, message: error.message };
187
+ }
188
+ const merged = mergeAgentTeams(read.settings, { overrideOptOut });
189
+ if (merged.setting === "disabled-by-user")
190
+ return {
191
+ ...base,
192
+ action: "disabled-by-user",
193
+ claudeDetected: true,
194
+ message: `Claude Code agent teams are disabled by user in ${settingsPath} (${AGENT_TEAMS_ENV}); ` +
195
+ "left unchanged. Enable explicitly: tmux-ide integration agent-teams enable",
196
+ };
197
+ if (merged.changes.length === 0)
198
+ return {
199
+ ...base,
200
+ action: "unchanged",
201
+ claudeDetected: true,
202
+ message: `Claude Code agent teams already enabled in ${settingsPath}.`,
203
+ };
204
+ const backup = writeSettings(settingsPath, merged.settings, read.exists);
205
+ return {
206
+ settingsPath,
207
+ changes: merged.changes,
208
+ backup,
209
+ action: "enabled",
210
+ claudeDetected: true,
211
+ message: `Enabled Claude Code agent teams in ${settingsPath} (${merged.changes.join(", ")}` +
212
+ `${backup ? `; backup: ${backup}` : ""}). Disable: tmux-ide integration agent-teams disable`,
213
+ };
214
+ }
215
+ /** Record the user's opt-out so neither the installer nor `enable` turns it back on silently. */
216
+ export function disableClaudeAgentTeams(paths) {
217
+ const resolved = guarded(paths);
218
+ const { settingsPath } = resolved;
219
+ const base = { settingsPath, changes: [], backup: null, claudeDetected: true };
220
+ let read;
221
+ try {
222
+ read = readSettings(settingsPath);
223
+ }
224
+ catch (error) {
225
+ return { ...base, action: "invalid", message: error.message };
226
+ }
227
+ const next = disableAgentTeams(read.settings);
228
+ if (next.changes.length === 0)
229
+ return {
230
+ ...base,
231
+ action: "unchanged",
232
+ message: `Claude Code agent teams already disabled in ${settingsPath}.`,
233
+ };
234
+ const backup = writeSettings(settingsPath, next.settings, read.exists);
235
+ return {
236
+ ...base,
237
+ changes: next.changes,
238
+ backup,
239
+ action: "disabled",
240
+ message: `Disabled Claude Code agent teams in ${settingsPath} (${next.changes.join(", ")}` +
241
+ `${backup ? `; backup: ${backup}` : ""}). Re-enable: tmux-ide integration agent-teams enable`,
242
+ };
243
+ }
244
+ export function claudeAgentTeamsStatus(paths = agentTeamsPaths(), onPath = () => claudeOnPath()) {
245
+ const claudeDetected = detected(paths, onPath);
246
+ try {
247
+ const { settings } = readSettings(paths.settingsPath);
248
+ const mode = settings.teammateMode;
249
+ return {
250
+ settingsPath: paths.settingsPath,
251
+ claudeDetected,
252
+ setting: agentTeamsSetting(settings),
253
+ teammateMode: typeof mode === "string" ? mode : null,
254
+ };
255
+ }
256
+ catch {
257
+ return {
258
+ settingsPath: paths.settingsPath,
259
+ claudeDetected,
260
+ setting: "invalid-settings",
261
+ teammateMode: null,
262
+ };
263
+ }
264
+ }
@@ -16,6 +16,8 @@
16
16
  * Rows span the box interior in x: [left+1, left+width-1).
17
17
  */
18
18
  import { fuzzyFilter } from "../team/fuzzy.js";
19
+ import { wrapText } from "./terminal-text.js";
20
+ export { wrapText } from "./terminal-text.js";
19
21
  // ── Constants ────────────────────────────────────────────────────────────────
20
22
  /** Box width — matches the palette so the two overlays read as one family. */
21
23
  export const DIALOG_W = 60;
@@ -66,41 +68,6 @@ export function dialogPos(termW, termH, width) {
66
68
  top: Math.max(1, Math.floor(termH / 6)),
67
69
  };
68
70
  }
69
- /** PURE — greedy word-wrap for the confirm body (never returns empty for
70
- * non-empty text; words longer than the width hard-break). */
71
- export function wrapText(text, width) {
72
- if (width <= 0)
73
- return [text];
74
- const out = [];
75
- for (const para of text.split("\n")) {
76
- let line = "";
77
- for (const word of para.split(/\s+/).filter(Boolean)) {
78
- if (line.length === 0) {
79
- let w = word;
80
- while (w.length > width) {
81
- out.push(w.slice(0, width));
82
- w = w.slice(width);
83
- }
84
- line = w;
85
- }
86
- else if (line.length + 1 + word.length <= width) {
87
- line += ` ${word}`;
88
- }
89
- else {
90
- out.push(line);
91
- let w = word;
92
- while (w.length > width) {
93
- out.push(w.slice(0, width));
94
- w = w.slice(width);
95
- }
96
- line = w;
97
- }
98
- }
99
- if (line.length > 0 || para.length === 0)
100
- out.push(line);
101
- }
102
- return out.length > 0 ? out : [""];
103
- }
104
71
  /** PURE — the interior text width of the box (borders + 1-cell padding each side). */
105
72
  export function dialogInnerW(width) {
106
73
  return width - 4;
@@ -4,6 +4,7 @@ import { watch } from "node:fs";
4
4
  import { basename, dirname } from "node:path";
5
5
  import { getCanonicalDaemonInfoPath, isCanonicalDaemonAlive, readCanonicalDaemonInfo, } from "../../../lib/canonical-daemon.js";
6
6
  import { SshConnectionError, openSshDaemonTransport, probeSshDaemonIdentity, } from "../../../lib/ssh-daemon-transport.js";
7
+ import { tuiPerfMark } from "./application-performance-log.js";
7
8
  const defaults = {
8
9
  readLocal: readCanonicalDaemonInfo,
9
10
  isLocalAlive: isCanonicalDaemonAlive,
@@ -97,6 +98,13 @@ export function createApplicationDaemonAuthority(dependencies = defaults) {
97
98
  const statusListeners = new Set();
98
99
  const updateStatus = (next) => {
99
100
  diagnostic = Object.freeze(next);
101
+ tuiPerfMark("machine-connection-status", {
102
+ machine: label,
103
+ ...next,
104
+ instanceId: remote?.instanceId ?? null,
105
+ pid: remote?.pid ?? null,
106
+ port: remote?.port ?? null,
107
+ });
100
108
  for (const listener of statusListeners) {
101
109
  try {
102
110
  listener();
@@ -49,6 +49,23 @@ export function projectHomeFleet(catalog, groups, filter) {
49
49
  rows.sort((a, b) => a.key.localeCompare(b.key));
50
50
  const missing = machines.filter((machine) => machine.state !== "ready" ||
51
51
  !groups.find((group) => group.machineId === machine.id)?.available);
52
+ const activityPending = (machineId) => {
53
+ const observation = groups.find((group) => group.machineId === machineId)?.observation;
54
+ // A live observation whose binding is no longer available belongs to a
55
+ // retiring endpoint; its replacement must publish before it is current.
56
+ return !observation || observation.phase === "loading" || observation.phase === "live";
57
+ };
58
+ const coverageNotes = missing.map((machine) => {
59
+ if (machine.state !== "ready")
60
+ return `${machine.label}: ${machine.state}`;
61
+ const observation = groups.find((group) => group.machineId === machine.id)?.observation;
62
+ const status = activityPending(machine.id)
63
+ ? "loading agent activity"
64
+ : observation?.phase === "partial"
65
+ ? "agent activity incomplete"
66
+ : "agent activity unavailable";
67
+ return `${machine.label}: connected; ${status}`;
68
+ });
52
69
  const query = filter.query?.trim().toLocaleLowerCase() ?? "";
53
70
  const matching = query
54
71
  ? rows.filter((row) => [
@@ -65,7 +82,8 @@ export function projectHomeFleet(catalog, groups, filter) {
65
82
  phase: missing.length
66
83
  ? rows.length || observedSessions
67
84
  ? "partial"
68
- : machines.some((machine) => machine.state === "connecting")
85
+ : machines.some((machine) => machine.state === "connecting") ||
86
+ missing.some((machine) => machine.state === "ready" && activityPending(machine.id))
69
87
  ? "loading"
70
88
  : "unavailable"
71
89
  : "live",
@@ -78,7 +96,7 @@ export function projectHomeFleet(catalog, groups, filter) {
78
96
  refreshingSessionKeys: [],
79
97
  unavailableSessionKeys: [...unavailable],
80
98
  note: missing.length
81
- ? `${missing.map((machine) => `${machine.label}: ${machine.state === "ready" ? "partial observations" : machine.state}`).join(" · ")} · unavailable rows show last observed activity`
99
+ ? `${coverageNotes.join(" · ")}${unavailable.size > 0 ? " · unavailable rows show last observed activity" : ""}`
82
100
  : rows.length === 0 && filter.attentionOnly
83
101
  ? "No agents need attention in this view."
84
102
  : null,
@@ -1,3 +1,4 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import { machineResourceKey } from "@tmux-ide/core";
2
3
  import { applicationMachineAuthorityManager, } from "./application-machine-authority.js";
3
4
  import { createApplicationHomeAgentObserver, } from "./application-home-agent-observer.js";
@@ -29,7 +30,7 @@ export function createApplicationMachineAgents(options) {
29
30
  const publish = () => {
30
31
  if (disposed)
31
32
  return;
32
- snapshot = Object.freeze(catalog.groups.map((group) => {
33
+ const next = Object.freeze(catalog.groups.map((group) => {
33
34
  const entry = entries.get(group.id);
34
35
  const current = !!entry && entry.binding === bindingFor(entry.handle);
35
36
  return Object.freeze({
@@ -47,6 +48,11 @@ export function createApplicationMachineAgents(options) {
47
48
  }))),
48
49
  });
49
50
  }));
51
+ // Fresh authority checks above still run on every observation. Identical
52
+ // presentation data must not invalidate the Home/sidebar/tab subscribers.
53
+ if (isDeepStrictEqual(snapshot, next))
54
+ return;
55
+ snapshot = next;
50
56
  for (const listener of listeners) {
51
57
  try {
52
58
  listener(snapshot);