tmux-ide 2.9.2 → 2.9.3

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 (263) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +2071 -625
  3. package/package.json +14 -5
  4. package/packages/contracts/src/desktop-host.ts +3 -0
  5. package/packages/contracts/src/saved-machines.ts +19 -0
  6. package/packages/contracts/src/terminal-replica.ts +1 -1
  7. package/packages/core/package.json +3 -2
  8. package/packages/core/src/terminal-delivery.ts +51 -3
  9. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  10. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  11. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  12. package/packages/core/src/terminal-replica.ts +13 -28
  13. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  14. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  15. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  16. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  17. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  18. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  19. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  20. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  21. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  22. package/packages/daemon/dist/lib/install-origin.js +2 -0
  23. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  24. package/packages/daemon/dist/lib/log.js +6 -10
  25. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  26. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  27. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  28. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  29. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  30. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  31. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  32. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  33. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  34. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  35. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  36. package/packages/daemon/dist/lib/update.js +18 -0
  37. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  38. package/packages/daemon/dist/machines.js +40 -4
  39. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  43. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  44. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  45. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  46. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  47. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  48. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  49. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  50. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  51. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  52. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  53. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  54. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  55. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  56. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  57. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  61. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  62. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  63. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  64. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  65. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  66. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  67. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  68. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  69. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  70. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  71. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  72. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  73. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  74. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  75. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  76. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  77. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  78. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  79. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  80. package/packages/daemon/src/lib/install-origin.ts +5 -0
  81. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  82. package/packages/daemon/src/lib/log.ts +13 -9
  83. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  84. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  85. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  86. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  87. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  88. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  89. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  90. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  91. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  92. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  93. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  94. package/packages/daemon/src/lib/update.ts +24 -0
  95. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  96. package/packages/daemon/src/machines.ts +53 -5
  97. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  98. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  99. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  100. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  101. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  102. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  103. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  104. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  105. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  106. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  107. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  108. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  109. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  110. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  111. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  112. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  115. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  116. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  117. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  118. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  119. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  120. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  121. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  122. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  123. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  124. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  125. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  126. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  127. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  128. package/packages/daemon-client/src/workspace-client.ts +4 -1
  129. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  130. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  131. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  132. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  133. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  134. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  135. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  136. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  137. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  138. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  139. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  140. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  141. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  142. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  143. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  144. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  145. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  146. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  147. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  148. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  149. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  150. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  151. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  152. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  153. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  154. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  155. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  156. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  157. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  158. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  159. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  160. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  161. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  162. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  163. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  164. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  165. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  166. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  167. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  168. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  169. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  170. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  171. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  172. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  173. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  174. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  175. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  176. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  177. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  178. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  179. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  180. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  181. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  182. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  183. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  184. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  185. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  186. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  187. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  188. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  189. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  190. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  191. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  192. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  193. package/packages/core/src/agent-provisioning.test.ts +0 -42
  194. package/packages/core/src/application-shell-session.test.ts +0 -105
  195. package/packages/core/src/interaction-evidence.test.ts +0 -134
  196. package/packages/core/src/interaction-flow.test.ts +0 -66
  197. package/packages/core/src/interaction-receipts.test.ts +0 -671
  198. package/packages/core/src/navigator.test.ts +0 -34
  199. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  200. package/packages/core/src/optimistic-projection.test.ts +0 -118
  201. package/packages/core/src/performance-metrics.test.ts +0 -121
  202. package/packages/core/src/performance-qualification.test.ts +0 -283
  203. package/packages/core/src/saved-machines.test.ts +0 -80
  204. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  205. package/packages/core/src/terminal-conformance.test.ts +0 -48
  206. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  207. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  208. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  209. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  210. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  211. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  212. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  213. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  214. package/packages/core/src/terminal-replica.test.ts +0 -347
  215. package/packages/core/src/workspace-selection.test.ts +0 -63
  216. package/packages/daemon/dist/__tests__/support.js +0 -33
  217. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  218. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  219. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  220. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  221. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  222. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  223. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  224. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  225. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  226. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  227. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  228. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  229. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  230. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  231. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  232. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  236. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  237. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  238. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  239. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  240. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  241. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  242. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  243. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  244. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  245. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  246. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  247. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  248. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  249. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  250. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  251. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  252. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  253. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  254. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  255. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  256. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  257. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  258. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  259. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  260. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  261. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  262. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  263. 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;
