aimux-cli 0.1.15 → 0.1.18

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 (299) hide show
  1. package/README.md +184 -67
  2. package/bin/aimux-dev +10 -0
  3. package/dist/alert-display.d.ts +21 -0
  4. package/dist/alert-display.js +86 -0
  5. package/dist/alert-display.js.map +1 -0
  6. package/dist/attachment-store.d.ts +0 -7
  7. package/dist/attachment-store.js +2 -86
  8. package/dist/attachment-store.js.map +1 -1
  9. package/dist/builtin-metadata-watchers.js +4 -4
  10. package/dist/builtin-metadata-watchers.js.map +1 -1
  11. package/dist/claude-hooks.d.ts +1 -0
  12. package/dist/claude-hooks.js +25 -0
  13. package/dist/claude-hooks.js.map +1 -1
  14. package/dist/config.d.ts +19 -13
  15. package/dist/config.js +28 -14
  16. package/dist/config.js.map +1 -1
  17. package/dist/connection-targets.d.ts +8 -0
  18. package/dist/connection-targets.js +28 -0
  19. package/dist/connection-targets.js.map +1 -0
  20. package/dist/credentials.d.ts +12 -0
  21. package/dist/credentials.js +49 -0
  22. package/dist/credentials.js.map +1 -0
  23. package/dist/daemon.d.ts +23 -0
  24. package/dist/daemon.js +391 -66
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/dashboard/index.d.ts +13 -10
  27. package/dist/dashboard/index.js +3 -26
  28. package/dist/dashboard/index.js.map +1 -1
  29. package/dist/dashboard/order.d.ts +22 -0
  30. package/dist/dashboard/order.js +55 -0
  31. package/dist/dashboard/order.js.map +1 -0
  32. package/dist/dashboard/pending-actions.d.ts +39 -10
  33. package/dist/dashboard/pending-actions.js +166 -36
  34. package/dist/dashboard/pending-actions.js.map +1 -1
  35. package/dist/dashboard/quick-jump.d.ts +2 -1
  36. package/dist/dashboard/quick-jump.js +7 -4
  37. package/dist/dashboard/quick-jump.js.map +1 -1
  38. package/dist/dashboard/session-actions.d.ts +4 -4
  39. package/dist/dashboard/session-actions.js +1 -1
  40. package/dist/dashboard/session-actions.js.map +1 -1
  41. package/dist/dashboard/session-registry.d.ts +4 -3
  42. package/dist/dashboard/session-registry.js +16 -50
  43. package/dist/dashboard/session-registry.js.map +1 -1
  44. package/dist/dashboard/state.d.ts +1 -1
  45. package/dist/dashboard/state.js.map +1 -1
  46. package/dist/dashboard/ui-state-store.d.ts +16 -1
  47. package/dist/dashboard/ui-state-store.js +73 -2
  48. package/dist/dashboard/ui-state-store.js.map +1 -1
  49. package/dist/debug-state.d.ts +97 -0
  50. package/dist/debug-state.js +541 -0
  51. package/dist/debug-state.js.map +1 -0
  52. package/dist/debug.d.ts +38 -0
  53. package/dist/debug.js +219 -15
  54. package/dist/debug.js.map +1 -1
  55. package/dist/default-plugins/gh-pr-context.d.ts +2 -1
  56. package/dist/default-plugins/gh-pr-context.js +17 -11
  57. package/dist/default-plugins/gh-pr-context.js.map +1 -1
  58. package/dist/default-plugins/transcript-length.js +15 -2
  59. package/dist/default-plugins/transcript-length.js.map +1 -1
  60. package/dist/fast-control.js +37 -19
  61. package/dist/fast-control.js.map +1 -1
  62. package/dist/http-client.js +31 -2
  63. package/dist/http-client.js.map +1 -1
  64. package/dist/local-ui-server.d.ts +22 -0
  65. package/dist/local-ui-server.js +186 -0
  66. package/dist/local-ui-server.js.map +1 -0
  67. package/dist/login-flow.d.ts +7 -0
  68. package/dist/login-flow.js +120 -0
  69. package/dist/login-flow.js.map +1 -0
  70. package/dist/main.js +824 -152
  71. package/dist/main.js.map +1 -1
  72. package/dist/managed-launch-env.js +14 -0
  73. package/dist/managed-launch-env.js.map +1 -1
  74. package/dist/metadata-server.d.ts +36 -36
  75. package/dist/metadata-server.js +638 -137
  76. package/dist/metadata-server.js.map +1 -1
  77. package/dist/metadata-store.d.ts +4 -1
  78. package/dist/metadata-store.js +30 -2
  79. package/dist/metadata-store.js.map +1 -1
  80. package/dist/multiplexer/agent-io-methods.d.ts +2 -10
  81. package/dist/multiplexer/agent-io-methods.js +12 -43
  82. package/dist/multiplexer/agent-io-methods.js.map +1 -1
  83. package/dist/multiplexer/archives.js +8 -9
  84. package/dist/multiplexer/archives.js.map +1 -1
  85. package/dist/multiplexer/dashboard-control.js +45 -13
  86. package/dist/multiplexer/dashboard-control.js.map +1 -1
  87. package/dist/multiplexer/dashboard-interaction.d.ts +8 -2
  88. package/dist/multiplexer/dashboard-interaction.js +187 -28
  89. package/dist/multiplexer/dashboard-interaction.js.map +1 -1
  90. package/dist/multiplexer/dashboard-model.d.ts +10 -3
  91. package/dist/multiplexer/dashboard-model.js +417 -35
  92. package/dist/multiplexer/dashboard-model.js.map +1 -1
  93. package/dist/multiplexer/dashboard-ops.d.ts +9 -7
  94. package/dist/multiplexer/dashboard-ops.js +178 -68
  95. package/dist/multiplexer/dashboard-ops.js.map +1 -1
  96. package/dist/multiplexer/dashboard-state-methods.d.ts +2 -1
  97. package/dist/multiplexer/dashboard-state-methods.js +3 -2
  98. package/dist/multiplexer/dashboard-state-methods.js.map +1 -1
  99. package/dist/multiplexer/dashboard-tail-methods.d.ts +22 -10
  100. package/dist/multiplexer/dashboard-tail-methods.js +164 -47
  101. package/dist/multiplexer/dashboard-tail-methods.js.map +1 -1
  102. package/dist/multiplexer/dashboard-view-methods.d.ts +1 -1
  103. package/dist/multiplexer/dashboard-view-methods.js +23 -8
  104. package/dist/multiplexer/dashboard-view-methods.js.map +1 -1
  105. package/dist/multiplexer/graveyard-view-model.d.ts +9 -1
  106. package/dist/multiplexer/graveyard-view-model.js +39 -0
  107. package/dist/multiplexer/graveyard-view-model.js.map +1 -1
  108. package/dist/multiplexer/index.d.ts +15 -12
  109. package/dist/multiplexer/index.js +64 -43
  110. package/dist/multiplexer/index.js.map +1 -1
  111. package/dist/multiplexer/notifications.js +107 -24
  112. package/dist/multiplexer/notifications.js.map +1 -1
  113. package/dist/multiplexer/persistence-methods.d.ts +31 -4
  114. package/dist/multiplexer/persistence-methods.js +304 -308
  115. package/dist/multiplexer/persistence-methods.js.map +1 -1
  116. package/dist/multiplexer/runtime-lifecycle-methods.d.ts +8 -10
  117. package/dist/multiplexer/runtime-lifecycle-methods.js +104 -86
  118. package/dist/multiplexer/runtime-lifecycle-methods.js.map +1 -1
  119. package/dist/multiplexer/runtime-state.d.ts +8 -10
  120. package/dist/multiplexer/runtime-state.js +82 -145
  121. package/dist/multiplexer/runtime-state.js.map +1 -1
  122. package/dist/multiplexer/runtime-sync.d.ts +2 -10
  123. package/dist/multiplexer/runtime-sync.js +3 -18
  124. package/dist/multiplexer/runtime-sync.js.map +1 -1
  125. package/dist/multiplexer/service-state-snapshot.d.ts +2 -4
  126. package/dist/multiplexer/service-state-snapshot.js +4 -51
  127. package/dist/multiplexer/service-state-snapshot.js.map +1 -1
  128. package/dist/multiplexer/services.d.ts +1 -0
  129. package/dist/multiplexer/services.js +55 -5
  130. package/dist/multiplexer/services.js.map +1 -1
  131. package/dist/multiplexer/session-capture.d.ts +1 -0
  132. package/dist/multiplexer/session-capture.js +24 -0
  133. package/dist/multiplexer/session-capture.js.map +1 -0
  134. package/dist/multiplexer/session-launch.d.ts +4 -1
  135. package/dist/multiplexer/session-launch.js +152 -63
  136. package/dist/multiplexer/session-launch.js.map +1 -1
  137. package/dist/multiplexer/session-runtime-core.d.ts +8 -20
  138. package/dist/multiplexer/session-runtime-core.js +40 -135
  139. package/dist/multiplexer/session-runtime-core.js.map +1 -1
  140. package/dist/multiplexer/subscreens.js +10 -3
  141. package/dist/multiplexer/subscreens.js.map +1 -1
  142. package/dist/multiplexer/worktree-graveyard.d.ts +0 -1
  143. package/dist/multiplexer/worktree-graveyard.js +15 -16
  144. package/dist/multiplexer/worktree-graveyard.js.map +1 -1
  145. package/dist/multiplexer/worktrees.js +96 -40
  146. package/dist/multiplexer/worktrees.js.map +1 -1
  147. package/dist/notification-context.js +8 -4
  148. package/dist/notification-context.js.map +1 -1
  149. package/dist/notifications.js +163 -101
  150. package/dist/notifications.js.map +1 -1
  151. package/dist/notify.d.ts +4 -0
  152. package/dist/notify.js +14 -0
  153. package/dist/notify.js.map +1 -1
  154. package/dist/paths.d.ts +32 -7
  155. package/dist/paths.js +82 -58
  156. package/dist/paths.js.map +1 -1
  157. package/dist/pending-actions.d.ts +5 -0
  158. package/dist/pending-actions.js +14 -0
  159. package/dist/pending-actions.js.map +1 -0
  160. package/dist/plugin-runtime.js +9 -2
  161. package/dist/plugin-runtime.js.map +1 -1
  162. package/dist/project-events.d.ts +1 -10
  163. package/dist/project-events.js +0 -10
  164. package/dist/project-events.js.map +1 -1
  165. package/dist/project-scanner.d.ts +2 -3
  166. package/dist/project-scanner.js +58 -129
  167. package/dist/project-scanner.js.map +1 -1
  168. package/dist/project-service-manifest.d.ts +1 -3
  169. package/dist/project-service-manifest.js +1 -3
  170. package/dist/project-service-manifest.js.map +1 -1
  171. package/dist/relay-client.d.ts +30 -0
  172. package/dist/relay-client.js +191 -0
  173. package/dist/relay-client.js.map +1 -0
  174. package/dist/remote-access.d.ts +16 -0
  175. package/dist/remote-access.js +91 -0
  176. package/dist/remote-access.js.map +1 -0
  177. package/dist/runtime-core/exchange-derived.d.ts +2 -0
  178. package/dist/runtime-core/exchange-derived.js +154 -0
  179. package/dist/runtime-core/exchange-derived.js.map +1 -0
  180. package/dist/runtime-core/exchange-import.d.ts +24 -0
  181. package/dist/runtime-core/exchange-import.js +318 -0
  182. package/dist/runtime-core/exchange-import.js.map +1 -0
  183. package/dist/runtime-core/exchange-store.d.ts +157 -0
  184. package/dist/runtime-core/exchange-store.js +453 -0
  185. package/dist/runtime-core/exchange-store.js.map +1 -0
  186. package/dist/runtime-core/topology-services.d.ts +38 -0
  187. package/dist/runtime-core/topology-services.js +171 -0
  188. package/dist/runtime-core/topology-services.js.map +1 -0
  189. package/dist/runtime-core/topology-sessions.d.ts +52 -0
  190. package/dist/runtime-core/topology-sessions.js +239 -0
  191. package/dist/runtime-core/topology-sessions.js.map +1 -0
  192. package/dist/runtime-core/topology-store.d.ts +171 -0
  193. package/dist/runtime-core/topology-store.js +420 -0
  194. package/dist/runtime-core/topology-store.js.map +1 -0
  195. package/dist/runtime-core/topology-worktrees.d.ts +60 -0
  196. package/dist/runtime-core/topology-worktrees.js +200 -0
  197. package/dist/runtime-core/topology-worktrees.js.map +1 -0
  198. package/dist/runtime-migration.d.ts +69 -0
  199. package/dist/runtime-migration.js +399 -0
  200. package/dist/runtime-migration.js.map +1 -0
  201. package/dist/session-bootstrap.d.ts +8 -6
  202. package/dist/session-bootstrap.js +51 -158
  203. package/dist/session-bootstrap.js.map +1 -1
  204. package/dist/session-runtime.d.ts +2 -0
  205. package/dist/session-runtime.js +1 -0
  206. package/dist/session-runtime.js.map +1 -1
  207. package/dist/session-semantics.d.ts +12 -4
  208. package/dist/session-semantics.js +14 -0
  209. package/dist/session-semantics.js.map +1 -1
  210. package/dist/shell-hooks.js +32 -10
  211. package/dist/shell-hooks.js.map +1 -1
  212. package/dist/shell-state.d.ts +2 -0
  213. package/dist/shell-state.js +26 -1
  214. package/dist/shell-state.js.map +1 -1
  215. package/dist/statusline-model.d.ts +10 -2
  216. package/dist/statusline-model.js +106 -30
  217. package/dist/statusline-model.js.map +1 -1
  218. package/dist/task-workflow.d.ts +6 -9
  219. package/dist/task-workflow.js +37 -84
  220. package/dist/task-workflow.js.map +1 -1
  221. package/dist/tasks.d.ts +6 -33
  222. package/dist/tasks.js +46 -88
  223. package/dist/tasks.js.map +1 -1
  224. package/dist/team.d.ts +29 -0
  225. package/dist/team.js +40 -0
  226. package/dist/team.js.map +1 -1
  227. package/dist/threads.d.ts +6 -35
  228. package/dist/threads.js +89 -98
  229. package/dist/threads.js.map +1 -1
  230. package/dist/tmux/inbox-popup.js +37 -15
  231. package/dist/tmux/inbox-popup.js.map +1 -1
  232. package/dist/tmux/runtime-manager.d.ts +3 -0
  233. package/dist/tmux/runtime-manager.js +21 -4
  234. package/dist/tmux/runtime-manager.js.map +1 -1
  235. package/dist/tmux/statusline.js +49 -9
  236. package/dist/tmux/statusline.js.map +1 -1
  237. package/dist/tmux/window-open.js +1 -2
  238. package/dist/tmux/window-open.js.map +1 -1
  239. package/dist/tool-output-watchers.d.ts +0 -18
  240. package/dist/tool-output-watchers.js +0 -322
  241. package/dist/tool-output-watchers.js.map +1 -1
  242. package/dist/tui/screens/dashboard-renderers.js +37 -25
  243. package/dist/tui/screens/dashboard-renderers.js.map +1 -1
  244. package/dist/tui/screens/overlay-renderers.d.ts +2 -0
  245. package/dist/tui/screens/overlay-renderers.js +37 -1
  246. package/dist/tui/screens/overlay-renderers.js.map +1 -1
  247. package/dist/tui/screens/subscreen-renderers.js +7 -0
  248. package/dist/tui/screens/subscreen-renderers.js.map +1 -1
  249. package/dist/worktree.js +17 -0
  250. package/dist/worktree.js.map +1 -1
  251. package/dist-ui/_expo/static/css/web-30453ede1678c16acb08b97e83e8646d.css +1 -0
  252. package/dist-ui/_expo/static/js/web/entry-477c745b2adc79367a4380ecf07d9ff6.js +14620 -0
  253. package/dist-ui/assets/assets/images/icon.a5413dcd2e811c9f2317d01a28118d8a.png +0 -0
  254. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
  255. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
  256. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
  257. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
  258. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
  259. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
  260. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
  261. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
  262. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
  263. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
  264. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
  265. package/dist-ui/assets/node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
  266. package/dist-ui/assets/node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
  267. package/dist-ui/assets/node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
  268. package/dist-ui/assets/node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
  269. package/dist-ui/assets/node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
  270. package/dist-ui/assets/node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
  271. package/dist-ui/assets/node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
  272. package/dist-ui/favicon.ico +0 -0
  273. package/dist-ui/index.html +38 -0
  274. package/dist-ui/metadata.json +1 -0
  275. package/package.json +31 -15
  276. package/dist/agent-message-parts.d.ts +0 -17
  277. package/dist/agent-message-parts.js +0 -31
  278. package/dist/agent-message-parts.js.map +0 -1
  279. package/dist/instance-directory.d.ts +0 -32
  280. package/dist/instance-directory.js +0 -82
  281. package/dist/instance-directory.js.map +0 -1
  282. package/dist/instance-registry.d.ts +0 -39
  283. package/dist/instance-registry.js +0 -208
  284. package/dist/instance-registry.js.map +0 -1
  285. package/dist/multiplexer/session-actions.d.ts +0 -40
  286. package/dist/multiplexer/session-actions.js +0 -110
  287. package/dist/multiplexer/session-actions.js.map +0 -1
  288. package/dist/orchestration-dispatcher.d.ts +0 -25
  289. package/dist/orchestration-dispatcher.js +0 -59
  290. package/dist/orchestration-dispatcher.js.map +0 -1
  291. package/dist/session-input-operations.d.ts +0 -19
  292. package/dist/session-input-operations.js +0 -46
  293. package/dist/session-input-operations.js.map +0 -1
  294. package/dist/session-message-history.d.ts +0 -27
  295. package/dist/session-message-history.js +0 -105
  296. package/dist/session-message-history.js.map +0 -1
  297. package/dist/task-dispatcher.d.ts +0 -64
  298. package/dist/task-dispatcher.js +0 -213
  299. package/dist/task-dispatcher.js.map +0 -1
