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
@@ -123,7 +123,10 @@ const STRUCTURAL_NOTIFICATIONS = new Set([
123
123
  "window-add",
124
124
  "window-close",
125
125
  "window-renamed",
126
+ "unlinked-window-add",
126
127
  "unlinked-window-close",
128
+ "session-renamed",
129
+ "sessions-changed",
127
130
  ]);
128
131
  const NATIVE_CLIENT_NOTIFICATIONS = new Set([
129
132
  "client-attached",
@@ -239,6 +242,10 @@ export interface MirrorFlowRecoveryObservation {
239
242
  }
240
243
 
241
244
  export interface SessionChannelOptions {
245
+ onWindowMembership?: (windows: readonly string[]) => void;
246
+ hasSharedWindowConflict?: () => boolean;
247
+ beforeIdentityRepair?: () => Promise<void>;
248
+ onWindowTopologyChanged?: () => void;
242
249
  ownedViewer?: Pick<OwnedViewerAdapter, "bindIo" | "tryDispatch" | "dispose"> &
243
250
  Partial<Pick<OwnedViewerAdapter, "atomicSnapshotEpoch">>;
244
251
  executeWindowLinkGuard?: (args: string[]) => Promise<{ status: number | null; stdout: string }>;
@@ -441,6 +448,7 @@ function layoutIdentitiesEqual(left: WindowLayout, right: WindowLayout): boolean
441
448
  }
442
449
 
443
450
  interface WindowSyncStage {
451
+ readonly observedAuthorityOrdinal: number;
444
452
  readonly windows: Map<string, WindowRecord>;
445
453
  readonly layouts: Map<string, ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout }>;
446
454
  readonly currentWindow: string;
@@ -502,6 +510,7 @@ export class SessionChannel {
502
510
  private nativeClientProbePending = false;
503
511
  // Native captures fence every geometry event; inventory fences identity only.
504
512
  private windowAuthorityOrdinal = 0;
513
+ private readonly layoutNotificationOrdinals = new Map<string, number>();
505
514
  private windowIdentityOrdinal = 0;
506
515
  private paneIncarnation = 0;
507
516
  private recoveryOrdinal = 0;
@@ -1186,9 +1195,31 @@ export class SessionChannel {
1186
1195
  }
1187
1196
  this.input.flush();
1188
1197
  this.clearWindowViewports();
1198
+ this.restoreWindowSizing(this.attachedIdentity?.runtimeSessionId ?? `=${this.opts.session}`);
1189
1199
  this.io.send(`refresh-client -C ${cols}x${rows}`);
1190
1200
  }
1191
1201
 
1202
+ private restoreWindowSizing(target: string): void {
1203
+ if (!this.geometryParticipating) return;
1204
+ // resize-window pins a window (including through inherited session options).
1205
+ // Evaluate inside the existing control connection: no shell, polling, or extra
1206
+ // request/response round trip. The shell also pre-fits hidden windows;
1207
+ // only the active window may have its manual policy repaired.
1208
+ const quoted = tmuxSingleQuote(target);
1209
+ // if-shell emits its own reply and the selected branch emits another.
1210
+ // Keep both branches at one command so either policy consumes exactly two
1211
+ // FIFO slots; a discard-only send would misroute the next capture replies.
1212
+ this.io.commandListInline(
1213
+ `if-shell -F -t ${quoted} '#{&&:#{window_active},#{==:#{window-size},manual}}' ` +
1214
+ tmuxSingleQuote(`set-option -w -t ${quoted} window-size latest`) +
1215
+ " " +
1216
+ tmuxSingleQuote(`display-message -p -t ${quoted} ''`),
1217
+ 2,
1218
+ 1,
1219
+ () => {},
1220
+ );
1221
+ }
1222
+
1192
1223
  /**
1193
1224
  * Experimental: window-specific overrides stay private to this control client.
1194
1225
  * Native tmux still falls back to the client size for unscoped neighbours;
@@ -1214,6 +1245,7 @@ export class SessionChannel {
1214
1245
  const previous = this.fittedWindows.get(window.runtimeId);
1215
1246
  if (previous?.cols === cols && previous.rows === rows) return;
1216
1247
  this.input.flush();
1248
+ this.restoreWindowSizing(window.runtimeId);
1217
1249
  this.io.send(`refresh-client -C ${window.runtimeId}:${cols}x${rows}`);
1218
1250
  this.fittedWindows.set(window.runtimeId, { cols, rows });
1219
1251
  }
@@ -1288,6 +1320,7 @@ export class SessionChannel {
1288
1320
  pane.subs.clear();
1289
1321
  }
1290
1322
  this.pendingLayoutOutput.clear();
1323
+ this.layoutNotificationOrdinals.clear();
1291
1324
  this.layoutSubscribers.clear();
1292
1325
  this.layoutAuthoritySubscribers.clear();
1293
1326
  await this.io.dispose();
@@ -2851,6 +2884,15 @@ export class SessionChannel {
2851
2884
  }
2852
2885
 
2853
2886
  private onNotify(name: string, rest: string): void {
2887
+ if (
2888
+ name === "window-add" ||
2889
+ name === "window-close" ||
2890
+ name === "unlinked-window-add" ||
2891
+ name === "unlinked-window-close" ||
2892
+ name === "session-renamed" ||
2893
+ name === "sessions-changed"
2894
+ )
2895
+ this.opts.onWindowTopologyChanged?.();
2854
2896
  if (name === "subscription-changed") {
2855
2897
  const policy =
2856
2898
  /^tmux-ide-scroll-on-clear\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+(%[0-9]+)\s+:\s+([01])\s*$/u.exec(
@@ -2976,6 +3018,7 @@ export class SessionChannel {
2976
3018
  this.scheduleSync(); // never guess from a failed parse
2977
3019
  return;
2978
3020
  }
3021
+ this.layoutNotificationOrdinals.set(change.windowId, this.windowAuthorityOrdinal);
2979
3022
  const pendingLayout = {
2980
3023
  ...parsed,
2981
3024
  zoomed: change.zoomed,
@@ -3294,8 +3337,10 @@ export class SessionChannel {
3294
3337
  previousCurrentWindow,
3295
3338
  syncOrdinal,
3296
3339
  );
3297
- for (const pane of this.panesByRuntime.values())
3340
+ for (const pane of this.panesByRuntime.values()) {
3341
+ if (pane.windowRuntimeId && this.pendingLayoutOutput.has(pane.windowRuntimeId)) continue;
3298
3342
  for (const sub of pane.subs) sub.resumeLayoutCapture?.(syncOrdinal);
3343
+ }
3299
3344
  this.discovery.discover(listed);
3300
3345
  }
3301
3346
 
@@ -3593,6 +3638,29 @@ export class SessionChannel {
3593
3638
  previousCurrentWindow = this.currentWindow,
3594
3639
  syncOrdinal?: number,
3595
3640
  ): void {
3641
+ // Control notifications in the same read chunk run before a promise-based
3642
+ // inventory read resumes. Preserve newer geometry (and its pending border
3643
+ // query) rather than letting that older inventory erase the notification.
3644
+ const newerLayouts = new Set(
3645
+ [...this.layoutNotificationOrdinals]
3646
+ .filter(([, ordinal]) => ordinal > stage.observedAuthorityOrdinal)
3647
+ .map(([runtimeId]) => runtimeId),
3648
+ );
3649
+ if (newerLayouts.size > 0) {
3650
+ stage = { ...stage, windows: new Map(stage.windows), layouts: new Map(stage.layouts) };
3651
+ for (const runtimeId of newerLayouts) {
3652
+ const currentLayout = this.layoutByWindow.get(runtimeId);
3653
+ const currentWindow = this.windowsByRuntime.get(runtimeId);
3654
+ const stagedWindow = stage.windows.get(runtimeId);
3655
+ if (currentLayout && currentWindow && stagedWindow) {
3656
+ stage.layouts.set(runtimeId, currentLayout);
3657
+ stage.windows.set(runtimeId, {
3658
+ ...stagedWindow,
3659
+ paneBorderStatus: currentWindow.paneBorderStatus,
3660
+ });
3661
+ }
3662
+ }
3663
+ }
3596
3664
  this.windowLinkAuthority?.reconcile(stage.links);
3597
3665
  this.latestWindowStage = stage;
3598
3666
  const changedWindows = new Set<string>();
@@ -3647,8 +3715,14 @@ export class SessionChannel {
3647
3715
  ),
3648
3716
  );
3649
3717
  for (const runtimeId of this.pendingLayoutOutput.keys())
3650
- if (!stage.windows.has(runtimeId)) this.pendingLayoutOutput.delete(runtimeId);
3651
- for (const runtimeId of layoutEmits) this.releasePendingLayout(runtimeId, syncOrdinal);
3718
+ if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
3719
+ this.pendingLayoutOutput.delete(runtimeId);
3720
+ for (const runtimeId of this.layoutNotificationOrdinals.keys())
3721
+ if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
3722
+ this.layoutNotificationOrdinals.delete(runtimeId);
3723
+ for (const runtimeId of layoutEmits)
3724
+ if (!newerLayouts.has(runtimeId) || !this.pendingLayoutOutput.has(runtimeId))
3725
+ this.releasePendingLayout(runtimeId, syncOrdinal);
3652
3726
  this.emitLayoutAuthority();
3653
3727
  }
3654
3728
 
@@ -3686,6 +3760,7 @@ export class SessionChannel {
3686
3760
  }
3687
3761
 
3688
3762
  private async stageWindows(target = this.opts.session): Promise<WindowSyncStage> {
3763
+ const observedAuthorityOrdinal = this.windowAuthorityOrdinal;
3689
3764
  const lines = await this.io.request(
3690
3765
  `list-windows -t "${target}" -F "#{window_id}\t#{qa:@tmux_ide_window_id}\t#{qa:window_name}\t#{window_active}\t#{window_visible_layout}\t#{?window_zoomed_flag,1,0}\t#{pane-border-status}\t#{window_layout}\t#{mode-keys}\t#{window_index}"`,
3691
3766
  );
@@ -3791,6 +3866,10 @@ export class SessionChannel {
3791
3866
  if (!unzoomed) throw new Error(`full window layout for ${this.opts.session} is malformed`);
3792
3867
  nextLayoutByWindow.set(row.runtimeId, { ...parsed, zoomed: row.zoomed, unzoomed });
3793
3868
  }
3869
+ // Publish validated physical membership before any identity repair writes.
3870
+ // The receiving session may introduce duplicate stamps by linking a window.
3871
+ this.opts.onWindowMembership?.([...backingRows.keys()]);
3872
+ await this.opts.beforeIdentityRepair?.();
3794
3873
  // Valid unique stamps are identity; missing/invalid/duplicated stamps are
3795
3874
  // ALL regenerated and stamped back (the pane policy, applied to windows).
3796
3875
  const stampCounts = new Map<string, number>();
@@ -3808,7 +3887,7 @@ export class SessionChannel {
3808
3887
  let semanticId: string | null = null;
3809
3888
  if (row.stamp && stampCounts.get(row.stamp) === 1) {
3810
3889
  semanticId = row.stamp;
3811
- } else {
3890
+ } else if (!this.opts.hasSharedWindowConflict?.()) {
3812
3891
  repairedIdentity = true;
3813
3892
  let candidate: string | null = null;
3814
3893
  for (let attempt = 0; attempt < 32 && !candidate; attempt += 1) {
@@ -3846,6 +3925,7 @@ export class SessionChannel {
3846
3925
  });
3847
3926
  }
3848
3927
  return {
3928
+ observedAuthorityOrdinal,
3849
3929
  windows: next,
3850
3930
  layouts: nextLayoutByWindow,
3851
3931
  currentWindow: nextCurrentWindow,
@@ -3864,6 +3944,10 @@ export class SessionChannel {
3864
3944
  listed: ReadonlySet<string>,
3865
3945
  ): Promise<boolean> {
3866
3946
  if (this.disposed) return false;
3947
+ if (this.opts.hasSharedWindowConflict?.()) {
3948
+ this.settleFirstJoin();
3949
+ return false;
3950
+ }
3867
3951
  const snapshots: WorkspaceTmuxPaneSnapshot[] = descriptors
3868
3952
  .filter((descriptor) => listed.has(descriptor.runtimePaneId))
3869
3953
  .map((descriptor) => ({
@@ -4005,6 +4089,10 @@ export class SessionChannel {
4005
4089
  descriptors: readonly SessionPaneDescriptor[],
4006
4090
  layouts: ReadonlyMap<string, ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout }>,
4007
4091
  ): Promise<boolean> {
4092
+ if (this.opts.hasSharedWindowConflict?.())
4093
+ throw new Error(
4094
+ "Linked windows across controlled sessions are unsupported. Unlink the shared window before controlling this session.",
4095
+ );
4008
4096
  const rectForRuntime = (runtimePaneId: string): WorkspacePaneRect => {
4009
4097
  for (const layout of layouts.values()) {
4010
4098
  const leaf = layout.leaves.find(({ id }) => id === runtimePaneId);
@@ -0,0 +1,42 @@
1
+ /** Event-fed physical window membership for one pinned tmux server. */
2
+ export class SharedWindowIndex<Owner> {
3
+ readonly #windows = new Map<string, Set<Owner>>();
4
+ readonly #owners = new Map<Owner, Set<string>>();
5
+ readonly #conflicts = new Set<Owner>();
6
+
7
+ conflicted(owner: Owner): boolean {
8
+ return this.#conflicts.has(owner);
9
+ }
10
+
11
+ update(owner: Owner, windows: readonly string[]): Map<Owner, boolean> {
12
+ const previous = this.#owners.get(owner) ?? new Set<string>();
13
+ const next = new Set(windows);
14
+ const affected = new Set<Owner>([owner]);
15
+ for (const window of new Set([...previous, ...next])) {
16
+ for (const peer of this.#windows.get(window) ?? []) affected.add(peer);
17
+ if (previous.has(window) && !next.has(window)) {
18
+ const peers = this.#windows.get(window)!;
19
+ peers.delete(owner);
20
+ if (!peers.size) this.#windows.delete(window);
21
+ }
22
+ if (next.has(window)) {
23
+ const peers = this.#windows.get(window) ?? new Set<Owner>();
24
+ peers.add(owner);
25
+ this.#windows.set(window, peers);
26
+ }
27
+ }
28
+ if (next.size) this.#owners.set(owner, next);
29
+ else this.#owners.delete(owner);
30
+ const changed = new Map<Owner, boolean>();
31
+ for (const peer of affected) {
32
+ const conflict = [...(this.#owners.get(peer) ?? [])].some(
33
+ (window) => (this.#windows.get(window)?.size ?? 0) > 1,
34
+ );
35
+ if (conflict === this.#conflicts.has(peer)) continue;
36
+ if (conflict) this.#conflicts.add(peer);
37
+ else this.#conflicts.delete(peer);
38
+ changed.set(peer, conflict);
39
+ }
40
+ return changed;
41
+ }
42
+ }
@@ -2219,7 +2219,18 @@ export class PaneStreamLiveConnection {
2219
2219
  !this.#sessionRuntimeBinding.authoritySnapshot
2220
2220
  )
2221
2221
  return this.#failProtocol("protocol-error");
2222
- const lease = this.#sessionRuntimeBinding.requestAuthority(frame.authority);
2222
+ let lease: SessionRuntimeAuthorityLease | null = null;
2223
+ try {
2224
+ lease = this.#sessionRuntimeBinding.requestAuthority(frame.authority);
2225
+ } catch (error) {
2226
+ // Topology changes can temporarily prevent ownership. Reject this
2227
+ // request without letting an expected conflict terminate the daemon.
2228
+ if (
2229
+ !(error instanceof SessionRuntimeControllerLeaseError) ||
2230
+ error.code !== "controller-conflict"
2231
+ )
2232
+ return this.#failProtocol("input-rejected");
2233
+ }
2223
2234
  if (lease) this.#requestedAuthorities.add(frame.authority);
2224
2235
  this.#sendFrame(null, {
2225
2236
  type: "authority-receipt",
@@ -26,6 +26,7 @@ export interface SessionRuntimeMultiplexerBackendOptions {
26
26
  | "bindExecutionSource"
27
27
  | "assertExecutionHandle"
28
28
  | "submitPaneCredentialIntent"
29
+ | "verifyWindowOwnership"
29
30
  >;
30
31
  readonly resolveSession: (workspaceName: string) => string | null;
31
32
  readonly resolvePaneSourceBinding?: (
@@ -192,6 +193,7 @@ export function createSessionRuntimeMultiplexerBackend(
192
193
  if (!ownerAuthorized) {
193
194
  throw new Error("Semantic mutation requires a live host, pane, or owner principal");
194
195
  }
196
+ await options.registry.verifyWindowOwnership();
195
197
  const owner = acquireOwner(session);
196
198
  try {
197
199
  let lease: ReturnType<SessionRuntimeConsumer["acquireController"]>;
@@ -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
  );
@@ -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);