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
@@ -273,7 +273,10 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
273
273
  readonly #createTraceCorrelator: (scheduler: SessionRuntimeScheduler) => RuntimeTraceCorrelator;
274
274
  readonly #sessions = new Map<string, SessionRuntime>();
275
275
  readonly #proofPrewarmOwnership = new Map<SessionRuntime, { owned: boolean; claims: number }>();
276
- readonly #trustedInventoryTokens = new WeakMap<object, SessionRuntime>();
276
+ readonly #trustedInventoryTokens = new WeakMap<
277
+ object,
278
+ { runtime: SessionRuntime; epoch: number }
279
+ >();
277
280
  readonly #executionHandles = new WeakMap<object, ExecutionHandleState>();
278
281
  readonly #stopExitObserver: () => void;
279
282
  #disposed = false;
@@ -397,6 +400,10 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
397
400
  this.#mirror = new MirrorService({
398
401
  ...options.mirror,
399
402
  ...diagnosticMirrorOptions,
403
+ onSharedWindowConflict: (session, conflict) => {
404
+ options.mirror?.onSharedWindowConflict?.(session, conflict);
405
+ if (conflict) this.#sessions.get(session)?.invalidateSharedWindowAuthority();
406
+ },
400
407
  onNativeClientActivity: (session) => {
401
408
  options.mirror?.onNativeClientActivity?.(session);
402
409
  this.#sessions.get(session)?.noteNativeGeometryActivity();
@@ -433,6 +440,7 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
433
440
  signal?.throwIfAborted();
434
441
  const runtime = this.#runtime(session);
435
442
  await abortable(runtime.whenReady(), signal);
443
+ await abortable(this.#mirror.verifyRegisteredWindows(), signal);
436
444
  if (this.#sessions.get(session) !== runtime) {
437
445
  throw new Error(`SessionRuntime ${session} was retired while prewarming`);
438
446
  }
@@ -493,6 +501,7 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
493
501
  throw new Error(`SessionRuntime ${session} is attached to a different tmux identity`);
494
502
  }
495
503
  signal?.throwIfAborted();
504
+ await abortable(this.#mirror.verifyRegisteredWindows(), signal);
496
505
  runtime.qualifyTrustedInventory(runtimeSessionId);
497
506
  } finally {
498
507
  this.#releaseProofPrewarmRuntime(runtime);
@@ -521,28 +530,34 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
521
530
  if (!runtime?.trustedInventoryQualified()) {
522
531
  throw new Error(`SessionRuntime ${session} has no proof-qualified inventory authority`);
523
532
  }
533
+ const epoch = runtime.trustedInventoryEpoch;
524
534
  const inventory = await this.#observeTerminalAttempt(
525
535
  "terminal-trusted-inventory-attempt",
526
536
  abortable(runtime.describeTrustedInventory(), signal),
527
537
  );
528
- if (this.#sessions.get(session) !== runtime || !runtime.trustedInventoryQualified()) {
538
+ if (
539
+ this.#sessions.get(session) !== runtime ||
540
+ !runtime.trustedInventoryQualified() ||
541
+ runtime.trustedInventoryEpoch !== epoch
542
+ ) {
529
543
  throw new Error(`SessionRuntime ${session} changed during trusted inventory discovery`);
530
544
  }
531
545
  if (!inventory) {
532
546
  throw new Error(`SessionRuntime ${session} lost its retained inventory authority`);
533
547
  }
534
548
  const token = Object.freeze({});
535
- this.#trustedInventoryTokens.set(token, runtime);
549
+ this.#trustedInventoryTokens.set(token, { runtime, epoch });
536
550
  return Object.freeze({ inventory, token });
537
551
  }
538
552
 
539
553
  isTrustedSessionInventoryCandidateCurrent(session: string, token: object): boolean {
540
554
  if (this.#disposed) return false;
541
- const runtime = this.#trustedInventoryTokens.get(token);
555
+ const candidate = this.#trustedInventoryTokens.get(token);
542
556
  return (
543
- runtime !== undefined &&
544
- this.#sessions.get(session) === runtime &&
545
- runtime.trustedInventoryQualified()
557
+ candidate !== undefined &&
558
+ this.#sessions.get(session) === candidate.runtime &&
559
+ candidate.runtime.trustedInventoryEpoch === candidate.epoch &&
560
+ candidate.runtime.trustedInventoryQualified()
546
561
  );
547
562
  }
548
563
 
@@ -567,6 +582,18 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
567
582
  return this.#mirror.executeWindowLinkAction(session, request);
568
583
  }
569
584
 
585
+ /** Resolve pending ownership before an asynchronous owner-authorized mutation. */
586
+ async verifyWindowOwnership(): Promise<void> {
587
+ await this.#mirror.verifyRegisteredWindows();
588
+ }
589
+
590
+ /** Install the bounded registered-session membership reader. */
591
+ setRegisteredWindowReader(
592
+ reader: import("../mirror/registered-window-guard.ts").RegisteredWindowReader,
593
+ ): ReturnType<MirrorService["setRegisteredWindowReader"]> {
594
+ return this.#mirror.setRegisteredWindowReader(reader);
595
+ }
596
+
570
597
  /** Retire one no-longer-registered session without disturbing siblings. */
571
598
  async retireSession(session: string): Promise<void> {
572
599
  const runtime = this.#sessions.get(session);
@@ -1105,6 +1132,11 @@ class SessionRuntime {
1105
1132
  readonly #releasedLeases = new Set<string>();
1106
1133
  #disposed = false;
1107
1134
  #trustedInventoryRuntimeSessionId: string | null = null;
1135
+ #trustedInventoryEpoch = 0;
1136
+
1137
+ get trustedInventoryEpoch(): number {
1138
+ return this.#trustedInventoryEpoch;
1139
+ }
1108
1140
  #activeCausalCellProbes = 0;
1109
1141
 
1110
1142
  constructor(
@@ -1195,6 +1227,7 @@ class SessionRuntime {
1195
1227
  clientId: string,
1196
1228
  authority: SessionRuntimeAuthorityKind,
1197
1229
  ): SessionRuntimeAuthorityLease | null {
1230
+ this.assertNoSharedWindow();
1198
1231
  if (authority === "input" && this.#controllerClientId !== clientId) {
1199
1232
  // Input's executable proof is the compatibility controller lease. The
1200
1233
  // transport synchronization seam must establish/handoff it first.
@@ -1261,6 +1294,7 @@ class SessionRuntime {
1261
1294
  }
1262
1295
 
1263
1296
  acquireController(clientId: string): SessionRuntimeControllerLease {
1297
+ this.assertNoSharedWindow();
1264
1298
  this.#assertConnected(clientId);
1265
1299
  if (this.#controllerClientId === clientId) {
1266
1300
  this.#authority.updatePresence(clientId, "foreground");
@@ -1335,6 +1369,7 @@ class SessionRuntime {
1335
1369
  }
1336
1370
 
1337
1371
  assertController(lease: SessionRuntimeControllerLease, callerClientId?: string): void {
1372
+ this.assertNoSharedWindow();
1338
1373
  const parsedLease = this.#validatedLease(lease);
1339
1374
  if (
1340
1375
  parsedLease.generation !== this.generation ||
@@ -1547,6 +1582,7 @@ class SessionRuntime {
1547
1582
  rows: number,
1548
1583
  semanticWindowId?: string,
1549
1584
  ): void {
1585
+ this.assertNoSharedWindow();
1550
1586
  const parsed = SessionRuntimeAuthorityLeaseSchemaZ.parse(lease);
1551
1587
  let exact: SessionRuntimeAuthorityLease;
1552
1588
  try {
@@ -1594,6 +1630,7 @@ class SessionRuntime {
1594
1630
  }
1595
1631
 
1596
1632
  qualifyTrustedInventory(runtimeSessionId: string): void {
1633
+ this.assertNoSharedWindow();
1597
1634
  if (this.#disposed || !this.#retention) {
1598
1635
  throw new Error(`SessionRuntime ${this.session} is not retained`);
1599
1636
  }
@@ -1601,7 +1638,11 @@ class SessionRuntime {
1601
1638
  }
1602
1639
 
1603
1640
  trustedInventoryQualified(): boolean {
1604
- return !this.#disposed && this.#trustedInventoryRuntimeSessionId !== null;
1641
+ return (
1642
+ !this.#disposed &&
1643
+ this.#trustedInventoryRuntimeSessionId !== null &&
1644
+ !this.#mirror.hasSharedWindowConflict(this.session)
1645
+ );
1605
1646
  }
1606
1647
 
1607
1648
  async describeTrustedInventory(): Promise<TrustedMirrorSessionInventory | null> {
@@ -1812,8 +1853,28 @@ class SessionRuntime {
1812
1853
  this.#publishAuthority();
1813
1854
  }
1814
1855
 
1856
+ private assertNoSharedWindow(): void {
1857
+ const message = this.#mirror.windowOwnershipMessage(this.session);
1858
+ if (message) {
1859
+ throw new SessionRuntimeControllerLeaseError("controller-conflict", message);
1860
+ }
1861
+ }
1862
+
1863
+ invalidateSharedWindowAuthority(): void {
1864
+ this.#trustedInventoryRuntimeSessionId = null;
1865
+ this.#trustedInventoryEpoch += 1;
1866
+ this.#clearController();
1867
+ for (const clientId of this.#consumersByClientId.keys()) {
1868
+ for (const authority of ["input", "focus", "geometry"] as const)
1869
+ this.#authority.release(clientId, authority);
1870
+ }
1871
+ this.#completedHandoffs.clear();
1872
+ this.#publishAuthority();
1873
+ }
1874
+
1815
1875
  noteControlExit(): void {
1816
1876
  this.#trustedInventoryRuntimeSessionId = null;
1877
+ this.#trustedInventoryEpoch += 1;
1817
1878
  const retention = this.#retention;
1818
1879
  this.#retention = null;
1819
1880
  this.#startPromise = null;
@@ -1833,6 +1894,7 @@ class SessionRuntime {
1833
1894
  if (this.#disposed) return;
1834
1895
  this.#disposed = true;
1835
1896
  this.#trustedInventoryRuntimeSessionId = null;
1897
+ this.#trustedInventoryEpoch += 1;
1836
1898
  const consumers = [...this.#consumers];
1837
1899
  await Promise.allSettled(consumers.map((consumer) => consumer.close()));
1838
1900
  this.#clearController();
@@ -509,7 +509,9 @@ export class SessionSemanticMutationExecutor {
509
509
  if (this.#observability.enabled && this.#options.traceAuthority) {
510
510
  try {
511
511
  trace = this.#observability.beginTrace(
512
- intent.verb === "workspace.pane.select" ? "window-switch" : "semantic-mutation",
512
+ intent.verb === "workspace.pane.select" || intent.verb === "workspace.window.link.select"
513
+ ? "window-switch"
514
+ : "semantic-mutation",
513
515
  this.#options.traceAuthority,
514
516
  operationId,
515
517
  );
@@ -0,0 +1,334 @@
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 {
24
+ chmodSync,
25
+ existsSync,
26
+ mkdirSync,
27
+ readFileSync,
28
+ readdirSync,
29
+ renameSync,
30
+ statSync,
31
+ copyFileSync,
32
+ unlinkSync,
33
+ writeFileSync,
34
+ accessSync,
35
+ constants,
36
+ } from "node:fs";
37
+ import { basename, delimiter, dirname, join } from "node:path";
38
+ import { resolveRuntimeNamespace, runtimeOwnedPath } from "../../lib/runtime-namespace.ts";
39
+
40
+ export const AGENT_TEAMS_ENV = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS";
41
+ export const DEFAULT_TEAMMATE_MODE = "auto";
42
+ const BACKUP_INFIX = ".tmux-ide-backup-";
43
+
44
+ type Settings = Record<string, unknown>;
45
+
46
+ export type AgentTeamsSetting = "enabled" | "disabled-by-user" | "not-configured";
47
+
48
+ /** PURE — an explicit off value is the user's opt-out. */
49
+ export function isOptOut(value: unknown): boolean {
50
+ if (value === false || value === 0) return true;
51
+ return typeof value === "string" && /^\s*(0|false|off|no)\s*$/iu.test(value);
52
+ }
53
+
54
+ function envOf(settings: Settings): Record<string, unknown> | undefined {
55
+ const env = settings.env;
56
+ if (env === undefined) return undefined;
57
+ if (!env || typeof env !== "object" || Array.isArray(env))
58
+ throw new Error("settings env must be an object");
59
+ return env as Record<string, unknown>;
60
+ }
61
+
62
+ /** PURE — how the settings currently stand for agent teams. */
63
+ export function agentTeamsSetting(settings: Settings): AgentTeamsSetting {
64
+ const value = envOf(settings)?.[AGENT_TEAMS_ENV];
65
+ if (value === undefined) return "not-configured";
66
+ return isOptOut(value) ? "disabled-by-user" : "enabled";
67
+ }
68
+
69
+ export interface AgentTeamsMerge {
70
+ readonly settings: Settings;
71
+ /** Human-readable `key=value` list of what was set; empty when unchanged. */
72
+ readonly changes: readonly string[];
73
+ readonly setting: AgentTeamsSetting;
74
+ }
75
+
76
+ /**
77
+ * PURE — enable agent teams without overriding anything the user chose, unless
78
+ * `overrideOptOut` (the user explicitly asked to enable) replaces their "0".
79
+ */
80
+ export function mergeAgentTeams(
81
+ settings: Settings,
82
+ { overrideOptOut = false }: { readonly overrideOptOut?: boolean } = {},
83
+ ): AgentTeamsMerge {
84
+ const setting = agentTeamsSetting(settings);
85
+ if (setting === "disabled-by-user" && !overrideOptOut) return { settings, changes: [], setting };
86
+ const changes: string[] = [];
87
+ let next: Settings = settings;
88
+ if (setting !== "enabled") {
89
+ next = { ...next, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "1" } };
90
+ changes.push(`env.${AGENT_TEAMS_ENV}=1`);
91
+ }
92
+ if (!("teammateMode" in settings)) {
93
+ next = { ...next, teammateMode: DEFAULT_TEAMMATE_MODE };
94
+ changes.push(`teammateMode=${DEFAULT_TEAMMATE_MODE}`);
95
+ }
96
+ return { settings: next, changes, setting: "enabled" };
97
+ }
98
+
99
+ /** PURE — record an explicit opt-out ("0"), which later enables respect. */
100
+ export function disableAgentTeams(settings: Settings): AgentTeamsMerge {
101
+ if (agentTeamsSetting(settings) === "disabled-by-user")
102
+ return { settings, changes: [], setting: "disabled-by-user" };
103
+ return {
104
+ settings: { ...settings, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "0" } },
105
+ changes: [`env.${AGENT_TEAMS_ENV}=0`],
106
+ setting: "disabled-by-user",
107
+ };
108
+ }
109
+
110
+ export interface AgentTeamsPaths {
111
+ /** Claude Code's user settings file. */
112
+ readonly settingsPath: string;
113
+ /** Claude Code's home (~/.claude); its presence means Claude is installed. */
114
+ readonly claudeDir: string;
115
+ }
116
+
117
+ export function agentTeamsPaths(): AgentTeamsPaths {
118
+ const namespace = resolveRuntimeNamespace();
119
+ return { settingsPath: namespace.claudeSettingsPath, claudeDir: namespace.claudeDir };
120
+ }
121
+
122
+ /** `claude` on PATH, without spawning anything. */
123
+ export function claudeOnPath(path = process.env.PATH ?? ""): boolean {
124
+ return path
125
+ .split(delimiter)
126
+ .filter(Boolean)
127
+ .some((dir) => {
128
+ try {
129
+ accessSync(join(dir, "claude"), constants.X_OK);
130
+ return statSync(join(dir, "claude")).isFile();
131
+ } catch {
132
+ return false;
133
+ }
134
+ });
135
+ }
136
+
137
+ export class InvalidClaudeSettingsError extends Error {}
138
+
139
+ function readSettings(path: string): { settings: Settings; exists: boolean } {
140
+ if (!existsSync(path)) return { settings: {}, exists: false };
141
+ try {
142
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
143
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
144
+ throw new Error("not an object");
145
+ envOf(parsed as Settings);
146
+ return { settings: parsed as Settings, exists: true };
147
+ } catch {
148
+ throw new InvalidClaudeSettingsError(
149
+ `${path} is not valid settings JSON — fix or move it, then retry`,
150
+ );
151
+ }
152
+ }
153
+
154
+ /** Existing one-time backup, if tmux-ide already made one for this file. */
155
+ function existingBackup(settingsPath: string): string | null {
156
+ const dir = dirname(settingsPath);
157
+ const prefix = `${basename(settingsPath)}${BACKUP_INFIX}`;
158
+ try {
159
+ const found = readdirSync(dir).find((name) => name.startsWith(prefix));
160
+ return found ? join(dir, found) : null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ function writeSettings(path: string, settings: Settings, exists: boolean): string | null {
167
+ mkdirSync(dirname(path), { recursive: true });
168
+ let backup: string | null = null;
169
+ if (exists) {
170
+ backup = existingBackup(path);
171
+ if (!backup) {
172
+ const stamp = new Date()
173
+ .toISOString()
174
+ .replace(/[-:]/gu, "")
175
+ .replace(/\.\d+Z$/u, "Z");
176
+ backup = `${path}${BACKUP_INFIX}${stamp}`;
177
+ copyFileSync(path, backup);
178
+ }
179
+ }
180
+ const temporary = join(dirname(path), `.${basename(path)}.tmux-ide-${process.pid}.tmp`);
181
+ try {
182
+ writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
183
+ if (exists) chmodSync(temporary, statSync(path).mode & 0o777);
184
+ renameSync(temporary, path);
185
+ } catch (error) {
186
+ try {
187
+ unlinkSync(temporary);
188
+ } catch {
189
+ /* already renamed or never written */
190
+ }
191
+ throw error;
192
+ }
193
+ return backup;
194
+ }
195
+
196
+ export interface AgentTeamsResult {
197
+ readonly action:
198
+ | "enabled"
199
+ | "disabled"
200
+ | "unchanged"
201
+ | "disabled-by-user"
202
+ | "skipped"
203
+ | "invalid";
204
+ readonly settingsPath: string;
205
+ readonly changes: readonly string[];
206
+ readonly backup: string | null;
207
+ readonly claudeDetected: boolean;
208
+ readonly message: string;
209
+ }
210
+
211
+ function detected(paths: AgentTeamsPaths, onPath: () => boolean): boolean {
212
+ return existsSync(paths.claudeDir) || onPath();
213
+ }
214
+
215
+ function guarded(paths: AgentTeamsPaths | undefined): AgentTeamsPaths {
216
+ if (!paths && resolveRuntimeNamespace().development)
217
+ throw new Error("Development agent-teams changes require explicit fixture paths");
218
+ const resolved = paths ?? agentTeamsPaths();
219
+ runtimeOwnedPath(resolved.settingsPath);
220
+ return resolved;
221
+ }
222
+
223
+ /** Enable agent teams when Claude Code is installed and the user hasn't opted out. */
224
+ export function enableClaudeAgentTeams(
225
+ paths?: AgentTeamsPaths,
226
+ onPath: () => boolean = () => claudeOnPath(),
227
+ { overrideOptOut = false }: { readonly overrideOptOut?: boolean } = {},
228
+ ): AgentTeamsResult {
229
+ const resolved = guarded(paths);
230
+ const { settingsPath } = resolved;
231
+ const base = { settingsPath, changes: [], backup: null } as const;
232
+ if (!detected(resolved, onPath))
233
+ return {
234
+ ...base,
235
+ action: "skipped",
236
+ claudeDetected: false,
237
+ message: `Claude Code not found (no ${resolved.claudeDir}, no claude on PATH); agent teams not configured.`,
238
+ };
239
+ let read: ReturnType<typeof readSettings>;
240
+ try {
241
+ read = readSettings(settingsPath);
242
+ } catch (error) {
243
+ return { ...base, action: "invalid", claudeDetected: true, message: (error as Error).message };
244
+ }
245
+ const merged = mergeAgentTeams(read.settings, { overrideOptOut });
246
+ if (merged.setting === "disabled-by-user")
247
+ return {
248
+ ...base,
249
+ action: "disabled-by-user",
250
+ claudeDetected: true,
251
+ message:
252
+ `Claude Code agent teams are disabled by user in ${settingsPath} (${AGENT_TEAMS_ENV}); ` +
253
+ "left unchanged. Enable explicitly: tmux-ide integration agent-teams enable",
254
+ };
255
+ if (merged.changes.length === 0)
256
+ return {
257
+ ...base,
258
+ action: "unchanged",
259
+ claudeDetected: true,
260
+ message: `Claude Code agent teams already enabled in ${settingsPath}.`,
261
+ };
262
+ const backup = writeSettings(settingsPath, merged.settings, read.exists);
263
+ return {
264
+ settingsPath,
265
+ changes: merged.changes,
266
+ backup,
267
+ action: "enabled",
268
+ claudeDetected: true,
269
+ message:
270
+ `Enabled Claude Code agent teams in ${settingsPath} (${merged.changes.join(", ")}` +
271
+ `${backup ? `; backup: ${backup}` : ""}). Disable: tmux-ide integration agent-teams disable`,
272
+ };
273
+ }
274
+
275
+ /** Record the user's opt-out so neither the installer nor `enable` turns it back on silently. */
276
+ export function disableClaudeAgentTeams(paths?: AgentTeamsPaths): AgentTeamsResult {
277
+ const resolved = guarded(paths);
278
+ const { settingsPath } = resolved;
279
+ const base = { settingsPath, changes: [], backup: null, claudeDetected: true } as const;
280
+ let read: ReturnType<typeof readSettings>;
281
+ try {
282
+ read = readSettings(settingsPath);
283
+ } catch (error) {
284
+ return { ...base, action: "invalid", message: (error as Error).message };
285
+ }
286
+ const next = disableAgentTeams(read.settings);
287
+ if (next.changes.length === 0)
288
+ return {
289
+ ...base,
290
+ action: "unchanged",
291
+ message: `Claude Code agent teams already disabled in ${settingsPath}.`,
292
+ };
293
+ const backup = writeSettings(settingsPath, next.settings, read.exists);
294
+ return {
295
+ ...base,
296
+ changes: next.changes,
297
+ backup,
298
+ action: "disabled",
299
+ message:
300
+ `Disabled Claude Code agent teams in ${settingsPath} (${next.changes.join(", ")}` +
301
+ `${backup ? `; backup: ${backup}` : ""}). Re-enable: tmux-ide integration agent-teams enable`,
302
+ };
303
+ }
304
+
305
+ export interface AgentTeamsStatus {
306
+ readonly settingsPath: string;
307
+ readonly claudeDetected: boolean;
308
+ readonly setting: AgentTeamsSetting | "invalid-settings";
309
+ readonly teammateMode: string | null;
310
+ }
311
+
312
+ export function claudeAgentTeamsStatus(
313
+ paths: AgentTeamsPaths = agentTeamsPaths(),
314
+ onPath: () => boolean = () => claudeOnPath(),
315
+ ): AgentTeamsStatus {
316
+ const claudeDetected = detected(paths, onPath);
317
+ try {
318
+ const { settings } = readSettings(paths.settingsPath);
319
+ const mode = settings.teammateMode;
320
+ return {
321
+ settingsPath: paths.settingsPath,
322
+ claudeDetected,
323
+ setting: agentTeamsSetting(settings),
324
+ teammateMode: typeof mode === "string" ? mode : null,
325
+ };
326
+ } catch {
327
+ return {
328
+ settingsPath: paths.settingsPath,
329
+ claudeDetected,
330
+ setting: "invalid-settings",
331
+ teammateMode: null,
332
+ };
333
+ }
334
+ }
@@ -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.ts";
19
+ import { wrapText } from "./terminal-text.ts";
20
+ export { wrapText } from "./terminal-text.ts";
19
21
 
20
22
  // ── Specs ────────────────────────────────────────────────────────────────────
21
23
 
@@ -176,38 +178,6 @@ export function dialogPos(
176
178
  };
177
179
  }
178
180
 
179
- /** PURE — greedy word-wrap for the confirm body (never returns empty for
180
- * non-empty text; words longer than the width hard-break). */
181
- export function wrapText(text: string, width: number): string[] {
182
- if (width <= 0) return [text];
183
- const out: string[] = [];
184
- for (const para of text.split("\n")) {
185
- let line = "";
186
- for (const word of para.split(/\s+/).filter(Boolean)) {
187
- if (line.length === 0) {
188
- let w = word;
189
- while (w.length > width) {
190
- out.push(w.slice(0, width));
191
- w = w.slice(width);
192
- }
193
- line = w;
194
- } else if (line.length + 1 + word.length <= width) {
195
- line += ` ${word}`;
196
- } else {
197
- out.push(line);
198
- let w = word;
199
- while (w.length > width) {
200
- out.push(w.slice(0, width));
201
- w = w.slice(width);
202
- }
203
- line = w;
204
- }
205
- }
206
- if (line.length > 0 || para.length === 0) out.push(line);
207
- }
208
- return out.length > 0 ? out : [""];
209
- }
210
-
211
181
  /** PURE — the interior text width of the box (borders + 1-cell padding each side). */
212
182
  export function dialogInnerW(width: number): number {
213
183
  return width - 4;
@@ -20,6 +20,7 @@ import {
20
20
  openSshDaemonTransport,
21
21
  probeSshDaemonIdentity,
22
22
  } from "../../../lib/ssh-daemon-transport.ts";
23
+ import { tuiPerfMark } from "./application-performance-log.ts";
23
24
 
24
25
  type Connection = Awaited<ReturnType<typeof openSshDaemonTransport>>;
25
26
  type Listener = (generation: string | null) => void;
@@ -136,6 +137,13 @@ export function createApplicationDaemonAuthority(
136
137
  const statusListeners = new Set<() => void>();
137
138
  const updateStatus = (next: FleetConnectionStatus) => {
138
139
  diagnostic = Object.freeze(next);
140
+ tuiPerfMark("machine-connection-status", {
141
+ machine: label,
142
+ ...next,
143
+ instanceId: remote?.instanceId ?? null,
144
+ pid: remote?.pid ?? null,
145
+ port: remote?.port ?? null,
146
+ });
139
147
  for (const listener of statusListeners) {
140
148
  try {
141
149
  listener();
@@ -80,6 +80,22 @@ export function projectHomeFleet(
80
80
  machine.state !== "ready" ||
81
81
  !groups.find((group) => group.machineId === machine.id)?.available,
82
82
  );
83
+ const activityPending = (machineId: string) => {
84
+ const observation = groups.find((group) => group.machineId === machineId)?.observation;
85
+ // A live observation whose binding is no longer available belongs to a
86
+ // retiring endpoint; its replacement must publish before it is current.
87
+ return !observation || observation.phase === "loading" || observation.phase === "live";
88
+ };
89
+ const coverageNotes = missing.map((machine) => {
90
+ if (machine.state !== "ready") return `${machine.label}: ${machine.state}`;
91
+ const observation = groups.find((group) => group.machineId === machine.id)?.observation;
92
+ const status = activityPending(machine.id)
93
+ ? "loading agent activity"
94
+ : observation?.phase === "partial"
95
+ ? "agent activity incomplete"
96
+ : "agent activity unavailable";
97
+ return `${machine.label}: connected; ${status}`;
98
+ });
83
99
  const query = filter.query?.trim().toLocaleLowerCase() ?? "";
84
100
  const matching = query
85
101
  ? rows.filter((row) =>
@@ -98,7 +114,8 @@ export function projectHomeFleet(
98
114
  phase: missing.length
99
115
  ? rows.length || observedSessions
100
116
  ? "partial"
101
- : machines.some((machine) => machine.state === "connecting")
117
+ : machines.some((machine) => machine.state === "connecting") ||
118
+ missing.some((machine) => machine.state === "ready" && activityPending(machine.id))
102
119
  ? "loading"
103
120
  : "unavailable"
104
121
  : "live",
@@ -111,7 +128,7 @@ export function projectHomeFleet(
111
128
  refreshingSessionKeys: [],
112
129
  unavailableSessionKeys: [...unavailable],
113
130
  note: missing.length
114
- ? `${missing.map((machine) => `${machine.label}: ${machine.state === "ready" ? "partial observations" : machine.state}`).join(" · ")} · unavailable rows show last observed activity`
131
+ ? `${coverageNotes.join(" · ")}${unavailable.size > 0 ? " · unavailable rows show last observed activity" : ""}`
115
132
  : rows.length === 0 && filter.attentionOnly
116
133
  ? "No agents need attention in this view."
117
134
  : null,
@@ -1,3 +1,4 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import { machineResourceKey } from "@tmux-ide/core";
2
3
  import {
3
4
  applicationMachineAuthorityManager,
@@ -69,7 +70,7 @@ export function createApplicationMachineAgents(options: {
69
70
  let started = false;
70
71
  const publish = () => {
71
72
  if (disposed) return;
72
- snapshot = Object.freeze(
73
+ const next = Object.freeze(
73
74
  catalog.groups.map((group) => {
74
75
  const entry = entries.get(group.id);
75
76
  const current = !!entry && entry.binding === bindingFor(entry.handle);
@@ -95,6 +96,10 @@ export function createApplicationMachineAgents(options: {
95
96
  });
96
97
  }),
97
98
  );
99
+ // Fresh authority checks above still run on every observation. Identical
100
+ // presentation data must not invalidate the Home/sidebar/tab subscribers.
101
+ if (isDeepStrictEqual(snapshot, next)) return;
102
+ snapshot = next;
98
103
  for (const listener of listeners) {
99
104
  try {
100
105
  listener(snapshot);