@@ -1,18 +1,21 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { basename, join } from "node:path";
4
- import { debug } from "../debug.js";
5
- import { DashboardPendingActions } from "../dashboard/pending-actions.js";
6
4
  import { addDashboardOperationFailure, clearDashboardOperationFailures, listDashboardOperationFailures, } from "../dashboard/operation-failures.js";
7
5
  import { composeDashboardWorktreeGroups } from "./dashboard-model.js";
8
6
  import { loadDaemonInfo } from "../daemon.js";
9
- import { getGraveyardPath, getLocalAimuxDir, getProjectStateDir, getStatePath } from "../paths.js";
7
+ import { getProjectStateDir, getStatePath } from "../paths.js";
10
8
  import { loadMetadataState } from "../metadata-store.js";
11
- import { renderCurrentDashboardView as renderCurrentDashboardViewImpl, stopSessionToOffline } from "./runtime-state.js";
12
- import { listWorktreeGraveyardEntries as listWorktreeGraveyardEntriesImpl, listWorktreeGraveyardPaths, writeWorktreeGraveyardEntries, } from "./worktree-graveyard.js";
9
+ import { createRuntimeExchangeStore } from "../runtime-core/exchange-store.js";
10
+ import { renderCurrentDashboardView as renderCurrentDashboardViewImpl } from "./runtime-state.js";
11
+ import { listWorktreeGraveyardEntries as listWorktreeGraveyardEntriesImpl, listWorktreeGraveyardPaths, } from "./worktree-graveyard.js";
13
12
  import { loadStatusline, renderTmuxStatuslineFromData } from "../tmux/statusline.js";
