tmux-ide 2.9.2 → 2.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -140
  3. package/bin/cli.js +2457 -727
  4. package/package.json +23 -8
  5. package/packages/contracts/src/desktop-host.ts +3 -0
  6. package/packages/contracts/src/saved-machines.ts +19 -0
  7. package/packages/contracts/src/terminal-replica.ts +1 -1
  8. package/packages/core/package.json +3 -2
  9. package/packages/core/src/terminal-delivery.ts +51 -3
  10. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  11. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  12. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  13. package/packages/core/src/terminal-replica.ts +13 -28
  14. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  15. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  16. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  17. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  18. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  19. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  20. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  21. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  22. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  23. package/packages/daemon/dist/lib/install-origin.js +2 -0
  24. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  25. package/packages/daemon/dist/lib/log.js +6 -10
  26. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  27. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  28. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  29. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  30. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  31. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  32. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  33. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  34. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  35. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  36. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  37. package/packages/daemon/dist/lib/update.js +18 -0
  38. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  39. package/packages/daemon/dist/machines.js +40 -4
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  43. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  44. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  45. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  46. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  47. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  48. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  49. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  50. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  51. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  52. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  53. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  54. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  55. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  56. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  57. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  61. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  62. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  63. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  64. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  65. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  66. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  67. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  68. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  69. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  70. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  71. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  72. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  73. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  74. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  75. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  76. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  78. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  79. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  80. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  81. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  82. package/packages/daemon/src/lib/install-origin.ts +5 -0
  83. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  84. package/packages/daemon/src/lib/log.ts +13 -9
  85. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  86. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  87. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  88. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  89. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  90. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  91. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  92. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  93. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  94. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  95. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  96. package/packages/daemon/src/lib/update.ts +24 -0
  97. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  98. package/packages/daemon/src/machines.ts +53 -5
  99. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  100. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  101. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  102. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  103. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  104. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  105. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  106. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  107. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  108. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  109. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  110. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  111. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  112. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  115. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  116. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  117. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  118. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  119. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  120. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  121. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  122. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  123. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  124. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  125. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  126. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  127. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  128. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  129. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  130. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  131. package/packages/daemon-client/src/workspace-client.ts +4 -1
  132. package/skill/SKILL.md +30 -30
  133. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  134. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  135. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  136. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  137. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  138. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  139. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  140. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  141. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  142. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  143. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  144. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  145. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  146. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  147. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  148. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  149. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  150. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  151. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  152. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  153. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  154. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  155. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  156. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  157. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  158. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  159. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  160. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  161. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  162. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  163. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  164. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  165. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  166. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  167. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  168. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  169. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  170. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  171. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  172. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  173. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  174. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  175. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  176. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  177. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  178. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  179. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  180. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  181. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  182. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  183. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  184. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  185. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  186. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  187. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  188. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  189. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  190. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  191. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  192. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  193. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  194. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  195. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  196. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  197. package/packages/core/src/agent-provisioning.test.ts +0 -42
  198. package/packages/core/src/application-shell-session.test.ts +0 -105
  199. package/packages/core/src/interaction-evidence.test.ts +0 -134
  200. package/packages/core/src/interaction-flow.test.ts +0 -66
  201. package/packages/core/src/interaction-receipts.test.ts +0 -671
  202. package/packages/core/src/navigator.test.ts +0 -34
  203. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  204. package/packages/core/src/optimistic-projection.test.ts +0 -118
  205. package/packages/core/src/performance-metrics.test.ts +0 -121
  206. package/packages/core/src/performance-qualification.test.ts +0 -283
  207. package/packages/core/src/saved-machines.test.ts +0 -80
  208. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  209. package/packages/core/src/terminal-conformance.test.ts +0 -48
  210. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  211. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  212. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  213. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  214. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  215. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  216. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  217. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  218. package/packages/core/src/terminal-replica.test.ts +0 -347
  219. package/packages/core/src/workspace-selection.test.ts +0 -63
  220. package/packages/daemon/dist/__tests__/support.js +0 -33
  221. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  222. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  223. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  224. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  225. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  226. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  227. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  228. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  229. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  230. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  231. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  232. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  236. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  237. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  238. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  239. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  240. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  241. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  242. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  243. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  244. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  245. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  246. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  247. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  248. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  249. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  250. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  251. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  252. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  253. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  254. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  255. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  256. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  257. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  258. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  259. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  260. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  261. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  262. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  263. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  264. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  265. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  266. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  267. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -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)}/>
