@compilr-dev/cli 0.4.0 → 0.5.1

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 (634) hide show
  1. package/README.md +254 -68
  2. package/dist/.tsbuildinfo.app +1 -0
  3. package/dist/.tsbuildinfo.data +1 -0
  4. package/dist/.tsbuildinfo.domain +1 -0
  5. package/dist/.tsbuildinfo.foundation +1 -0
  6. package/dist/agent.d.ts +134 -4
  7. package/dist/agent.js +345 -166
  8. package/dist/anchors/index.d.ts +9 -0
  9. package/dist/anchors/index.js +9 -0
  10. package/dist/anchors/project-anchors.d.ts +79 -0
  11. package/dist/anchors/project-anchors.js +202 -0
  12. package/dist/auth/api-client.d.ts +124 -0
  13. package/dist/auth/api-client.js +261 -0
  14. package/dist/auth/index.d.ts +172 -0
  15. package/dist/auth/index.js +545 -0
  16. package/dist/auth/storage.d.ts +52 -0
  17. package/dist/auth/storage.js +118 -0
  18. package/dist/changelog/index.d.ts +16 -0
  19. package/dist/changelog/index.js +24 -0
  20. package/dist/changelog/releases.d.ts +17 -0
  21. package/dist/changelog/releases.js +63 -0
  22. package/dist/commands/index.d.ts +2 -1
  23. package/dist/commands-v2/handlers/auth.d.ts +10 -0
  24. package/dist/commands-v2/handlers/auth.js +118 -0
  25. package/dist/commands-v2/handlers/background.d.ts +14 -0
  26. package/dist/commands-v2/handlers/background.js +276 -0
  27. package/dist/commands-v2/handlers/context.d.ts +13 -0
  28. package/dist/commands-v2/handlers/context.js +533 -0
  29. package/dist/commands-v2/handlers/core.d.ts +14 -0
  30. package/dist/commands-v2/handlers/core.js +290 -0
  31. package/dist/commands-v2/handlers/debug.d.ts +11 -0
  32. package/dist/commands-v2/handlers/debug.js +177 -0
  33. package/dist/commands-v2/handlers/delegations.d.ts +8 -0
  34. package/dist/commands-v2/handlers/delegations.js +29 -0
  35. package/dist/commands-v2/handlers/files.d.ts +8 -0
  36. package/dist/commands-v2/handlers/files.js +162 -0
  37. package/dist/commands-v2/handlers/filter.d.ts +9 -0
  38. package/dist/commands-v2/handlers/filter.js +130 -0
  39. package/dist/commands-v2/handlers/games.d.ts +7 -0
  40. package/dist/commands-v2/handlers/games.js +57 -0
  41. package/dist/commands-v2/handlers/index.d.ts +25 -0
  42. package/dist/commands-v2/handlers/index.js +63 -0
  43. package/dist/commands-v2/handlers/mcp.d.ts +8 -0
  44. package/dist/commands-v2/handlers/mcp.js +39 -0
  45. package/dist/commands-v2/handlers/notifications.d.ts +9 -0
  46. package/dist/commands-v2/handlers/notifications.js +34 -0
  47. package/dist/commands-v2/handlers/project.d.ts +22 -0
  48. package/dist/commands-v2/handlers/project.js +1035 -0
  49. package/dist/commands-v2/handlers/reset.d.ts +11 -0
  50. package/dist/commands-v2/handlers/reset.js +118 -0
  51. package/dist/commands-v2/handlers/session.d.ts +161 -0
  52. package/dist/commands-v2/handlers/session.js +805 -0
  53. package/dist/commands-v2/handlers/settings.d.ts +17 -0
  54. package/dist/commands-v2/handlers/settings.js +417 -0
  55. package/dist/commands-v2/handlers/tasks.d.ts +5 -0
  56. package/dist/commands-v2/handlers/tasks.js +36 -0
  57. package/dist/commands-v2/handlers/team.d.ts +9 -0
  58. package/dist/commands-v2/handlers/team.js +549 -0
  59. package/dist/commands-v2/handlers/terminals.d.ts +9 -0
  60. package/dist/commands-v2/handlers/terminals.js +34 -0
  61. package/dist/commands-v2/index.d.ts +14 -0
  62. package/dist/commands-v2/index.js +18 -0
  63. package/dist/commands-v2/registry.d.ts +52 -0
  64. package/dist/commands-v2/registry.js +114 -0
  65. package/dist/commands-v2/types.d.ts +153 -0
  66. package/dist/commands-v2/types.js +7 -0
  67. package/dist/commands.js +123 -7
  68. package/dist/compilr-diff-companion.vsix +0 -0
  69. package/dist/db/index.js +98 -4
  70. package/dist/db/repositories/document-repository.d.ts +2 -0
  71. package/dist/db/repositories/document-repository.js +5 -1
  72. package/dist/db/repositories/index.d.ts +2 -0
  73. package/dist/db/repositories/index.js +1 -0
  74. package/dist/db/repositories/plan-repository.d.ts +101 -0
  75. package/dist/db/repositories/plan-repository.js +275 -0
  76. package/dist/db/repositories/project-repository.d.ts +6 -0
  77. package/dist/db/repositories/project-repository.js +41 -0
  78. package/dist/db/repositories/work-item-repository.d.ts +15 -0
  79. package/dist/db/repositories/work-item-repository.js +69 -4
  80. package/dist/db/schema.d.ts +40 -3
  81. package/dist/db/schema.js +66 -3
  82. package/dist/episodes/index.d.ts +20 -0
  83. package/dist/episodes/index.js +27 -0
  84. package/dist/episodes/recorder.d.ts +51 -0
  85. package/dist/episodes/recorder.js +195 -0
  86. package/dist/episodes/significant-work.d.ts +21 -0
  87. package/dist/episodes/significant-work.js +56 -0
  88. package/dist/episodes/store.d.ts +38 -0
  89. package/dist/episodes/store.js +199 -0
  90. package/dist/episodes/types.d.ts +35 -0
  91. package/dist/episodes/types.js +6 -0
  92. package/dist/episodes/work-at-risk.d.ts +12 -0
  93. package/dist/episodes/work-at-risk.js +38 -0
  94. package/dist/episodes/work-summary-anchor.d.ts +23 -0
  95. package/dist/episodes/work-summary-anchor.js +73 -0
  96. package/dist/games/coins.d.ts +66 -0
  97. package/dist/games/coins.js +165 -0
  98. package/dist/games/game-base.d.ts +84 -0
  99. package/dist/games/game-base.js +204 -0
  100. package/dist/games/index.d.ts +16 -0
  101. package/dist/games/index.js +49 -0
  102. package/dist/games/scores.d.ts +69 -0
  103. package/dist/games/scores.js +191 -0
  104. package/dist/games/tetris/board.d.ts +59 -0
  105. package/dist/games/tetris/board.js +170 -0
  106. package/dist/games/tetris/index.d.ts +109 -0
  107. package/dist/games/tetris/index.js +610 -0
  108. package/dist/games/tetris/pieces.d.ts +44 -0
  109. package/dist/games/tetris/pieces.js +271 -0
  110. package/dist/games/tetris/renderer.d.ts +26 -0
  111. package/dist/games/tetris/renderer.js +77 -0
  112. package/dist/guide/guide-content.d.ts +23 -0
  113. package/dist/guide/guide-content.js +196 -0
  114. package/dist/guide/index.d.ts +8 -0
  115. package/dist/guide/index.js +7 -0
  116. package/dist/guide/shared-content.d.ts +37 -0
  117. package/dist/guide/shared-content.js +1272 -0
  118. package/dist/guide/tutorial-helpers.d.ts +57 -0
  119. package/dist/guide/tutorial-helpers.js +147 -0
  120. package/dist/handlers/ask-user-handlers.d.ts +32 -0
  121. package/dist/handlers/ask-user-handlers.js +104 -0
  122. package/dist/handlers/delegation-handlers.d.ts +34 -0
  123. package/dist/handlers/delegation-handlers.js +291 -0
  124. package/dist/handlers/permission-handler.d.ts +30 -0
  125. package/dist/handlers/permission-handler.js +205 -0
  126. package/dist/index.d.ts +11 -1
  127. package/dist/index.js +615 -179
  128. package/dist/input-handlers/index.d.ts +7 -0
  129. package/dist/input-handlers/index.js +7 -0
  130. package/dist/input-handlers/memory-handler.d.ts +26 -0
  131. package/dist/input-handlers/memory-handler.js +69 -0
  132. package/dist/models/index.d.ts +10 -0
  133. package/dist/models/index.js +12 -0
  134. package/dist/models/model-registry.d.ts +38 -0
  135. package/dist/models/model-registry.js +69 -0
  136. package/dist/models/model-tiers.d.ts +28 -0
  137. package/dist/models/model-tiers.js +71 -0
  138. package/dist/models/model-validation.d.ts +25 -0
  139. package/dist/models/model-validation.js +291 -0
  140. package/dist/models/ollama-models.d.ts +73 -0
  141. package/dist/models/ollama-models.js +178 -0
  142. package/dist/models/provider-types.d.ts +6 -0
  143. package/dist/models/provider-types.js +1 -0
  144. package/dist/models/providers.d.ts +35 -0
  145. package/dist/models/providers.js +58 -0
  146. package/dist/models/types.d.ts +4 -0
  147. package/dist/models/types.js +4 -0
  148. package/dist/multi-agent/activity.d.ts +21 -0
  149. package/dist/multi-agent/activity.js +34 -0
  150. package/dist/multi-agent/agent-selection.d.ts +55 -0
  151. package/dist/multi-agent/agent-selection.js +90 -0
  152. package/dist/multi-agent/artifacts.d.ts +197 -0
  153. package/dist/multi-agent/artifacts.js +379 -0
  154. package/dist/multi-agent/checkpointer.d.ts +138 -0
  155. package/dist/multi-agent/checkpointer.js +471 -0
  156. package/dist/multi-agent/collision-utils.d.ts +16 -0
  157. package/dist/multi-agent/collision-utils.js +28 -0
  158. package/dist/multi-agent/context-resolver.d.ts +97 -0
  159. package/dist/multi-agent/context-resolver.js +316 -0
  160. package/dist/multi-agent/custom-agents.d.ts +83 -0
  161. package/dist/multi-agent/custom-agents.js +227 -0
  162. package/dist/multi-agent/delegation-tracker.d.ts +157 -0
  163. package/dist/multi-agent/delegation-tracker.js +243 -0
  164. package/dist/multi-agent/file-lock-hook.d.ts +29 -0
  165. package/dist/multi-agent/file-lock-hook.js +97 -0
  166. package/dist/multi-agent/file-locks.d.ts +58 -0
  167. package/dist/multi-agent/file-locks.js +194 -0
  168. package/dist/multi-agent/index.d.ts +24 -0
  169. package/dist/multi-agent/index.js +30 -0
  170. package/dist/multi-agent/mention-parser.d.ts +64 -0
  171. package/dist/multi-agent/mention-parser.js +146 -0
  172. package/dist/multi-agent/notification-manager.d.ts +84 -0
  173. package/dist/multi-agent/notification-manager.js +224 -0
  174. package/dist/multi-agent/pending-requests.d.ts +122 -0
  175. package/dist/multi-agent/pending-requests.js +155 -0
  176. package/dist/multi-agent/session-registry.d.ts +139 -0
  177. package/dist/multi-agent/session-registry.js +514 -0
  178. package/dist/multi-agent/shared-context.d.ts +293 -0
  179. package/dist/multi-agent/shared-context.js +671 -0
  180. package/dist/multi-agent/skill-requirements.d.ts +66 -0
  181. package/dist/multi-agent/skill-requirements.js +178 -0
  182. package/dist/multi-agent/task-assignment.d.ts +69 -0
  183. package/dist/multi-agent/task-assignment.js +123 -0
  184. package/dist/multi-agent/task-suggestion.d.ts +31 -0
  185. package/dist/multi-agent/task-suggestion.js +72 -0
  186. package/dist/multi-agent/team-agent.d.ts +201 -0
  187. package/dist/multi-agent/team-agent.js +488 -0
  188. package/dist/multi-agent/team.d.ts +286 -0
  189. package/dist/multi-agent/team.js +610 -0
  190. package/dist/multi-agent/tool-config.d.ts +110 -0
  191. package/dist/multi-agent/tool-config.js +661 -0
  192. package/dist/multi-agent/types.d.ts +211 -0
  193. package/dist/multi-agent/types.js +617 -0
  194. package/dist/prompts/plan-mode-prompt.d.ts +11 -0
  195. package/dist/prompts/plan-mode-prompt.js +95 -0
  196. package/dist/repl-helpers.d.ts +63 -0
  197. package/dist/repl-helpers.js +319 -0
  198. package/dist/repl-v2.d.ts +554 -0
  199. package/dist/repl-v2.js +3286 -0
  200. package/dist/session/index.d.ts +6 -0
  201. package/dist/session/index.js +6 -0
  202. package/dist/session/project-session-manager.d.ts +158 -0
  203. package/dist/session/project-session-manager.js +650 -0
  204. package/dist/settings/index.d.ts +156 -13
  205. package/dist/settings/index.js +377 -24
  206. package/dist/settings/mcp-config.d.ts +76 -0
  207. package/dist/settings/mcp-config.js +143 -0
  208. package/dist/settings/paths.d.ts +114 -0
  209. package/dist/settings/paths.js +270 -0
  210. package/dist/shared-handlers.d.ts +62 -0
  211. package/dist/shared-handlers.js +48 -0
  212. package/dist/system-prompt/builder.d.ts +5 -0
  213. package/dist/system-prompt/builder.js +4 -0
  214. package/dist/system-prompt/index.d.ts +18 -0
  215. package/dist/system-prompt/index.js +18 -0
  216. package/dist/system-prompt/modules.d.ts +5 -0
  217. package/dist/system-prompt/modules.js +4 -0
  218. package/dist/tabbed-menu.js +2 -1
  219. package/dist/templates/compilr-md-import.d.ts +16 -0
  220. package/dist/templates/compilr-md-import.js +241 -0
  221. package/dist/templates/compilr-md.js +10 -58
  222. package/dist/templates/config-json.d.ts +1 -25
  223. package/dist/templates/index.d.ts +2 -0
  224. package/dist/templates/index.js +35 -75
  225. package/dist/themes/colors.js +3 -1
  226. package/dist/themes/registry.d.ts +5 -36
  227. package/dist/themes/registry.js +11 -95
  228. package/dist/themes/types.d.ts +3 -38
  229. package/dist/themes/types.js +2 -2
  230. package/dist/tool-names.d.ts +108 -0
  231. package/dist/tool-names.js +227 -0
  232. package/dist/tools/anchor-tools.d.ts +31 -0
  233. package/dist/tools/anchor-tools.js +255 -0
  234. package/dist/tools/artifact-tools.d.ts +42 -0
  235. package/dist/tools/artifact-tools.js +328 -0
  236. package/dist/tools/ask-user-simple.d.ts +1 -1
  237. package/dist/tools/ask-user-simple.js +2 -1
  238. package/dist/tools/ask-user.d.ts +1 -1
  239. package/dist/tools/ask-user.js +2 -1
  240. package/dist/tools/backlog-wrappers.d.ts +56 -0
  241. package/dist/tools/backlog-wrappers.js +353 -0
  242. package/dist/tools/backlog.d.ts +2 -2
  243. package/dist/tools/backlog.js +2 -2
  244. package/dist/tools/db-tools.d.ts +12 -0
  245. package/dist/tools/db-tools.js +14 -0
  246. package/dist/tools/delegate-background.d.ts +27 -0
  247. package/dist/tools/delegate-background.js +115 -0
  248. package/dist/tools/delegate.d.ts +22 -0
  249. package/dist/tools/delegate.js +97 -0
  250. package/dist/tools/delegation-status.d.ts +16 -0
  251. package/dist/tools/delegation-status.js +128 -0
  252. package/dist/tools/document-db.d.ts +43 -0
  253. package/dist/tools/document-db.js +220 -0
  254. package/dist/tools/guide-tool.d.ts +12 -0
  255. package/dist/tools/guide-tool.js +59 -0
  256. package/dist/tools/handoff.d.ts +25 -0
  257. package/dist/tools/handoff.js +99 -0
  258. package/dist/tools/meta-tools.d.ts +26 -0
  259. package/dist/tools/meta-tools.js +47 -0
  260. package/dist/tools/plan-tools.d.ts +54 -0
  261. package/dist/tools/plan-tools.js +338 -0
  262. package/dist/tools/platform-adapter.d.ts +29 -0
  263. package/dist/tools/platform-adapter.js +394 -0
  264. package/dist/tools/project-db.d.ts +34 -0
  265. package/dist/tools/project-db.js +39 -0
  266. package/dist/tools/recall-work-tool.d.ts +18 -0
  267. package/dist/tools/recall-work-tool.js +82 -0
  268. package/dist/tools/workitem-db.d.ts +135 -0
  269. package/dist/tools/workitem-db.js +730 -0
  270. package/dist/tools.d.ts +67 -2
  271. package/dist/tools.js +238 -38
  272. package/dist/ui/ask-user-overlay.d.ts +2 -2
  273. package/dist/ui/ask-user-overlay.js +443 -535
  274. package/dist/ui/ask-user-simple-overlay.d.ts +2 -2
  275. package/dist/ui/ask-user-simple-overlay.js +182 -209
  276. package/dist/ui/autocomplete-controller.d.ts +42 -0
  277. package/dist/ui/autocomplete-controller.js +384 -0
  278. package/dist/ui/base/index.d.ts +26 -0
  279. package/dist/ui/base/index.js +33 -0
  280. package/dist/ui/base/inline-overlay-utils.d.ts +217 -0
  281. package/dist/ui/base/inline-overlay-utils.js +320 -0
  282. package/dist/ui/base/inline-overlay.d.ts +159 -0
  283. package/dist/ui/base/inline-overlay.js +257 -0
  284. package/dist/ui/base/key-utils.d.ts +15 -0
  285. package/dist/ui/base/key-utils.js +30 -0
  286. package/dist/ui/base/overlay-base-v2.d.ts +203 -0
  287. package/dist/ui/base/overlay-base-v2.js +260 -0
  288. package/dist/ui/base/overlay-base.d.ts +156 -0
  289. package/dist/ui/base/overlay-base.js +238 -0
  290. package/dist/ui/base/overlay-lifecycle.d.ts +65 -0
  291. package/dist/ui/base/overlay-lifecycle.js +159 -0
  292. package/dist/ui/base/overlay-types.d.ts +185 -0
  293. package/dist/ui/base/overlay-types.js +7 -0
  294. package/dist/ui/base/render-utils.d.ts +27 -0
  295. package/dist/ui/base/render-utils.js +36 -0
  296. package/dist/ui/base/screen-stack.d.ts +148 -0
  297. package/dist/ui/base/screen-stack.js +184 -0
  298. package/dist/ui/base/tabbed-list-overlay-v2.d.ts +118 -0
  299. package/dist/ui/base/tabbed-list-overlay-v2.js +335 -0
  300. package/dist/ui/base/tabbed-list-overlay.d.ts +153 -0
  301. package/dist/ui/base/tabbed-list-overlay.js +369 -0
  302. package/dist/ui/constants/labels.d.ts +14 -0
  303. package/dist/ui/constants/labels.js +51 -0
  304. package/dist/ui/conversation-store.d.ts +55 -0
  305. package/dist/ui/conversation-store.js +107 -0
  306. package/dist/ui/conversation.d.ts +75 -4
  307. package/dist/ui/conversation.js +376 -165
  308. package/dist/ui/diff.d.ts +7 -1
  309. package/dist/ui/diff.js +85 -48
  310. package/dist/ui/ephemeral.d.ts +1 -1
  311. package/dist/ui/ephemeral.js +4 -10
  312. package/dist/ui/features/index.d.ts +34 -0
  313. package/dist/ui/features/index.js +34 -0
  314. package/dist/ui/features/input-feature.d.ts +85 -0
  315. package/dist/ui/features/input-feature.js +238 -0
  316. package/dist/ui/features/list-feature.d.ts +155 -0
  317. package/dist/ui/features/list-feature.js +244 -0
  318. package/dist/ui/features/pagination-feature.d.ts +154 -0
  319. package/dist/ui/features/pagination-feature.js +238 -0
  320. package/dist/ui/features/search-feature.d.ts +148 -0
  321. package/dist/ui/features/search-feature.js +185 -0
  322. package/dist/ui/features/tab-feature.d.ts +194 -0
  323. package/dist/ui/features/tab-feature.js +307 -0
  324. package/dist/ui/file-autocomplete.d.ts +24 -0
  325. package/dist/ui/file-autocomplete.js +56 -0
  326. package/dist/ui/footer-renderer.d.ts +69 -0
  327. package/dist/ui/footer-renderer.js +431 -0
  328. package/dist/ui/footer.d.ts +181 -7
  329. package/dist/ui/footer.js +523 -74
  330. package/dist/ui/guardrail-overlay.d.ts +29 -0
  331. package/dist/ui/guardrail-overlay.js +145 -0
  332. package/dist/ui/index.d.ts +1 -1
  333. package/dist/ui/index.js +1 -3
  334. package/dist/ui/input-controller.d.ts +51 -0
  335. package/dist/ui/input-controller.js +176 -0
  336. package/dist/ui/input-prompt.d.ts +135 -33
  337. package/dist/ui/input-prompt.js +728 -337
  338. package/dist/ui/iteration-limit-overlay.d.ts +2 -2
  339. package/dist/ui/iteration-limit-overlay.js +92 -128
  340. package/dist/ui/keyboard-handler.d.ts +57 -0
  341. package/dist/ui/keyboard-handler.js +557 -0
  342. package/dist/ui/keys-overlay.d.ts +1 -0
  343. package/dist/ui/keys-overlay.js +203 -141
  344. package/dist/ui/line-utils.d.ts +88 -0
  345. package/dist/ui/line-utils.js +150 -0
  346. package/dist/ui/live-region-facade.d.ts +42 -0
  347. package/dist/ui/live-region-facade.js +205 -0
  348. package/dist/ui/live-region.d.ts +157 -0
  349. package/dist/ui/live-region.js +379 -0
  350. package/dist/ui/mascot/expressions.d.ts +32 -0
  351. package/dist/ui/mascot/expressions.js +213 -0
  352. package/dist/ui/mascot/index.d.ts +8 -0
  353. package/dist/ui/mascot/index.js +8 -0
  354. package/dist/ui/mascot/renderer.d.ts +19 -0
  355. package/dist/ui/mascot/renderer.js +132 -0
  356. package/dist/ui/overlay/data/tutorial-content.d.ts +9 -0
  357. package/dist/ui/overlay/data/tutorial-content.js +9 -0
  358. package/dist/ui/overlay/data/tutorial-registry.d.ts +12 -0
  359. package/dist/ui/overlay/data/tutorial-registry.js +116 -0
  360. package/dist/ui/overlay/data/tutorial-types.d.ts +35 -0
  361. package/dist/ui/overlay/data/tutorial-types.js +6 -0
  362. package/dist/ui/overlay/data/tutorials/basics/first-conversation.d.ts +7 -0
  363. package/dist/ui/overlay/data/tutorials/basics/first-conversation.js +220 -0
  364. package/dist/ui/overlay/data/tutorials/basics/first-project.d.ts +7 -0
  365. package/dist/ui/overlay/data/tutorials/basics/first-project.js +284 -0
  366. package/dist/ui/overlay/data/tutorials/basics/navigation.d.ts +8 -0
  367. package/dist/ui/overlay/data/tutorials/basics/navigation.js +22 -0
  368. package/dist/ui/overlay/data/tutorials/basics/welcome.d.ts +7 -0
  369. package/dist/ui/overlay/data/tutorials/basics/welcome.js +174 -0
  370. package/dist/ui/overlay/data/tutorials/config/context-management.d.ts +7 -0
  371. package/dist/ui/overlay/data/tutorials/config/context-management.js +158 -0
  372. package/dist/ui/overlay/data/tutorials/config/mcp-servers.d.ts +8 -0
  373. package/dist/ui/overlay/data/tutorials/config/mcp-servers.js +155 -0
  374. package/dist/ui/overlay/data/tutorials/config/model-selection.d.ts +7 -0
  375. package/dist/ui/overlay/data/tutorials/config/model-selection.js +162 -0
  376. package/dist/ui/overlay/data/tutorials/config/permissions-safety.d.ts +7 -0
  377. package/dist/ui/overlay/data/tutorials/config/permissions-safety.js +163 -0
  378. package/dist/ui/overlay/data/tutorials/config/settings-config.d.ts +7 -0
  379. package/dist/ui/overlay/data/tutorials/config/settings-config.js +166 -0
  380. package/dist/ui/overlay/data/tutorials/planning/arch.d.ts +7 -0
  381. package/dist/ui/overlay/data/tutorials/planning/arch.js +168 -0
  382. package/dist/ui/overlay/data/tutorials/planning/backlog.d.ts +7 -0
  383. package/dist/ui/overlay/data/tutorials/planning/backlog.js +103 -0
  384. package/dist/ui/overlay/data/tutorials/planning/build.d.ts +7 -0
  385. package/dist/ui/overlay/data/tutorials/planning/build.js +173 -0
  386. package/dist/ui/overlay/data/tutorials/planning/design.d.ts +7 -0
  387. package/dist/ui/overlay/data/tutorials/planning/design.js +205 -0
  388. package/dist/ui/overlay/data/tutorials/planning/docs.d.ts +7 -0
  389. package/dist/ui/overlay/data/tutorials/planning/docs.js +143 -0
  390. package/dist/ui/overlay/data/tutorials/planning/prd.d.ts +7 -0
  391. package/dist/ui/overlay/data/tutorials/planning/prd.js +173 -0
  392. package/dist/ui/overlay/data/tutorials/planning/scaffold.d.ts +7 -0
  393. package/dist/ui/overlay/data/tutorials/planning/scaffold.js +164 -0
  394. package/dist/ui/overlay/data/tutorials/planning/sketch.d.ts +7 -0
  395. package/dist/ui/overlay/data/tutorials/planning/sketch.js +58 -0
  396. package/dist/ui/overlay/data/tutorials/projects/anchors.d.ts +7 -0
  397. package/dist/ui/overlay/data/tutorials/projects/anchors.js +248 -0
  398. package/dist/ui/overlay/data/tutorials/projects/import-project.d.ts +7 -0
  399. package/dist/ui/overlay/data/tutorials/projects/import-project.js +172 -0
  400. package/dist/ui/overlay/data/tutorials/projects/managing-projects.d.ts +8 -0
  401. package/dist/ui/overlay/data/tutorials/projects/managing-projects.js +212 -0
  402. package/dist/ui/overlay/data/tutorials/projects/new-project.d.ts +7 -0
  403. package/dist/ui/overlay/data/tutorials/projects/new-project.js +251 -0
  404. package/dist/ui/overlay/data/tutorials/projects/session-management.d.ts +7 -0
  405. package/dist/ui/overlay/data/tutorials/projects/session-management.js +169 -0
  406. package/dist/ui/overlay/data/tutorials/teams/background-execution.d.ts +7 -0
  407. package/dist/ui/overlay/data/tutorials/teams/background-execution.js +171 -0
  408. package/dist/ui/overlay/data/tutorials/teams/multi-terminal.d.ts +8 -0
  409. package/dist/ui/overlay/data/tutorials/teams/multi-terminal.js +147 -0
  410. package/dist/ui/overlay/data/tutorials/teams/task-assignment.d.ts +7 -0
  411. package/dist/ui/overlay/data/tutorials/teams/task-assignment.js +204 -0
  412. package/dist/ui/overlay/data/tutorials/teams/team-overview.d.ts +7 -0
  413. package/dist/ui/overlay/data/tutorials/teams/team-overview.js +165 -0
  414. package/dist/ui/overlay/data/tutorials/teams/working-with-agents.d.ts +7 -0
  415. package/dist/ui/overlay/data/tutorials/teams/working-with-agents.js +172 -0
  416. package/dist/ui/overlay/impl/agents-overlay-v2.d.ts +45 -0
  417. package/dist/ui/overlay/impl/agents-overlay-v2.js +814 -0
  418. package/dist/ui/overlay/impl/anchors-overlay-v2.d.ts +47 -0
  419. package/dist/ui/overlay/impl/anchors-overlay-v2.js +749 -0
  420. package/dist/ui/overlay/impl/arch-type-overlay-v2.d.ts +37 -0
  421. package/dist/ui/overlay/impl/arch-type-overlay-v2.js +240 -0
  422. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.d.ts +43 -0
  423. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +232 -0
  424. package/dist/ui/overlay/impl/artifact-overlay-v2.d.ts +40 -0
  425. package/dist/ui/overlay/impl/artifact-overlay-v2.js +115 -0
  426. package/dist/ui/overlay/impl/ask-user-overlay-v2.d.ts +72 -0
  427. package/dist/ui/overlay/impl/ask-user-overlay-v2.js +581 -0
  428. package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.d.ts +46 -0
  429. package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.js +204 -0
  430. package/dist/ui/overlay/impl/background-overlay-v2.d.ts +40 -0
  431. package/dist/ui/overlay/impl/background-overlay-v2.js +147 -0
  432. package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +52 -0
  433. package/dist/ui/overlay/impl/backlog-overlay-v2.js +681 -0
  434. package/dist/ui/overlay/impl/changelog-overlay-v2.d.ts +44 -0
  435. package/dist/ui/overlay/impl/changelog-overlay-v2.js +165 -0
  436. package/dist/ui/overlay/impl/commands-overlay-v2.d.ts +33 -0
  437. package/dist/ui/overlay/impl/commands-overlay-v2.js +439 -0
  438. package/dist/ui/overlay/impl/config-overlay-v2.d.ts +111 -0
  439. package/dist/ui/overlay/impl/config-overlay-v2.js +718 -0
  440. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +83 -0
  441. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +711 -0
  442. package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +57 -0
  443. package/dist/ui/overlay/impl/dashboard-overlay-v2.js +382 -0
  444. package/dist/ui/overlay/impl/delegations-overlay-v2.d.ts +28 -0
  445. package/dist/ui/overlay/impl/delegations-overlay-v2.js +279 -0
  446. package/dist/ui/overlay/impl/docs-overlay-v2.d.ts +45 -0
  447. package/dist/ui/overlay/impl/docs-overlay-v2.js +117 -0
  448. package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +84 -0
  449. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1112 -0
  450. package/dist/ui/overlay/impl/filter-overlay-v2.d.ts +41 -0
  451. package/dist/ui/overlay/impl/filter-overlay-v2.js +110 -0
  452. package/dist/ui/overlay/impl/games-overlay-v2.d.ts +31 -0
  453. package/dist/ui/overlay/impl/games-overlay-v2.js +135 -0
  454. package/dist/ui/overlay/impl/guardrail-overlay-v2.d.ts +43 -0
  455. package/dist/ui/overlay/impl/guardrail-overlay-v2.js +114 -0
  456. package/dist/ui/overlay/impl/help-overlay-v2.d.ts +57 -0
  457. package/dist/ui/overlay/impl/help-overlay-v2.js +287 -0
  458. package/dist/ui/overlay/impl/init-setup-overlay-v2.d.ts +25 -0
  459. package/dist/ui/overlay/impl/init-setup-overlay-v2.js +97 -0
  460. package/dist/ui/overlay/impl/iteration-limit-overlay-v2.d.ts +35 -0
  461. package/dist/ui/overlay/impl/iteration-limit-overlay-v2.js +105 -0
  462. package/dist/ui/overlay/impl/keys-overlay-v2.d.ts +41 -0
  463. package/dist/ui/overlay/impl/keys-overlay-v2.js +248 -0
  464. package/dist/ui/overlay/impl/login-overlay-v2.d.ts +49 -0
  465. package/dist/ui/overlay/impl/login-overlay-v2.js +277 -0
  466. package/dist/ui/overlay/impl/mascot-overlay-v2.d.ts +41 -0
  467. package/dist/ui/overlay/impl/mascot-overlay-v2.js +138 -0
  468. package/dist/ui/overlay/impl/mcp-overlay-v2.d.ts +63 -0
  469. package/dist/ui/overlay/impl/mcp-overlay-v2.js +907 -0
  470. package/dist/ui/overlay/impl/model-overlay-v2.d.ts +93 -0
  471. package/dist/ui/overlay/impl/model-overlay-v2.js +1143 -0
  472. package/dist/ui/overlay/impl/model-warning-overlay-v2.d.ts +46 -0
  473. package/dist/ui/overlay/impl/model-warning-overlay-v2.js +132 -0
  474. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +108 -0
  475. package/dist/ui/overlay/impl/new-overlay-v2.js +1243 -0
  476. package/dist/ui/overlay/impl/notifications-overlay-v2.d.ts +20 -0
  477. package/dist/ui/overlay/impl/notifications-overlay-v2.js +116 -0
  478. package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.d.ts +76 -0
  479. package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.js +728 -0
  480. package/dist/ui/overlay/impl/pending-overlay-v2.d.ts +51 -0
  481. package/dist/ui/overlay/impl/pending-overlay-v2.js +445 -0
  482. package/dist/ui/overlay/impl/permission-overlay-v2.d.ts +36 -0
  483. package/dist/ui/overlay/impl/permission-overlay-v2.js +380 -0
  484. package/dist/ui/overlay/impl/permissions-overlay-v2.d.ts +85 -0
  485. package/dist/ui/overlay/impl/permissions-overlay-v2.js +820 -0
  486. package/dist/ui/overlay/impl/plan-approval-overlay-v2.d.ts +35 -0
  487. package/dist/ui/overlay/impl/plan-approval-overlay-v2.js +181 -0
  488. package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +36 -0
  489. package/dist/ui/overlay/impl/project-edit-overlay-v2.js +195 -0
  490. package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +37 -0
  491. package/dist/ui/overlay/impl/projects-overlay-v2.js +733 -0
  492. package/dist/ui/overlay/impl/reset-overlay-v2.d.ts +39 -0
  493. package/dist/ui/overlay/impl/reset-overlay-v2.js +107 -0
  494. package/dist/ui/overlay/impl/resume-overlay-v2.d.ts +60 -0
  495. package/dist/ui/overlay/impl/resume-overlay-v2.js +414 -0
  496. package/dist/ui/overlay/impl/session-mode-overlay-v2.d.ts +43 -0
  497. package/dist/ui/overlay/impl/session-mode-overlay-v2.js +124 -0
  498. package/dist/ui/overlay/impl/tasks-overlay-v2.d.ts +28 -0
  499. package/dist/ui/overlay/impl/tasks-overlay-v2.js +283 -0
  500. package/dist/ui/overlay/impl/team-overlay-v2.d.ts +86 -0
  501. package/dist/ui/overlay/impl/team-overlay-v2.js +692 -0
  502. package/dist/ui/overlay/impl/terminals-overlay-v2.d.ts +26 -0
  503. package/dist/ui/overlay/impl/terminals-overlay-v2.js +217 -0
  504. package/dist/ui/overlay/impl/theme-overlay-v2.d.ts +42 -0
  505. package/dist/ui/overlay/impl/theme-overlay-v2.js +135 -0
  506. package/dist/ui/overlay/impl/tools-overlay-v2.d.ts +47 -0
  507. package/dist/ui/overlay/impl/tools-overlay-v2.js +214 -0
  508. package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +45 -0
  509. package/dist/ui/overlay/impl/tutorial-overlay-v2.js +212 -0
  510. package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +80 -0
  511. package/dist/ui/overlay/impl/workflow-overlay-v2.js +641 -0
  512. package/dist/ui/overlay/index.d.ts +52 -0
  513. package/dist/ui/overlay/index.js +54 -0
  514. package/dist/ui/overlay/key-utils.d.ts +6 -0
  515. package/dist/ui/overlay/key-utils.js +6 -0
  516. package/dist/ui/overlay/types.d.ts +140 -0
  517. package/dist/ui/overlay/types.js +22 -0
  518. package/dist/ui/overlay-manager.d.ts +43 -0
  519. package/dist/ui/overlay-manager.js +238 -0
  520. package/dist/ui/overlays.d.ts +0 -4
  521. package/dist/ui/overlays.js +4 -460
  522. package/dist/ui/permission-overlay.d.ts +1 -1
  523. package/dist/ui/permission-overlay.js +189 -300
  524. package/dist/ui/providers/types.d.ts +178 -0
  525. package/dist/ui/providers/types.js +9 -0
  526. package/dist/ui/render-modes.d.ts +36 -0
  527. package/dist/ui/render-modes.js +44 -0
  528. package/dist/ui/startup-menu.d.ts +36 -0
  529. package/dist/ui/startup-menu.js +236 -0
  530. package/dist/ui/status-bar-controller.d.ts +33 -0
  531. package/dist/ui/status-bar-controller.js +99 -0
  532. package/dist/ui/subagent-renderer.d.ts +117 -0
  533. package/dist/ui/subagent-renderer.js +318 -0
  534. package/dist/ui/terminal-autocomplete-utils.d.ts +23 -0
  535. package/dist/ui/terminal-autocomplete-utils.js +83 -0
  536. package/dist/ui/terminal-codes.d.ts +94 -0
  537. package/dist/ui/terminal-codes.js +124 -0
  538. package/dist/ui/terminal-line-builders.d.ts +17 -0
  539. package/dist/ui/terminal-line-builders.js +42 -0
  540. package/dist/ui/terminal-render-item.d.ts +16 -0
  541. package/dist/ui/terminal-render-item.js +267 -0
  542. package/dist/ui/terminal-renderer.d.ts +220 -0
  543. package/dist/ui/terminal-renderer.js +750 -0
  544. package/dist/ui/terminal-types.d.ts +179 -0
  545. package/dist/ui/terminal-types.js +34 -0
  546. package/dist/ui/terminal-ui.d.ts +331 -0
  547. package/dist/ui/terminal-ui.js +819 -0
  548. package/dist/ui/terminal.d.ts +20 -0
  549. package/dist/ui/terminal.js +72 -0
  550. package/dist/ui/todo-zone.d.ts +19 -1
  551. package/dist/ui/todo-zone.js +124 -38
  552. package/dist/ui/tool-formatters.d.ts +16 -0
  553. package/dist/ui/tool-formatters.js +1027 -0
  554. package/dist/ui/turn-metrics.d.ts +56 -0
  555. package/dist/ui/turn-metrics.js +75 -0
  556. package/dist/ui/types.d.ts +28 -0
  557. package/dist/ui/types.js +1 -0
  558. package/dist/ui/vscode-diff-ipc.d.ts +102 -0
  559. package/dist/ui/vscode-diff-ipc.js +385 -0
  560. package/dist/utils/credentials.d.ts +24 -5
  561. package/dist/utils/credentials.js +123 -9
  562. package/dist/utils/debug-log.d.ts +28 -0
  563. package/dist/utils/debug-log.js +57 -0
  564. package/dist/utils/format-tokens.d.ts +13 -0
  565. package/dist/utils/format-tokens.js +18 -0
  566. package/dist/utils/git-config.d.ts +26 -0
  567. package/dist/utils/git-config.js +54 -0
  568. package/dist/utils/message-utils.d.ts +61 -0
  569. package/dist/utils/message-utils.js +72 -0
  570. package/dist/utils/model-tiers.d.ts +8 -1
  571. package/dist/utils/model-tiers.js +39 -17
  572. package/dist/utils/open-browser.d.ts +5 -0
  573. package/dist/utils/open-browser.js +32 -0
  574. package/dist/utils/path-safety.d.ts +56 -0
  575. package/dist/utils/path-safety.js +240 -0
  576. package/dist/utils/project-detection.d.ts +58 -0
  577. package/dist/utils/project-detection.js +424 -0
  578. package/dist/utils/project-memory.js +2 -1
  579. package/dist/utils/project-status.d.ts +2 -2
  580. package/dist/utils/startup-perf.d.ts +18 -0
  581. package/dist/utils/startup-perf.js +60 -0
  582. package/dist/utils/token-tracker.d.ts +62 -0
  583. package/dist/utils/token-tracker.js +150 -0
  584. package/dist/utils/token-types.d.ts +23 -0
  585. package/dist/utils/token-types.js +18 -0
  586. package/dist/utils/types/config-types.d.ts +32 -0
  587. package/dist/utils/types/config-types.js +8 -0
  588. package/dist/utils/update-checker.d.ts +28 -0
  589. package/dist/utils/update-checker.js +106 -0
  590. package/dist/utils/version.d.ts +7 -0
  591. package/dist/utils/version.js +10 -0
  592. package/dist/utils/vscode-detect.d.ts +39 -0
  593. package/dist/utils/vscode-detect.js +137 -0
  594. package/dist/workflow/guided-mode-injector.d.ts +42 -0
  595. package/dist/workflow/guided-mode-injector.js +191 -0
  596. package/dist/workflow/index.d.ts +8 -0
  597. package/dist/workflow/index.js +8 -0
  598. package/dist/workflow/step-criteria.d.ts +62 -0
  599. package/dist/workflow/step-criteria.js +150 -0
  600. package/dist/workflow/step-tracker.d.ts +92 -0
  601. package/dist/workflow/step-tracker.js +141 -0
  602. package/package.json +32 -12
  603. package/dist/index.old.d.ts +0 -7
  604. package/dist/index.old.js +0 -1014
  605. package/dist/repl.d.ts +0 -121
  606. package/dist/repl.js +0 -1878
  607. package/dist/templates/claude-md.d.ts +0 -7
  608. package/dist/templates/claude-md.js +0 -189
  609. package/dist/test-autocomplete.d.ts +0 -7
  610. package/dist/test-autocomplete.js +0 -85
  611. package/dist/test-tabbed-menu.d.ts +0 -7
  612. package/dist/test-tabbed-menu.js +0 -25
  613. package/dist/tool-selector.d.ts +0 -71
  614. package/dist/tool-selector.js +0 -184
  615. package/dist/ui/agents-overlay.d.ts +0 -12
  616. package/dist/ui/agents-overlay.js +0 -501
  617. package/dist/ui/arch-type-overlay.d.ts +0 -20
  618. package/dist/ui/arch-type-overlay.js +0 -229
  619. package/dist/ui/backlog-overlay.d.ts +0 -17
  620. package/dist/ui/backlog-overlay.js +0 -786
  621. package/dist/ui/commands-overlay.d.ts +0 -11
  622. package/dist/ui/commands-overlay.js +0 -410
  623. package/dist/ui/config-overlay.d.ts +0 -34
  624. package/dist/ui/config-overlay.js +0 -977
  625. package/dist/ui/init-overlay.d.ts +0 -24
  626. package/dist/ui/init-overlay.js +0 -525
  627. package/dist/ui/input-prompt-v2.d.ts +0 -179
  628. package/dist/ui/input-prompt-v2.js +0 -991
  629. package/dist/ui/model-warning-overlay.d.ts +0 -30
  630. package/dist/ui/model-warning-overlay.js +0 -171
  631. package/dist/ui/tools-overlay.d.ts +0 -26
  632. package/dist/ui/tools-overlay.js +0 -278
  633. package/dist/ui/tutorial-overlay.d.ts +0 -10
  634. package/dist/ui/tutorial-overlay.js +0 -936