@@ -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);
@@ -14,6 +14,7 @@ export function createApplicationMachineAuthorityManager(dependencies = {
14
14
  let selectedId = LOCAL_MACHINE_ID;
15
15
  let selectedEpoch = 0;
16
16
  let disposed = false;
17
+ let stopProfiles = null;
17
18
  const safe = (callback) => {
18
19
  try {
19
20
  callback();
@@ -31,7 +32,7 @@ export function createApplicationMachineAuthorityManager(dependencies = {
31
32
  safe(() => listener(generation));
32
33
  };
33
34
  const changed = (machine, generation) => {
34
- if (disposed)
35
+ if (disposed || machine.retired)
35
36
  return;
36
37
  machine.epoch++;
37
38
  for (const listener of machine.listeners)
@@ -52,7 +53,7 @@ export function createApplicationMachineAuthorityManager(dependencies = {
52
53
  changed(machine, generation);
53
54
  })
54
55
  .then((stop) => {
55
- if (disposed || machine.owner !== owner)
56
+ if (disposed || machine.retired || machine.owner !== owner)
56
57
  stop();
57
58
  else
58
59
  machine.stopObserver = stop;
@@ -80,23 +81,28 @@ export function createApplicationMachineAuthorityManager(dependencies = {
80
81
  stopObserver: null,
81
82
  observingOwner: null,
82
83
  stopStatus: null,
84
+ retired: false,
83
85
  };
84
86
  const handle = Object.freeze({
85
87
  id,
86
- label,
88
+ get label() {
89
+ return machine.label;
90
+ },
87
91
  kind: machine.kind,
88
92
  ready,
89
- read: () => (disposed ? null : machine.owner.read()),
90
- isAlive: (info) => disposed ? Promise.resolve(false) : machine.owner.isAlive(info),
93
+ read: () => (disposed || machine.retired ? null : machine.owner.read()),
94
+ isAlive: (info) => disposed || machine.retired ? Promise.resolve(false) : machine.owner.isAlive(info),
91
95
  endpoint: () => Object.freeze({
92
96
  ...machine.owner.endpoint(),
93
97
  kind: machine.kind,
94
- label: profile ? label : null,
98
+ label: machine.profile ? machine.label : null,
95
99
  epoch: machine.epoch,
96
- ...(disposed ? { state: "disconnected", remote: null, localBaseUrl: null } : {}),
100
+ ...(disposed || machine.retired
101
+ ? { state: "disconnected", remote: null, localBaseUrl: null }
102
+ : {}),
97
103
  }),
98
104
  observe: async (listener) => {
99
- if (disposed)
105
+ if (disposed || machine.retired)
100
106
  return () => { };
101
107
  if (machine.kind === "local")
102
108
  observe(machine, machine.owner);
@@ -119,7 +125,7 @@ export function createApplicationMachineAuthorityManager(dependencies = {
119
125
  });
120
126
  observe(machine, owner);
121
127
  publish();
122
- void initializing.then(() => settle(!disposed), () => settle(false));
128
+ void initializing.then(() => settle(!disposed && !machine.retired), () => settle(false));
123
129
  return machine;
124
130
  };
125
131
  makeMachine(LOCAL_MACHINE_ID, "Local");
@@ -206,6 +212,48 @@ export function createApplicationMachineAuthorityManager(dependencies = {
206
212
  if (profile.enabled)
207
213
  manager.add(profile);
208
214
  },
215
+ reconcile(profiles) {
216
+ if (disposed)
217
+ return;
218
+ const next = SavedMachineRegistrySchema.parse({ version: 1, machines: profiles });
219
+ const previousSelection = selectedId;
220
+ for (const machine of [...machines.values()]) {
221
+ if (!machine.profile)
222
+ continue;
223
+ const profile = next.machines.find((candidate) => candidate.id === machine.id && candidate.enabled);
224
+ if (profile &&
225
+ profile.sshTarget === machine.profile.sshTarget &&
226
+ profile.expectedEnvironmentId === machine.profile.expectedEnvironmentId) {
227
+ // Labels are presentation metadata, not transport identity. Preserve
228
+ // the connected owner and every consumer holding its stable handle.
229
+ machine.label = profile.label;
230
+ machine.profile = profile;
231
+ continue;
232
+ }
233
+ if (selectedId === machine.id)
234
+ manager.select(LOCAL_MACHINE_ID);
235
+ machine.retired = true;
236
+ machine.stopStatus?.();
237
+ machine.stopObserver?.();
238
+ for (const listener of machine.listeners)
239
+ safe(() => listener(null));
240
+ machine.owner.dispose();
241
+ machine.listeners.clear();
242
+ machines.delete(machine.id);
243
+ }
244
+ for (const profile of next.machines)
245
+ if (profile.enabled)
246
+ manager.add(profile);
247
+ if (machines.has(previousSelection))
248
+ manager.select(previousSelection);
249
+ publish();
250
+ },
251
+ followProfiles(subscribe) {
252
+ if (disposed)
253
+ return;
254
+ stopProfiles?.();
255
+ stopProfiles = subscribe(manager.reconcile);
256
+ },
209
257
  read: () => machines.get(selectedId)?.handle.read() ?? null,
210
258
  isAlive: (info) => machines.get(selectedId)?.handle.isAlive(info) ?? Promise.resolve(false),
211
259
  endpoint: () => Object.freeze({ ...machines.get(selectedId).handle.endpoint(), epoch: selectedEpoch }),
@@ -223,6 +271,8 @@ export function createApplicationMachineAuthorityManager(dependencies = {
223
271
  if (disposed)
224
272
  return;
225
273
  disposed = true;
274
+ stopProfiles?.();
275
+ stopProfiles = null;
226
276
  lifetime.abort();
227
277
  selectedEpoch++;
228
278
  selectedGeneration(null);
@@ -123,7 +123,9 @@ export function createApplicationMachineNavigation(options) {
123
123
  return false;
124
124
  }
125
125
  const selected = candidates[0];
126
- const sameSelection = activeSessionKey() === selected.id && options.sessionName() === name;
126
+ const sameSelection = manager.snapshot().selectedMachineId === id &&
127
+ activeSessionKey() === selected.id &&
128
+ options.sessionName() === name;
127
129
  setActiveSessionKey(selected.id);
128
130
  expectedLiveSessionId = selected.liveSessionId;
129
131
  server = selected.server;
@@ -222,18 +224,25 @@ export function createApplicationMachineNavigation(options) {
222
224
  tabs.observe(tab.key, tabAgents(tab));
223
225
  });
224
226
  const sidebar = {
225
- groups: () => snapshot().groups.map((group) => ({
226
- ...group,
227
- agentsAvailable: group.sessions.some((session) => isDefaultSession(group.id, session)) &&
228
- (agentGroups().find((value) => value.machineId === group.id)?.available ?? false),
229
- agents: !group.sessions.some((session) => isDefaultSession(group.id, session))
230
- ? []
231
- : (agentGroups().find((value) => value.machineId === group.id)?.agents ?? []).map((agent) => ({
232
- ...agent,
233
- server: group.sessions.find((session) => session.liveSessionId === agent.liveSessionId &&
234
- isDefaultSession(group.id, session))?.server,
235
- })),
236
- })),
227
+ groups: () => {
228
+ const groupsById = new Map(agentGroups().map((group) => [group.machineId, group]));
229
+ return snapshot().groups.map((group) => {
230
+ // Read once per projection: local authority reads validate the record on disk.
231
+ // The next projection (and action admission) still observes replacement immediately.
232
+ const instanceId = manager.getMachine(group.id)?.read()?.instanceId;
233
+ const defaultSessions = group.sessions.filter((session) => !session.server || session.server.generation === instanceId);
234
+ const agents = defaultSessions.length ? groupsById.get(group.id) : undefined;
235
+ return {
236
+ ...group,
237
+ agentsAvailable: agents?.available ?? false,
238
+ agents: (agents?.agents ?? []).map((agent) => ({
239
+ ...agent,
240
+ server: defaultSessions.find((session) => session.liveSessionId === agent.liveSessionId)
241
+ ?.server,
242
+ })),
243
+ };
244
+ });
245
+ },
237
246
  tabs: () => {
238
247
  tabRevision();
239
248
  const value = tabs.snapshot();
@@ -409,7 +418,9 @@ export function createApplicationMachineNavigation(options) {
409
418
  paletteCommands() {
410
419
  const groupsById = new Map(agentGroups().map((group) => [group.machineId, group]));
411
420
  return snapshot().groups.flatMap((group) => {
412
- const agentGroup = !group.sessions.some((session) => isDefaultSession(group.id, session))
421
+ const daemon = manager.getMachine(group.id)?.read();
422
+ const isDefault = (session) => !session.server || session.server.generation === daemon?.instanceId;
423
+ const agentGroup = !group.sessions.some((session) => isDefault(session))
413
424
  ? undefined
414
425
  : groupsById.get(group.id);
415
426
  const agentsBySession = new Map();
@@ -420,7 +431,6 @@ export function createApplicationMachineNavigation(options) {
420
431
  list.push(agent);
421
432
  agentsBySession.set(agent.liveSessionId, list);
422
433
  }
423
- const daemon = manager.getMachine(group.id)?.read();
424
434
  const sessions = group.sessions.flatMap((session) => {
425
435
  if (!session.liveSessionId)
426
436
  return [];
@@ -433,7 +443,7 @@ export function createApplicationMachineNavigation(options) {
433
443
  liveSessionId: session.liveSessionId,
434
444
  server: session.server,
435
445
  hostLabel: `${group.label}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
436
- agentActivities: isDefaultSession(group.id, session) && agentGroup?.available
446
+ agentActivities: isDefault(session) && agentGroup?.available
437
447
  ? (agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
438
448
  paneId: a.paneId,
439
449
  attention: a.attention,
@@ -445,9 +455,7 @@ export function createApplicationMachineNavigation(options) {
445
455
  };
446
456
  return [
447
457
  { kind: "open-session", sessionName: session.name, label: session.name, fleet },
448
- ...(isDefaultSession(group.id, session)
449
- ? (agentsBySession.get(session.liveSessionId) ?? [])
450
- : []).map((a) => ({
458
+ ...(isDefault(session) ? (agentsBySession.get(session.liveSessionId) ?? []) : []).map((a) => ({
451
459
  kind: "jump-agent",
452
460
  sessionName: session.name,
453
461
  paneId: a.paneId,
@@ -5,13 +5,16 @@ import { fleetHostColor } from "./fleet-presentation.js";
5
5
  /* @jsxImportSource @opentui/solid */
6
6
  import { fleetConnectionMessage, } from "@tmux-ide/daemon-client/fleet-connection-status";
7
7
  import { For, Show, createEffect, createMemo, createSignal, } from "solid-js";
8
- import { friendlySessionLabel } from "../terminal-text.js";
8
+ import { friendlySessionLabel, wrapText } from "../terminal-text.js";
9
9
  import { KeyHint } from "../ui/key-hint.jsx";
10
10
  import { NavigationRow } from "../ui/navigation-row.jsx";
11
11
  import { Surface } from "../ui/surface.jsx";
12
12
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
13
13
  /** Pure machine navigation. All connection and session authority stays with the caller. */
14
14
  export function ApplicationMachineSidebar(props) {
15
+ // All visible sections share one projection for the current reactive inputs.
16
+ // Event handlers retain the model's fresh authority checks at action time.
17
+ const groups = createMemo(() => props.model.groups());
15
18
  const [localFocused, setLocalFocused] = createSignal(false);
16
19
  const focused = () => props.model.focused?.() ?? localFocused();
17
20
  const [localCollapsed, setCollapsed] = createSignal(new Set());
@@ -19,16 +22,14 @@ export function ApplicationMachineSidebar(props) {
19
22
  const [selectedKey, setSelectedKey] = createSignal(null);
20
23
  let scroll;
21
24
  const agentHeight = () => (props.agentRows ?? 0) > 0
22
- ? Math.min((props.agentRows ?? 0) + 2, Math.max(0, Math.floor(props.height / 2)))
25
+ ? Math.min((props.agentRows ?? 0) + 2, Math.max(0, Math.floor(props.height / 2)), Math.max(0, props.height - fixedHeight() - controlsHeight() - 2))
23
26
  : 0;
24
- const controlsHeight = () => (props.height >= 8 && controlGroup() ? 4 : 0);
27
+ const controlsHeight = () => (props.height >= 8 && controlGroup() ? controlTextHeight() + 2 : 0);
25
28
  const searchHeight = () => (props.model.onOpenSwitcher ? 1 : 0) + (props.model.onOpenAttention ? 1 : 0);
26
- const machineHeight = () => Math.max(0, props.height -
27
- 1 -
28
- (props.model.onAddMachine ? 1 : 0) -
29
- agentHeight() -
30
- controlsHeight() -
31
- searchHeight());
29
+ const fixedHeight = () => 1 + (props.model.onAddMachine ? 1 : 0) + searchHeight();
30
+ // Keep both action rows and at least two machine rows available on short terminals.
31
+ const controlTextHeight = () => Math.min(controlTextLines().length, Math.max(1, props.height - fixedHeight() - 4));
32
+ const machineHeight = () => Math.max(0, props.height - fixedHeight() - agentHeight() - controlsHeight());
32
33
  const active = (row) => row.group.id === props.model.activeMachineId() &&
33
34
  (row.agent
34
35
  ? row.group.state === "ready" &&
@@ -46,7 +47,7 @@ export function ApplicationMachineSidebar(props) {
46
47
  : row.session?.name === props.model.activeSessionName());
47
48
  const preferenceKey = (group) => group.environmentId ?? group.id;
48
49
  const rows = createMemo(() => [
49
- ...props.model.groups().flatMap((group) => groupApplicationTeamRows(group.agents ?? []).map((agent) => ({
50
+ ...groups().flatMap((group) => groupApplicationTeamRows(group.agents ?? []).map((agent) => ({
50
51
  key: JSON.stringify([
51
52
  group.id,
52
53
  "agent",
@@ -57,7 +58,7 @@ export function ApplicationMachineSidebar(props) {
57
58
  group,
58
59
  agent,
59
60
  }))),
60
- ...props.model.groups().flatMap((group) => [
61
+ ...groups().flatMap((group) => [
61
62
  { key: JSON.stringify([group.id]), group },
62
63
  ...group.sessions
63
64
  .filter((session) => !collapsed().has(preferenceKey(group)) ||
@@ -118,6 +119,12 @@ export function ApplicationMachineSidebar(props) {
118
119
  : "disconnected"
119
120
  : diagnostic.phase;
120
121
  };
122
+ const controlTextLines = createMemo(() => {
123
+ const group = controlGroup();
124
+ return group
125
+ ? wrapText(group.diagnostic ? fleetConnectionMessage(group.diagnostic) : connectionDetail(group), Math.max(1, props.width))
126
+ : [];
127
+ });
121
128
  const toggle = (group, value = !collapsed().has(preferenceKey(group))) => {
122
129
  const next = new Set(collapsed());
123
130
  if (value)
@@ -163,7 +170,7 @@ export function ApplicationMachineSidebar(props) {
163
170
  const sectionGap = (row) => (!row.session && !row.agent && !row.server ? 1 : 0);
164
171
  const rowHeight = (row) => (row.agent ? 2 : row.serverHeading ? 2 : 1) +
165
172
  sectionGap(row) +
166
- (hasAgents() && row.key === JSON.stringify([props.model.groups()[0]?.id]) ? 1 : 0);
173
+ (hasAgents() && row.key === JSON.stringify([groups()[0]?.id]) ? 1 : 0);
167
174
  const revealFocusedRow = () => {
168
175
  if (!focused() || !scroll)
169
176
  return;
@@ -346,7 +353,7 @@ export function ApplicationMachineSidebar(props) {
346
353
  props.model.onSelectServer?.(row.group.id, row.session.server);
347
354
  }}/>
348
355
  </Show>
349
- <Show when={hasAgents() && row.key === JSON.stringify([props.model.groups()[0]?.id])}>
356
+ <Show when={hasAgents() && row.key === JSON.stringify([groups()[0]?.id])}>
350
357
  <text height={1} fg={props.theme.roles.text.secondary}>
351
358
  {" "}
352
359
  Machines
@@ -399,14 +406,12 @@ export function ApplicationMachineSidebar(props) {
399
406
  <KeyHint theme={props.theme} keys={CHROME_ACTIONS.sessions.keys} label="Browse all sessions" width={props.width} quiet button onPress={() => props.model.onOpenSwitcher?.()}/>
400
407
  </Show>
401
408
  <Show when={props.model.onOpenAttention}>
402
- <KeyHint theme={props.theme} keys={CHROME_ACTIONS.attention.keys} label={`${CHROME_ACTIONS.attention.label} (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`} width={props.width} quiet button onPress={() => props.model.onOpenAttention?.()}/>
409
+ <KeyHint theme={props.theme} keys={CHROME_ACTIONS.attention.keys} label={`${CHROME_ACTIONS.attention.label} (${groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`} width={props.width} quiet button onPress={() => props.model.onOpenAttention?.()}/>
403
410
  </Show>
404
411
  <Show when={controlsHeight() > 0 && controlGroup()}>
405
412
  {(group) => (<>
406
- <text height={2} fg={props.theme.roles.text.secondary}>
407
- {group().diagnostic
408
- ? fleetConnectionMessage(group().diagnostic)
409
- : connectionDetail(group())}
413
+ <text height={controlTextHeight()} flexShrink={0} fg={props.theme.roles.text.secondary}>
414
+ {controlTextLines().join("\n")}
410
415
  </text>
411
416
  <KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.retry.keys} label={SIDEBAR_ACTIONS.retry.label} width={props.width} quiet button onPress={() => props.model.onRetryMachine?.(group().id)}/>
412
417
  <KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.disconnect.keys} label={SIDEBAR_ACTIONS.disconnect.label} width={props.width} quiet button onPress={() => props.model.onDisconnectMachine?.(group().id)}/>