14
13
  import { ensureTmuxStatuslineDir, invalidateTmuxStatuslineArtifacts } from "../tmux/statusline-cache.js";
15
14
  import { markLastUsed } from "../last-used.js";
15
+ import { isTeammateSession } from "../team.js";
16
+ import { listTopologySessionStates, removeTopologySessionsForWorktree, resurrectTopologySession, } from "../runtime-core/topology-sessions.js";
17
+ import { removeTopologyServicesForWorktree, upsertTopologyService } from "../runtime-core/topology-services.js";
18
+ import { deleteTopologyWorktreeGraveyardEntry, listTopologyWorktreeGraveyard, moveTopologyWorktreeToGraveyard, removeTopologyWorktree, resurrectTopologyWorktreeFromGraveyard, upsertTopologyWorktree, } from "../runtime-core/topology-worktrees.js";
16
19
  import { findMainRepo, getWorktreeBaseDir, getWorktreeAddArgs, getWorktreeCreatePath, isToolInternalWorktree, listWorktrees as listAllWorktrees, } from "../worktree.js";
17
20
  function recordDashboardFailure(host, input) {
18
21
  const failure = addDashboardOperationFailure(input);
@@ -25,21 +28,37 @@ function recordDashboardFailure(host, input) {
25
28
  });
26
29
  return failure;
27
30
  }