@@ -1,8 +1,9 @@
1
1
  import { resolveRuntimeNamespace } from "../../../lib/runtime-namespace.js";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SavedMachineSchema } from "@tmux-ide/contracts";
4
- import { loadSavedMachines } from "../../../lib/saved-machines.js";
4
+ import { loadSavedMachines, watchSavedMachines } from "../../../lib/saved-machines.js";
5
5
  import { applicationMachineAuthorityManager } from "./application-machine-authority.js";
6
+ import { tuiPerfMark } from "./application-performance-log.js";
6
7
  /** Labels describe access routes; they never become routing identities. */
7
8
  export function ephemeralMachineProfile(alias, existing) {
8
9
  const target = SavedMachineSchema.shape.sshTarget.parse(alias);
@@ -20,17 +21,45 @@ export function ephemeralMachineProfile(alias, existing) {
20
21
  }
21
22
  /** Local discovery and rendering never wait for any remote handshake. */
22
23
  export function initializeApplicationMachines(aliases) {
23
- const profiles = resolveRuntimeNamespace().development ? [] : [...loadSavedMachines().machines];
24
+ const development = resolveRuntimeNamespace().development;
25
+ const profiles = development ? [] : [...loadSavedMachines().machines];
26
+ const ephemeral = [];
24
27
  let preferred = null;
25
28
  for (const alias of aliases) {
26
29
  let profile = profiles.find((p) => p.sshTarget === alias && p.enabled);
27
30
  if (!profile) {
28
31
  profile = ephemeralMachineProfile(alias, profiles);
29
32
  profiles.push(profile);
33
+ ephemeral.push(profile);
30
34
  }
31
35
  preferred ??= profile.id;
32
36
  }
33
37
  applicationMachineAuthorityManager.initialize(profiles);
34
38
  if (preferred)
35
39
  applicationMachineAuthorityManager.select(preferred);
40
+ if (!development) {
41
+ const failed = () => tuiPerfMark("machine-registry-error", { reason: "registry-unavailable-or-invalid" });
42
+ try {
43
+ applicationMachineAuthorityManager.followProfiles((onChange) => watchSavedMachines((registry) => {
44
+ const next = [...registry.machines];
45
+ for (let index = 0; index < ephemeral.length; index++) {
46
+ let profile = ephemeral[index];
47
+ // A newly saved route may claim an ephemeral display label. Keep the
48
+ // explicit connection's identity while making the label unambiguous.
49
+ if (next.some((entry) => entry.label.normalize("NFKC").toLowerCase() ===
50
+ profile.label.normalize("NFKC").toLowerCase()))
51
+ profile = {
52
+ ...profile,
53
+ label: ephemeralMachineProfile(profile.sshTarget, next).label,
54
+ };
55
+ ephemeral[index] = profile;
56
+ next.push(profile);
57
+ }
58
+ onChange(next);
59
+ }, failed));
60
+ }
61
+ catch {
62
+ failed();
63
+ }
64
+ }
36
65
  }
@@ -1,4 +1,4 @@
1
- import { createMemo, createSignal } from "solid-js";
1
+ import { createMemo, createSignal, untrack } from "solid-js";
2
2
  import { applicationCommandDescription, filterApplicationCommands, } from "../workspace/application-command-description.js";
3
3
  import { applicationPaletteKeyboardDisposition, } from "./application-palette-input.js";
4
4
  import { applicationPaneRenameKeyAction, applicationPaneRenamePaste, } from "./application-pane-rename-input.js";
@@ -11,9 +11,13 @@ export function createApplicationPaletteSearchOwner(options) {
11
11
  const [help, setHelp] = createSignal(false);
12
12
  const [query, setQuery] = createSignal("");
13
13
  const [selectedId, setSelectedId] = createSignal(null);
14
- const commands = createMemo(() => filterApplicationCommands(options
14
+ const readCommands = () => filterApplicationCommands(options
15
15
  .commands()
16
- .filter((c) => !localOnly() || typeof c === "string" || !c.fleet || c.fleet.machineId === "local"), query()));
16
+ .filter((c) => !localOnly() || typeof c === "string" || !c.fleet || c.fleet.machineId === "local"), query());
17
+ // Closed overlays must not subscribe to catalog churn. Explicit reads still
18
+ // resolve current commands, including reset() before the open action commits.
19
+ const activeCommands = createMemo(() => (options.open() ? readCommands() : null));
20
+ const commands = () => activeCommands() ?? untrack(readCommands);
17
21
  const selection = () => Math.max(0, commands().findIndex((command) => applicationCommandDescription(command).id === selectedId()));
18
22
  const select = (index) => {
19
23
  const command = commands()[index];
@@ -29,6 +29,8 @@ stream?.on("drain", drain);
29
29
  export const tuiLifecycleStream = stream ? Object.freeze({ enabled: true }) : null;
30
30
  export const tuiPerfStream = TUI_PERF_LOG ? tuiLifecycleStream : null;
31
31
  const LIFECYCLE_PHASES = new Set([
32
+ "machine-connection-status",
33
+ "machine-registry-error",
32
34
  "generation-status",
33
35
  "generation-connection-start",
34
36
  "generation-connection-resolved",
@@ -132,6 +134,7 @@ export function closeTuiPerfMarks() {
132
134
  closePromise = (async () => {
133
135
  if (!stream)
134
136
  return;
137
+ await writer?.flush();
135
138
  await flushTuiPerfMarks();
136
139
  // No drain callback may enqueue another critical record after end().
137
140
  stream.off("drain", drain);
@@ -7,6 +7,133 @@ import { terminalInputForOpenTuiKey, terminalInputsForPaste, } from "./terminal-
7
7
  export function createApplicationTerminalInputIngress(interaction, generation, sessionOwner, focusedPane, setNote) {
8
8
  const pending = createApplicationPendingTerminalInputOwner();
9
9
  let ownsNote = false;
10
+ let recovery = null;
11
+ const scope = () => {
12
+ const active = generation();
13
+ const owner = sessionOwner();
14
+ const pane = focusedPane();
15
+ const session = owner?.sessionName();
16
+ if (!active ||
17
+ !["live", "rebinding"].includes(active.status) ||
18
+ !active.connection ||
19
+ !active.client ||
20
+ !active.daemonGeneration ||
21
+ !owner ||
22
+ !session ||
23
+ !pane)
24
+ return null;
25
+ try {
26
+ const clientGeneration = active.client.getSnapshot().generation;
27
+ if (!Number.isSafeInteger(clientGeneration))
28
+ return null;
29
+ return {
30
+ owner,
31
+ connection: active.connection,
32
+ client: active.client,
33
+ daemon: active.daemonGeneration,
34
+ clientGeneration: clientGeneration,
35
+ session: session,
36
+ pane,
37
+ };
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ };
43
+ const sameScope = (left, right) => right !== null &&
44
+ left.owner === right.owner &&
45
+ left.connection === right.connection &&
46
+ left.client === right.client &&
47
+ left.daemon === right.daemon &&
48
+ left.clientGeneration === right.clientGeneration &&
49
+ left.session === right.session &&
50
+ left.pane === right.pane;
51
+ let recoveryVersion = 0;
52
+ const cancelRecovery = () => {
53
+ recoveryVersion++;
54
+ if (recovery)
55
+ clearTimeout(recovery.timer);
56
+ recovery = null;
57
+ };
58
+ const recover = (inputs, bytes, kind) => {
59
+ if (pending.snapshot().sessionName !== null ||
60
+ (!recovery && generation()?.status !== "rebinding"))
61
+ return false;
62
+ const target = scope();
63
+ if (!target)
64
+ return false;
65
+ if (recovery && !sameScope(recovery.scope, target))
66
+ cancelRecovery();
67
+ if (!recovery) {
68
+ const timer = setTimeout(() => {
69
+ cancelRecovery();
70
+ setNote("terminal recovery timed out · queued input was not sent");
71
+ }, 5_000);
72
+ timer.unref?.();
73
+ recovery = { scope: target, inputs: [], weight: 0, admitted: 0, draining: false, timer };
74
+ }
75
+ const weight = bytes.byteLength;
76
+ if (recovery.admitted >= 64 || recovery.weight + weight > 1024 * 1024) {
77
+ setNote("terminal input queue full · wait for the session to connect");
78
+ return true;
79
+ }
80
+ recovery.weight += weight;
81
+ recovery.admitted++;
82
+ const version = recoveryVersion;
83
+ const parserOrigin = captureOrigin()
84
+ ? {
85
+ origin: kind === "paste" ? "bracketed-paste" : "keyboard",
86
+ payload: Buffer.from(bytes),
87
+ }
88
+ : undefined;
89
+ recovery.inputs.push(async () => {
90
+ for (const [index, input] of inputs.entries()) {
91
+ if (version !== recoveryVersion ||
92
+ !sameScope(target, scope()) ||
93
+ generation()?.status !== "live" ||
94
+ !(await interaction.sendInputToPane(target.pane, input, index === 0 ? parserOrigin : undefined)))
95
+ return false;
96
+ }
97
+ return true;
98
+ });
99
+ setNote(`${target.session} · terminal ${kind} queued`);
100
+ return true;
101
+ };
102
+ const flushRecovery = () => {
103
+ const waiting = recovery;
104
+ if (!waiting || waiting.draining)
105
+ return;
106
+ if (!sameScope(waiting.scope, scope())) {
107
+ cancelRecovery();
108
+ setNote("terminal changed · queued input was not sent");
109
+ return;
110
+ }
111
+ if (generation()?.status !== "live" || !generation()?.fastLane)
112
+ return;
113
+ waiting.draining = true;
114
+ const version = recoveryVersion;
115
+ void (async () => {
116
+ while (version === recoveryVersion && waiting.inputs.length > 0) {
117
+ const send = waiting.inputs.shift();
118
+ try {
119
+ if (await send())
120
+ continue;
121
+ }
122
+ catch {
123
+ /* An uncertain send is never retried. */
124
+ }
125
+ if (version !== recoveryVersion)
126
+ return;
127
+ cancelRecovery();
128
+ setNote("terminal unavailable · queued input was not sent");
129
+ return;
130
+ }
131
+ if (version === recoveryVersion) {
132
+ cancelRecovery();
133
+ setNote(null);
134
+ }
135
+ })();
136
+ };
10
137
  const captureOrigin = () => Boolean(currentTuiPerformanceEventSink()?.terminalInputOrigin);
11
138
  const flush = () => {
12
139
  const owner = sessionOwner();
@@ -36,9 +163,14 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
36
163
  else
37
164
  setNote(`terminal unavailable · ${kind} was not sent`);
38
165
  };
166
+ const cancelInteractionInput = () => {
167
+ cancelRecovery();
168
+ interaction.cancelPendingInput();
169
+ };
39
170
  return {
40
171
  wrapStarter(starter) {
41
172
  const start = async (...args) => {
173
+ cancelInteractionInput();
42
174
  const [sessionName] = args;
43
175
  const identity = pending.begin(sessionName);
44
176
  const result = await starter(...args);
@@ -52,24 +184,32 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
52
184
  };
53
185
  return Object.assign(start, {
54
186
  cancel() {
187
+ cancelInteractionInput();
55
188
  pending.dispose();
56
189
  starter.cancel();
57
190
  },
58
191
  });
59
192
  },
60
- adopt() {
193
+ adopt(terminalsActive = true) {
194
+ if (!terminalsActive)
195
+ cancelInteractionInput();
61
196
  flush();
197
+ flushRecovery();
62
198
  },
63
199
  routeKey(event) {
64
200
  const active = generation();
65
201
  if (pending.snapshot().sessionName === null &&
202
+ recovery === null &&
66
203
  active?.status === "live" &&
67
204
  active.fastLane &&
68
205
  focusedPane()) {
69
206
  sendApplicationTerminalKey(interaction, event, captureOrigin());
70
207
  return;
71
208
  }
72
- if (!terminalInputForOpenTuiKey(event))
209
+ const recoveryInput = terminalInputForOpenTuiKey(event);
210
+ if (!recoveryInput)
211
+ return;
212
+ if (recover([recoveryInput], Buffer.from(recoveryInput.data, "utf8"), "input"))
73
213
  return;
74
214
  const copy = { name: event.name, ctrl: event.ctrl, meta: event.meta, shift: event.shift };
75
215
  noteAdmission(pending.enqueue(() => sendApplicationTerminalKey(interaction, copy, captureOrigin()), Buffer.byteLength(event.name, "utf8")), "input");
@@ -87,15 +227,19 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
87
227
  }
88
228
  const active = generation();
89
229
  if (pending.snapshot().sessionName === null &&
230
+ recovery === null &&
90
231
  active?.status === "live" &&
91
232
  active.fastLane &&
92
233
  focusedPane()) {
93
234
  sendApplicationTerminalPaste(interaction, copy, captureOrigin());
94
235
  return;
95
236
  }
237
+ if (recover(terminalInputsForPaste(Buffer.from(copy).toString("utf8")), copy, "paste"))
238
+ return;
96
239
  noteAdmission(pending.enqueue(() => sendApplicationTerminalPaste(interaction, copy, captureOrigin()), copy.byteLength), "paste");
97
240
  },
98
241
  dispose() {
242
+ cancelInteractionInput();
99
243
  pending.dispose();
100
244
  },
101
245
  };