@@ -0,0 +1,3286 @@
1
+ /**
2
+ * REPL V2 - New Terminal UI based REPL
3
+ *
4
+ * Uses TerminalUI with:
5
+ * - Persistent footer (todo list + input prompt)
6
+ * - Overlay system for commands
7
+ * - Real agent integration (or simulation mode for testing)
8
+ *
9
+ * Can be run standalone: npx tsx src/repl-v2.ts
10
+ * Or imported and used with a real agent from index.ts
11
+ */
12
+ import { appendFileSync } from 'fs';
13
+ import { generateEditDiff, generateWriteDiff } from './ui/diff.js';
14
+ import { getPendingRequestsManager, setActiveSharedContext } from './multi-agent/index.js';
15
+ import { parseInputForMentions, hasReferences, buildMessageWithContext, ContextResolver, buildContextMap, } from './multi-agent/index.js';
16
+ import { getTeamCheckpointer } from './multi-agent/checkpointer.js';
17
+ import { getDelegationTracker } from './multi-agent/delegation-tracker.js';
18
+ import { initSessionRegistry, getSessionRegistry, setActiveTerminalSessionId, getActiveTerminalSessionId } from './multi-agent/session-registry.js';
19
+ import { initFileLockManager } from './multi-agent/file-locks.js';
20
+ import { initNotificationManager, getNotificationManager } from './multi-agent/notification-manager.js';
21
+ import { getSessionsPath } from './settings/paths.js';
22
+ import { setMetaToolFilter } from './tools.js';
23
+ import { getCurrentProject } from './tools/project-db.js';
24
+ import { TerminalUI } from './ui/terminal-ui.js';
25
+ import * as terminal from './ui/terminal.js';
26
+ import { getStyles } from './themes/index.js';
27
+ import { getSettings, getStartupMode, syncPermissionModeFromUI, getPermissionMode, permissionModeToAgentMode, getProjectSessionMode, isFirstRunComplete, getSessionRetentionDays, updateSettings } from './settings/index.js';
28
+ import { renderMascotWithLogo } from './ui/mascot/renderer.js';
29
+ import { getStartupHighlights } from './changelog/index.js';
30
+ import { registerCommands, executeCommand, allCommands, getAutocompleteCommands, saveCurrentSession, saveCurrentTeam, loadProjectSession, archiveCurrentSession, convertMessagesToItems, } from './commands-v2/index.js';
31
+ import { getCustomCommandRegistry } from './commands/custom-registry.js';
32
+ import { getPlanModePrompt } from './prompts/plan-mode-prompt.js';
33
+ import { planRepository } from './db/repositories/index.js';
34
+ import { PlanApprovalOverlayV2, } from './ui/overlay/impl/plan-approval-overlay-v2.js';
35
+ import { PermissionOverlayV2, } from './ui/overlay/impl/permission-overlay-v2.js';
36
+ import { stripThinkingBlocks } from './utils/message-utils.js';
37
+ import { truncate } from './ui/base/index.js';
38
+ import { AskUserSimpleOverlayV2, } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
39
+ import { GuardrailOverlayV2, } from './ui/overlay/impl/guardrail-overlay-v2.js';
40
+ import { AskUserOverlayV2, } from './ui/overlay/impl/ask-user-overlay-v2.js';
41
+ import { IterationLimitOverlayV2, } from './ui/overlay/impl/iteration-limit-overlay-v2.js';
42
+ import { OnboardingWizardOverlayV2, } from './ui/overlay/impl/onboarding-wizard-overlay-v2.js';
43
+ import { LoginOverlayV2, } from './ui/overlay/impl/login-overlay-v2.js';
44
+ import { getAuthManager } from './auth/index.js';
45
+ import { KeysOverlayV2 } from './ui/overlay/impl/keys-overlay-v2.js';
46
+ import { SessionModeOverlayV2, } from './ui/overlay/impl/session-mode-overlay-v2.js';
47
+ import { DocumentDetailOverlayV2 } from './ui/overlay/impl/document-detail-overlay-v2.js';
48
+ import { formatToolResult } from './ui/tool-formatters.js';
49
+ import { formatTokens } from './ui/base/index.js';
50
+ import { isMemoryInput } from './input-handlers/index.js';
51
+ import { addAnchor } from './anchors/index.js';
52
+ import { getActiveProject } from './tools/project-db.js';
53
+ import { getProjectSessionManager } from './session/project-session-manager.js';
54
+ import { ResumeOverlayV2 } from './ui/overlay/impl/resume-overlay-v2.js';
55
+ import { updateCacheInfo, updateHistoryEstimate, formatBreakdownCompact, formatCacheInfo, estimateTokens } from './utils/token-tracker.js';
56
+ // Version for display (when running standalone)
57
+ const STANDALONE_VERSION = '0.4.0-v2';
58
+ // Register all commands on module load
59
+ registerCommands(allCommands);
60
+ /** Maximum messages per agent queue */
61
+ const MAX_QUEUE_SIZE = 10;
62
+ // =============================================================================
63
+ // Debug Logging (set DEBUG_AGENT_REQUESTS=1 to enable)
64
+ // =============================================================================
65
+ const DEBUG_LOG_FILE = '/tmp/compilr-agent-debug.log';
66
+ const DEBUG_ENABLED = process.env.DEBUG_AGENT_REQUESTS === '1';
67
+ let debugRequestCount = 0;
68
+ // Import tool registry functions for debug logging
69
+ import { getDirectTools, getMetaTools, getToolStats } from './tools.js';
70
+ import { getRegisteredMetaTools } from './tools/meta-tools.js';
71
+ /**
72
+ * Write debug info to log file before agent request.
73
+ * Enable with: DEBUG_AGENT_REQUESTS=1 compilr
74
+ * View with: tail -f /tmp/compilr-agent-debug.log
75
+ */
76
+ function debugLogAgentRequest(agent, message) {
77
+ if (!DEBUG_ENABLED)
78
+ return;
79
+ try {
80
+ debugRequestCount++;
81
+ const state = agent.serialize();
82
+ const timestamp = new Date().toISOString();
83
+ const systemPromptChars = state.systemPrompt.length;
84
+ const systemPromptTokens = estimateTokens(state.systemPrompt);
85
+ const messagesJson = JSON.stringify(state.messages);
86
+ const messagesTokens = estimateTokens(messagesJson);
87
+ // Get tool definitions for logging
88
+ const directTools = getDirectTools();
89
+ const metaTools = getMetaTools();
90
+ const metaRegistryTools = getRegisteredMetaTools();
91
+ const toolStats = getToolStats();
92
+ // Calculate tool definition sizes
93
+ const directToolsJson = JSON.stringify(directTools.map(t => ({
94
+ name: t.definition.name,
95
+ description: t.definition.description,
96
+ inputSchema: t.definition.inputSchema,
97
+ })));
98
+ const metaToolsJson = JSON.stringify(metaTools.map(t => ({
99
+ name: t.definition.name,
100
+ description: t.definition.description,
101
+ inputSchema: t.definition.inputSchema,
102
+ })));
103
+ const directToolsTokens = estimateTokens(directToolsJson);
104
+ const metaToolsTokens = estimateTokens(metaToolsJson);
105
+ const totalToolTokens = directToolsTokens + metaToolsTokens;
106
+ const totalEstimate = systemPromptTokens + messagesTokens + totalToolTokens;
107
+ const logEntry = `
108
+ ################################################################################
109
+ # REQUEST #${String(debugRequestCount)} @ ${timestamp}
110
+ ################################################################################
111
+
112
+ USER MESSAGE:
113
+ ${message.slice(0, 500)}${message.length > 500 ? '...(truncated)' : ''}
114
+
115
+ CONTEXT SIZES:
116
+ - System Prompt: ${systemPromptChars.toLocaleString()} chars (~${systemPromptTokens.toLocaleString()} tokens)
117
+ - Messages: ${messagesJson.length.toLocaleString()} chars (~${messagesTokens.toLocaleString()} tokens)
118
+ - Message Count: ${String(state.messages.length)}
119
+ - Direct Tools: ${directToolsJson.length.toLocaleString()} chars (~${directToolsTokens.toLocaleString()} tokens) [${String(directTools.length)} tools]
120
+ - Meta Tools: ${metaToolsJson.length.toLocaleString()} chars (~${metaToolsTokens.toLocaleString()} tokens) [${String(metaTools.length)} tools]
121
+ - Meta Registry: ${String(metaRegistryTools.length)} tools (accessed via use_tool)
122
+ --------------------------------------------------------------------------------
123
+ ESTIMATED TOTAL: ~${totalEstimate.toLocaleString()} tokens
124
+ (prompt: ${systemPromptTokens.toLocaleString()} + messages: ${messagesTokens.toLocaleString()} + tools: ${totalToolTokens.toLocaleString()})
125
+
126
+ Tool Stats: Direct=${String(toolStats.directTools)}, MetaRegistry=${String(toolStats.metaRegistryTools)}, Savings=~${String(toolStats.tokenSavings)}
127
+
128
+ ================================================================================
129
+ FULL SYSTEM PROMPT (${systemPromptChars.toLocaleString()} chars):
130
+ ================================================================================
131
+ ${state.systemPrompt}
132
+
133
+ ================================================================================
134
+ DIRECT TOOLS (${String(directTools.length)} tools, ~${directToolsTokens.toLocaleString()} tokens):
135
+ ================================================================================
136
+ ${directTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
137
+
138
+ ================================================================================
139
+ META TOOLS (${String(metaTools.length)} tools, ~${metaToolsTokens.toLocaleString()} tokens):
140
+ ================================================================================
141
+ ${metaTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
142
+
143
+ ================================================================================
144
+ META REGISTRY TOOLS (${String(metaRegistryTools.length)} tools, accessed via use_tool):
145
+ ================================================================================
146
+ ${metaRegistryTools.map(t => `- ${t.definition.name}`).join('\n')}
147
+
148
+ ================================================================================
149
+ MESSAGES HISTORY (${String(state.messages.length)} messages):
150
+ ================================================================================
151
+ ${JSON.stringify(state.messages, null, 2)}
152
+
153
+ `;
154
+ appendFileSync(DEBUG_LOG_FILE, logEntry);
155
+ console.log(`[DEBUG] Request #${String(debugRequestCount)} logged to ${DEBUG_LOG_FILE}`);
156
+ console.log(`[DEBUG] Estimated: sys:${String(systemPromptTokens)}tok + msg:${String(messagesTokens)}tok + tools:${String(totalToolTokens)}tok = ${String(totalEstimate)}tok`);
157
+ }
158
+ catch (error) {
159
+ console.error('[DEBUG] Failed to log agent request:', error);
160
+ }
161
+ }
162
+ // =============================================================================
163
+ // REPL V2 Class
164
+ // =============================================================================
165
+ export class ReplV2 {
166
+ agent;
167
+ team;
168
+ agentFactory;
169
+ model;
170
+ provider;
171
+ version;
172
+ updateAvailable;
173
+ projectName;
174
+ // Multi-session tracking for background agents (Phase 3)
175
+ sessions = new Map();
176
+ foregroundAgentId = null;
177
+ // Per-agent message queues (Phase 3 - queued messages when agent is busy)
178
+ agentMessageQueues = new Map();
179
+ // Foreground session (backward compatible - will be replaced by sessions map)
180
+ currentSession = null;
181
+ ui;
182
+ onUIReady;
183
+ onTextBufferReady;
184
+ onAskUserSimpleReady;
185
+ onGuardrailReady;
186
+ onAskUserReady;
187
+ onIterationLimitReady;
188
+ onAgentFinish;
189
+ onSuggestionReady;
190
+ onSubagentReady;
191
+ clearSubagentTracking;
192
+ showOnboarding;
193
+ showLogin;
194
+ onDelegateReady;
195
+ onPendingDelegation;
196
+ onBackgroundAgentReady;
197
+ onBackgroundDelegationReady;
198
+ onSessionRegistered;
199
+ onTeamReplaced;
200
+ authOnly;
201
+ restoredSessionInfo = null;
202
+ // Phase 3b: Track which background agent is currently executing (for permission routing)
203
+ // This is set during the background agent's stream loop so the permission handler knows
204
+ // which agent is making the request.
205
+ currentExecutingBackgroundAgentId = null;
206
+ // Session stats
207
+ startTime = new Date();
208
+ sessionInputTokens = 0;
209
+ sessionOutputTokens = 0;
210
+ sessionRequests = 0;
211
+ // Text accumulator for agent output (class-level so it can be flushed externally)
212
+ textAccumulator = '';
213
+ // Note: Subagent tracking is now simplified - callbacks receive toolUseId directly
214
+ // from agent.ts, so no more FIFO queue correlation needed!
215
+ // Bash abort controllers for Ctrl+B backgrounding feature
216
+ // Maps toolUseId -> AbortController for each running bash command
217
+ bashAbortControllers = new Map();
218
+ constructor(options = {}) {
219
+ this.agent = options.agent;
220
+ this.team = options.team;
221
+ this.agentFactory = options.agentFactory;
222
+ this.model = options.model ?? 'simulation';
223
+ this.provider = options.provider ?? 'simulation';
224
+ this.version = options.version ?? STANDALONE_VERSION;
225
+ this.updateAvailable = options.updateAvailable ?? null;
226
+ this.projectName = options.projectName ?? process.cwd().split('/').pop() ?? 'Project';
227
+ this.onUIReady = options.onUIReady;
228
+ this.onTextBufferReady = options.onTextBufferReady;
229
+ this.onAskUserSimpleReady = options.onAskUserSimpleReady;
230
+ this.onGuardrailReady = options.onGuardrailReady;
231
+ this.onAskUserReady = options.onAskUserReady;
232
+ this.onIterationLimitReady = options.onIterationLimitReady;
233
+ this.onAgentFinish = options.onAgentFinish;
234
+ this.onSuggestionReady = options.onSuggestionReady;
235
+ this.onSubagentReady = options.onSubagentReady;
236
+ this.clearSubagentTracking = options.clearSubagentTracking;
237
+ this.showOnboarding = options.showOnboarding ?? false;
238
+ this.showLogin = options.showLogin ?? false;
239
+ this.onDelegateReady = options.onDelegateReady;
240
+ this.onPendingDelegation = options.onPendingDelegation;
241
+ this.onBackgroundAgentReady = options.onBackgroundAgentReady;
242
+ this.onBackgroundDelegationReady = options.onBackgroundDelegationReady;
243
+ this.onSessionRegistered = options.onSessionRegistered;
244
+ this.onTeamReplaced = options.onTeamReplaced;
245
+ this.authOnly = options.authOnly ?? false;
246
+ }
247
+ /**
248
+ * Flush accumulated agent text to the UI.
249
+ * Called externally (e.g., before showing permission overlay) to ensure
250
+ * agent's "I'll do X" message appears before the overlay.
251
+ */
252
+ flushTextBuffer() {
253
+ if (this.textAccumulator.trim()) {
254
+ this.ui.print({
255
+ type: 'agent-text',
256
+ text: this.textAccumulator.trim(),
257
+ expression: this.getActiveAgentMascot(),
258
+ });
259
+ this.textAccumulator = '';
260
+ }
261
+ }
262
+ /**
263
+ * Get the mascot expression for the currently active agent.
264
+ * Returns the team agent's mascot if active, otherwise default neutral.
265
+ */
266
+ getActiveAgentMascot() {
267
+ if (this.team) {
268
+ const activeAgent = this.team.getActive();
269
+ return activeAgent.mascot;
270
+ }
271
+ return '[•_•]'; // Default neutral mascot
272
+ }
273
+ // ===========================================================================
274
+ // Session Management (Phase 3 - Background Agents)
275
+ // ===========================================================================
276
+ /**
277
+ * Start a new agent session (foreground or background).
278
+ * For foreground: sets currentSession and foregroundAgentId
279
+ * For background: only adds to sessions map
280
+ */
281
+ startSession(session) {
282
+ this.sessions.set(session.agentId, session);
283
+ if (!session.isBackground) {
284
+ this.currentSession = session;
285
+ this.foregroundAgentId = session.agentId;
286
+ }
287
+ }
288
+ /**
289
+ * End an agent session.
290
+ * Removes from sessions map, clears currentSession if foreground,
291
+ * and processes any queued messages for this agent.
292
+ */
293
+ endSession(agentId) {
294
+ this.sessions.delete(agentId);
295
+ if (this.foregroundAgentId === agentId) {
296
+ this.currentSession = null;
297
+ this.foregroundAgentId = null;
298
+ }
299
+ // Update background agent count in UI
300
+ this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
301
+ // Process any queued messages for this agent
302
+ this.processAgentQueue(agentId);
303
+ }
304
+ /**
305
+ * Get a session by agent ID.
306
+ */
307
+ getSession(agentId) {
308
+ return this.sessions.get(agentId);
309
+ }
310
+ /**
311
+ * Get all background sessions (non-foreground).
312
+ */
313
+ getBackgroundSessions() {
314
+ return Array.from(this.sessions.values()).filter(s => s.isBackground);
315
+ }
316
+ /**
317
+ * Check if an agent has a running session.
318
+ */
319
+ hasActiveSession(agentId) {
320
+ return this.sessions.has(agentId);
321
+ }
322
+ /**
323
+ * Check if any background agents are running.
324
+ */
325
+ hasBackgroundAgents() {
326
+ return this.getBackgroundSessions().length > 0;
327
+ }
328
+ // ===========================================================================
329
+ // Per-Agent Message Queue (Phase 3)
330
+ // ===========================================================================
331
+ /**
332
+ * Queue a message for an agent that is currently busy.
333
+ * Returns the queue position (1-based).
334
+ */
335
+ queueMessage(agentId, message, displayMessage) {
336
+ const queue = this.agentMessageQueues.get(agentId) ?? [];
337
+ // Check max queue size
338
+ if (queue.length >= MAX_QUEUE_SIZE) {
339
+ return null; // Queue is full
340
+ }
341
+ const queuedMessage = {
342
+ id: `${agentId}-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
343
+ message,
344
+ displayMessage,
345
+ queuedAt: new Date(),
346
+ };
347
+ queue.push(queuedMessage);
348
+ this.agentMessageQueues.set(agentId, queue);
349
+ return queue.length; // Return position (1-based)
350
+ }
351
+ /**
352
+ * Get the number of messages queued for an agent.
353
+ */
354
+ getQueueLength(agentId) {
355
+ return this.agentMessageQueues.get(agentId)?.length ?? 0;
356
+ }
357
+ /**
358
+ * Clear all queued messages for an agent.
359
+ * Returns the number of messages that were cleared.
360
+ */
361
+ clearQueue(agentId) {
362
+ const queue = this.agentMessageQueues.get(agentId);
363
+ const count = queue?.length ?? 0;
364
+ this.agentMessageQueues.delete(agentId);
365
+ return count;
366
+ }
367
+ /**
368
+ * Clear all message queues for all agents.
369
+ * Returns the total number of messages cleared.
370
+ */
371
+ clearAllQueues() {
372
+ let total = 0;
373
+ for (const queue of this.agentMessageQueues.values()) {
374
+ total += queue.length;
375
+ }
376
+ this.agentMessageQueues.clear();
377
+ return total;
378
+ }
379
+ /**
380
+ * Process the next message in an agent's queue.
381
+ * Called after an agent completes a task.
382
+ */
383
+ processAgentQueue(agentId) {
384
+ const queue = this.agentMessageQueues.get(agentId);
385
+ if (!queue || queue.length === 0) {
386
+ return;
387
+ }
388
+ const nextMessage = queue.shift();
389
+ if (!nextMessage) {
390
+ return; // Should never happen due to length check above
391
+ }
392
+ this.agentMessageQueues.set(agentId, queue);
393
+ // Show that we're processing a queued message
394
+ const label = this.getAgentMascotLabel(agentId);
395
+ const preview = truncate(nextMessage.displayMessage, 50);
396
+ this.ui.print({
397
+ type: 'info',
398
+ message: `${label} processing queued message: "${preview}"`,
399
+ });
400
+ // Run the queued message as a background task (non-blocking)
401
+ void this.runAgentBackground(agentId, nextMessage.message);
402
+ }
403
+ /**
404
+ * Get detailed info about background sessions (for /bg overlay).
405
+ * Phase 3c: Background Status & Control
406
+ */
407
+ getBackgroundSessionsInfo() {
408
+ const sessions = this.getBackgroundSessions();
409
+ const pendingManager = getPendingRequestsManager();
410
+ return sessions.map((session) => {
411
+ const teamAgent = this.team?.get(session.agentId);
412
+ const pendingForAgent = pendingManager.getByAgent(session.agentId);
413
+ const queuedCount = this.getQueueLength(session.agentId);
414
+ return {
415
+ agentId: session.agentId,
416
+ displayName: teamAgent?.displayName ?? session.agentId,
417
+ mascot: teamAgent?.mascot ?? '[•_•]',
418
+ currentAction: session.currentAction,
419
+ isBlocked: pendingForAgent.length > 0,
420
+ pendingCount: pendingForAgent.length,
421
+ queuedCount,
422
+ };
423
+ });
424
+ }
425
+ /**
426
+ * Send the current foreground agent to background without aborting it.
427
+ * The agent continues running; the user regains the input prompt.
428
+ * Returns the agent ID that was sent to background, or null if no foreground session.
429
+ */
430
+ sendForegroundToBackground() {
431
+ if (!this.currentSession || !this.foregroundAgentId) {
432
+ return null;
433
+ }
434
+ const agentId = this.foregroundAgentId;
435
+ // Reclassify session as background (agent keeps running)
436
+ this.currentSession.isBackground = true;
437
+ this.foregroundAgentId = null;
438
+ this.currentSession = null;
439
+ // Update UI state — user regains input prompt
440
+ this.ui.setAgentRunning(false);
441
+ this.ui.setThinking(false);
442
+ return agentId;
443
+ }
444
+ /**
445
+ * Consolidated agent-switching method.
446
+ *
447
+ * Performs ALL operations needed to switch the foreground to a different agent:
448
+ * 1. Demote current foreground agent to background (if busy + different agent)
449
+ * 2. team.switchTo(agentId)
450
+ * 3. Update this.agent reference
451
+ * 4. Update meta-tool filter
452
+ * 5. Update footer UI
453
+ * 6. Update foreground session tracking (restore agentRunning/spinner)
454
+ * 7. Record telemetry
455
+ *
456
+ * Callers handle their own messaging (different styles/wording per context).
457
+ */
458
+ async switchAgent(agentId, options = {}) {
459
+ const { demoteCurrentIfBusy = true, trackSession = true, recordTelemetry = true, } = options;
460
+ if (!this.team) {
461
+ throw new Error('Cannot switch agent: no team configured');
462
+ }
463
+ // 1. Demote current foreground if busy + switching to a different agent
464
+ let demotedAgentId = null;
465
+ if (demoteCurrentIfBusy &&
466
+ this.currentSession &&
467
+ this.foregroundAgentId &&
468
+ this.foregroundAgentId !== agentId) {
469
+ demotedAgentId = this.sendForegroundToBackground();
470
+ }
471
+ // 2. Switch the team's active agent
472
+ const teamAgent = await this.team.switchTo(agentId);
473
+ // 3. Update the REPL's agent reference
474
+ if (teamAgent.agent) {
475
+ this.agent = teamAgent.agent;
476
+ }
477
+ // 4. Update meta-tool filter
478
+ setMetaToolFilter(teamAgent.toolFilter ?? null);
479
+ // 5. Update footer UI
480
+ if (agentId !== 'default') {
481
+ this.ui.setActiveTeamAgent({
482
+ id: teamAgent.id,
483
+ displayName: teamAgent.displayName,
484
+ mascot: teamAgent.mascot,
485
+ });
486
+ }
487
+ else {
488
+ this.ui.setActiveTeamAgent(null);
489
+ }
490
+ // 5b. Update session registry with active agent
491
+ try {
492
+ const registry = getSessionRegistry();
493
+ if (registry) {
494
+ registry.setActiveAgent(agentId);
495
+ const agents = this.team.getAll();
496
+ registry.updateAgents(agents.map(a => ({
497
+ id: a.id,
498
+ displayName: a.displayName,
499
+ mascot: a.mascot,
500
+ })));
501
+ }
502
+ }
503
+ catch {
504
+ // Best effort
505
+ }
506
+ // 6. Update foreground session tracking
507
+ if (trackSession) {
508
+ const existingSession = this.sessions.get(agentId);
509
+ if (existingSession && existingSession.isBackground) {
510
+ // Agent has a background session — bring it to foreground
511
+ existingSession.isBackground = false;
512
+ this.foregroundAgentId = agentId;
513
+ this.currentSession = existingSession;
514
+ // Restore busy UI state — session exists means agent is still running
515
+ this.ui.setAgentRunning(true);
516
+ if (existingSession.currentAction) {
517
+ this.ui.setSpinnerText(existingSession.currentAction);
518
+ }
519
+ }
520
+ else if (!existingSession) {
521
+ // No session yet — just update foreground ID so next run creates it correctly
522
+ this.foregroundAgentId = agentId;
523
+ this.currentSession = null;
524
+ }
525
+ // If existingSession exists and is already foreground, leave it as-is
526
+ }
527
+ // 7. Record telemetry
528
+ if (recordTelemetry) {
529
+ getAuthManager().recordAgent(agentId);
530
+ }
531
+ return {
532
+ teamAgent: {
533
+ id: teamAgent.id,
534
+ displayName: teamAgent.displayName,
535
+ mascot: teamAgent.mascot,
536
+ agent: teamAgent.agent,
537
+ toolFilter: teamAgent.toolFilter,
538
+ },
539
+ demotedAgentId,
540
+ };
541
+ }
542
+ /**
543
+ * Bring a background agent to foreground.
544
+ * Validates the agent has a background session, then delegates to switchAgent().
545
+ * Phase 3c: Background Status & Control
546
+ */
547
+ async bringAgentToForeground(agentId) {
548
+ const session = this.sessions.get(agentId);
549
+ if (!session || !session.isBackground) {
550
+ return false;
551
+ }
552
+ const { demotedAgentId } = await this.switchAgent(agentId);
553
+ // Print demotion notification (if a different agent was sent to background)
554
+ if (demotedAgentId) {
555
+ const demotedLabel = this.getAgentMascotLabel(demotedAgentId);
556
+ this.ui.print({
557
+ type: 'info',
558
+ message: `${demotedLabel} sent to background`,
559
+ });
560
+ }
561
+ // Print foreground notification
562
+ const label = this.getAgentMascotLabel(agentId);
563
+ this.ui.print({
564
+ type: 'info',
565
+ message: `${label} brought to foreground`,
566
+ });
567
+ return true;
568
+ }
569
+ /**
570
+ * Kill/cancel a background agent.
571
+ * Phase 3c: Background Status & Control
572
+ */
573
+ killAgent(agentId) {
574
+ const session = this.sessions.get(agentId);
575
+ if (!session) {
576
+ return false;
577
+ }
578
+ // Abort the session
579
+ session.abortController.abort();
580
+ // Cancel any pending requests from this agent
581
+ const pendingManager = getPendingRequestsManager();
582
+ pendingManager.rejectAllForAgent(agentId, 'Agent killed by user');
583
+ // Clear queued messages (don't process on kill)
584
+ const discardedCount = this.clearQueue(agentId);
585
+ // Print notification
586
+ const label = this.getAgentMascotLabel(agentId);
587
+ let message = `${label} killed`;
588
+ if (discardedCount > 0) {
589
+ message += `. ${String(discardedCount)} queued message${discardedCount !== 1 ? 's' : ''} discarded.`;
590
+ }
591
+ this.ui.print({
592
+ type: 'warning',
593
+ message,
594
+ });
595
+ // Cancel any active delegations for this agent (no completion events)
596
+ getDelegationTracker().cancelAllForAgent(agentId);
597
+ // Session cleanup happens in the finally block of runAgentInBackground
598
+ // Note: We cleared the queue above, so endSession() won't process it
599
+ // Update background agent count immediately for responsive UI
600
+ this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
601
+ return true;
602
+ }
603
+ /**
604
+ * Get mascot label for an agent (for output labeling).
605
+ * Format: "[◈_◈] $arch" - mascot followed by agent reference
606
+ */
607
+ getAgentMascotLabel(agentId) {
608
+ if (this.team) {
609
+ const teamAgent = this.team.get(agentId);
610
+ if (teamAgent) {
611
+ return `${teamAgent.mascot} $${agentId}`;
612
+ }
613
+ }
614
+ return `$${agentId}`;
615
+ }
616
+ /**
617
+ * Auto-expand conversation filter if user is sending to an agent not in the filter.
618
+ * Shows info message when filter is expanded.
619
+ * @returns true if filter was expanded, false otherwise
620
+ */
621
+ autoExpandFilterIfNeeded(agentId) {
622
+ const wasAdded = this.ui.addToConversationFilter(agentId);
623
+ if (wasAdded) {
624
+ this.ui.print({
625
+ type: 'info',
626
+ message: `Added $${agentId} to conversation filter`,
627
+ });
628
+ }
629
+ return wasAdded;
630
+ }
631
+ /**
632
+ * Regex to detect PLAN_READY marker in agent text.
633
+ * Format: [PLAN_READY:plan_id:plan_name]
634
+ */
635
+ static PLAN_READY_REGEX = /\[PLAN_READY:(\d+):([^\]]+)\]/;
636
+ /**
637
+ * Check if text contains PLAN_READY marker and extract plan info.
638
+ */
639
+ detectPlanReady(text) {
640
+ const match = ReplV2.PLAN_READY_REGEX.exec(text);
641
+ if (match) {
642
+ return {
643
+ planId: parseInt(match[1], 10),
644
+ planName: match[2],
645
+ };
646
+ }
647
+ return null;
648
+ }
649
+ /**
650
+ * Handle plan approval flow.
651
+ * Shows the approval overlay and handles the user's choice.
652
+ */
653
+ async handlePlanApproval(planId, planName) {
654
+ const overlay = new PlanApprovalOverlayV2({ planId, planName });
655
+ const result = await this.ui.showOverlay(overlay);
656
+ if (!result) {
657
+ // User cancelled or overlay closed without result
658
+ return;
659
+ }
660
+ switch (result.action) {
661
+ case 'approve-auto':
662
+ // Update plan status to approved, switch to auto-accept mode
663
+ try {
664
+ planRepository.update(planId, { status: 'approved' });
665
+ this.ui.setMode('auto-accept');
666
+ this.ui.setActivePlan(planId, planName);
667
+ this.ui.print({ type: 'info', message: `Plan "${planName}" approved. Switching to auto-accept mode.` });
668
+ }
669
+ catch (e) {
670
+ this.ui.print({ type: 'error', message: `Failed to approve plan: ${e instanceof Error ? e.message : String(e)}` });
671
+ }
672
+ break;
673
+ case 'approve':
674
+ // Update plan status to approved, switch to normal mode
675
+ try {
676
+ planRepository.update(planId, { status: 'approved' });
677
+ this.ui.setMode('normal');
678
+ this.ui.setActivePlan(planId, planName);
679
+ this.ui.print({ type: 'info', message: `Plan "${planName}" approved. Switching to normal mode.` });
680
+ }
681
+ catch (e) {
682
+ this.ui.print({ type: 'error', message: `Failed to approve plan: ${e instanceof Error ? e.message : String(e)}` });
683
+ }
684
+ break;
685
+ case 'revise':
686
+ // Keep plan as draft, show input for feedback
687
+ this.ui.print({ type: 'info', message: 'Please provide feedback on what you\'d like to change:' });
688
+ // The user will type their feedback as the next message
689
+ // The agent will receive it and update the plan
690
+ break;
691
+ case 'cancel':
692
+ // Mark plan as abandoned
693
+ try {
694
+ planRepository.update(planId, { status: 'abandoned' });
695
+ this.ui.clearActivePlan();
696
+ this.ui.print({ type: 'warning', message: `Plan "${planName}" abandoned.` });
697
+ }
698
+ catch (e) {
699
+ this.ui.print({ type: 'error', message: `Failed to abandon plan: ${e instanceof Error ? e.message : String(e)}` });
700
+ }
701
+ break;
702
+ case 'view':
703
+ // Show the full plan in document detail overlay
704
+ try {
705
+ const plan = planRepository.getById(planId);
706
+ if (plan) {
707
+ // Convert Plan to ProjectDocument for the detail overlay
708
+ const doc = {
709
+ id: plan.id,
710
+ projectId: plan.projectId,
711
+ docType: 'plan',
712
+ title: plan.name,
713
+ content: plan.content,
714
+ status: plan.status,
715
+ workItemId: plan.workItemId,
716
+ createdAt: plan.createdAt,
717
+ updatedAt: plan.updatedAt,
718
+ };
719
+ const detailOverlay = new DocumentDetailOverlayV2(doc);
720
+ await this.ui.showOverlay(detailOverlay);
721
+ }
722
+ }
723
+ catch (e) {
724
+ this.ui.print({ type: 'error', message: `Failed to load plan: ${e instanceof Error ? e.message : String(e)}` });
725
+ }
726
+ // After viewing, re-show the approval overlay
727
+ await this.handlePlanApproval(planId, planName);
728
+ break;
729
+ }
730
+ }
731
+ /**
732
+ * Restore the agent from a previous state.
733
+ * Called by /resume command to swap in a restored agent.
734
+ */
735
+ restoreAgent(newAgent) {
736
+ this.agent = newAgent;
737
+ }
738
+ /**
739
+ * Get the current agent instance.
740
+ * Used by commands that need to serialize/checkpoint the agent state.
741
+ */
742
+ getAgent() {
743
+ return this.agent;
744
+ }
745
+ /**
746
+ * Start the REPL.
747
+ */
748
+ start() {
749
+ // Register this terminal in the session registry (before UI so footer can subscribe)
750
+ try {
751
+ const registry = initSessionRegistry(getSessionsPath());
752
+ const activeProject = getActiveProject();
753
+ const sessionId = registry.register(activeProject?.id ?? null);
754
+ setActiveTerminalSessionId(sessionId);
755
+ // Notify index.ts of session ID (for VS Code diff IPC prefix)
756
+ this.onSessionRegistered?.(sessionId);
757
+ // Initialize file lock manager (uses same shared DB)
758
+ initFileLockManager();
759
+ // Initialize notification manager (cross-session notifications)
760
+ const notifManager = initNotificationManager();
761
+ notifManager.startPolling();
762
+ // Subscribe to pending requests — notify other terminals
763
+ getPendingRequestsManager().on('request-added', (request) => {
764
+ try {
765
+ const sid = getActiveTerminalSessionId();
766
+ const proj = getActiveProject();
767
+ if (sid && proj) {
768
+ notifManager.insert({
769
+ projectId: proj.id,
770
+ fromSessionId: sid,
771
+ type: 'pending_request',
772
+ title: `${request.agentId} needs ${request.type === 'permission' ? 'approval' : 'input'}`,
773
+ message: request.prompt.slice(0, 200),
774
+ });
775
+ }
776
+ }
777
+ catch {
778
+ // Best effort
779
+ }
780
+ });
781
+ // Sync initial team roster to registry (covers restored teams from disk)
782
+ if (this.team) {
783
+ const agents = this.team.getAll();
784
+ registry.updateAgents(agents.map(a => ({
785
+ id: a.id,
786
+ displayName: a.displayName,
787
+ mascot: a.mascot,
788
+ })));
789
+ }
790
+ }
791
+ catch {
792
+ // Best effort — session registry is non-critical
793
+ }
794
+ // Create TerminalUI with mode synced from settings
795
+ const savedPermissionMode = getPermissionMode();
796
+ const initialUiMode = permissionModeToAgentMode(savedPermissionMode);
797
+ this.ui = new TerminalUI({
798
+ initialMode: initialUiMode,
799
+ });
800
+ // Print welcome screen
801
+ // Skip if: login overlay shows its own branded welcome,
802
+ // or startup mode is 'menu' (the /menu command prints its own logo)
803
+ if (!this.showLogin && getStartupMode() !== 'menu') {
804
+ this.printWelcome();
805
+ }
806
+ // Set initial state
807
+ this.ui.setProjectName(this.projectName);
808
+ this.ui.setCommandsCallback(getAutocompleteCommands);
809
+ // Set up agent suggestions callback for $ autocomplete (if team is available)
810
+ if (this.team) {
811
+ const team = this.team; // Capture for closure
812
+ this.ui.setAgentSuggestionsCallback(() => {
813
+ const agents = team.getAll();
814
+ return agents.map((agent) => ({
815
+ id: agent.id,
816
+ displayName: agent.displayName,
817
+ mascot: agent.mascot,
818
+ }));
819
+ });
820
+ // Set the active agent in the footer (for restored teams with non-default active agent)
821
+ const activeAgent = team.getActive();
822
+ if (activeAgent.id !== 'default') {
823
+ this.ui.setActiveTeamAgent({
824
+ id: activeAgent.id,
825
+ displayName: activeAgent.displayName,
826
+ mascot: activeAgent.mascot,
827
+ });
828
+ }
829
+ }
830
+ // Notify caller that UI is ready and provide the V2 permission overlay function
831
+ if (this.onUIReady) {
832
+ const ui = this.ui;
833
+ this.onUIReady(async (options) => {
834
+ const overlay = new PermissionOverlayV2(options);
835
+ return ui.showOverlay(overlay);
836
+ });
837
+ }
838
+ // Provide the text buffer flush function to the caller
839
+ if (this.onTextBufferReady) {
840
+ this.onTextBufferReady(() => {
841
+ this.flushTextBuffer();
842
+ });
843
+ }
844
+ // Provide the ask_user_simple overlay function to the caller
845
+ if (this.onAskUserSimpleReady) {
846
+ const ui = this.ui;
847
+ this.onAskUserSimpleReady(async (options) => {
848
+ const overlay = new AskUserSimpleOverlayV2(options);
849
+ const result = await ui.showOverlay(overlay);
850
+ return result ?? { answer: '', skipped: true };
851
+ });
852
+ }
853
+ // Provide the guardrail overlay function to the caller
854
+ if (this.onGuardrailReady) {
855
+ const ui = this.ui;
856
+ this.onGuardrailReady(async (options) => {
857
+ const overlay = new GuardrailOverlayV2(options);
858
+ const result = await ui.showOverlay(overlay);
859
+ return result ?? { approved: false };
860
+ });
861
+ }
862
+ // Provide the ask_user overlay function to the caller
863
+ if (this.onAskUserReady) {
864
+ const ui = this.ui;
865
+ this.onAskUserReady(async (options) => {
866
+ const overlay = new AskUserOverlayV2(options);
867
+ const result = await ui.showOverlay(overlay);
868
+ return result ?? { answers: {}, skipped: options.questions.map((q) => q.id) };
869
+ });
870
+ }
871
+ // Provide the iteration_limit overlay function to the caller
872
+ if (this.onIterationLimitReady) {
873
+ const ui = this.ui;
874
+ this.onIterationLimitReady(async (options) => {
875
+ const overlay = new IterationLimitOverlayV2(options);
876
+ const result = await ui.showOverlay(overlay);
877
+ return result ?? { continue: false };
878
+ });
879
+ }
880
+ // Provide the delegate overlay function to the caller
881
+ // Uses AskUserSimpleOverlayV2 with a formatted delegation question
882
+ if (this.onDelegateReady) {
883
+ const ui = this.ui;
884
+ this.onDelegateReady(async (options) => {
885
+ // Build a formatted question for the delegation approval
886
+ const question = options.reason
887
+ ? `Delegate to $${options.agentId} (${options.agentDisplayName})?\nReason: ${options.reason}\nTask: ${truncate(options.task, 100)}`
888
+ : `Delegate to $${options.agentId} (${options.agentDisplayName})?\nTask: ${truncate(options.task, 100)}`;
889
+ const overlay = new AskUserSimpleOverlayV2({
890
+ question,
891
+ options: ['Yes, delegate', 'No, continue'],
892
+ allowCustom: false,
893
+ });
894
+ const result = await ui.showOverlay(overlay);
895
+ return { approved: !result?.skipped && result?.answer === 'Yes, delegate' };
896
+ });
897
+ }
898
+ // Provide the suggestion setter function to the caller
899
+ if (this.onSuggestionReady) {
900
+ const ui = this.ui;
901
+ this.onSuggestionReady((action) => {
902
+ ui.setSuggestion(action);
903
+ });
904
+ }
905
+ // Provide subagent tracking callbacks to the caller for LiveRegion rendering
906
+ // Now using toolUseId directly - no more FIFO queue correlation needed!
907
+ if (this.onSubagentReady) {
908
+ const ui = this.ui;
909
+ this.onSubagentReady({
910
+ onStart: (toolUseId, _agentType, _description) => {
911
+ // The subagent entry was already added in tool_start with toolUseId
912
+ // Just mark it as "started" (no-op for now, already rendering)
913
+ void toolUseId; // Used for correlation, entry already exists
914
+ },
915
+ onToolUse: (toolUseId, toolName, summary) => {
916
+ // Direct update using toolUseId - no mapping needed!
917
+ ui.updateSubagentTool(toolUseId, toolName, summary ?? '');
918
+ },
919
+ onEnd: (toolUseId, _success, tokenCount, _error) => {
920
+ // DON'T mark as complete here! That changes the line count in LiveRegion,
921
+ // and if render loop fires before tool_end, lastRenderHeight becomes wrong.
922
+ // Just store the info for tool_end to use when committing.
923
+ const item = ui.getLiveRegion().getItem(toolUseId);
924
+ if (item && item.type === 'subagent') {
925
+ ui.getLiveRegion().updateItem(toolUseId, {
926
+ tokenCount,
927
+ endTime: Date.now(),
928
+ });
929
+ }
930
+ },
931
+ });
932
+ }
933
+ // Phase 3b: Provide background agent tracking callbacks
934
+ if (this.onBackgroundAgentReady) {
935
+ this.onBackgroundAgentReady({
936
+ // Check if an agent is running in background
937
+ isBackgroundAgent: (agentId) => {
938
+ if (agentId) {
939
+ const session = this.sessions.get(agentId);
940
+ return session?.isBackground ?? false;
941
+ }
942
+ // If no agentId provided, check if we're currently executing a background agent
943
+ return this.currentExecutingBackgroundAgentId !== null;
944
+ },
945
+ // Get the ID of the background agent currently executing (for permission routing)
946
+ getActiveBackgroundAgentId: () => {
947
+ return this.currentExecutingBackgroundAgentId;
948
+ },
949
+ // Get info about a background agent (mascot, etc.)
950
+ getBackgroundAgentInfo: (agentId) => {
951
+ if (!this.team)
952
+ return null;
953
+ const teamAgent = this.team.get(agentId);
954
+ if (!teamAgent)
955
+ return null;
956
+ return { mascot: teamAgent.mascot };
957
+ },
958
+ });
959
+ }
960
+ // Phase 3d-beta: Provide background delegation callback
961
+ if (this.onBackgroundDelegationReady) {
962
+ this.onBackgroundDelegationReady((agentId, message) => {
963
+ if (!this.agent)
964
+ return null;
965
+ // Check if agent has an active session (busy)
966
+ const existingSession = this.sessions.get(agentId);
967
+ if (existingSession) {
968
+ // Agent is busy — queue the delegation message
969
+ const position = this.queueMessage(agentId, message, message.slice(0, 80));
970
+ if (position === null) {
971
+ return null; // Queue is full
972
+ }
973
+ return 'queued';
974
+ }
975
+ // Agent is free — start background session immediately
976
+ void this.runAgentBackground(agentId, message);
977
+ return 'running';
978
+ });
979
+ }
980
+ // Event handlers
981
+ this.ui.on('submit', (input) => {
982
+ void this.processInput(input);
983
+ });
984
+ this.ui.on('command', (cmd, args) => {
985
+ void (async () => {
986
+ await this.handleCommand(cmd, args);
987
+ await this.processQueue();
988
+ })();
989
+ });
990
+ this.ui.on('escape', () => {
991
+ // Single Esc when no agent - do nothing
992
+ });
993
+ this.ui.on('interrupt', () => {
994
+ if (this.currentSession) {
995
+ this.cancelSession();
996
+ }
997
+ else {
998
+ this.ui.stop();
999
+ console.log('\nGoodbye!\n');
1000
+ process.exit(0);
1001
+ }
1002
+ });
1003
+ this.ui.on('cancel', () => {
1004
+ if (this.currentSession) {
1005
+ this.cancelSession();
1006
+ }
1007
+ });
1008
+ this.ui.on('modeChange', () => {
1009
+ const modes = ['normal', 'auto-accept', 'plan'];
1010
+ const currentMode = this.ui.getMode();
1011
+ const currentIndex = modes.indexOf(currentMode);
1012
+ const nextMode = modes[(currentIndex + 1) % modes.length];
1013
+ this.ui.setMode(nextMode);
1014
+ // Sync the permission mode setting with UI mode
1015
+ syncPermissionModeFromUI(nextMode);
1016
+ // Clear session grants when switching FROM auto-accept TO a more restrictive mode
1017
+ let grantsCleared = 0;
1018
+ if (currentMode === 'auto-accept' && nextMode !== 'auto-accept' && this.agent) {
1019
+ const grants = this.agent.getSessionPermissions();
1020
+ if (grants.length > 0) {
1021
+ grantsCleared = grants.length;
1022
+ this.agent.clearSessionPermissions();
1023
+ }
1024
+ }
1025
+ // Show mode change message (with grant clearing info if applicable)
1026
+ if (grantsCleared > 0) {
1027
+ this.ui.print({
1028
+ type: 'info',
1029
+ message: `Mode: ${nextMode} (${String(grantsCleared)} grant${grantsCleared > 1 ? 's' : ''} cleared)`,
1030
+ });
1031
+ }
1032
+ else {
1033
+ this.ui.print({ type: 'info', message: `Mode: ${nextMode}` });
1034
+ }
1035
+ });
1036
+ // Ctrl+B - show background tasks overlay
1037
+ this.ui.on('showTasks', () => {
1038
+ void (async () => {
1039
+ const { TasksOverlayV2 } = await import('./ui/overlay/impl/tasks-overlay-v2.js');
1040
+ const overlay = new TasksOverlayV2();
1041
+ await this.ui.showOverlay(overlay);
1042
+ })();
1043
+ });
1044
+ // Double Tab - show pending requests overlay
1045
+ this.ui.on('showPending', () => {
1046
+ void executeCommand('pending', '', this.createCommandContext());
1047
+ });
1048
+ // Ctrl+B during bash execution - move to background
1049
+ this.ui.on('backgroundBash', (info) => {
1050
+ const controller = this.bashAbortControllers.get(info.toolUseId);
1051
+ if (controller) {
1052
+ // Signal the bash tool to move process to ShellManager
1053
+ controller.abort('background');
1054
+ this.bashAbortControllers.delete(info.toolUseId);
1055
+ }
1056
+ });
1057
+ // Start UI (skip initial render if a startup overlay will appear immediately)
1058
+ const hasStartupOverlay = this.showLogin || this.showOnboarding || getStartupMode() === 'menu';
1059
+ this.ui.start(hasStartupOverlay);
1060
+ // Startup flow: login → onboarding → normal startup
1061
+ if (this.showLogin) {
1062
+ // Not authenticated — show branded login overlay first
1063
+ void this.handleLoginFlow();
1064
+ }
1065
+ else if (this.showOnboarding) {
1066
+ // Authenticated but first run — show onboarding wizard
1067
+ void this.showOnboardingWizard();
1068
+ }
1069
+ else {
1070
+ // Normal startup — authenticated, onboarding complete
1071
+ void this.handleStartupSequence();
1072
+ }
1073
+ }
1074
+ /**
1075
+ * Handle login flow: show branded login overlay, then continue to onboarding or normal startup.
1076
+ */
1077
+ async handleLoginFlow() {
1078
+ const loginOverlay = new LoginOverlayV2(this.version);
1079
+ const result = await this.ui.showOverlay(loginOverlay);
1080
+ if (!result?.success) {
1081
+ // Login failed or cancelled — exit
1082
+ this.stop();
1083
+ process.exit(1);
1084
+ }
1085
+ // Send forced heartbeat in background (don't block UI)
1086
+ const auth = getAuthManager();
1087
+ void auth.sendForcedHeartbeat();
1088
+ // Now check if first run
1089
+ if (!isFirstRunComplete()) {
1090
+ await this.showOnboardingWizard();
1091
+ }
1092
+ else {
1093
+ // Re-login (user did /logout then /login or restarted)
1094
+ // Print welcome banner now (was skipped before login overlay)
1095
+ // Skip if startup mode is 'menu' — the /menu command prints its own logo
1096
+ if (getStartupMode() !== 'menu') {
1097
+ this.printWelcome();
1098
+ }
1099
+ // Start session in background, show startup immediately
1100
+ void auth.startSession();
1101
+ auth.setLLMInfo(this.provider, this.model);
1102
+ await this.handleStartupSequence();
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Handle startup sequence: session restoration first, then menu if applicable.
1107
+ * This ensures proper ordering of startup overlays.
1108
+ */
1109
+ async handleStartupSequence() {
1110
+ // Start heartbeat session in background (don't block UI)
1111
+ // Note: auth was already verified in main() — no need to re-check here
1112
+ if (!this.showLogin) {
1113
+ const auth = getAuthManager();
1114
+ void auth.startSession();
1115
+ auth.setLLMInfo(this.provider, this.model);
1116
+ }
1117
+ // Non-blocking cleanup of legacy (pre-multi-terminal) files and old history
1118
+ void getProjectSessionManager().cleanupLegacyFiles();
1119
+ const retentionDays = getSessionRetentionDays();
1120
+ if (retentionDays > 0) {
1121
+ void getProjectSessionManager().cleanupOldSessions(retentionDays);
1122
+ }
1123
+ // If startup mode is 'menu', show menu first (project selection),
1124
+ // then handle session restoration for the selected project.
1125
+ const startupMode = getStartupMode();
1126
+ if (startupMode === 'menu') {
1127
+ await this.handleCommand('menu', '');
1128
+ }
1129
+ // Check session mode setting (runs after project is known)
1130
+ const sessionMode = getProjectSessionMode();
1131
+ if (sessionMode === 'auto') {
1132
+ // Auto mode: silently restore session + team
1133
+ await this.autoRestoreSession();
1134
+ }
1135
+ else if (sessionMode === 'ask') {
1136
+ // Ask mode: show overlay to let user choose
1137
+ await this.handleSessionModePrompt();
1138
+ }
1139
+ // 'fresh' mode: do nothing - start with clean session
1140
+ }
1141
+ /**
1142
+ * Restore a persisted team for a given terminal prefix.
1143
+ * Replaces the current (fresh) team with the one from disk.
1144
+ * Returns true if a team was restored.
1145
+ */
1146
+ restoreTeamForPrefix(projectId, terminalPrefix) {
1147
+ if (!this.team || !this.agentFactory)
1148
+ return false;
1149
+ const checkpointer = getTeamCheckpointer();
1150
+ const persistedTeam = checkpointer.loadTeamByPrefix(projectId, terminalPrefix, this.agentFactory);
1151
+ if (!persistedTeam || persistedTeam.size <= 1)
1152
+ return false;
1153
+ // Set the current agent as default in the restored team
1154
+ const currentAgent = this.agent;
1155
+ if (currentAgent) {
1156
+ persistedTeam.setDefaultAgent(currentAgent);
1157
+ }
1158
+ // Replace the team and update UI autocomplete
1159
+ this.team = persistedTeam;
1160
+ this.onTeamReplaced?.(persistedTeam);
1161
+ setActiveSharedContext(persistedTeam.sharedContext);
1162
+ this.ui.setAgentSuggestionsCallback(() => {
1163
+ const agents = persistedTeam.getAll();
1164
+ return agents.map(a => ({ id: a.id, displayName: a.displayName, mascot: a.mascot }));
1165
+ });
1166
+ // Populate project info in shared context
1167
+ const activeProject = getActiveProject();
1168
+ if (activeProject) {
1169
+ persistedTeam.sharedContext.setProject({
1170
+ id: activeProject.id,
1171
+ name: activeProject.displayName,
1172
+ path: activeProject.path,
1173
+ });
1174
+ }
1175
+ // Set team roster as anchor on the default agent
1176
+ if (persistedTeam.sharedContext.hasTeamRoster() && currentAgent?.hasAnchors()) {
1177
+ currentAgent.addAnchor({
1178
+ id: 'team-roster',
1179
+ content: persistedTeam.sharedContext.formatTeamRoster(),
1180
+ priority: 'info',
1181
+ scope: 'session',
1182
+ });
1183
+ }
1184
+ // Sync roster to session registry
1185
+ const registry = getSessionRegistry();
1186
+ if (registry) {
1187
+ const agents = persistedTeam.getAll();
1188
+ registry.updateAgents(agents.map(a => ({
1189
+ id: a.id,
1190
+ displayName: a.displayName,
1191
+ mascot: a.mascot,
1192
+ })));
1193
+ }
1194
+ return true;
1195
+ }
1196
+ /**
1197
+ * Auto-restore session silently (for 'auto' mode).
1198
+ * Restores both conversation history AND team roster.
1199
+ * Called at startup when projectSessionMode='auto'.
1200
+ *
1201
+ * Behavior depends on whether other terminals are active on the same project:
1202
+ * - Single terminal: silently restore the most recent available session + claim it
1203
+ * - Multiple terminals: fall back to 'ask' mode (show overlay)
1204
+ */
1205
+ async autoRestoreSession() {
1206
+ if (!this.agent)
1207
+ return;
1208
+ const activeProject = getActiveProject();
1209
+ const projectId = activeProject?.id ?? null;
1210
+ const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
1211
+ // Check how many terminals are active on this project
1212
+ // (our own terminal is already registered, so count=1 means we're alone)
1213
+ const registry = getSessionRegistry();
1214
+ const activeCount = registry?.getActiveSessionCount(projectId) ?? 1;
1215
+ if (activeCount > 1) {
1216
+ // Multiple terminals — can't auto-pick, fall back to ask mode (overlay)
1217
+ await this.offerOtherTerminalSessions(projectId);
1218
+ return;
1219
+ }
1220
+ // Single terminal — silently restore the most recent available session
1221
+ const psm = getProjectSessionManager();
1222
+ const allSessions = await psm.listProjectSessions(projectId);
1223
+ // Filter to available sessions (not from active terminals, has content)
1224
+ const available = allSessions.filter(s => {
1225
+ if (s.isActiveTerminal)
1226
+ return false;
1227
+ return s.messageCount > 0 || (s.teamAgentCount !== undefined && s.teamAgentCount > 1);
1228
+ });
1229
+ if (available.length === 0) {
1230
+ // No sessions to restore — start fresh silently
1231
+ return;
1232
+ }
1233
+ // Pick the most recent session (already sorted by updatedAt descending)
1234
+ const target = available[0];
1235
+ const targetPrefix = target.terminalPrefix;
1236
+ // Restore team roster
1237
+ if (targetPrefix) {
1238
+ this.restoreTeamForPrefix(projectId, targetPrefix);
1239
+ }
1240
+ // Load and restore conversation
1241
+ const session = targetPrefix
1242
+ ? await psm.loadProjectSessionByPrefix(projectId, targetPrefix)
1243
+ : null;
1244
+ // Claim the session files (rename to our prefix)
1245
+ if (targetPrefix && ownPrefix && targetPrefix !== ownPrefix) {
1246
+ psm.claimSession(projectId, targetPrefix, ownPrefix);
1247
+ getTeamCheckpointer().claimTeamFiles(projectId, targetPrefix, ownPrefix);
1248
+ }
1249
+ if (session && session.messages.length > 0) {
1250
+ try {
1251
+ const filteredMessages = stripThinkingBlocks(session.messages);
1252
+ await this.agent.setHistory(filteredMessages, { turnCount: session.turnCount });
1253
+ const historyItems = convertMessagesToItems(filteredMessages);
1254
+ this.ui.setRestoredHistory(historyItems);
1255
+ const parts = [`${String(session.messages.length)} messages`];
1256
+ if (this.team && this.team.size > 1) {
1257
+ parts.push(`${String(this.team.size)} agents`);
1258
+ }
1259
+ const msg = `Session restored (${parts.join(', ')}). Tip: Use Ctrl+R to view conversation history.`;
1260
+ if (getStartupMode() === 'menu') {
1261
+ this.restoredSessionInfo = msg;
1262
+ }
1263
+ else {
1264
+ this.ui.print({ type: 'info', message: msg });
1265
+ }
1266
+ }
1267
+ catch {
1268
+ // Silently fail - session restoration is best-effort
1269
+ }
1270
+ }
1271
+ else if (this.team && this.team.size > 1) {
1272
+ // 0-message session but team was restored
1273
+ const msg = `Team restored (${String(this.team.size)} agents).`;
1274
+ if (getStartupMode() === 'menu') {
1275
+ this.restoredSessionInfo = msg;
1276
+ }
1277
+ else {
1278
+ this.ui.print({ type: 'info', message: msg });
1279
+ }
1280
+ }
1281
+ }
1282
+ /**
1283
+ * When this terminal has no session but other terminals do, show the resume
1284
+ * overlay so the user can pick one or start fresh.
1285
+ */
1286
+ async offerOtherTerminalSessions(projectId) {
1287
+ if (!this.agent)
1288
+ return;
1289
+ const psm = getProjectSessionManager();
1290
+ const otherSessions = await psm.listProjectSessions(projectId);
1291
+ // Filter out sessions from active terminals (they're "in use")
1292
+ // but include 0-message sessions (they may have a team roster)
1293
+ const validSessions = otherSessions.filter(s => {
1294
+ // Exclude sessions that are actively in use by another terminal
1295
+ if (s.isActiveTerminal)
1296
+ return false;
1297
+ // Include sessions that have messages OR a team roster
1298
+ return s.messageCount > 0 || (s.teamAgentCount !== undefined && s.teamAgentCount > 1);
1299
+ });
1300
+ if (validSessions.length === 0)
1301
+ return;
1302
+ // Show info about available sessions
1303
+ const count = validSessions.length;
1304
+ this.ui.print({ type: 'info', message: `Found ${String(count)} previous session${count > 1 ? 's' : ''} for this project.` });
1305
+ // Show resume overlay
1306
+ const currentProject = getActiveProject();
1307
+ const overlay = new ResumeOverlayV2({
1308
+ sessions: validSessions,
1309
+ currentProjectId: currentProject?.id ?? null,
1310
+ onDelete: async (sessionId) => {
1311
+ // Inline delete handler (same logic as the /resume command)
1312
+ const parsed = this.parseCurrentSessionIdForStartup(sessionId);
1313
+ if (parsed?.terminalPrefix) {
1314
+ return psm.deleteSessionByPrefix(parsed.projectId, parsed.terminalPrefix);
1315
+ }
1316
+ if (parsed) {
1317
+ return psm.deleteCurrentSession(parsed.projectId);
1318
+ }
1319
+ return false;
1320
+ },
1321
+ });
1322
+ const result = await this.ui.showOverlay(overlay);
1323
+ if (result?.deleted) {
1324
+ this.ui.print({ type: 'success', message: 'Session deleted.' });
1325
+ return;
1326
+ }
1327
+ if (result?.sessionId) {
1328
+ const parsed = this.parseCurrentSessionIdForStartup(result.sessionId);
1329
+ if (!parsed)
1330
+ return;
1331
+ // Restore team roster if the selected session has a terminal prefix
1332
+ if (parsed.terminalPrefix) {
1333
+ this.restoreTeamForPrefix(parsed.projectId, parsed.terminalPrefix);
1334
+ }
1335
+ const selectedSession = parsed.terminalPrefix
1336
+ ? await psm.loadProjectSessionByPrefix(parsed.projectId, parsed.terminalPrefix)
1337
+ : await psm.loadProjectSession(parsed.projectId);
1338
+ if (!selectedSession) {
1339
+ this.ui.print({ type: 'error', message: 'Failed to load session.' });
1340
+ return;
1341
+ }
1342
+ // Claim: rename files from old prefix to this terminal's prefix
1343
+ const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
1344
+ if (parsed.terminalPrefix && ownPrefix && parsed.terminalPrefix !== ownPrefix) {
1345
+ psm.claimSession(parsed.projectId, parsed.terminalPrefix, ownPrefix);
1346
+ getTeamCheckpointer().claimTeamFiles(parsed.projectId, parsed.terminalPrefix, ownPrefix);
1347
+ }
1348
+ // Restore conversation messages (if any)
1349
+ if (selectedSession.messages.length > 0) {
1350
+ try {
1351
+ const filteredMessages = stripThinkingBlocks(selectedSession.messages);
1352
+ await this.agent.setHistory(filteredMessages, { turnCount: selectedSession.turnCount });
1353
+ this.ui.setRestoredHistory(convertMessagesToItems(filteredMessages));
1354
+ }
1355
+ catch {
1356
+ this.ui.print({ type: 'error', message: 'Failed to restore session.' });
1357
+ return;
1358
+ }
1359
+ }
1360
+ // Report what was restored
1361
+ const parts = [];
1362
+ if (selectedSession.messages.length > 0) {
1363
+ parts.push(`${String(selectedSession.messages.length)} messages`);
1364
+ }
1365
+ if (this.team && this.team.size > 1) {
1366
+ parts.push(`${String(this.team.size)} agents`);
1367
+ }
1368
+ if (parts.length > 0) {
1369
+ this.ui.print({ type: 'success', message: `Session restored (${parts.join(', ')}).` });
1370
+ }
1371
+ else {
1372
+ this.ui.print({ type: 'success', message: 'Session restored.' });
1373
+ }
1374
+ return;
1375
+ }
1376
+ // User cancelled — start fresh
1377
+ }
1378
+ /**
1379
+ * Parse session ID format for startup flow (duplicates session handler logic
1380
+ * to avoid circular import).
1381
+ */
1382
+ parseCurrentSessionIdForStartup(sessionId) {
1383
+ if (!sessionId.startsWith('current-'))
1384
+ return null;
1385
+ const rest = sessionId.slice('current-'.length);
1386
+ const hexMatch = /^([a-f0-9]{8})-(.+)$/.exec(rest);
1387
+ if (hexMatch) {
1388
+ const projectIdStr = hexMatch[2];
1389
+ return { terminalPrefix: hexMatch[1], projectId: projectIdStr === 'global' ? null : parseInt(projectIdStr, 10) };
1390
+ }
1391
+ return { terminalPrefix: null, projectId: rest === 'global' ? null : parseInt(rest, 10) };
1392
+ }
1393
+ /**
1394
+ * Handle session mode prompt at startup when projectSessionMode='ask'.
1395
+ * Shows overlay to let user choose how to handle existing session.
1396
+ */
1397
+ async handleSessionModePrompt() {
1398
+ // Get current project
1399
+ const activeProject = getActiveProject();
1400
+ const projectId = activeProject?.id ?? null;
1401
+ const projectName = activeProject?.displayName ?? 'Global';
1402
+ // Check if there's a session to load for THIS terminal
1403
+ const session = await loadProjectSession(projectId);
1404
+ if (!session) {
1405
+ // No own session — check other terminals
1406
+ await this.offerOtherTerminalSessions(projectId);
1407
+ return;
1408
+ }
1409
+ // Restore team for own session prefix
1410
+ const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
1411
+ if (ownPrefix) {
1412
+ this.restoreTeamForPrefix(projectId, ownPrefix);
1413
+ }
1414
+ if (session.messages.length === 0) {
1415
+ // Session exists but no messages — team was already restored above
1416
+ if (this.team && this.team.size > 1) {
1417
+ this.ui.print({ type: 'info', message: `Team restored (${String(this.team.size)} agents).` });
1418
+ }
1419
+ // Also check other terminals for sessions to resume
1420
+ await this.offerOtherTerminalSessions(projectId);
1421
+ return;
1422
+ }
1423
+ // Get last user message for preview
1424
+ let lastMessagePreview;
1425
+ const lastUserMsg = session.messages.filter(m => m.role === 'user').pop();
1426
+ if (lastUserMsg) {
1427
+ if (typeof lastUserMsg.content === 'string') {
1428
+ lastMessagePreview = lastUserMsg.content;
1429
+ }
1430
+ else if (Array.isArray(lastUserMsg.content)) {
1431
+ const textBlock = lastUserMsg.content.find((b) => b.type === 'text');
1432
+ if (textBlock) {
1433
+ lastMessagePreview = textBlock.text;
1434
+ }
1435
+ }
1436
+ }
1437
+ // Show the session mode overlay
1438
+ const overlay = new SessionModeOverlayV2({
1439
+ projectName,
1440
+ messageCount: session.messages.length,
1441
+ lastActivity: new Date(session.updatedAt),
1442
+ lastMessagePreview,
1443
+ });
1444
+ const result = await this.ui.showOverlay(overlay);
1445
+ if (!result) {
1446
+ // Cancelled - default to resume-full
1447
+ return this.handleSessionModeResult('resume-full', session);
1448
+ }
1449
+ await this.handleSessionModeResult(result.option, session);
1450
+ }
1451
+ /**
1452
+ * Apply the user's session mode choice.
1453
+ */
1454
+ async handleSessionModeResult(option, session) {
1455
+ if (!session)
1456
+ return;
1457
+ switch (option) {
1458
+ case 'resume-full':
1459
+ // Load the full session
1460
+ if (this.agent) {
1461
+ try {
1462
+ const filteredFull = stripThinkingBlocks(session.messages);
1463
+ await this.agent.setHistory(filteredFull, { turnCount: session.turnCount });
1464
+ this.ui.setRestoredHistory(convertMessagesToItems(filteredFull));
1465
+ this.ui.print({ type: 'success', message: `Session restored (${String(session.messages.length)} messages).` });
1466
+ this.ui.print({ type: 'info', message: 'Tip: Use Ctrl+R to view conversation history.' });
1467
+ }
1468
+ catch {
1469
+ this.ui.print({ type: 'error', message: 'Failed to restore session.' });
1470
+ }
1471
+ }
1472
+ break;
1473
+ case 'resume-messages':
1474
+ // Load messages only (context refresh is deferred - same as resume-full for now)
1475
+ if (this.agent) {
1476
+ try {
1477
+ const filteredMsgs = stripThinkingBlocks(session.messages);
1478
+ await this.agent.setHistory(filteredMsgs, { turnCount: session.turnCount });
1479
+ this.ui.setRestoredHistory(convertMessagesToItems(filteredMsgs));
1480
+ this.ui.print({ type: 'success', message: `Messages restored (${String(session.messages.length)} messages).` });
1481
+ this.ui.print({ type: 'info', message: 'Tip: Use Ctrl+R to view conversation history.' });
1482
+ }
1483
+ catch {
1484
+ this.ui.print({ type: 'error', message: 'Failed to restore session.' });
1485
+ }
1486
+ }
1487
+ break;
1488
+ case 'start-fresh':
1489
+ // Archive old session and start fresh
1490
+ try {
1491
+ await archiveCurrentSession();
1492
+ this.ui.print({ type: 'info', message: 'Previous session archived. Starting fresh.' });
1493
+ }
1494
+ catch {
1495
+ // Silent fail - archiving is best-effort
1496
+ this.ui.print({ type: 'info', message: 'Starting fresh.' });
1497
+ }
1498
+ break;
1499
+ }
1500
+ }
1501
+ /**
1502
+ * Show the onboarding wizard for first-time users.
1503
+ * Called on first run, or manually via /init command.
1504
+ */
1505
+ async showOnboardingWizard() {
1506
+ const wizard = new OnboardingWizardOverlayV2();
1507
+ const result = await this.ui.showOverlay(wizard);
1508
+ // Handle result
1509
+ if (result?.openKeys) {
1510
+ // User chose to configure keys - show keys overlay
1511
+ const keysOverlay = new KeysOverlayV2();
1512
+ await this.ui.showOverlay(keysOverlay);
1513
+ // After keys, re-show the onboarding wizard to continue
1514
+ await this.showOnboardingWizard();
1515
+ return;
1516
+ }
1517
+ // Start session heartbeat in background (don't block UI)
1518
+ const auth = getAuthManager();
1519
+ void auth.startSession();
1520
+ const startupMode = getStartupMode();
1521
+ if (startupMode === 'menu') {
1522
+ await this.handleCommand('menu', '');
1523
+ }
1524
+ }
1525
+ /**
1526
+ * Stop the REPL.
1527
+ * Aborts all running sessions (foreground and background).
1528
+ */
1529
+ /** Set MCP loading state on the footer status bar. */
1530
+ setMCPLoading(loading, toolCount) {
1531
+ this.ui.setMCPLoading(loading, toolCount);
1532
+ }
1533
+ stop() {
1534
+ // Stop notification polling before unregistering session
1535
+ try {
1536
+ const notifMgr = getNotificationManager();
1537
+ if (notifMgr) {
1538
+ notifMgr.stopPolling();
1539
+ }
1540
+ }
1541
+ catch {
1542
+ // Best effort
1543
+ }
1544
+ // Unregister from session registry
1545
+ try {
1546
+ const registry = getSessionRegistry();
1547
+ if (registry) {
1548
+ registry.unregister();
1549
+ }
1550
+ setActiveTerminalSessionId(null);
1551
+ }
1552
+ catch {
1553
+ // Best effort
1554
+ }
1555
+ // Abort all sessions
1556
+ for (const [agentId, session] of this.sessions) {
1557
+ session.abortController.abort();
1558
+ this.sessions.delete(agentId);
1559
+ }
1560
+ // Clear all message queues (don't process on shutdown)
1561
+ this.agentMessageQueues.clear();
1562
+ this.currentSession = null;
1563
+ this.foregroundAgentId = null;
1564
+ this.ui.stop();
1565
+ }
1566
+ // ===========================================================================
1567
+ // Welcome Message
1568
+ // ===========================================================================
1569
+ /**
1570
+ * Print just the logo to the scrolling area.
1571
+ * Used for dashboard mode where hints are not needed.
1572
+ */
1573
+ printLogo() {
1574
+ const settings = getSettings();
1575
+ const s = getStyles();
1576
+ terminal.clearScreen();
1577
+ // Show "What's New" highlights only when version has changed since last seen
1578
+ const isNewVersion = settings.lastSeenVersion !== this.version;
1579
+ const highlights = isNewVersion ? getStartupHighlights(this.version) : undefined;
1580
+ const logoLines = renderMascotWithLogo(settings.mascot, this.version, null, highlights);
1581
+ if (isNewVersion) {
1582
+ updateSettings({ lastSeenVersion: this.version });
1583
+ }
1584
+ console.log('');
1585
+ for (const line of logoLines) {
1586
+ console.log(line);
1587
+ }
1588
+ console.log('');
1589
+ // Show agent status
1590
+ if (this.agent) {
1591
+ console.log(s.success(' ✓ Agent connected'));
1592
+ }
1593
+ else {
1594
+ console.log(s.warning(' ⚠ Simulation mode (no agent)'));
1595
+ }
1596
+ console.log('');
1597
+ // Show model info
1598
+ console.log(s.muted('Model: ') + s.secondary(this.model));
1599
+ console.log(s.muted('Provider: ') + s.secondary(this.provider));
1600
+ console.log('');
1601
+ }
1602
+ /**
1603
+ * Print full welcome message for REPL mode.
1604
+ */
1605
+ printWelcome() {
1606
+ const s = getStyles();
1607
+ // Print logo with agent/model info
1608
+ this.printLogo();
1609
+ // Show update notification if available
1610
+ if (this.updateAvailable) {
1611
+ console.log(s.warning(` ⬆ Update available: v${this.updateAvailable.current} → v${this.updateAvailable.latest}`));
1612
+ console.log(s.muted(' Run: npm update -g @compilr-dev/cli'));
1613
+ console.log('');
1614
+ }
1615
+ // Show session hint if recent session exists (< 24h)
1616
+ // Uses fast O(1) stat check instead of scanning all session directories
1617
+ try {
1618
+ const activeProject = getActiveProject();
1619
+ const psm = getProjectSessionManager();
1620
+ if (psm.hasRecentSessionFast(activeProject?.id ?? null)) {
1621
+ console.log(s.info('Tip: Recent session available. Use /resume to continue.'));
1622
+ console.log('');
1623
+ }
1624
+ }
1625
+ catch {
1626
+ // Silently ignore errors checking for sessions
1627
+ }
1628
+ // Show in_progress plans notification
1629
+ try {
1630
+ const activeProject = getActiveProject();
1631
+ if (activeProject) {
1632
+ const inProgressPlans = planRepository.getInProgress(activeProject.id);
1633
+ if (inProgressPlans.length === 1) {
1634
+ console.log(s.info(`Tip: Plan "${inProgressPlans[0].name}" is in progress. Use /docs or Plan Mode to continue.`));
1635
+ console.log('');
1636
+ }
1637
+ else if (inProgressPlans.length > 1) {
1638
+ console.log(s.info(`Tip: You have ${String(inProgressPlans.length)} plan(s) in progress. Use /docs to view.`));
1639
+ console.log('');
1640
+ }
1641
+ }
1642
+ }
1643
+ catch {
1644
+ // Silently ignore errors checking for plans
1645
+ }
1646
+ // Hints
1647
+ console.log(s.muted('Type a message to start'));
1648
+ console.log(s.muted('Type /help for commands, /exit to quit'));
1649
+ console.log('');
1650
+ }
1651
+ // ===========================================================================
1652
+ // Command Handling
1653
+ // ===========================================================================
1654
+ createCommandContext() {
1655
+ return {
1656
+ ui: this.ui,
1657
+ version: this.version,
1658
+ updateAvailable: this.updateAvailable,
1659
+ printWelcome: () => { this.printWelcome(); return Promise.resolve(); },
1660
+ printLogo: () => { this.printLogo(); },
1661
+ consumeRestoredSessionInfo: () => {
1662
+ const info = this.restoredSessionInfo;
1663
+ this.restoredSessionInfo = null;
1664
+ return info;
1665
+ },
1666
+ // Agent integration
1667
+ agent: this.agent,
1668
+ team: this.team,
1669
+ agentFactory: this.agentFactory,
1670
+ setTeam: (newTeam) => {
1671
+ const oldTeam = this.team;
1672
+ this.team = newTeam;
1673
+ this.onTeamReplaced?.(newTeam);
1674
+ // Update agent suggestions callback with new team
1675
+ this.ui.setAgentSuggestionsCallback(() => {
1676
+ const agents = newTeam.getAll();
1677
+ return agents.map((agent) => ({
1678
+ id: agent.id,
1679
+ displayName: agent.displayName,
1680
+ mascot: agent.mascot,
1681
+ }));
1682
+ });
1683
+ return oldTeam;
1684
+ },
1685
+ restoreAgent: (newAgent) => {
1686
+ this.restoreAgent(newAgent);
1687
+ },
1688
+ model: this.model,
1689
+ provider: this.provider,
1690
+ getContextManager: () => {
1691
+ return this.agent?.getContextManager() ?? null;
1692
+ },
1693
+ getHistory: () => {
1694
+ return this.agent?.getHistory() ?? [];
1695
+ },
1696
+ // Queue agent message (for commands that invoke the agent)
1697
+ queueAgentMessage: (options) => {
1698
+ this.ui.queueAgentMessage(options);
1699
+ },
1700
+ // Session stats
1701
+ startTime: this.startTime,
1702
+ sessionInputTokens: this.sessionInputTokens,
1703
+ sessionOutputTokens: this.sessionOutputTokens,
1704
+ sessionRequests: this.sessionRequests,
1705
+ // Background session management (Phase 3c)
1706
+ getBackgroundSessions: () => {
1707
+ return this.getBackgroundSessionsInfo();
1708
+ },
1709
+ bringToForeground: async (agentId) => {
1710
+ return this.bringAgentToForeground(agentId);
1711
+ },
1712
+ switchAgent: async (agentId) => {
1713
+ await this.switchAgent(agentId);
1714
+ },
1715
+ killBackgroundAgent: (agentId) => {
1716
+ return this.killAgent(agentId);
1717
+ },
1718
+ sendForegroundToBackground: () => {
1719
+ return this.sendForegroundToBackground();
1720
+ },
1721
+ clearAllQueues: () => {
1722
+ return this.clearAllQueues();
1723
+ },
1724
+ };
1725
+ }
1726
+ async handleCommand(cmd, args) {
1727
+ // Record command usage for telemetry
1728
+ getAuthManager().recordCommand(cmd);
1729
+ const ctx = this.createCommandContext();
1730
+ const result = await executeCommand(cmd, args, ctx);
1731
+ if (result === null) {
1732
+ // Check if it's a custom command
1733
+ const customRegistry = getCustomCommandRegistry();
1734
+ if (customRegistry.has(cmd)) {
1735
+ // Expand custom command and send as message
1736
+ const customArgs = args ? args.split(' ').filter(a => a.trim()) : [];
1737
+ const expanded = customRegistry.expand(cmd, customArgs);
1738
+ if (expanded) {
1739
+ // Show user what command is being run
1740
+ this.ui.print({ type: 'user-message', text: `/${cmd}${args ? ' ' + args : ''}` });
1741
+ // Send expanded prompt to agent
1742
+ if (this.agent) {
1743
+ await this.runAgentReal(expanded);
1744
+ }
1745
+ else {
1746
+ await this.runAgentSimulation(expanded);
1747
+ }
1748
+ return;
1749
+ }
1750
+ }
1751
+ this.ui.print({ type: 'warning', message: `Unknown command: /${cmd}` });
1752
+ this.ui.print({ type: 'info', message: 'Type /help to see available commands' });
1753
+ }
1754
+ else if (!result) {
1755
+ // result === false means exit — clean up before exiting
1756
+ this.stop();
1757
+ process.exit(0);
1758
+ }
1759
+ // Process any queued agent messages (from commands like /arch, /design)
1760
+ await this.processAgentMessageQueue();
1761
+ }
1762
+ /**
1763
+ * Process queued agent messages.
1764
+ * Commands can queue messages to invoke the agent programmatically.
1765
+ */
1766
+ async processAgentMessageQueue() {
1767
+ while (this.ui.hasAgentMessage()) {
1768
+ const agentMsg = this.ui.popAgentMessage();
1769
+ if (agentMsg) {
1770
+ // Show display message to user (or full message if no display message)
1771
+ this.ui.print({
1772
+ type: 'user-message',
1773
+ text: agentMsg.displayMessage ?? agentMsg.message,
1774
+ });
1775
+ // Send full message to agent
1776
+ if (this.agent) {
1777
+ await this.runAgentReal(agentMsg.message);
1778
+ }
1779
+ else {
1780
+ await this.runAgentSimulation(agentMsg.message);
1781
+ }
1782
+ }
1783
+ }
1784
+ }
1785
+ // ===========================================================================
1786
+ // Input Processing
1787
+ // ===========================================================================
1788
+ async processInput(input) {
1789
+ // Ignore empty input
1790
+ if (!input.trim()) {
1791
+ return;
1792
+ }
1793
+ // Auth-only mode: restrict to /login, /logout, /exit, /help
1794
+ if (this.authOnly) {
1795
+ const allowedCommands = ['login', 'logout', 'exit', 'help'];
1796
+ if (input.startsWith('/')) {
1797
+ const spaceIndex = input.indexOf(' ');
1798
+ const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
1799
+ if (allowedCommands.includes(cmd.toLowerCase())) {
1800
+ const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
1801
+ await this.handleCommand(cmd, args);
1802
+ return;
1803
+ }
1804
+ }
1805
+ // Block all other input
1806
+ this.ui.print({ type: 'warning', message: 'Please log in first to use the CLI.' });
1807
+ this.ui.print({ type: 'info', message: 'Use /login to authenticate or /exit to quit.' });
1808
+ return;
1809
+ }
1810
+ // Handle $name prefix (agent switching)
1811
+ // e.g., "$arch Design the API" → switch to arch, send message
1812
+ // e.g., "$arch Design the API &" → switch to arch, run in background
1813
+ // e.g., "$pm /backlog" → switch to pm, run /backlog command
1814
+ // e.g., "$arch" → just switch to arch (no message sent)
1815
+ if (input.startsWith('$') && this.team) {
1816
+ const parsed = await this.parseAgentPrefix(input);
1817
+ if (parsed) {
1818
+ // Only process remaining input if there is one
1819
+ if (parsed.remainingInput.trim()) {
1820
+ if (parsed.isBackground) {
1821
+ // Background execution - fire and forget
1822
+ this.processInputBackground(parsed.agentId, parsed.remainingInput);
1823
+ }
1824
+ else {
1825
+ // Foreground execution - await
1826
+ await this.processInput(parsed.remainingInput);
1827
+ }
1828
+ }
1829
+ // Otherwise just switch was done, nothing more to do
1830
+ return;
1831
+ }
1832
+ // If parsing failed (unknown agent), fall through to treat as regular input
1833
+ }
1834
+ if (input.startsWith('/')) {
1835
+ const spaceIndex = input.indexOf(' ');
1836
+ const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
1837
+ const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
1838
+ await this.handleCommand(cmd, args);
1839
+ await this.processQueue();
1840
+ }
1841
+ else if (input.startsWith('!')) {
1842
+ // Handle bash command ("!ls", "!git status", etc.)
1843
+ await this.handleBashCommand(input.slice(1).trim());
1844
+ await this.processQueue();
1845
+ }
1846
+ else if (isMemoryInput(input)) {
1847
+ // Handle memory note ("# note")
1848
+ this.handleMemoryInputV2(input);
1849
+ }
1850
+ else {
1851
+ // Check for & suffix (background execution with current agent)
1852
+ const isBackground = input.trim().endsWith('&');
1853
+ const cleanInput = isBackground ? input.slice(0, -1).trim() : input;
1854
+ if (isBackground && this.team && this.agent) {
1855
+ // Background execution with current active agent
1856
+ const activeAgent = this.team.getActive();
1857
+ // Auto-expand filter if user is sending to agent not in filter
1858
+ this.autoExpandFilterIfNeeded(activeAgent.id);
1859
+ this.ui.print({ type: 'user-message', text: `${cleanInput} [background: $${activeAgent.id}]`, agentId: activeAgent.id });
1860
+ const messageToSend = this.resolveAndInjectMentions(cleanInput);
1861
+ const label = this.getAgentMascotLabel(activeAgent.id);
1862
+ this.ui.print({
1863
+ type: 'info',
1864
+ message: `${label} Started in background`,
1865
+ });
1866
+ void this.runAgentBackground(activeAgent.id, messageToSend);
1867
+ return;
1868
+ }
1869
+ // Get active agent ID for tracking
1870
+ const activeAgentId = this.team?.getActive().id ?? 'default';
1871
+ // Auto-expand filter if user is sending to agent not in filter
1872
+ this.autoExpandFilterIfNeeded(activeAgentId);
1873
+ // Print user message
1874
+ this.ui.print({ type: 'user-message', text: cleanInput, agentId: activeAgentId });
1875
+ // Resolve $agent mention references and inject context
1876
+ const messageToSend = this.resolveAndInjectMentions(cleanInput);
1877
+ // Check if the current agent has an active session (still running)
1878
+ // If so, queue the message and return immediately (non-blocking)
1879
+ const existingSession = this.sessions.get(activeAgentId);
1880
+ if (existingSession) {
1881
+ const label = this.getAgentMascotLabel(activeAgentId);
1882
+ const position = this.queueMessage(activeAgentId, messageToSend, cleanInput);
1883
+ if (position === null) {
1884
+ // Queue is full
1885
+ this.ui.print({
1886
+ type: 'warning',
1887
+ message: `${label} queue is full (${String(MAX_QUEUE_SIZE)} messages). Wait for some to complete.`,
1888
+ });
1889
+ }
1890
+ else {
1891
+ // Message queued successfully
1892
+ const queueInfo = position === 1 ? 'next in queue' : `position ${String(position)} in queue`;
1893
+ this.ui.print({
1894
+ type: 'info',
1895
+ message: `${label} is busy, message queued (${queueInfo})`,
1896
+ });
1897
+ }
1898
+ return; // Return immediately - user can switch agents or continue
1899
+ }
1900
+ // Run agent (real or simulation)
1901
+ if (this.agent) {
1902
+ await this.runAgentReal(messageToSend);
1903
+ }
1904
+ else {
1905
+ await this.runAgentSimulation(messageToSend);
1906
+ }
1907
+ await this.processQueue();
1908
+ }
1909
+ }
1910
+ /**
1911
+ * Process input in background mode.
1912
+ * Starts the agent without blocking, allowing user to continue with foreground work.
1913
+ *
1914
+ * @param agentId - The agent to run in background
1915
+ * @param input - The message to send to the agent
1916
+ */
1917
+ processInputBackground(agentId, input) {
1918
+ // Check if agent already has a running session
1919
+ if (this.hasActiveSession(agentId)) {
1920
+ this.ui.print({
1921
+ type: 'error',
1922
+ message: `Agent '${agentId}' is already running. Use /kill $${agentId} to stop it first.`,
1923
+ });
1924
+ return;
1925
+ }
1926
+ // Get mascot label for the agent
1927
+ const label = this.getAgentMascotLabel(agentId);
1928
+ // Auto-expand filter if user is sending to agent not in filter
1929
+ this.autoExpandFilterIfNeeded(agentId);
1930
+ // Print user message with background indicator
1931
+ this.ui.print({ type: 'user-message', text: `${input} [background: $${agentId}]`, agentId });
1932
+ // Resolve $agent mention references and inject context
1933
+ const messageToSend = this.resolveAndInjectMentions(input);
1934
+ // Show that background agent is starting
1935
+ this.ui.print({
1936
+ type: 'info',
1937
+ message: `${label} Started in background`,
1938
+ });
1939
+ // Start agent in background (don't await)
1940
+ if (this.agent) {
1941
+ void this.runAgentBackground(agentId, messageToSend);
1942
+ }
1943
+ else {
1944
+ // Simulation mode - just show a message
1945
+ this.ui.print({
1946
+ type: 'info',
1947
+ message: `${label} [simulation] Would run: "${messageToSend}"`,
1948
+ });
1949
+ }
1950
+ }
1951
+ /**
1952
+ * Parse $name prefix from input.
1953
+ * Returns the agent ID, remaining input, and whether this is a background request.
1954
+ * Returns null if the agent doesn't exist.
1955
+ *
1956
+ * Examples:
1957
+ * "$arch Design the API" → { agentId: 'arch', remainingInput: 'Design the API', isBackground: false }
1958
+ * "$arch Design the API &" → { agentId: 'arch', remainingInput: 'Design the API', isBackground: true }
1959
+ * "$pm /backlog" → { agentId: 'pm', remainingInput: '/backlog', isBackground: false }
1960
+ * "$arch" → { agentId: 'arch', remainingInput: '', isBackground: false }
1961
+ */
1962
+ async parseAgentPrefix(input) {
1963
+ if (!this.team || !input.startsWith('$')) {
1964
+ return null;
1965
+ }
1966
+ // Check for background flag (& at end of input)
1967
+ const isBackground = input.trim().endsWith('&');
1968
+ const cleanInput = isBackground ? input.slice(0, -1).trim() : input;
1969
+ // Extract agent ID: everything after $ until space or end
1970
+ // Note: Agent IDs are case-insensitive (stored lowercase by /team command)
1971
+ const spaceIndex = cleanInput.indexOf(' ');
1972
+ const agentId = (spaceIndex > 0 ? cleanInput.slice(1, spaceIndex) : cleanInput.slice(1)).toLowerCase();
1973
+ const remainingInput = spaceIndex > 0 ? cleanInput.slice(spaceIndex + 1).trim() : '';
1974
+ // Check if agent exists in team
1975
+ if (!this.team.has(agentId)) {
1976
+ this.ui.print({
1977
+ type: 'error',
1978
+ message: `Agent '${agentId}' not found in team`,
1979
+ });
1980
+ this.ui.print({
1981
+ type: 'info',
1982
+ message: `Available agents: ${this.team.getAgentIds().join(', ')}`,
1983
+ });
1984
+ return null;
1985
+ }
1986
+ // Switch to the agent (consolidated logic)
1987
+ try {
1988
+ const { teamAgent, demotedAgentId } = await this.switchAgent(agentId);
1989
+ if (demotedAgentId) {
1990
+ const demotedLabel = this.getAgentMascotLabel(demotedAgentId);
1991
+ this.ui.print({
1992
+ type: 'info',
1993
+ message: `${demotedLabel} sent to background (still running)`,
1994
+ });
1995
+ }
1996
+ this.ui.print({
1997
+ type: 'info',
1998
+ message: `Switched to ${teamAgent.mascot} ${teamAgent.displayName}`,
1999
+ });
2000
+ }
2001
+ catch (err) {
2002
+ this.ui.print({
2003
+ type: 'error',
2004
+ message: `Failed to switch to '${agentId}': ${err instanceof Error ? err.message : 'Unknown error'}`,
2005
+ });
2006
+ return null;
2007
+ }
2008
+ return { agentId, remainingInput, isBackground };
2009
+ }
2010
+ /**
2011
+ * Resolve $agent mention references in input and inject context.
2012
+ *
2013
+ * This handles mid-sentence mentions like:
2014
+ * "What did $arch propose for the API?"
2015
+ * "Based on $pm's plan, estimate the timeline"
2016
+ *
2017
+ * The resolved context is prepended to the message before sending to the agent.
2018
+ */
2019
+ resolveAndInjectMentions(input) {
2020
+ // Skip if no team
2021
+ if (!this.team) {
2022
+ return input;
2023
+ }
2024
+ // Get valid agent IDs
2025
+ const validAgents = new Set(this.team.getAgentIds());
2026
+ // Parse input for mentions
2027
+ const parsed = parseInputForMentions(input, validAgents);
2028
+ // If no references (just a switch or no mentions), return as-is
2029
+ if (!hasReferences(parsed)) {
2030
+ return input;
2031
+ }
2032
+ // Get artifact store for context resolution
2033
+ const currentProject = getCurrentProject();
2034
+ const projectId = currentProject?.id ?? null;
2035
+ const checkpointer = getTeamCheckpointer();
2036
+ const artifactStore = checkpointer.getArtifactStore(projectId);
2037
+ // Create context resolver
2038
+ const resolver = new ContextResolver(this.team, artifactStore);
2039
+ // Resolve all mentions
2040
+ const resolved = resolver.resolveAll(parsed.references);
2041
+ // Check if any were resolved
2042
+ let anyResolved = false;
2043
+ for (const r of resolved.values()) {
2044
+ if (r.resolved) {
2045
+ anyResolved = true;
2046
+ break;
2047
+ }
2048
+ }
2049
+ if (!anyResolved) {
2050
+ return input;
2051
+ }
2052
+ // Build context map and inject into message
2053
+ const contextMap = buildContextMap(resolved);
2054
+ const enhancedMessage = buildMessageWithContext(parsed, contextMap);
2055
+ // Log that context was injected (for debugging)
2056
+ const resolvedAgents = Array.from(resolved.entries())
2057
+ .filter(([_, r]) => r.resolved)
2058
+ .map(([id, r]) => `$${id} (${r.source}: ${r.sourceName})`);
2059
+ this.ui.print({
2060
+ type: 'info',
2061
+ message: `Injected context from: ${resolvedAgents.join(', ')}`,
2062
+ });
2063
+ return enhancedMessage;
2064
+ }
2065
+ /**
2066
+ * Handle memory note input ("# note") - V2 compatible version
2067
+ */
2068
+ handleMemoryInputV2(input) {
2069
+ const note = input.slice(1).trim();
2070
+ // Get active project to determine scope
2071
+ const activeProject = getActiveProject();
2072
+ const projectId = activeProject?.id;
2073
+ // Determine priority based on keywords
2074
+ let priority = 'info';
2075
+ const lowerNote = note.toLowerCase();
2076
+ if (lowerNote.includes('never') || lowerNote.includes('always') || lowerNote.includes('must')) {
2077
+ priority = 'safety';
2078
+ }
2079
+ if (lowerNote.includes('critical') || lowerNote.includes('important')) {
2080
+ priority = 'critical';
2081
+ }
2082
+ // Create the anchor (persisted to file)
2083
+ const anchor = addAnchor({
2084
+ content: note,
2085
+ priority,
2086
+ scope: 'persistent',
2087
+ projectId: projectId?.toString(),
2088
+ tags: ['user-note'],
2089
+ });
2090
+ // Show confirmation
2091
+ const scope = activeProject ? `project "${activeProject.displayName || activeProject.name}"` : 'global';
2092
+ const priorityLabel = priority === 'info' ? '' : ` (${priority})`;
2093
+ const truncatedNote = truncate(note, 50);
2094
+ this.ui.print({ type: 'success', message: `Saved${priorityLabel}: "${truncatedNote}" → ${scope}` });
2095
+ // Also add to agent's anchor manager if enabled (for current session)
2096
+ if (this.agent?.getAnchorManager()) {
2097
+ this.agent.addAnchor({
2098
+ id: anchor.id,
2099
+ content: note,
2100
+ priority,
2101
+ scope: 'persistent',
2102
+ tags: ['user-note'],
2103
+ });
2104
+ }
2105
+ }
2106
+ /**
2107
+ * Handle bash command ("!ls", "!git status", etc.)
2108
+ * Runs the command directly and displays output.
2109
+ * Special handling for 'cd' to change the CLI's working directory.
2110
+ */
2111
+ async handleBashCommand(command) {
2112
+ if (!command) {
2113
+ this.ui.print({ type: 'error', message: 'Usage: !<command> (e.g., !ls, !git status, !cd <path>)' });
2114
+ return;
2115
+ }
2116
+ // Special handling for 'cd' command - use process.chdir() instead of subprocess
2117
+ // This allows the CLI's working directory to actually change
2118
+ const cdMatch = command.match(/^cd\s+(.+)$/);
2119
+ if (cdMatch || command === 'cd') {
2120
+ const targetDir = cdMatch ? cdMatch[1].trim() : process.env.HOME || '/';
2121
+ // Remove quotes if present (e.g., cd "path with spaces")
2122
+ const cleanDir = targetDir.replace(/^["']|["']$/g, '');
2123
+ try {
2124
+ const path = await import('path');
2125
+ const resolvedPath = path.resolve(process.cwd(), cleanDir);
2126
+ process.chdir(resolvedPath);
2127
+ this.ui.print({ type: 'info', message: `$ cd ${cleanDir}` });
2128
+ this.ui.print({ type: 'success', message: process.cwd() });
2129
+ }
2130
+ catch (error) {
2131
+ this.ui.print({ type: 'info', message: `$ cd ${cleanDir}` });
2132
+ const err = error;
2133
+ this.ui.print({ type: 'error', message: err.message || `Cannot change to directory: ${cleanDir}` });
2134
+ }
2135
+ return;
2136
+ }
2137
+ // Show the command being run
2138
+ this.ui.print({ type: 'info', message: `$ ${command}` });
2139
+ try {
2140
+ const { execSync } = await import('child_process');
2141
+ const output = execSync(command, {
2142
+ encoding: 'utf8',
2143
+ cwd: process.cwd(),
2144
+ stdio: ['pipe', 'pipe', 'pipe'],
2145
+ timeout: 30000, // 30 second timeout
2146
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
2147
+ });
2148
+ if (output.trim()) {
2149
+ // Print output using raw type (no formatting, proper footer handling)
2150
+ this.ui.print({ type: 'raw', text: output.trimEnd() });
2151
+ }
2152
+ }
2153
+ catch (error) {
2154
+ const err = error;
2155
+ if (err.stderr) {
2156
+ // Print stderr using raw type
2157
+ this.ui.print({ type: 'raw', text: err.stderr.trimEnd() });
2158
+ }
2159
+ else if (err.message) {
2160
+ this.ui.print({ type: 'error', message: err.message });
2161
+ }
2162
+ if (err.status !== undefined) {
2163
+ this.ui.print({ type: 'info', message: `Exit code: ${String(err.status)}` });
2164
+ }
2165
+ }
2166
+ }
2167
+ async processQueue() {
2168
+ while (this.ui.hasQueuedInput()) {
2169
+ const queued = this.ui.popQueuedInput();
2170
+ if (queued) {
2171
+ await this.processInput(queued);
2172
+ }
2173
+ }
2174
+ }
2175
+ // ===========================================================================
2176
+ // Real Agent Execution
2177
+ // ===========================================================================
2178
+ async runAgentReal(userMessage) {
2179
+ if (!this.agent)
2180
+ return;
2181
+ const abortController = new AbortController();
2182
+ const signal = abortController.signal;
2183
+ const setAction = (action) => {
2184
+ if (this.currentSession) {
2185
+ this.currentSession.currentAction = action;
2186
+ }
2187
+ };
2188
+ // Clear subagent tracking state from previous runs
2189
+ // - Agent.ts tracking (activeSubagents map)
2190
+ this.clearSubagentTracking?.();
2191
+ // - LiveRegion entries (visual display of running tools)
2192
+ this.ui.clearLiveRegion();
2193
+ // Get active agent ID for session tracking
2194
+ const activeAgentId = this.team?.getActive().id ?? 'default';
2195
+ // Create session object
2196
+ const session = {
2197
+ agentId: activeAgentId,
2198
+ isBackground: false, // Foreground execution
2199
+ abortController,
2200
+ currentAction: null,
2201
+ promise: (async () => {
2202
+ try {
2203
+ this.ui.setAgentRunning(true);
2204
+ this.ui.resetTurnMetrics(); // Reset token tracking for this turn
2205
+ setAction('Thinking...');
2206
+ this.ui.setSpinnerText('Thinking...');
2207
+ // Reset text accumulator for this run (class-level so it can be flushed externally)
2208
+ this.textAccumulator = '';
2209
+ let lastToolInput = null;
2210
+ let pendingDiffLines;
2211
+ // Stream agent events (agent is guaranteed to exist in this method)
2212
+ const agent = this.agent;
2213
+ if (!agent)
2214
+ return; // Type guard
2215
+ // Update history token estimate for breakdown display
2216
+ const historyMessages = agent.getHistory();
2217
+ const historyJson = JSON.stringify(historyMessages);
2218
+ updateHistoryEstimate(estimateTokens(historyJson));
2219
+ // In plan mode, prepend plan mode instructions to the first message
2220
+ // This dynamically injects the plan mode context without recreating the agent
2221
+ let messageToSend = userMessage;
2222
+ if (this.ui.getMode() === 'plan') {
2223
+ const activePlan = this.ui.getActivePlan();
2224
+ const planModePrompt = getPlanModePrompt(activePlan.id ?? undefined, activePlan.name ?? undefined);
2225
+ messageToSend = `${planModePrompt}\n\n---\n\nUser request: ${userMessage}`;
2226
+ }
2227
+ // Inject delegation completion notifications (deferred delivery)
2228
+ // If background agents completed while coordinator was busy,
2229
+ // prepend their results to the next coordinator message
2230
+ const delegationTracker = getDelegationTracker();
2231
+ if (delegationTracker.hasCompletionEvents()) {
2232
+ const completionEvents = delegationTracker.drainCompletionEvents();
2233
+ const completionPrefix = this.formatCompletionMessage(completionEvents);
2234
+ messageToSend = `${completionPrefix}\n\n---\n\n${messageToSend}`;
2235
+ }
2236
+ // Debug logging (enable with DEBUG_AGENT_REQUESTS=1)
2237
+ debugLogAgentRequest(agent, messageToSend);
2238
+ for await (const event of agent.stream(messageToSend, {
2239
+ signal,
2240
+ chatOptions: { model: this.model },
2241
+ // Provide tool-specific context for bash backgrounding (Ctrl+B)
2242
+ getToolContext: (toolName, toolUseId) => {
2243
+ if (toolName === 'bash') {
2244
+ // Create an AbortController for this bash command
2245
+ const bashAbortController = new AbortController();
2246
+ this.bashAbortControllers.set(toolUseId, bashAbortController);
2247
+ return {
2248
+ abortSignal: bashAbortController.signal,
2249
+ onBackground: (_shellId, _partialOutput) => {
2250
+ // Clean up the controller after backgrounding
2251
+ this.bashAbortControllers.delete(toolUseId);
2252
+ // The UI already handled the notification
2253
+ },
2254
+ };
2255
+ }
2256
+ return {};
2257
+ },
2258
+ })) {
2259
+ if (signal.aborted)
2260
+ break;
2261
+ if (event.type === 'llm_chunk') {
2262
+ if (event.chunk.type === 'text' && event.chunk.text) {
2263
+ this.textAccumulator += event.chunk.text;
2264
+ }
2265
+ else if (event.chunk.type === 'thinking_start') {
2266
+ // Extended thinking started - show indicator
2267
+ this.ui.setThinking(true);
2268
+ }
2269
+ else if (event.chunk.type === 'thinking_end') {
2270
+ // Extended thinking ended - hide indicator
2271
+ this.ui.setThinking(false);
2272
+ }
2273
+ else if (event.chunk.type === 'done' && event.chunk.usage) {
2274
+ this.sessionInputTokens += event.chunk.usage.inputTokens;
2275
+ this.sessionOutputTokens += event.chunk.usage.outputTokens;
2276
+ this.sessionRequests++;
2277
+ // Record message for telemetry heartbeat
2278
+ getAuthManager().recordMessage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
2279
+ // Update live token display (including thinking tokens for Gemini 2.5+)
2280
+ const usage = event.chunk.usage;
2281
+ this.ui.addTokens(usage.inputTokens, usage.outputTokens, usage.thinkingTokens, usage.cacheReadTokens, usage.debugPayload);
2282
+ this.ui.incrementApiCalls();
2283
+ // Debug: log each API call's token usage to file (not console, to avoid UI artifacts)
2284
+ if (DEBUG_ENABLED) {
2285
+ const metrics = this.ui.getTurnMetrics();
2286
+ const debugLine = `[API Call #${String(metrics.apiCalls)}] +${String(usage.inputTokens)} in, +${String(usage.outputTokens)} out → Turn total: ${String(metrics.inputTokens)} in, ${String(metrics.outputTokens)} out\n`;
2287
+ appendFileSync(DEBUG_LOG_FILE, debugLine);
2288
+ // Log raw usage object to see all fields
2289
+ appendFileSync(DEBUG_LOG_FILE, ` Raw usage: ${JSON.stringify(event.chunk.usage)}\n`);
2290
+ }
2291
+ // Update cache info for breakdown display
2292
+ updateCacheInfo(usage.cacheReadTokens ?? 0, usage.cacheCreationTokens ?? 0);
2293
+ // Update team agent's token tracking
2294
+ if (this.team) {
2295
+ const activeTeamAgent = this.team.getActive();
2296
+ activeTeamAgent.updateTokenUsage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
2297
+ }
2298
+ }
2299
+ }
2300
+ else if (event.type === 'tool_start') {
2301
+ // Flush accumulated text before tool
2302
+ if (this.textAccumulator.trim()) {
2303
+ this.ui.print({
2304
+ type: 'agent-text',
2305
+ text: this.textAccumulator.trim(),
2306
+ expression: this.getActiveAgentMascot(),
2307
+ agentId: activeAgentId,
2308
+ });
2309
+ this.textAccumulator = '';
2310
+ }
2311
+ // Track tool calls for per-turn metrics
2312
+ this.ui.incrementToolCalls();
2313
+ // Record tool usage for telemetry
2314
+ if (event.name) {
2315
+ getAuthManager().recordTool(event.name);
2316
+ }
2317
+ lastToolInput = event.input;
2318
+ // Pre-compute diff for edit tool (must happen BEFORE tool modifies the file)
2319
+ pendingDiffLines = undefined;
2320
+ {
2321
+ // Determine the actual tool name (handle use_tool wrapping)
2322
+ const actualToolName = event.name === 'use_tool' && typeof event.input.tool_name === 'string'
2323
+ ? event.input.tool_name
2324
+ : event.name;
2325
+ const actualInput = event.name === 'use_tool' && typeof event.input.args === 'object' && event.input.args !== null
2326
+ ? event.input.args
2327
+ : event.input;
2328
+ if (actualToolName === 'edit') {
2329
+ const filePath = typeof actualInput.filePath === 'string' ? actualInput.filePath : '';
2330
+ const oldText = typeof actualInput.oldString === 'string' ? actualInput.oldString : '';
2331
+ const newText = typeof actualInput.newString === 'string' ? actualInput.newString : '';
2332
+ const replaceAll = Boolean(actualInput.replaceAll);
2333
+ if (filePath && oldText && newText) {
2334
+ const editDiff = generateEditDiff(filePath, oldText, newText, replaceAll);
2335
+ if (editDiff) {
2336
+ pendingDiffLines = editDiff.lines;
2337
+ }
2338
+ }
2339
+ }
2340
+ }
2341
+ // Debug: log tool calls to file
2342
+ if (DEBUG_ENABLED) {
2343
+ const toolDebugLine = `[Tool Call] ${event.name}(${JSON.stringify(event.input).slice(0, 200)})\n`;
2344
+ appendFileSync(DEBUG_LOG_FILE, toolDebugLine);
2345
+ }
2346
+ // For bash tool, add to live region for streaming output
2347
+ if (event.name === 'bash' && event.toolUseId) {
2348
+ const command = typeof lastToolInput.command === 'string'
2349
+ ? lastToolInput.command
2350
+ : '';
2351
+ this.ui.addBashCommand(event.toolUseId, command);
2352
+ }
2353
+ // Update spinner with tool name (skip silent tools)
2354
+ if (!this.agent?.isToolSilent(event.name)) {
2355
+ if (event.name === 'task' && event.toolUseId) {
2356
+ const subagentType = typeof lastToolInput.subagent_type === 'string'
2357
+ ? lastToolInput.subagent_type
2358
+ : 'general';
2359
+ const description = typeof lastToolInput.description === 'string'
2360
+ ? lastToolInput.description
2361
+ : '';
2362
+ setAction(`task(${subagentType}): ${description}`);
2363
+ this.ui.setSpinnerText(`task(${subagentType}): ${description}`);
2364
+ // Add subagent to LiveRegion with toolUseId
2365
+ // No queue needed - callbacks now receive toolUseId directly!
2366
+ this.ui.addSubagent(event.toolUseId, subagentType, description);
2367
+ }
2368
+ else if (event.name === 'use_tool') {
2369
+ // Show the inner tool name being called
2370
+ const innerToolName = typeof lastToolInput.tool_name === 'string'
2371
+ ? lastToolInput.tool_name
2372
+ : 'unknown';
2373
+ setAction(innerToolName);
2374
+ this.ui.setSpinnerText(`Running ${innerToolName}...`);
2375
+ }
2376
+ else if (event.name === 'list_tools') {
2377
+ // Quiet display for list_tools
2378
+ setAction('list_tools');
2379
+ this.ui.setSpinnerText('Discovering tools...');
2380
+ }
2381
+ else if (event.name === 'get_tool_info') {
2382
+ // Quiet display for get_tool_info
2383
+ const toolName = typeof lastToolInput.tool_name === 'string'
2384
+ ? lastToolInput.tool_name
2385
+ : '?';
2386
+ setAction('get_tool_info');
2387
+ this.ui.setSpinnerText(`Getting schema for ${toolName}...`);
2388
+ }
2389
+ else if (event.name !== 'task') {
2390
+ setAction(event.name);
2391
+ this.ui.setSpinnerText(`Running ${event.name}...`);
2392
+ }
2393
+ }
2394
+ }
2395
+ else if (event.type === 'tool_output') {
2396
+ // Stream bash output to live region
2397
+ if (event.toolUseId) {
2398
+ this.ui.updateBashOutput(event.toolUseId, event.output);
2399
+ }
2400
+ }
2401
+ else if (event.type === 'tool_end') {
2402
+ const toolName = event.name;
2403
+ const toolInput = lastToolInput;
2404
+ const result = event.result;
2405
+ lastToolInput = null;
2406
+ // Complete bash command in live region
2407
+ if (toolName === 'bash' && event.toolUseId) {
2408
+ // Clean up abort controller
2409
+ this.bashAbortControllers.delete(event.toolUseId);
2410
+ // Check if command was backgrounded
2411
+ if (result.success && typeof result.result === 'object' && result.result !== null) {
2412
+ const res = result.result;
2413
+ if (res.backgrounded === true) {
2414
+ // Command was moved to background - don't commit, just clear
2415
+ this.ui.setCurrentTool(null);
2416
+ continue;
2417
+ }
2418
+ }
2419
+ let exitCode = result.success ? 0 : 1;
2420
+ if (result.success && typeof result.result === 'object' && result.result !== null) {
2421
+ const res = result.result;
2422
+ if (typeof res.exitCode === 'number') {
2423
+ exitCode = res.exitCode;
2424
+ }
2425
+ }
2426
+ this.ui.completeBashCommand(event.toolUseId, exitCode);
2427
+ this.ui.commitBashCommand(event.toolUseId);
2428
+ this.ui.setCurrentTool(null);
2429
+ continue;
2430
+ }
2431
+ // Handle todo_write - update UI state
2432
+ if (toolName === 'todo_write' && toolInput && Array.isArray(toolInput.todos)) {
2433
+ const todos = toolInput.todos.map((t) => {
2434
+ const content = typeof t.content === 'string' ? t.content :
2435
+ typeof t.title === 'string' ? t.title :
2436
+ typeof t.task === 'string' ? t.task :
2437
+ typeof t.description === 'string' ? t.description :
2438
+ typeof t.text === 'string' ? t.text : 'Untitled task';
2439
+ const status = (typeof t.status === 'string' && ['pending', 'in_progress', 'completed'].includes(t.status)
2440
+ ? t.status
2441
+ : 'pending');
2442
+ const activeForm = typeof t.activeForm === 'string' ? t.activeForm : undefined;
2443
+ const owner = typeof t.owner === 'string' ? t.owner : undefined;
2444
+ const blockedBy = Array.isArray(t.blockedBy)
2445
+ ? t.blockedBy.filter((n) => typeof n === 'number')
2446
+ : undefined;
2447
+ return { content, status, activeForm, owner, blockedBy };
2448
+ });
2449
+ this.ui.setTodos(todos);
2450
+ this.ui.setCurrentTool(null);
2451
+ continue;
2452
+ }
2453
+ // Skip silent tools (no output needed)
2454
+ if (this.agent?.isToolSilent(toolName)) {
2455
+ this.ui.setCurrentTool(null);
2456
+ continue;
2457
+ }
2458
+ // Handle list_tools - show compact summary
2459
+ if (toolName === 'list_tools') {
2460
+ const innerResult = result.success && typeof result.result === 'object'
2461
+ ? result.result
2462
+ : {};
2463
+ const count = typeof innerResult.count === 'number' ? innerResult.count : 0;
2464
+ const categoryParam = toolInput && typeof toolInput.category === 'string'
2465
+ ? `category: ${toolInput.category}`
2466
+ : '';
2467
+ this.ui.print({
2468
+ type: 'tool-result',
2469
+ name: 'list_tools',
2470
+ params: categoryParam,
2471
+ summary: `${String(count)} tools available`,
2472
+ success: result.success,
2473
+ agentId: activeAgentId,
2474
+ });
2475
+ this.ui.setCurrentTool(null);
2476
+ continue;
2477
+ }
2478
+ // Handle use_tool - show the inner tool name instead
2479
+ if (toolName === 'use_tool' && toolInput) {
2480
+ const innerToolName = typeof toolInput.tool_name === 'string'
2481
+ ? toolInput.tool_name
2482
+ : 'unknown';
2483
+ const innerArgs = typeof toolInput.args === 'object' && toolInput.args !== null
2484
+ ? toolInput.args
2485
+ : {};
2486
+ // Extract and format using the inner tool name
2487
+ const { content, summary, success } = this.extractToolResult(innerToolName, result);
2488
+ // Resolve diff lines for edit (pre-computed) or write_file (computed now)
2489
+ let innerDiffLines = pendingDiffLines;
2490
+ pendingDiffLines = undefined;
2491
+ if (!innerDiffLines && (innerToolName === 'write_file' || innerToolName === 'write')) {
2492
+ const filePath = typeof innerArgs.path === 'string' ? innerArgs.path : '';
2493
+ const writeContent = typeof innerArgs.content === 'string' ? innerArgs.content : '';
2494
+ if (filePath && writeContent && success) {
2495
+ const writeDiff = generateWriteDiff(filePath, writeContent, true);
2496
+ innerDiffLines = writeDiff.lines;
2497
+ }
2498
+ }
2499
+ this.ui.print({
2500
+ type: 'tool-result',
2501
+ name: innerToolName,
2502
+ params: this.formatToolParams(innerArgs),
2503
+ summary,
2504
+ content: content || undefined,
2505
+ diffLines: innerDiffLines,
2506
+ success,
2507
+ agentId: activeAgentId,
2508
+ });
2509
+ this.ui.setCurrentTool(null);
2510
+ continue;
2511
+ }
2512
+ // Handle get_tool_info - show compact summary
2513
+ if (toolName === 'get_tool_info') {
2514
+ const innerResult = result.success && typeof result.result === 'object'
2515
+ ? result.result
2516
+ : {};
2517
+ const name = typeof innerResult.name === 'string' ? innerResult.name : '?';
2518
+ this.ui.print({
2519
+ type: 'tool-result',
2520
+ name: 'get_tool_info',
2521
+ params: name,
2522
+ summary: `Schema for ${name}`,
2523
+ success: result.success,
2524
+ agentId: activeAgentId,
2525
+ });
2526
+ this.ui.setCurrentTool(null);
2527
+ continue;
2528
+ }
2529
+ // Handle task tool like bash - use atomic commitSubagent
2530
+ if (toolName === 'task' && event.toolUseId) {
2531
+ // Extract result content
2532
+ const { content, success } = this.extractToolResult(toolName, result);
2533
+ // Use atomic commitSubagent (same pattern as commitBashCommand)
2534
+ // This removes from LiveRegion and prints in one atomic operation
2535
+ this.ui.commitSubagent(event.toolUseId, content, success);
2536
+ this.ui.setCurrentTool(null);
2537
+ continue; // Skip generic handling
2538
+ }
2539
+ // Extract and format tool result
2540
+ const { content, summary, success } = this.extractToolResult(toolName, result);
2541
+ // Resolve diff lines for edit/write_file tools
2542
+ let diffLines = pendingDiffLines;
2543
+ pendingDiffLines = undefined;
2544
+ // For write_file, generate diff from input (doesn't need pre-edit state)
2545
+ if (!diffLines && toolInput) {
2546
+ const actualToolName = toolName === 'use_tool' && typeof toolInput.tool_name === 'string'
2547
+ ? toolInput.tool_name
2548
+ : toolName;
2549
+ const actualInput = toolName === 'use_tool' && typeof toolInput.args === 'object' && toolInput.args !== null
2550
+ ? toolInput.args
2551
+ : toolInput;
2552
+ if (actualToolName === 'write_file' || actualToolName === 'write') {
2553
+ const filePath = typeof actualInput.path === 'string' ? actualInput.path : '';
2554
+ const writeContent = typeof actualInput.content === 'string' ? actualInput.content : '';
2555
+ if (filePath && writeContent && success) {
2556
+ const writeDiff = generateWriteDiff(filePath, writeContent, true);
2557
+ diffLines = writeDiff.lines;
2558
+ }
2559
+ }
2560
+ }
2561
+ this.ui.print({
2562
+ type: 'tool-result',
2563
+ name: toolName,
2564
+ params: this.formatToolParams(toolInput),
2565
+ summary,
2566
+ content: content || undefined,
2567
+ diffLines,
2568
+ success,
2569
+ agentId: activeAgentId,
2570
+ });
2571
+ this.ui.setCurrentTool(null);
2572
+ }
2573
+ else if (event.type === 'context_compacted') {
2574
+ // Context was compacted - show file restoration hints
2575
+ const hints = agent.formatRestorationHints();
2576
+ const savedTokens = event.tokensBefore - event.tokensAfter;
2577
+ const savedPct = Math.round((savedTokens / event.tokensBefore) * 100);
2578
+ this.ui.print({
2579
+ type: 'info',
2580
+ message: `Context compacted: ${event.tokensBefore.toLocaleString()} → ${event.tokensAfter.toLocaleString()} tokens (${String(savedPct)}% reduction)`,
2581
+ });
2582
+ if (hints) {
2583
+ // Display file restoration hints
2584
+ this.ui.print({
2585
+ type: 'info',
2586
+ message: hints,
2587
+ });
2588
+ }
2589
+ // Hint about Ctrl+O
2590
+ this.ui.print({
2591
+ type: 'info',
2592
+ message: 'Tip: Use Ctrl+O to view full conversation history',
2593
+ });
2594
+ }
2595
+ else if (event.type === 'context_summarized') {
2596
+ // Context was summarized (LLM-based) - more aggressive
2597
+ const savedTokens = event.tokensBefore - event.tokensAfter;
2598
+ const savedPct = Math.round((savedTokens / event.tokensBefore) * 100);
2599
+ this.ui.print({
2600
+ type: 'info',
2601
+ message: `Context summarized (${String(event.rounds)} round${event.rounds > 1 ? 's' : ''}): ${event.tokensBefore.toLocaleString()} → ${event.tokensAfter.toLocaleString()} tokens (${String(savedPct)}% reduction)`,
2602
+ });
2603
+ const hints = agent.formatRestorationHints();
2604
+ if (hints) {
2605
+ this.ui.print({
2606
+ type: 'info',
2607
+ message: hints,
2608
+ });
2609
+ }
2610
+ }
2611
+ else if (event.type === 'llm_retry') {
2612
+ // LLM call failed, retrying with exponential backoff
2613
+ const delaySec = (event.delayMs / 1000).toFixed(1);
2614
+ this.ui.print({
2615
+ type: 'warning',
2616
+ message: `Connection failed (attempt ${String(event.attempt)}/${String(event.maxAttempts)}): ${event.error}. Retrying in ${delaySec}s...`,
2617
+ });
2618
+ }
2619
+ else if (event.type === 'custom' && typeof event.name === 'string' && event.name.startsWith('delegation:')) {
2620
+ // Tool result delegation events
2621
+ const data = event.data;
2622
+ if (event.name === 'delegation:completed') {
2623
+ const originalTokens = data.originalTokens;
2624
+ const summaryTokens = data.summaryTokens;
2625
+ const toolName = data.toolName;
2626
+ this.ui.setSpinnerText(`Delegated ${toolName} result: ${String(originalTokens)} → ${String(summaryTokens)} tokens`);
2627
+ }
2628
+ }
2629
+ else if (event.type === 'llm_retry_exhausted') {
2630
+ // All retry attempts failed
2631
+ this.ui.print({
2632
+ type: 'error',
2633
+ message: `All ${String(event.attempts)} retry attempts failed: ${event.error}`,
2634
+ });
2635
+ }
2636
+ }
2637
+ // Flush remaining text and check for PLAN_READY marker
2638
+ if (this.textAccumulator.trim()) {
2639
+ const text = this.textAccumulator.trim();
2640
+ // Check for PLAN_READY marker
2641
+ const planReadyInfo = this.detectPlanReady(text);
2642
+ // Print the text (remove the marker from display if present)
2643
+ const displayText = planReadyInfo
2644
+ ? text.replace(ReplV2.PLAN_READY_REGEX, '').trim()
2645
+ : text;
2646
+ if (displayText) {
2647
+ this.ui.print({ type: 'agent-text', text: displayText, expression: this.getActiveAgentMascot(), agentId: activeAgentId });
2648
+ }
2649
+ // If PLAN_READY marker found, show approval overlay
2650
+ if (planReadyInfo) {
2651
+ await this.handlePlanApproval(planReadyInfo.planId, planReadyInfo.planName);
2652
+ }
2653
+ }
2654
+ // Print per-turn summary before stopping agent
2655
+ this.printTurnSummary();
2656
+ this.ui.setAgentRunning(false);
2657
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
2658
+ // NOTE: Don't clear todos here - they should persist after agent finishes
2659
+ // to show the final state. Only clear on explicit user action or new conversation.
2660
+ // Notify that agent has finished (for applying deferred suggestions)
2661
+ this.onAgentFinish?.();
2662
+ // Auto-save session after successful agent response
2663
+ this.autoSaveSession().catch((err) => {
2664
+ // Log but don't interrupt - saving is best-effort
2665
+ console.error('Failed to save session:', err);
2666
+ });
2667
+ }
2668
+ catch (err) {
2669
+ if (!signal.aborted) {
2670
+ this.ui.print({ type: 'error', message: String(err) });
2671
+ }
2672
+ this.ui.setAgentRunning(false);
2673
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
2674
+ // Still notify on error so suggestions can be applied
2675
+ this.onAgentFinish?.();
2676
+ }
2677
+ })(),
2678
+ };
2679
+ // Register the session (foreground)
2680
+ this.startSession(session);
2681
+ // Wait for session to complete
2682
+ await session.promise;
2683
+ this.endSession(activeAgentId);
2684
+ // Check for pending delegation and handle it
2685
+ await this.handlePendingDelegation();
2686
+ }
2687
+ // ===========================================================================
2688
+ // Delegation Completion (Phase 2 - Coordinator Mode)
2689
+ // ===========================================================================
2690
+ /**
2691
+ * Format completion events into a structured message for the coordinator.
2692
+ * Groups events by agent and includes task, result summary, and artifact info.
2693
+ */
2694
+ formatCompletionMessage(events) {
2695
+ const lines = ['[DELEGATION COMPLETIONS]', ''];
2696
+ for (const event of events) {
2697
+ const delegation = getDelegationTracker().get(event.delegationId);
2698
+ const agentLabel = this.team?.get(event.agentId)?.displayName ?? event.agentId;
2699
+ const status = event.status === 'completed' ? 'COMPLETED' : 'FAILED';
2700
+ lines.push(`## $${event.agentId} (${agentLabel}) — ${status}`);
2701
+ if (delegation) {
2702
+ lines.push(`Task: ${delegation.task}`);
2703
+ }
2704
+ if (event.result.success) {
2705
+ lines.push(`Result: ${event.result.summary}`);
2706
+ }
2707
+ else {
2708
+ lines.push(`Error: ${event.result.error ?? event.result.summary}`);
2709
+ }
2710
+ if (event.result.artifactIds.length > 0) {
2711
+ lines.push(`Artifacts: ${event.result.artifactIds.join(', ')}`);
2712
+ }
2713
+ lines.push('');
2714
+ }
2715
+ lines.push('Review the results above and decide next steps.');
2716
+ return lines.join('\n');
2717
+ }
2718
+ /**
2719
+ * Check and process delegation completions for an agent that just finished.
2720
+ * Marks all running delegations for this agent as completed or failed,
2721
+ * then attempts to auto-resume the coordinator if idle.
2722
+ *
2723
+ * @param agentId - The agent that just finished
2724
+ * @param error - If present, the agent failed with this error
2725
+ */
2726
+ checkDelegationCompletion(agentId, error) {
2727
+ const tracker = getDelegationTracker();
2728
+ const running = tracker.getByAgent(agentId).filter(d => d.status === 'running');
2729
+ if (running.length === 0)
2730
+ return;
2731
+ for (const delegation of running) {
2732
+ if (error) {
2733
+ tracker.fail(delegation.id, error);
2734
+ }
2735
+ else {
2736
+ // Gather artifacts created during this delegation
2737
+ const artifactIds = [];
2738
+ if (this.team) {
2739
+ const currentProject = getCurrentProject();
2740
+ const projectId = currentProject?.id ?? null;
2741
+ const artifactStore = getTeamCheckpointer().getArtifactStore(projectId);
2742
+ const agentArtifacts = artifactStore.listByAgent(agentId);
2743
+ for (const artifact of agentArtifacts) {
2744
+ if (artifact.createdAt >= delegation.createdAt) {
2745
+ artifactIds.push(artifact.id);
2746
+ }
2747
+ }
2748
+ }
2749
+ tracker.complete(delegation.id, {
2750
+ success: true,
2751
+ summary: `Task completed successfully${artifactIds.length > 0 ? ` with ${String(artifactIds.length)} artifact(s)` : ''}`,
2752
+ artifactIds,
2753
+ });
2754
+ }
2755
+ }
2756
+ // Try to auto-resume the coordinator (fire-and-forget — async switch must complete before runAgentReal)
2757
+ void this.maybeResumeCoordinator();
2758
+ }
2759
+ /**
2760
+ * Auto-resume the coordinator if it's idle and there are pending completion events.
2761
+ * If the coordinator is busy, events stay in queue for deferred delivery.
2762
+ *
2763
+ * Must be async to properly await switchAgent before running the coordinator.
2764
+ * Called fire-and-forget from checkDelegationCompletion — this ensures the
2765
+ * background agent's endSession() runs independently (no session conflicts).
2766
+ */
2767
+ async maybeResumeCoordinator() {
2768
+ const tracker = getDelegationTracker();
2769
+ if (!tracker.hasCompletionEvents())
2770
+ return;
2771
+ // Check if coordinator (default agent) is idle
2772
+ if (this.sessions.has('default'))
2773
+ return; // Coordinator is busy
2774
+ // Drain events and format the completion message
2775
+ const events = tracker.drainCompletionEvents();
2776
+ const completionMessage = this.formatCompletionMessage(events);
2777
+ // Switch to coordinator if not already active — MUST await before runAgentReal
2778
+ // so that this.team.getActive().id returns 'default' and the foreground session
2779
+ // is registered under the correct agent ID
2780
+ if (this.team && this.team.getActive().id !== 'default') {
2781
+ try {
2782
+ await this.switchAgent('default', {
2783
+ demoteCurrentIfBusy: false,
2784
+ trackSession: false,
2785
+ });
2786
+ }
2787
+ catch (err) {
2788
+ this.ui.print({
2789
+ type: 'error',
2790
+ message: `Failed to switch to coordinator: ${err instanceof Error ? err.message : String(err)}`,
2791
+ });
2792
+ return;
2793
+ }
2794
+ }
2795
+ // Run coordinator foreground with the completion message
2796
+ this.ui.print({
2797
+ type: 'info',
2798
+ message: 'Delegation completed — resuming coordinator...',
2799
+ });
2800
+ void this.runAgentReal(completionMessage);
2801
+ }
2802
+ /**
2803
+ * Run an agent in background (Phase 3 - Background Agents).
2804
+ * Similar to runAgentReal but:
2805
+ * - Creates a background session (isBackground: true)
2806
+ * - Labels all output with agent mascot
2807
+ * - Doesn't block the caller
2808
+ * - Shows completion notification when done
2809
+ *
2810
+ * @param agentId - The agent ID to run
2811
+ * @param userMessage - The message to send to the agent
2812
+ */
2813
+ async runAgentBackground(agentId, userMessage) {
2814
+ if (!this.agent)
2815
+ return;
2816
+ const abortController = new AbortController();
2817
+ const signal = abortController.signal;
2818
+ const label = this.getAgentMascotLabel(agentId);
2819
+ const mascot = this.team?.get(agentId)?.mascot ?? '[•_•]';
2820
+ // Create background session
2821
+ const session = {
2822
+ agentId,
2823
+ isBackground: true,
2824
+ abortController,
2825
+ currentAction: null,
2826
+ taskDescription: truncate(userMessage, 50),
2827
+ promise: (async () => {
2828
+ try {
2829
+ // Note: Don't set UI to "running" state - that's for foreground only
2830
+ let textAccumulator = '';
2831
+ // Stream agent events
2832
+ const agent = this.agent;
2833
+ if (!agent)
2834
+ return; // Type guard
2835
+ // Phase 3b: Set the executing context so permission handler knows which agent is calling
2836
+ this.currentExecutingBackgroundAgentId = agentId;
2837
+ for await (const event of agent.stream(userMessage, {
2838
+ signal,
2839
+ chatOptions: { model: this.model },
2840
+ })) {
2841
+ if (signal.aborted)
2842
+ break;
2843
+ if (event.type === 'llm_chunk') {
2844
+ if (event.chunk.type === 'text' && event.chunk.text) {
2845
+ textAccumulator += event.chunk.text;
2846
+ }
2847
+ else if (event.chunk.type === 'done' && event.chunk.usage) {
2848
+ // Update session token tracking
2849
+ this.sessionInputTokens += event.chunk.usage.inputTokens;
2850
+ this.sessionOutputTokens += event.chunk.usage.outputTokens;
2851
+ this.sessionRequests++;
2852
+ // Record message for telemetry heartbeat
2853
+ getAuthManager().recordMessage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
2854
+ // Update team agent's token tracking
2855
+ if (this.team) {
2856
+ const teamAgent = this.team.get(agentId);
2857
+ teamAgent?.updateTokenUsage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
2858
+ }
2859
+ }
2860
+ }
2861
+ else if (event.type === 'tool_start') {
2862
+ // Record tool usage for telemetry
2863
+ if (event.name) {
2864
+ getAuthManager().recordTool(event.name);
2865
+ }
2866
+ // Flush accumulated text before tool (with label)
2867
+ if (textAccumulator.trim()) {
2868
+ // Use 'background-agent' type for proper labeling without double mascot
2869
+ this.ui.print({
2870
+ type: 'background-agent',
2871
+ agentId,
2872
+ mascot,
2873
+ text: textAccumulator.trim(),
2874
+ });
2875
+ textAccumulator = '';
2876
+ }
2877
+ // Show tool start with label
2878
+ this.ui.print({
2879
+ type: 'background-agent',
2880
+ agentId,
2881
+ mascot,
2882
+ text: `Running: ${event.name}`,
2883
+ });
2884
+ }
2885
+ }
2886
+ // Flush any remaining text
2887
+ if (textAccumulator.trim()) {
2888
+ this.ui.print({
2889
+ type: 'background-agent',
2890
+ agentId,
2891
+ mascot,
2892
+ text: textAccumulator.trim(),
2893
+ });
2894
+ }
2895
+ // Show completion notification
2896
+ this.ui.print({
2897
+ type: 'info',
2898
+ message: `${label} Finished ✓`,
2899
+ });
2900
+ // Notify other terminals about completion
2901
+ try {
2902
+ const sid = getActiveTerminalSessionId();
2903
+ const proj = getActiveProject();
2904
+ if (sid && proj) {
2905
+ getNotificationManager()?.insert({
2906
+ projectId: proj.id,
2907
+ fromSessionId: sid,
2908
+ type: 'agent_finished',
2909
+ title: `${agentId} finished`,
2910
+ message: 'Task completed successfully.',
2911
+ });
2912
+ }
2913
+ }
2914
+ catch {
2915
+ // Best effort
2916
+ }
2917
+ // Check delegation completion (success path)
2918
+ this.checkDelegationCompletion(agentId);
2919
+ }
2920
+ catch (err) {
2921
+ if (!signal.aborted) {
2922
+ this.ui.print({ type: 'error', message: `${label} Error: ${String(err)}` });
2923
+ // Notify other terminals about failure
2924
+ try {
2925
+ const sid = getActiveTerminalSessionId();
2926
+ const proj = getActiveProject();
2927
+ if (sid && proj) {
2928
+ getNotificationManager()?.insert({
2929
+ projectId: proj.id,
2930
+ fromSessionId: sid,
2931
+ type: 'agent_finished',
2932
+ title: `${agentId} failed`,
2933
+ message: String(err).slice(0, 200),
2934
+ });
2935
+ }
2936
+ }
2937
+ catch {
2938
+ // Best effort
2939
+ }
2940
+ // Check delegation completion (failure path)
2941
+ this.checkDelegationCompletion(agentId, String(err));
2942
+ }
2943
+ else {
2944
+ // Killed by user — cancel delegations (no completion event)
2945
+ getDelegationTracker().cancelAllForAgent(agentId);
2946
+ }
2947
+ }
2948
+ finally {
2949
+ // Phase 3b: Clear the executing context
2950
+ if (this.currentExecutingBackgroundAgentId === agentId) {
2951
+ this.currentExecutingBackgroundAgentId = null;
2952
+ }
2953
+ }
2954
+ })(),
2955
+ };
2956
+ // Register the background session
2957
+ this.startSession(session);
2958
+ this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
2959
+ // Wait for completion (but caller doesn't await this method)
2960
+ await session.promise;
2961
+ this.endSession(agentId);
2962
+ }
2963
+ /**
2964
+ * Handle pending delegation after agent finishes.
2965
+ * If a delegation was approved during the agent run, switch to the
2966
+ * target agent and inject the task as a user message.
2967
+ */
2968
+ async handlePendingDelegation() {
2969
+ if (!this.onPendingDelegation || !this.team)
2970
+ return;
2971
+ const pending = this.onPendingDelegation();
2972
+ if (!pending)
2973
+ return;
2974
+ const { agentId, task } = pending;
2975
+ // Validate target agent still exists
2976
+ if (!this.team.has(agentId)) {
2977
+ this.ui.print({
2978
+ type: 'error',
2979
+ message: `Delegation failed: Agent '${agentId}' no longer exists in team.`,
2980
+ });
2981
+ return;
2982
+ }
2983
+ // Switch to target agent (no demotion — delegating agent just finished, no session tracking needed)
2984
+ try {
2985
+ const { teamAgent } = await this.switchAgent(agentId, {
2986
+ demoteCurrentIfBusy: false,
2987
+ trackSession: false,
2988
+ });
2989
+ this.ui.print({
2990
+ type: 'info',
2991
+ message: `Delegating to $${agentId} (${teamAgent.displayName})...`,
2992
+ });
2993
+ }
2994
+ catch (err) {
2995
+ this.ui.print({
2996
+ type: 'error',
2997
+ message: `Failed to switch to $${agentId}: ${err instanceof Error ? err.message : String(err)}`,
2998
+ });
2999
+ return;
3000
+ }
3001
+ if (!this.agent) {
3002
+ this.ui.print({
3003
+ type: 'error',
3004
+ message: `Agent $${agentId} is not initialized.`,
3005
+ });
3006
+ return;
3007
+ }
3008
+ // Inject the task as a user message and run the agent
3009
+ this.ui.print({
3010
+ type: 'user-message',
3011
+ text: task,
3012
+ });
3013
+ // Run the target agent with the delegated task
3014
+ await this.runAgentReal(task);
3015
+ }
3016
+ /**
3017
+ * Auto-save the current session and team state.
3018
+ * Called after each successful agent response.
3019
+ *
3020
+ * Saves two things:
3021
+ * 1. Default agent's session state (for /resume - conversation history)
3022
+ * 2. Team state (for team persistence - all agent states including roles)
3023
+ */
3024
+ async autoSaveSession() {
3025
+ if (!this.team)
3026
+ return;
3027
+ try {
3028
+ // Save default agent's session for /resume functionality
3029
+ const defaultAgent = this.team.get('default')?.agent;
3030
+ if (defaultAgent) {
3031
+ const state = defaultAgent.serialize();
3032
+ await saveCurrentSession(state);
3033
+ }
3034
+ // Save team state for team persistence (all agents including custom ones)
3035
+ // Only save if team has more than just the default agent, or if any agent has history
3036
+ const teamAgents = this.team.getAll();
3037
+ const hasNonDefaultAgents = teamAgents.some(ta => ta.id !== 'default');
3038
+ const hasAnyHistory = teamAgents.some(ta => {
3039
+ const agent = ta.agent;
3040
+ if (!agent)
3041
+ return false;
3042
+ const state = agent.serialize();
3043
+ return state.messages.length > 0;
3044
+ });
3045
+ if (hasNonDefaultAgents || hasAnyHistory) {
3046
+ saveCurrentTeam(this.team);
3047
+ }
3048
+ }
3049
+ catch {
3050
+ // Silently fail - session/team saving is best-effort
3051
+ }
3052
+ }
3053
+ formatToolParams(input) {
3054
+ if (!input)
3055
+ return '';
3056
+ // Format common params
3057
+ if ('path' in input)
3058
+ return String(input.path);
3059
+ if ('file_path' in input)
3060
+ return String(input.file_path);
3061
+ if ('pattern' in input)
3062
+ return String(input.pattern);
3063
+ if ('command' in input)
3064
+ return truncate(String(input.command), 50);
3065
+ // Default: show first param
3066
+ const keys = Object.keys(input);
3067
+ if (keys.length > 0) {
3068
+ const val = String(input[keys[0]]);
3069
+ return truncate(val, 50);
3070
+ }
3071
+ return '';
3072
+ }
3073
+ /**
3074
+ * Extract content and summary from tool result.
3075
+ * Delegates to tool-formatters module.
3076
+ */
3077
+ extractToolResult(toolName, result) {
3078
+ return formatToolResult(toolName, result);
3079
+ }
3080
+ // ===========================================================================
3081
+ // Agent Simulation (for standalone testing)
3082
+ // ===========================================================================
3083
+ async runAgentSimulation(userMessage) {
3084
+ const abortController = new AbortController();
3085
+ const signal = abortController.signal;
3086
+ const setAction = (action) => {
3087
+ if (this.currentSession) {
3088
+ this.currentSession.currentAction = action;
3089
+ }
3090
+ };
3091
+ // Get active agent ID for session tracking
3092
+ const activeAgentId = this.team?.getActive().id ?? 'default';
3093
+ // Create session object
3094
+ const session = {
3095
+ agentId: activeAgentId,
3096
+ isBackground: false, // Foreground execution
3097
+ abortController,
3098
+ currentAction: null,
3099
+ promise: (async () => {
3100
+ try {
3101
+ this.ui.setAgentRunning(true);
3102
+ setAction('Thinking...');
3103
+ this.ui.setSpinnerText('Thinking...');
3104
+ // 1. Initial response
3105
+ await this.sleep(1000, signal);
3106
+ if (signal.aborted)
3107
+ return;
3108
+ const response = `I understand you want me to help with "${userMessage}". Let me analyze this.`;
3109
+ this.ui.print({ type: 'agent-text', text: response, expression: this.getActiveAgentMascot() });
3110
+ // 2. Add todos
3111
+ this.ui.setTodos([
3112
+ { content: 'Analyze the request', status: 'completed' },
3113
+ { content: 'Search for relevant files', status: 'in_progress' },
3114
+ { content: 'Make changes', status: 'pending' },
3115
+ ]);
3116
+ // 3. Simulate tool
3117
+ await this.sleep(800, signal);
3118
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can be aborted externally
3119
+ if (signal.aborted)
3120
+ return;
3121
+ setAction('Glob(**/*.ts)');
3122
+ this.ui.setSpinnerText('Running Glob...');
3123
+ await this.sleep(600, signal);
3124
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can be aborted externally
3125
+ if (signal.aborted)
3126
+ return;
3127
+ this.ui.print({
3128
+ type: 'tool-result',
3129
+ name: 'Glob',
3130
+ params: '**/*.ts',
3131
+ summary: 'Found 42 files',
3132
+ content: 'src/index.ts\nsrc/config.ts\nsrc/utils/helpers.ts\n... and 39 more',
3133
+ });
3134
+ this.ui.setTodos([
3135
+ { content: 'Analyze the request', status: 'completed' },
3136
+ { content: 'Search for relevant files', status: 'completed' },
3137
+ { content: 'Make changes', status: 'in_progress' },
3138
+ ]);
3139
+ // 4. Conclusion
3140
+ await this.sleep(500, signal);
3141
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can be aborted externally
3142
+ if (signal.aborted)
3143
+ return;
3144
+ this.ui.setTodos([
3145
+ { content: 'Analyze the request', status: 'completed' },
3146
+ { content: 'Search for relevant files', status: 'completed' },
3147
+ { content: 'Make changes', status: 'completed' },
3148
+ ]);
3149
+ this.ui.print({
3150
+ type: 'agent-text',
3151
+ text: 'Done! I have completed the analysis.',
3152
+ expression: 'success',
3153
+ });
3154
+ this.ui.setAgentRunning(false);
3155
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
3156
+ // NOTE: Don't clear todos - let them persist to show final state
3157
+ // Notify that agent has finished (for applying deferred suggestions)
3158
+ this.onAgentFinish?.();
3159
+ }
3160
+ catch (err) {
3161
+ if (!signal.aborted) {
3162
+ this.ui.print({ type: 'error', message: String(err) });
3163
+ }
3164
+ this.ui.setAgentRunning(false);
3165
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
3166
+ this.onAgentFinish?.();
3167
+ }
3168
+ })(),
3169
+ };
3170
+ // Register the session (foreground)
3171
+ this.startSession(session);
3172
+ // Wait for session to complete
3173
+ await session.promise;
3174
+ this.endSession(activeAgentId);
3175
+ }
3176
+ async sleep(ms, signal) {
3177
+ return new Promise((resolve) => {
3178
+ const timeout = setTimeout(resolve, ms);
3179
+ signal?.addEventListener('abort', () => {
3180
+ clearTimeout(timeout);
3181
+ resolve();
3182
+ });
3183
+ });
3184
+ }
3185
+ // ===========================================================================
3186
+ // Turn Summary
3187
+ // ===========================================================================
3188
+ /**
3189
+ * Print per-turn summary (tokens, duration, tool calls)
3190
+ */
3191
+ printTurnSummary() {
3192
+ const metrics = this.ui.getTurnMetrics();
3193
+ // Only print summary if there were tokens used
3194
+ if (metrics.inputTokens === 0 && metrics.outputTokens === 0) {
3195
+ return;
3196
+ }
3197
+ const formatDuration = (ms) => {
3198
+ const seconds = Math.floor(ms / 1000);
3199
+ if (seconds < 60)
3200
+ return `${String(seconds)}s`;
3201
+ const minutes = Math.floor(seconds / 60);
3202
+ const remainingSeconds = seconds % 60;
3203
+ return `${String(minutes)}m ${String(remainingSeconds)}s`;
3204
+ };
3205
+ // Build summary parts
3206
+ const parts = [];
3207
+ parts.push(formatDuration(metrics.durationMs));
3208
+ // Token display with optional thinking/cache tokens
3209
+ let tokenDisplay = `↓ ${formatTokens(metrics.inputTokens)} ↑ ${formatTokens(metrics.outputTokens)}`;
3210
+ const extras = [];
3211
+ if (metrics.thinkingTokens > 0) {
3212
+ extras.push(`think: ${formatTokens(metrics.thinkingTokens)}`);
3213
+ }
3214
+ if (metrics.cacheReadTokens > 0) {
3215
+ extras.push(`cache: ${formatTokens(metrics.cacheReadTokens)}`);
3216
+ }
3217
+ if (extras.length > 0) {
3218
+ tokenDisplay += ` (${extras.join(', ')})`;
3219
+ }
3220
+ parts.push(tokenDisplay);
3221
+ // Add token breakdown
3222
+ const cacheInfo = formatCacheInfo();
3223
+ if (cacheInfo) {
3224
+ // Show cache info if available
3225
+ parts.push(cacheInfo);
3226
+ }
3227
+ else {
3228
+ // Show component breakdown
3229
+ const breakdown = formatBreakdownCompact();
3230
+ if (breakdown) {
3231
+ parts.push(breakdown);
3232
+ }
3233
+ }
3234
+ if (metrics.toolCalls > 0) {
3235
+ parts.push(`${String(metrics.toolCalls)} tool${metrics.toolCalls > 1 ? 's' : ''}`);
3236
+ }
3237
+ // DEBUG: Show payload debug info (chars sent to provider)
3238
+ if (metrics.debugPayload) {
3239
+ const { systemChars, contentsChars, toolsChars } = metrics.debugPayload;
3240
+ const totalChars = systemChars + contentsChars + toolsChars;
3241
+ const estTokens = Math.ceil(totalChars / 4);
3242
+ // Format: [dbg 2calls sent:123k (~31k est) sys:10k cont:100k tools:13k]
3243
+ parts.push(`[dbg ${String(metrics.apiCalls)}calls sent:${formatTokens(totalChars)} (~${formatTokens(estTokens)} est) sys:${formatTokens(systemChars)} cont:${formatTokens(contentsChars)} tools:${formatTokens(toolsChars)}]`);
3244
+ }
3245
+ // Print as muted info line
3246
+ this.ui.print({ type: 'turn-summary', parts });
3247
+ }
3248
+ // ===========================================================================
3249
+ // Session Management
3250
+ // ===========================================================================
3251
+ /**
3252
+ * Cancel the foreground session.
3253
+ * Background agents continue running.
3254
+ */
3255
+ cancelSession() {
3256
+ const action = this.currentSession?.currentAction ?? undefined;
3257
+ if (this.currentSession && this.foregroundAgentId) {
3258
+ this.currentSession.abortController.abort();
3259
+ this.endSession(this.foregroundAgentId);
3260
+ }
3261
+ this.ui.setAgentRunning(false);
3262
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
3263
+ this.ui.setTodos([]);
3264
+ this.ui.clearQueue();
3265
+ this.ui.print({ type: 'interrupted', action, suggestion: 'what should I do instead?' });
3266
+ }
3267
+ }
3268
+ // =============================================================================
3269
+ // Standalone Entry Point
3270
+ // =============================================================================
3271
+ // Only run main() when executed directly (not when imported)
3272
+ if (import.meta.url === `file://${process.argv[1]}`) {
3273
+ const repl = new ReplV2();
3274
+ // Handle SIGINT
3275
+ process.on('SIGINT', () => {
3276
+ console.log('\n\nInterrupted\n');
3277
+ process.exit(0);
3278
+ });
3279
+ try {
3280
+ repl.start();
3281
+ }
3282
+ catch (err) {
3283
+ console.error('Failed to start REPL:', err);
3284
+ process.exit(1);
3285
+ }
3286
+ }