31
+ function refreshDashboardWorktreeProjection(host) {
32
+ host.invalidateDesktopStateSnapshot();
33
+ host.refreshLocalDashboardModel();
34
+ host.metadataServer?.notifyChange?.();
35
+ if (host.mode === "dashboard") {
36
+ host.renderDashboard();
37
+ }
38
+ }
39
+ function orderStatuslineItemsByWorktree(items, orderForWorktree) {
40
+ const grouped = new Map();
41
+ for (const item of items) {
42
+ const key = item.worktreePath ?? "__main__";
43
+ const group = grouped.get(key) ?? { worktreePath: item.worktreePath, items: [] };
44
+ group.items.push(item);
45
+ grouped.set(key, group);
46
+ }
47
+ return [...grouped.values()].flatMap((group) => orderForWorktree(group.items, group.worktreePath));
48
+ }
49
+ function exchangeTaskCounts() {
50
+ try {
51
+ const exchange = createRuntimeExchangeStore().read();
52
+ return {
53
+ pending: exchange.tasks.filter((task) => task.status === "pending").length,
54
+ assigned: exchange.tasks.filter((task) => task.status === "assigned" || task.status === "in_progress" || task.status === "blocked").length,
55
+ };
56
+ }
57
+ catch {
58
+ return { pending: 0, assigned: 0 };
59
+ }
60
+ }
28
61
  export const persistenceMethods = {
29
- writeSessionsFile() {
30
- const dir = getLocalAimuxDir();
31
- if (!existsSync(dir))
32
- mkdirSync(dir, { recursive: true });
33
- const localSessions = this.sessions.map((s) => ({
34
- id: s.id,
35
- tool: s.command,
36
- createdAt: s.startTime ? new Date(s.startTime).toISOString() : undefined,
37
- backendSessionId: s.backendSessionId,
38
- worktreePath: this.sessionWorktreePaths.get(s.id),
39
- }));
40
- const data = this.instanceDirectory.buildSessionsFileEntries(localSessions, this.instanceDirectory.getRemoteInstancesSafe(this.instanceId, process.cwd()));
41
- writeFileSync(`${dir}/sessions.json`, JSON.stringify(data, null, 2) + "\n");
42
- },
43
62
  writeStatuslineFile(input) {
44
63
  try {
45
64
  if (this.mode !== "project-service")
@@ -48,6 +67,7 @@ export const persistenceMethods = {
48
67
  for (const session of this.sessions) {
49
68
  this.syncTmuxWindowMetadata(session.id);
50
69
  }
70
+ this.dashboardUiStateStore.loadSharedState(this.dashboardState);
51
71
  this.refreshDesktopStateSnapshot();
52
72
  const dir = getProjectStateDir();
53
73
  const filePath = join(dir, "statusline.json");
@@ -115,7 +135,7 @@ export const persistenceMethods = {
115
135
  });
116
136
  this.writeStatuslineTextFile("top-dashboard.txt", dashboardTop);
117
137
  this.writeStatuslineTextFile("bottom-dashboard.txt", dashboardBottom);
118
- for (const entry of data.sessions) {
138
+ for (const entry of [...data.sessions, ...(data.teammates ?? [])]) {
119
139
  if (!entry.tmuxWindowId)
120
140
  continue;
121
141
  const renderOptions = {
@@ -139,7 +159,7 @@ export const persistenceMethods = {
139
159
  project: basename(process.cwd()),
140
160
  sessions: [],
141
161
  metadata: {},
142
- tasks: { pending: 0, assigned: 0 },
162
+ tasks: exchangeTaskCounts(),
143
163
  controlPlane: { daemonAlive: true, projectServiceAlive: false },
144
164
  flash: null,
145
165
  updatedAt: new Date().toISOString(),
@@ -154,11 +174,17 @@ export const persistenceMethods = {
154
174
  },
155
175
  buildStatuslineSnapshot() {
156
176
  const desktopState = this.desktopStateSnapshot ?? this.buildDesktopStateSnapshot();
177
+ const orderedSessions = orderStatuslineItemsByWorktree(desktopState.sessions, (sessions, worktreePath) => this.dashboardUiStateStore.orderSessionsForWorktree(sessions, worktreePath));
178
+ const teammateSessions = this.dashboardPendingActions
179
+ .applyToSessions(desktopState.teammates ?? [], { includeTeammates: true })
180
+ .filter((session) => isTeammateSession(session));
181
+ const orderedTeammates = orderStatuslineItemsByWorktree(teammateSessions, (sessions, worktreePath) => this.dashboardUiStateStore.orderSessionsForWorktree(sessions, worktreePath));
182
+ const orderedServices = orderStatuslineItemsByWorktree(desktopState.services, (services, worktreePath) => this.dashboardUiStateStore.orderServicesForWorktree(services, worktreePath));
157
183
  return {
158
184
  project: basename(process.cwd()),
159
185
  dashboardScreen: this.dashboardState.screen,
160
186
  sessions: [
161
- ...desktopState.sessions.map((session) => ({
187
+ ...orderedSessions.map((session) => ({
162
188
  id: session.id,
163
189
  kind: "agent",
164
190
  tool: session.command,
@@ -173,7 +199,7 @@ export const persistenceMethods = {
173
199
  worktreePath: session.worktreePath,
174
200
  semantic: session.semantic,
175
201
  })),
176
- ...desktopState.services.map((service) => ({
202
+ ...orderedServices.map((service) => ({
177
203
  id: service.id,
178
204
  kind: "service",
179
205
  tool: service.command,
@@ -185,9 +211,26 @@ export const persistenceMethods = {
185
211
  status: service.status,
186
212
  active: service.active,
187
213
  worktreePath: service.worktreePath,
214
+ launchCommandLine: service.launchCommandLine,
188
215
  })),
189
216
  ],
190
- tasks: this.taskDispatcher?.getTaskCounts() ?? { pending: 0, assigned: 0 },
217
+ teammates: orderedTeammates.map((session) => ({
218
+ id: session.id,
219
+ kind: "agent",
220
+ tool: session.command,
221
+ label: session.label,
222
+ tmuxWindowId: session.tmuxWindowId,
223
+ tmuxWindowIndex: session.tmuxWindowIndex,
224
+ windowName: session.command,
225
+ headline: session.headline,
226
+ status: session.status,
227
+ role: session.role,
228
+ active: session.active,
229
+ worktreePath: session.worktreePath,
230
+ semantic: session.semantic,
231
+ team: session.team,
232
+ })),
233
+ tasks: exchangeTaskCounts(),
191
234
  controlPlane: {
192
235
  daemonAlive: Boolean(loadDaemonInfo()),
193
236
  projectServiceAlive: true,
@@ -203,48 +246,33 @@ export const persistenceMethods = {
203
246
  }
204
247
  const desktopState = this.desktopStateSnapshot ?? this.buildDesktopStateSnapshot();
205
248
  return {
206
- sessions: desktopState.sessions,
207
- services: desktopState.services,
249
+ sessions: this.dashboardPendingActions.applyToSessions(desktopState.sessions),
250
+ teammates: this.dashboardPendingActions
251
+ .applyToSessions(desktopState.teammates ?? [], { includeTeammates: true })
252
+ .filter((session) => isTeammateSession(session)),
253
+ services: this.dashboardPendingActions.applyToServices(desktopState.services),
208
254
  statusline: this.buildStatuslineSnapshot(),
209
- worktrees: desktopState.worktrees,
255
+ worktrees: this.dashboardPendingActions.applyToWorktrees(desktopState.worktrees),
210
256
  operationFailures: desktopState.operationFailures,
211
257
  mainCheckoutInfo: desktopState.mainCheckoutInfo,
212
258
  mainCheckoutPath: desktopState.mainCheckoutPath,
213
259
  };
214
260
  },
215
261
  reapplyDashboardPendingActions() {
216
- this.dashboardSessionsCache = this.dashboardPendingActions.applyToSessions(this.dashboardSessionsCache.map(({ pendingAction: _pendingAction, optimistic: _optimistic, ...session }) => session));
217
- this.dashboardServicesCache = this.dashboardPendingActions.applyToServices(this.dashboardServicesCache.map(({ pendingAction: _pendingAction, optimistic: _optimistic, ...service }) => service));
218
- this.dashboardWorktreeGroupsCache = composeDashboardWorktreeGroups(this.dashboardPendingActions.applyToWorktrees(this.dashboardWorktreeGroupsCache.map(({ pendingAction: _pendingAction, optimistic: _optimistic, pending: _pending, removing: _removing, operationFailure: _operationFailure, ...wt }) => wt)), this.dashboardSessionsCache, this.dashboardServicesCache);
262
+ this.dashboardSessionsCache = this.dashboardPendingActions.applyToSessions(this.dashboardSessionsCache.map(({ pending: _pending, pendingAction: _pendingAction, optimistic: _optimistic, ...session }) => session));
263
+ this.dashboardTeammatesCache = this.dashboardPendingActions
264
+ .applyToSessions((this.dashboardTeammatesCache ?? []).map(({ pending: _pending, pendingAction: _pendingAction, optimistic: _optimistic, ...session }) => session), { includeTeammates: true })
265
+ .filter((session) => isTeammateSession(session));
266
+ this.dashboardServicesCache = this.dashboardPendingActions.applyToServices(this.dashboardServicesCache.map(({ pending: _pending, pendingAction: _pendingAction, optimistic: _optimistic, ...service }) => service));
267
+ this.dashboardWorktreeGroupsCache = this.dashboardUiStateStore.orderWorktreeGroups(composeDashboardWorktreeGroups(this.dashboardPendingActions.applyToWorktrees(this.dashboardWorktreeGroupsCache.map(({ pendingAction: _pendingAction, optimistic: _optimistic, pending: _pending, removing: _removing, ...wt }) => wt)), this.dashboardSessionsCache, this.dashboardServicesCache));
219
268
  },
220
269
  listDesktopWorktrees() {
221
- const pendingCreates = this.pendingWorktreeCreates;
222
- const pendingRemovals = this.pendingWorktreeRemovals;
223
270
  const hiddenPaths = listWorktreeGraveyardPaths();
224
271
  const worktrees = listAllWorktrees()
225
272
  .filter((wt) => !wt.isBare && !hiddenPaths.has(wt.path) && !isToolInternalWorktree(wt))
226
273
  .map((wt) => ({
227
274
  ...wt,
228
- pending: pendingRemovals?.has(wt.path) ?? false,
229
- removing: pendingRemovals?.has(wt.path) ?? false,
230
275
  }));
231
- if (pendingCreates?.size) {
232
- for (const path of pendingCreates.keys()) {
233
- if (worktrees.some((wt) => wt.path === path))
234
- continue;
235
- worktrees.push({
236
- name: basename(path),
237
- path,
238
- branch: "(creating)",
239
- isBare: false,
240
- createdAt: this.worktreeCreateJob?.path === path
241
- ? new Date(this.worktreeCreateJob.startedAt).toISOString()
242
- : undefined,
243
- pending: true,
244
- pendingAction: "creating",
245
- });
246
- }
247
- }
248
276
  const worktreePaths = new Set(worktrees.map((worktree) => worktree.path));
249
277
  for (const failure of listDashboardOperationFailures()) {
250
278
  if (failure.targetKind !== "worktree" || failure.operation !== "create" || !failure.worktreePath)
@@ -264,147 +292,76 @@ export const persistenceMethods = {
264
292
  sortDesktopWorktrees(worktrees);
265
293
  return worktrees;
266
294
  },
295
+ listProjectedDesktopWorktrees() {
296
+ return this.dashboardPendingActions.applyToWorktrees(this.listDesktopWorktrees());
297
+ },
267
298
  listWorktreeGraveyardEntries() {
268
299
  return listWorktreeGraveyardEntriesImpl();
269
300
  },
270
301
  async graveyardDesktopWorktree(path) {
271
- this.syncSessionsFromState();
272
302
  const mainRepo = findMainRepo();
273
303
  if (path === mainRepo) {
274
304
  throw new Error("Cannot graveyard the main checkout");
275
305
  }
276
- const matching = listAllWorktrees()
277
- .filter((worktree) => !worktree.isBare)
278
- .find((worktree) => worktree.path === path);
306
+ const matching = this.listDesktopWorktrees().find((worktree) => worktree.path === path);
279
307
  if (!matching) {
280
308
  throw new Error(`Worktree "${path}" not found`);
281
309
  }
282
- const worktreeGraveyardEntries = this.listWorktreeGraveyardEntries();
283
- if (worktreeGraveyardEntries.some((entry) => entry.path === path)) {
284
- throw new Error(`Worktree "${matching.name}" is already in the graveyard`);
285
- }
286
- const attachedServices = collectWorktreeServices(this, path);
287
- detachWorktreeServices(this, path);
288
- const liveSessions = this.sessions.filter((session) => this.sessionWorktreePaths.get(session.id) === path && this.isSessionRuntimeLive(session) && !session.exited);
289
- for (const session of liveSessions) {
290
- stopSessionToOffline(this, session);
291
- }
292
- await waitForWorktreeSessionsToStop(this, path);
293
- const attachedAgents = collectWorktreeAgents(this, path);
294
- const nextEntries = [
295
- ...worktreeGraveyardEntries.filter((entry) => entry.path !== path),
296
- {
297
- name: matching.name,
298
- path: matching.path,
299
- branch: matching.branch,
300
- createdAt: matching.createdAt,
301
- graveyardedAt: new Date().toISOString(),
302
- agents: attachedAgents,
303
- services: attachedServices,
304
- },
305
- ];
306
- writeWorktreeGraveyardEntries(nextEntries);
307
- this.worktreeGraveyardEntries = nextEntries;
308
- this.offlineSessions = this.offlineSessions.filter((session) => session.worktreePath !== path);
309
- this.saveState();
310
- this.invalidateDesktopStateSnapshot();
311
- this.refreshLocalDashboardModel();
310
+ const attachedSession = this.sessions?.find((session) => this.sessionWorktreePaths?.get(session.id) === path && this.isSessionRuntimeLive?.(session));
311
+ if (attachedSession) {
312
+ throw new Error(`Cannot graveyard "${matching.name}" while agent "${attachedSession.label || attachedSession.id}" is attached`);
313
+ }
314
+ stopWorktreeServicesForGraveyard(this, path);
315
+ upsertTopologyWorktree({
316
+ path,
317
+ name: matching.name,
318
+ branch: matching.branch,
319
+ createdAt: matching.createdAt,
320
+ }, "active");
321
+ const moved = moveTopologyWorktreeToGraveyard(path, { reason: "user-requested" });
322
+ if (!moved) {
323
+ throw new Error(`Unable to graveyard worktree "${path}"`);
324
+ }
325
+ this.saveState?.();
326
+ this.invalidateDesktopStateSnapshot?.();
327
+ this.refreshLocalDashboardModel?.();
312
328
  this.metadataServer?.notifyChange?.();
313
- if (this.mode === "dashboard") {
314
- this.renderDashboard();
315
- }
316
329
  return { path, status: "graveyarded" };
317
330
  },
318
331
  async resurrectGraveyardWorktree(path) {
319
- const entries = this.listWorktreeGraveyardEntries();
320
- const entry = entries.find((candidate) => candidate.path === path);
321
- if (!entry) {
322
- throw new Error(`Graveyard worktree "${path}" not found`);
323
- }
324
- const nextEntries = entries.filter((candidate) => candidate.path !== path);
325
- writeWorktreeGraveyardEntries(nextEntries);
326
- this.worktreeGraveyardEntries = nextEntries;
327
- const flatAgents = takeFlatGraveyardAgentsForWorktree(path);
328
- const seen = new Set(this.offlineSessions.map((session) => session.id));
329
- for (const agent of [...entry.agents, ...flatAgents]) {
330
- if (seen.has(agent.id))
331
- continue;
332
- this.offlineSessions.push(agent);
333
- seen.add(agent.id);
332
+ if (!existsSync(path)) {
333
+ throw new Error(`Cannot resurrect worktree "${path}" because the checkout is missing`);
334
334
  }
335
- const serviceSeen = new Set(this.offlineServices.map((service) => service.id));
336
- for (const service of entry.services ?? []) {
337
- if (serviceSeen.has(service.id))
338
- continue;
339
- this.offlineServices.push(service);
340
- serviceSeen.add(service.id);
335
+ const resurrected = resurrectTopologyWorktreeFromGraveyard(path);
336
+ if (!resurrected) {
337
+ throw new Error(`Graveyard worktree "${path}" not found`);
341
338
  }
342
- this.saveState();
343
- this.invalidateDesktopStateSnapshot();
344
- this.refreshLocalDashboardModel();
339
+ this.invalidateDesktopStateSnapshot?.();
340
+ this.refreshLocalDashboardModel?.();
345
341
  this.metadataServer?.notifyChange?.();
346
- if (this.mode === "dashboard") {
347
- this.renderDashboard();
348
- }
349
- return { path, status: "offline" };
342
+ return { path, status: "active" };
350
343
  },
351
344
  async deleteGraveyardWorktree(path) {
352
- const entries = this.listWorktreeGraveyardEntries();
353
- const entry = entries.find((candidate) => candidate.path === path);
354
- if (!entry) {
345
+ const existing = listTopologyWorktreeGraveyard().find((entry) => entry.path === path);
346
+ if (!existing) {
355
347
  throw new Error(`Graveyard worktree "${path}" not found`);
356
348
  }
357
349
  const mainRepo = findMainRepo();
358
- if (path !== mainRepo) {
359
- if (!existsSync(path)) {
360
- await removeOrphanedDesktopWorktree(this, mainRepo, path);
361
- }
362
- else {
363
- await new Promise((resolve, reject) => {
364
- let stderr = "";
365
- let child;
366
- try {
367
- child = spawn("git", ["worktree", "remove", path, "--force"], {
368
- cwd: mainRepo,
369
- stdio: ["ignore", "ignore", "pipe"],
370
- });
371
- }
372
- catch (error) {
373
- reject(error);
374
- return;
375
- }
376
- child.stderr.on("data", (chunk) => {
377
- stderr += chunk.toString();
378
- });
379
- child.on("error", reject);
380
- child.on("close", (code) => {
381
- if (code === 0) {
382
- resolve();
383
- return;
384
- }
385
- const detail = stderr
386
- .split("\n")
387
- .map((line) => line.trim())
388
- .filter(Boolean)
389
- .at(-1);
390
- reject(new Error(detail || `git worktree remove exited with code ${code ?? 1}`));
391
- });
392
- });
393
- }
350
+ if (path === mainRepo) {
351
+ throw new Error("Cannot remove the main checkout");
394
352
  }
395
- const nextEntries = entries.filter((candidate) => candidate.path !== path);
396
- writeWorktreeGraveyardEntries(nextEntries);
397
- this.worktreeGraveyardEntries = nextEntries;
398
- this.offlineSessions = this.offlineSessions.filter((session) => session.worktreePath !== path);
399
- this.offlineServices = this.offlineServices.filter((service) => service.worktreePath !== path);
400
- removeFlatGraveyardAgentsForWorktree(path);
401
- this.saveState();
402
- this.invalidateDesktopStateSnapshot();
403
- this.refreshLocalDashboardModel();
404
- this.metadataServer?.notifyChange?.();
405
- if (this.mode === "dashboard") {
406
- this.renderDashboard();
353
+ if (existsSync(path)) {
354
+ await removeGraveyardedDesktopWorktree(this, mainRepo, path);
407
355
  }
356
+ else {
357
+ removeWorktreeDependents(this, path);
358
+ removeTopologyWorktree(path);
359
+ this.saveState?.();
360
+ }
361
+ deleteTopologyWorktreeGraveyardEntry(path);
362
+ this.invalidateDesktopStateSnapshot?.();
363
+ this.refreshLocalDashboardModel?.();
364
+ this.metadataServer?.notifyChange?.();
408
365
  return { path, status: "removed" };
409
366
  },
410
367
  createDesktopWorktree(name) {
@@ -425,6 +382,18 @@ export const persistenceMethods = {
425
382
  rejectCreate = reject;
426
383
  });
427
384
  pendingCreates.set(targetPath, createPromise);
385
+ const createStartedAt = Date.now();
386
+ this.worktreeCreateJob = {
387
+ path: targetPath,
388
+ name,
389
+ startedAt: createStartedAt,
390
+ };
391
+ upsertTopologyWorktree({
392
+ path: targetPath,
393
+ name,
394
+ branch: name,
395
+ createdAt: new Date(createStartedAt).toISOString(),
396
+ }, "creating");
428
397
  const clearPendingCreate = () => {
429
398
  if (pendingCreates.get(targetPath) === createPromise) {
430
399
  pendingCreates.delete(targetPath);
@@ -433,7 +402,17 @@ export const persistenceMethods = {
433
402
  this.worktreeCreateJob = null;
434
403
  }
435
404
  };
436
- this.dashboardPendingActions.set(DashboardPendingActions.worktreeKey(targetPath), "creating", {
405
+ this.dashboardPendingActions.setWorktreeAction(targetPath, "creating", {
406
+ worktreeSeed: {
407
+ name,
408
+ branch: name,
409
+ path: targetPath,
410
+ createdAt: new Date(createStartedAt).toISOString(),
411
+ status: "offline",
412
+ isBare: false,
413
+ sessions: [],
414
+ services: [],
415
+ },
437
416
  timeoutMs: 180_000,
438
417
  onTimeout: () => {
439
418
  clearPendingCreate();
@@ -455,11 +434,6 @@ export const persistenceMethods = {
455
434
  }
456
435
  },
457
436
  });
458
- this.worktreeCreateJob = {
459
- path: targetPath,
460
- name,
461
- startedAt: Date.now(),
462
- };
463
437
  this.invalidateDesktopStateSnapshot();
464
438
  this.refreshLocalDashboardModel();
465
439
  if (this.mode === "dashboard") {
@@ -498,9 +472,23 @@ export const persistenceMethods = {
498
472
  reject(new Error(detail || `git worktree add exited with code ${code ?? 1}`));
499
473
  });
500
474
  });
475
+ upsertTopologyWorktree({
476
+ path: targetPath,
477
+ name,
478
+ branch: name,
479
+ basePath: mainRepo,
480
+ createdAt: new Date(createStartedAt).toISOString(),
481
+ }, "active");
501
482
  resolveCreate({ path: targetPath, status: "created" });
502
483
  }
503
484
  catch (error) {
485
+ upsertTopologyWorktree({
486
+ path: targetPath,
487
+ name,
488
+ branch: name,
489
+ createdAt: new Date(createStartedAt).toISOString(),
490
+ operationFailure: error instanceof Error ? error.message : String(error),
491
+ }, "error");
504
492
  rejectCreate(error);
505
493
  }
506
494
  })();
@@ -528,7 +516,7 @@ export const persistenceMethods = {
528
516
  });
529
517
  const finalizeCreate = () => {
530
518
  clearPendingCreate();
531
- this.dashboardPendingActions.set(DashboardPendingActions.worktreeKey(targetPath), null);
519
+ this.dashboardPendingActions.clearWorktreeAction(targetPath);
532
520
  this.invalidateDesktopStateSnapshot();
533
521
  this.refreshLocalDashboardModel();
534
522
  this.metadataServer?.notifyChange?.();
@@ -541,8 +529,30 @@ export const persistenceMethods = {
541
529
  },
542
530
  async removeDesktopWorktree(path) {
543
531
  const pendingRemovals = this.pendingWorktreeRemovals;
532
+ const setRemovePending = () => {
533
+ this.dashboardPendingActions.setWorktreeAction(path, "removing", {
534
+ timeoutMs: 180_000,
535
+ onTimeout: () => {
536
+ const name = path.split("/").pop() ?? path;
537
+ const message = `Timed out removing worktree "${name}"`;
538
+ recordDashboardFailure(this, {
539
+ targetKind: "worktree",
540
+ operation: "remove",
541
+ title: `Failed to remove worktree "${name}"`,
542
+ message,
543
+ worktreePath: path,
544
+ worktreeName: name,
545
+ });
546
+ this.footerFlash = message;
547
+ this.footerFlashTicks = 5;
548
+ refreshDashboardWorktreeProjection(this);
549
+ },
550
+ });
551
+ refreshDashboardWorktreeProjection(this);
552
+ };
544
553
  const existingRemoval = pendingRemovals.get(path);
545
554
  if (existingRemoval) {
555
+ setRemovePending();
546
556
  return existingRemoval;
547
557
  }
548
558
  let resolveRemoval;
@@ -552,42 +562,18 @@ export const persistenceMethods = {
552
562
  rejectRemoval = reject;
553
563
  });
554
564
  pendingRemovals.set(path, removalPromise);
555
- this.dashboardPendingActions.set(DashboardPendingActions.worktreeKey(path), "removing", {
556
- timeoutMs: 180_000,
557
- onTimeout: () => {
558
- const name = path.split("/").pop() ?? path;
559
- const message = `Timed out removing worktree "${name}"`;
560
- recordDashboardFailure(this, {
561
- targetKind: "worktree",
562
- operation: "remove",
563
- title: `Failed to remove worktree "${name}"`,
564
- message,
565
- worktreePath: path,
566
- worktreeName: name,
567
- });
568
- this.footerFlash = message;
569
- this.footerFlashTicks = 5;
570
- this.invalidateDesktopStateSnapshot();
571
- this.refreshLocalDashboardModel();
572
- if (this.mode === "dashboard") {
573
- this.renderDashboard();
574
- }
575
- },
576
- });
577
- this.invalidateDesktopStateSnapshot();
578
- this.refreshLocalDashboardModel();
579
- if (this.mode === "dashboard") {
580
- this.renderDashboard();
581
- }
565
+ setRemovePending();
566
+ let startedRemoval = false;
582
567
  void (async () => {
583
568
  try {
584
- this.syncSessionsFromState();
569
+ this.syncSessionsFromTopology();
585
570
  const mainRepo = findMainRepo();
586
571
  if (path === mainRepo) {
587
572
  throw new Error("Cannot remove the main checkout");
588
573
  }
589
574
  if (!existsSync(path)) {
590
575
  await removeOrphanedDesktopWorktree(this, mainRepo, path);
576
+ removeTopologyWorktree(path);
591
577
  clearDashboardOperationFailures({ targetKind: "worktree", operation: "remove", worktreePath: path });
592
578
  resolveRemoval({ path, status: "removed" });
593
579
  return;
@@ -597,16 +583,24 @@ export const persistenceMethods = {
597
583
  const worktreeBaseDir = getWorktreeBaseDir();
598
584
  if (path.startsWith(`${worktreeBaseDir}/`) || path === worktreeBaseDir) {
599
585
  await removeOrphanedDesktopWorktree(this, mainRepo, path);
586
+ removeTopologyWorktree(path);
600
587
  clearDashboardOperationFailures({ targetKind: "worktree", operation: "remove", worktreePath: path });
601
588
  resolveRemoval({ path, status: "removed" });
602
589
  return;
603
590
  }
604
591
  throw new Error(`Worktree "${path}" not found`);
605
592
  }
606
- const attachedSession = this.sessions.find((session) => session.worktreePath === path && this.isSessionRuntimeLive(session));
593
+ const attachedSession = this.sessions.find((session) => this.sessionWorktreePaths?.get(session.id) === path && this.isSessionRuntimeLive(session));
607
594
  if (attachedSession) {
608
595
  throw new Error(`Cannot remove "${matching.name}" while agent "${attachedSession.label || attachedSession.id}" is attached`);
609
596
  }
597
+ upsertTopologyWorktree({
598
+ path,
599
+ name: matching.name,
600
+ branch: matching.branch,
601
+ createdAt: matching.createdAt,
602
+ }, "removing");
603
+ startedRemoval = true;
610
604
  detachWorktreeServices(this, path);
611
605
  await new Promise((resolve, reject) => {
612
606
  let stderr = "";
@@ -645,14 +639,20 @@ export const persistenceMethods = {
645
639
  reject(new Error(detail || `git worktree remove exited with code ${code ?? 1}`));
646
640
  });
647
641
  });
648
- this.offlineSessions = this.offlineSessions.filter((session) => session.worktreePath !== path);
649
- this.offlineServices = this.offlineServices.filter((service) => service.worktreePath !== path);
650
- removeFlatGraveyardAgentsForWorktree(path);
642
+ removeWorktreeDependents(this, path);
643
+ removeTopologyWorktree(path);
651
644
  this.saveState();
652
645
  clearDashboardOperationFailures({ targetKind: "worktree", operation: "remove", worktreePath: path });
653
646
  resolveRemoval({ path, status: "removed" });
654
647
  }
655
648
  catch (error) {
649
+ if (startedRemoval) {
650
+ upsertTopologyWorktree({
651
+ path,
652
+ name: path.split("/").pop() ?? path,
653
+ operationFailure: error instanceof Error ? error.message : String(error),
654
+ }, "error");
655
+ }
656
656
  rejectRemoval(error);
657
657
  }
658
658
  })();
@@ -671,55 +671,39 @@ export const persistenceMethods = {
671
671
  this.footerFlashTicks = 5;
672
672
  });
673
673
  const finalizeRemoval = () => {
674
- this.dashboardPendingActions.set(DashboardPendingActions.worktreeKey(path), null);
674
+ this.dashboardPendingActions.clearWorktreeAction(path);
675
675
  pendingRemovals.delete(path);
676
- this.invalidateDesktopStateSnapshot();
677
- this.refreshLocalDashboardModel();
678
- this.metadataServer?.notifyChange?.();
679
- if (this.mode === "dashboard") {
680
- this.renderDashboard();
681
- }
676
+ refreshDashboardWorktreeProjection(this);
682
677
  };
683
678
  void removalPromise.then(finalizeRemoval, finalizeRemoval);
684
679
  return removalPromise;
685
680
  },
686
681
  listGraveyardEntries() {
687
- try {
688
- const content = readFileSync(getGraveyardPath(), "utf-8");
689
- return JSON.parse(content);
690
- }
691
- catch {
692
- return [];
693
- }
682
+ return listTopologySessionStates({ statuses: ["graveyard"] });
694
683
  },
695
684
  async resurrectGraveyardSession(sessionId) {
696
- this.loadOfflineSessions();
697
- const graveyardEntries = this.listGraveyardEntries();
698
- const entry = graveyardEntries.find((candidate) => candidate.id === sessionId);
699
- if (!entry) {
685
+ const restored = resurrectTopologySession(sessionId);
686
+ if (!restored) {
700
687
  throw new Error(`Graveyard session "${sessionId}" not found`);
701
688
  }
702
- const nextGraveyard = graveyardEntries.filter((candidate) => candidate.id !== sessionId);
703
- writeFileSync(getGraveyardPath(), JSON.stringify(nextGraveyard, null, 2) + "\n");
704
- this.offlineSessions.push(entry);
705
- const statePath = getStatePath();
706
- try {
707
- let state = { savedAt: new Date().toISOString(), cwd: process.cwd(), sessions: [] };
708
- if (existsSync(statePath)) {
709
- state = JSON.parse(readFileSync(statePath, "utf-8"));
689
+ const offlineEntry = { ...restored, lifecycle: "offline", status: "offline" };
690
+ if (Array.isArray(this.offlineSessions)) {
691
+ const existingIndex = this.offlineSessions.findIndex((session) => session.id === sessionId);
692
+ if (existingIndex >= 0) {
693
+ this.offlineSessions[existingIndex] = { ...this.offlineSessions[existingIndex], ...offlineEntry };
694
+ }
695
+ else {
696
+ this.offlineSessions.push(offlineEntry);
710
697
  }
711
- state.sessions.push(entry);
712
- writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
713
698
  }
714
- catch { }
715
- debug(`resurrected ${entry.id} from graveyard`, "session");
716
- return { sessionId, status: "offline" };
717
- },
718
- removeSessionsFile() {
719
- try {
720
- unlinkSync(`${getLocalAimuxDir()}/sessions.json`);
699
+ this.loadOfflineTopologySessions?.();
700
+ this.invalidateDesktopStateSnapshot?.();
701
+ this.writeStatuslineFile?.();
702
+ this.metadataServer?.notifyChange?.();
703
+ if (this.mode === "dashboard") {
704
+ this.renderCurrentDashboardView?.();
721
705
  }
722
- catch { }
706
+ return { sessionId, status: "offline" };
723
707
  },
724
708
  stripAnsi(text) {
725
709
  return text.replace(/\u001B\[[0-9;]*m/g, "");
@@ -764,57 +748,53 @@ function sortDesktopWorktrees(worktrees) {
764
748
  return a.name.localeCompare(b.name);
765
749
  });
766
750
  }
767
- function collectWorktreeAgents(host, path) {
768
- const byId = new Map();
769
- for (const session of host.offlineSessions) {
770
- if (session.worktreePath !== path)
771
- continue;
772
- byId.set(session.id, session);
773
- }
774
- return [...byId.values()].sort((a, b) => {
775
- const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
776
- const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
777
- return bTime - aTime || a.id.localeCompare(b.id);
778
- });
779
- }
780
- function collectWorktreeServices(host, path) {
781
- const byId = new Map();
782
- for (const service of host.offlineServices ?? []) {
783
- if (service.worktreePath !== path)
784
- continue;
785
- byId.set(service.id, service);
786
- }
787
- for (const service of host.buildLiveServiceStates?.() ?? []) {
788
- if (service.worktreePath !== path)
789
- continue;
790
- byId.set(service.id, service);
791
- }
792
- return [...byId.values()].sort((a, b) => {
793
- const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
794
- const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
795
- return bTime - aTime || a.id.localeCompare(b.id);
796
- });
797
- }
798
- async function waitForWorktreeSessionsToStop(host, path, timeoutMs = 10_000) {
799
- const deadline = Date.now() + timeoutMs;
800
- while (Date.now() < deadline) {
801
- const remaining = host.sessions.some((session) => host.sessionWorktreePaths.get(session.id) === path && host.isSessionRuntimeLive(session) && !session.exited);
802
- if (!remaining)
803
- return;
804
- await new Promise((resolve) => setTimeout(resolve, 100));
805
- }
806
- throw new Error(`Timed out offlining agents for worktree "${basename(path)}"`);
807
- }
808
751
  async function removeOrphanedDesktopWorktree(host, mainRepo, path) {
809
752
  await pruneGitWorktrees(mainRepo);
810
753
  if (existsSync(path)) {
811
754
  rmSync(path, { recursive: true, force: true });
812
755
  }
813
756
  await pruneGitWorktrees(mainRepo);
814
- host.offlineSessions = host.offlineSessions.filter((session) => session.worktreePath !== path);
815
- host.offlineServices = host.offlineServices.filter((service) => service.worktreePath !== path);
757
+ removeWorktreeDependents(host, path);
816
758
  host.saveState();
817
759
  }
760
+ async function removeGraveyardedDesktopWorktree(host, mainRepo, path) {
761
+ await removeGitWorktreeCheckout(mainRepo, path);
762
+ removeWorktreeDependents(host, path);
763
+ removeTopologyWorktree(path);
764
+ host.saveState?.();
765
+ }
766
+ async function removeGitWorktreeCheckout(mainRepo, path) {
767
+ await new Promise((resolve, reject) => {
768
+ let stderr = "";
769
+ let child;
770
+ try {
771
+ child = spawn("git", ["worktree", "remove", path, "--force"], {
772
+ cwd: mainRepo,
773
+ stdio: ["ignore", "ignore", "pipe"],
774
+ });
775
+ }
776
+ catch (error) {
777
+ reject(error);
778
+ return;
779
+ }
780
+ child.stderr.on("data", (chunk) => {
781
+ stderr += chunk.toString();
782
+ });
783
+ child.on("error", reject);
784
+ child.on("close", (code) => {
785
+ if (code === 0) {
786
+ resolve();
787
+ return;
788
+ }
789
+ const detail = stderr
790
+ .split("\n")
791
+ .map((line) => line.trim())
792
+ .filter(Boolean)
793
+ .at(-1);
794
+ reject(new Error(detail || `git worktree remove exited with code ${code ?? 1}`));
795
+ });
796
+ });
797
+ }
818
798
  function detachWorktreeServices(host, path) {
819
799
  for (const { target, metadata } of host.tmuxRuntimeManager.listProjectManagedWindows(process.cwd())) {
820
800
  if (metadata.kind !== "service" || metadata.worktreePath !== path)
@@ -826,6 +806,44 @@ function detachWorktreeServices(host, path) {
826
806
  catch { }
827
807
  }
828
808
  host.offlineServices = host.offlineServices.filter((service) => service.worktreePath !== path);
809
+ removeTopologyServicesForWorktree(path);
810
+ removePersistedServicesForWorktree(path);
811
+ }
812
+ function stopWorktreeServicesForGraveyard(host, path) {
813
+ for (const { target, metadata } of host.tmuxRuntimeManager.listProjectManagedWindows(process.cwd())) {
814
+ if (metadata.kind !== "service" || metadata.worktreePath !== path)
815
+ continue;
816
+ markLifecycleUsed(host, metadata.sessionId);
817
+ try {
818
+ host.tmuxRuntimeManager.killWindow(target);
819
+ }
820
+ catch { }
821
+ const serviceState = {
822
+ id: metadata.sessionId,
823
+ command: metadata.command,
824
+ args: metadata.args ?? [],
825
+ launchCommandLine: metadata.launchCommandLine,
826
+ worktreePath: metadata.worktreePath,
827
+ cwd: metadata.worktreePath,
828
+ label: metadata.label,
829
+ createdAt: metadata.createdAt,
830
+ };
831
+ upsertTopologyService(serviceState, "stopped");
832
+ host.offlineServices ??= [];
833
+ const existingIndex = host.offlineServices.findIndex((service) => service.id === metadata.sessionId);
834
+ if (existingIndex >= 0) {
835
+ host.offlineServices[existingIndex] = { ...host.offlineServices[existingIndex], ...serviceState };
836
+ }
837
+ else {
838
+ host.offlineServices.push(serviceState);
839
+ }
840
+ }
841
+ }
842
+ function removeWorktreeDependents(host, path) {
843
+ host.offlineSessions = (host.offlineSessions ?? []).filter((session) => session.worktreePath !== path);
844
+ host.offlineServices = (host.offlineServices ?? []).filter((service) => service.worktreePath !== path);
845
+ removeTopologySessionsForWorktree(path);
846
+ removeTopologyServicesForWorktree(path);
829
847
  removePersistedServicesForWorktree(path);
830
848
  }
831
849
  function removePersistedServicesForWorktree(path) {
@@ -856,26 +874,4 @@ function markLifecycleUsed(host, itemId) {
856
874
  }
857
875
  catch { }
858
876
  }
859
- function takeFlatGraveyardAgentsForWorktree(path) {
860
- const graveyardPath = getGraveyardPath();
861
- if (!existsSync(graveyardPath))
862
- return [];
863
- try {
864
- const entries = JSON.parse(readFileSync(graveyardPath, "utf-8"));
865
- if (!Array.isArray(entries))
866
- return [];
867
- const matching = entries.filter((entry) => entry?.worktreePath === path);
868
- const remaining = entries.filter((entry) => entry?.worktreePath !== path);
869
- if (matching.length > 0) {
870
- writeFileSync(graveyardPath, JSON.stringify(remaining, null, 2) + "\n");
871
- }
872
- return matching;
873
- }
874
- catch {
875
- return [];
876
- }
877
- }
878
- function removeFlatGraveyardAgentsForWorktree(path) {
879
- void takeFlatGraveyardAgentsForWorktree(path);
880
- }
881
877
  //# sourceMappingURL=persistence-methods.js.map