@compilr-dev/cli 0.5.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 (591) hide show
  1. package/README.md +236 -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 +61 -4
  7. package/dist/agent.js +241 -245
  8. package/dist/anchors/index.d.ts +1 -1
  9. package/dist/anchors/index.js +1 -1
  10. package/dist/anchors/project-anchors.d.ts +2 -2
  11. package/dist/anchors/project-anchors.js +1 -1
  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-v2/handlers/auth.d.ts +10 -0
  23. package/dist/commands-v2/handlers/auth.js +118 -0
  24. package/dist/commands-v2/handlers/background.d.ts +14 -0
  25. package/dist/commands-v2/handlers/background.js +276 -0
  26. package/dist/commands-v2/handlers/context.js +266 -81
  27. package/dist/commands-v2/handlers/core.d.ts +1 -0
  28. package/dist/commands-v2/handlers/core.js +133 -8
  29. package/dist/commands-v2/handlers/debug.js +18 -0
  30. package/dist/commands-v2/handlers/delegations.d.ts +8 -0
  31. package/dist/commands-v2/handlers/delegations.js +29 -0
  32. package/dist/commands-v2/handlers/files.d.ts +8 -0
  33. package/dist/commands-v2/handlers/files.js +162 -0
  34. package/dist/commands-v2/handlers/filter.d.ts +9 -0
  35. package/dist/commands-v2/handlers/filter.js +130 -0
  36. package/dist/commands-v2/handlers/games.d.ts +7 -0
  37. package/dist/commands-v2/handlers/games.js +57 -0
  38. package/dist/commands-v2/handlers/index.d.ts +13 -0
  39. package/dist/commands-v2/handlers/index.js +39 -0
  40. package/dist/commands-v2/handlers/mcp.d.ts +8 -0
  41. package/dist/commands-v2/handlers/mcp.js +39 -0
  42. package/dist/commands-v2/handlers/notifications.d.ts +9 -0
  43. package/dist/commands-v2/handlers/notifications.js +34 -0
  44. package/dist/commands-v2/handlers/project.js +244 -23
  45. package/dist/commands-v2/handlers/reset.d.ts +11 -0
  46. package/dist/commands-v2/handlers/reset.js +118 -0
  47. package/dist/commands-v2/handlers/session.d.ts +161 -0
  48. package/dist/commands-v2/handlers/session.js +805 -0
  49. package/dist/commands-v2/handlers/settings.d.ts +2 -0
  50. package/dist/commands-v2/handlers/settings.js +217 -35
  51. package/dist/commands-v2/handlers/tasks.d.ts +5 -0
  52. package/dist/commands-v2/handlers/tasks.js +36 -0
  53. package/dist/commands-v2/handlers/team.d.ts +9 -0
  54. package/dist/commands-v2/handlers/team.js +549 -0
  55. package/dist/commands-v2/handlers/terminals.d.ts +9 -0
  56. package/dist/commands-v2/handlers/terminals.js +34 -0
  57. package/dist/commands-v2/index.d.ts +3 -2
  58. package/dist/commands-v2/index.js +4 -1
  59. package/dist/commands-v2/registry.d.ts +15 -0
  60. package/dist/commands-v2/registry.js +34 -0
  61. package/dist/commands-v2/types.d.ts +81 -3
  62. package/dist/commands.js +13 -0
  63. package/dist/compilr-diff-companion.vsix +0 -0
  64. package/dist/db/index.js +98 -4
  65. package/dist/db/repositories/document-repository.d.ts +2 -0
  66. package/dist/db/repositories/document-repository.js +5 -1
  67. package/dist/db/repositories/index.d.ts +2 -0
  68. package/dist/db/repositories/index.js +1 -0
  69. package/dist/db/repositories/plan-repository.d.ts +101 -0
  70. package/dist/db/repositories/plan-repository.js +275 -0
  71. package/dist/db/repositories/project-repository.d.ts +6 -0
  72. package/dist/db/repositories/project-repository.js +41 -0
  73. package/dist/db/repositories/work-item-repository.d.ts +15 -0
  74. package/dist/db/repositories/work-item-repository.js +69 -4
  75. package/dist/db/schema.d.ts +40 -3
  76. package/dist/db/schema.js +66 -3
  77. package/dist/episodes/index.d.ts +20 -0
  78. package/dist/episodes/index.js +27 -0
  79. package/dist/episodes/recorder.d.ts +51 -0
  80. package/dist/episodes/recorder.js +195 -0
  81. package/dist/episodes/significant-work.d.ts +21 -0
  82. package/dist/episodes/significant-work.js +56 -0
  83. package/dist/episodes/store.d.ts +38 -0
  84. package/dist/episodes/store.js +199 -0
  85. package/dist/episodes/types.d.ts +35 -0
  86. package/dist/episodes/types.js +6 -0
  87. package/dist/episodes/work-at-risk.d.ts +12 -0
  88. package/dist/episodes/work-at-risk.js +38 -0
  89. package/dist/episodes/work-summary-anchor.d.ts +23 -0
  90. package/dist/episodes/work-summary-anchor.js +73 -0
  91. package/dist/games/coins.d.ts +66 -0
  92. package/dist/games/coins.js +165 -0
  93. package/dist/games/game-base.d.ts +84 -0
  94. package/dist/games/game-base.js +204 -0
  95. package/dist/games/index.d.ts +16 -0
  96. package/dist/games/index.js +49 -0
  97. package/dist/games/scores.d.ts +69 -0
  98. package/dist/games/scores.js +191 -0
  99. package/dist/games/tetris/board.d.ts +59 -0
  100. package/dist/games/tetris/board.js +170 -0
  101. package/dist/games/tetris/index.d.ts +109 -0
  102. package/dist/games/tetris/index.js +610 -0
  103. package/dist/games/tetris/pieces.d.ts +44 -0
  104. package/dist/games/tetris/pieces.js +271 -0
  105. package/dist/games/tetris/renderer.d.ts +26 -0
  106. package/dist/games/tetris/renderer.js +77 -0
  107. package/dist/guide/guide-content.d.ts +23 -0
  108. package/dist/guide/guide-content.js +196 -0
  109. package/dist/guide/index.d.ts +8 -0
  110. package/dist/guide/index.js +7 -0
  111. package/dist/guide/shared-content.d.ts +37 -0
  112. package/dist/guide/shared-content.js +1272 -0
  113. package/dist/guide/tutorial-helpers.d.ts +57 -0
  114. package/dist/guide/tutorial-helpers.js +147 -0
  115. package/dist/handlers/ask-user-handlers.d.ts +32 -0
  116. package/dist/handlers/ask-user-handlers.js +104 -0
  117. package/dist/handlers/delegation-handlers.d.ts +34 -0
  118. package/dist/handlers/delegation-handlers.js +291 -0
  119. package/dist/handlers/permission-handler.d.ts +30 -0
  120. package/dist/handlers/permission-handler.js +205 -0
  121. package/dist/index.d.ts +11 -1
  122. package/dist/index.js +448 -271
  123. package/dist/input-handlers/memory-handler.d.ts +1 -1
  124. package/dist/input-handlers/memory-handler.js +2 -1
  125. package/dist/models/index.d.ts +10 -0
  126. package/dist/models/index.js +12 -0
  127. package/dist/models/model-registry.d.ts +38 -0
  128. package/dist/models/model-registry.js +69 -0
  129. package/dist/models/model-tiers.d.ts +28 -0
  130. package/dist/models/model-tiers.js +71 -0
  131. package/dist/models/model-validation.d.ts +25 -0
  132. package/dist/models/model-validation.js +291 -0
  133. package/dist/models/ollama-models.d.ts +73 -0
  134. package/dist/models/ollama-models.js +178 -0
  135. package/dist/models/provider-types.d.ts +6 -0
  136. package/dist/models/provider-types.js +1 -0
  137. package/dist/models/providers.d.ts +35 -0
  138. package/dist/models/providers.js +58 -0
  139. package/dist/models/types.d.ts +4 -0
  140. package/dist/models/types.js +4 -0
  141. package/dist/multi-agent/activity.d.ts +21 -0
  142. package/dist/multi-agent/activity.js +34 -0
  143. package/dist/multi-agent/agent-selection.d.ts +55 -0
  144. package/dist/multi-agent/agent-selection.js +90 -0
  145. package/dist/multi-agent/artifacts.d.ts +197 -0
  146. package/dist/multi-agent/artifacts.js +379 -0
  147. package/dist/multi-agent/checkpointer.d.ts +138 -0
  148. package/dist/multi-agent/checkpointer.js +471 -0
  149. package/dist/multi-agent/collision-utils.d.ts +16 -0
  150. package/dist/multi-agent/collision-utils.js +28 -0
  151. package/dist/multi-agent/context-resolver.d.ts +97 -0
  152. package/dist/multi-agent/context-resolver.js +316 -0
  153. package/dist/multi-agent/custom-agents.d.ts +83 -0
  154. package/dist/multi-agent/custom-agents.js +227 -0
  155. package/dist/multi-agent/delegation-tracker.d.ts +157 -0
  156. package/dist/multi-agent/delegation-tracker.js +243 -0
  157. package/dist/multi-agent/file-lock-hook.d.ts +29 -0
  158. package/dist/multi-agent/file-lock-hook.js +97 -0
  159. package/dist/multi-agent/file-locks.d.ts +58 -0
  160. package/dist/multi-agent/file-locks.js +194 -0
  161. package/dist/multi-agent/index.d.ts +24 -0
  162. package/dist/multi-agent/index.js +30 -0
  163. package/dist/multi-agent/mention-parser.d.ts +64 -0
  164. package/dist/multi-agent/mention-parser.js +146 -0
  165. package/dist/multi-agent/notification-manager.d.ts +84 -0
  166. package/dist/multi-agent/notification-manager.js +224 -0
  167. package/dist/multi-agent/pending-requests.d.ts +122 -0
  168. package/dist/multi-agent/pending-requests.js +155 -0
  169. package/dist/multi-agent/session-registry.d.ts +139 -0
  170. package/dist/multi-agent/session-registry.js +514 -0
  171. package/dist/multi-agent/shared-context.d.ts +293 -0
  172. package/dist/multi-agent/shared-context.js +671 -0
  173. package/dist/multi-agent/skill-requirements.d.ts +66 -0
  174. package/dist/multi-agent/skill-requirements.js +178 -0
  175. package/dist/multi-agent/task-assignment.d.ts +69 -0
  176. package/dist/multi-agent/task-assignment.js +123 -0
  177. package/dist/multi-agent/task-suggestion.d.ts +31 -0
  178. package/dist/multi-agent/task-suggestion.js +72 -0
  179. package/dist/multi-agent/team-agent.d.ts +201 -0
  180. package/dist/multi-agent/team-agent.js +488 -0
  181. package/dist/multi-agent/team.d.ts +286 -0
  182. package/dist/multi-agent/team.js +610 -0
  183. package/dist/multi-agent/tool-config.d.ts +110 -0
  184. package/dist/multi-agent/tool-config.js +661 -0
  185. package/dist/multi-agent/types.d.ts +211 -0
  186. package/dist/multi-agent/types.js +617 -0
  187. package/dist/prompts/plan-mode-prompt.d.ts +11 -0
  188. package/dist/prompts/plan-mode-prompt.js +95 -0
  189. package/dist/repl-helpers.js +3 -2
  190. package/dist/repl-v2.d.ts +401 -2
  191. package/dist/repl-v2.js +2577 -65
  192. package/dist/session/index.d.ts +6 -0
  193. package/dist/session/index.js +6 -0
  194. package/dist/session/project-session-manager.d.ts +158 -0
  195. package/dist/session/project-session-manager.js +650 -0
  196. package/dist/settings/index.d.ts +133 -13
  197. package/dist/settings/index.js +329 -24
  198. package/dist/settings/mcp-config.d.ts +76 -0
  199. package/dist/settings/mcp-config.js +143 -0
  200. package/dist/settings/paths.d.ts +4 -0
  201. package/dist/settings/paths.js +6 -0
  202. package/dist/shared-handlers.d.ts +62 -0
  203. package/dist/shared-handlers.js +48 -0
  204. package/dist/system-prompt/builder.d.ts +5 -0
  205. package/dist/system-prompt/builder.js +4 -0
  206. package/dist/system-prompt/index.d.ts +18 -0
  207. package/dist/system-prompt/index.js +18 -0
  208. package/dist/system-prompt/modules.d.ts +5 -0
  209. package/dist/system-prompt/modules.js +4 -0
  210. package/dist/tabbed-menu.js +2 -1
  211. package/dist/templates/compilr-md-import.d.ts +16 -0
  212. package/dist/templates/compilr-md-import.js +241 -0
  213. package/dist/templates/compilr-md.js +10 -61
  214. package/dist/templates/config-json.d.ts +1 -25
  215. package/dist/templates/index.d.ts +2 -0
  216. package/dist/templates/index.js +34 -73
  217. package/dist/tool-names.d.ts +108 -0
  218. package/dist/tool-names.js +227 -0
  219. package/dist/tools/anchor-tools.d.ts +4 -4
  220. package/dist/tools/anchor-tools.js +1 -1
  221. package/dist/tools/artifact-tools.d.ts +42 -0
  222. package/dist/tools/artifact-tools.js +328 -0
  223. package/dist/tools/ask-user-simple.d.ts +1 -1
  224. package/dist/tools/ask-user-simple.js +2 -1
  225. package/dist/tools/ask-user.d.ts +1 -1
  226. package/dist/tools/ask-user.js +2 -1
  227. package/dist/tools/backlog-wrappers.d.ts +5 -3
  228. package/dist/tools/backlog-wrappers.js +16 -1
  229. package/dist/tools/backlog.d.ts +2 -2
  230. package/dist/tools/backlog.js +1 -1
  231. package/dist/tools/db-tools.d.ts +8 -61
  232. package/dist/tools/db-tools.js +8 -13
  233. package/dist/tools/delegate-background.d.ts +27 -0
  234. package/dist/tools/delegate-background.js +115 -0
  235. package/dist/tools/delegate.d.ts +22 -0
  236. package/dist/tools/delegate.js +97 -0
  237. package/dist/tools/delegation-status.d.ts +16 -0
  238. package/dist/tools/delegation-status.js +128 -0
  239. package/dist/tools/document-db.d.ts +5 -5
  240. package/dist/tools/document-db.js +1 -1
  241. package/dist/tools/guide-tool.d.ts +12 -0
  242. package/dist/tools/guide-tool.js +59 -0
  243. package/dist/tools/handoff.d.ts +25 -0
  244. package/dist/tools/handoff.js +99 -0
  245. package/dist/tools/meta-tools.d.ts +26 -0
  246. package/dist/tools/meta-tools.js +47 -0
  247. package/dist/tools/plan-tools.d.ts +54 -0
  248. package/dist/tools/plan-tools.js +338 -0
  249. package/dist/tools/platform-adapter.d.ts +29 -0
  250. package/dist/tools/platform-adapter.js +394 -0
  251. package/dist/tools/project-db.d.ts +5 -73
  252. package/dist/tools/project-db.js +5 -336
  253. package/dist/tools/recall-work-tool.d.ts +18 -0
  254. package/dist/tools/recall-work-tool.js +82 -0
  255. package/dist/tools/workitem-db.d.ts +43 -11
  256. package/dist/tools/workitem-db.js +186 -5
  257. package/dist/tools.d.ts +67 -2
  258. package/dist/tools.js +237 -47
  259. package/dist/ui/autocomplete-controller.d.ts +42 -0
  260. package/dist/ui/autocomplete-controller.js +384 -0
  261. package/dist/ui/base/index.d.ts +1 -1
  262. package/dist/ui/base/index.js +1 -1
  263. package/dist/ui/base/overlay-base-v2.d.ts +10 -0
  264. package/dist/ui/base/overlay-base-v2.js +14 -0
  265. package/dist/ui/base/render-utils.d.ts +19 -0
  266. package/dist/ui/base/render-utils.js +25 -0
  267. package/dist/ui/base/tabbed-list-overlay-v2.d.ts +16 -1
  268. package/dist/ui/base/tabbed-list-overlay-v2.js +19 -1
  269. package/dist/ui/constants/labels.d.ts +14 -0
  270. package/dist/ui/constants/labels.js +51 -0
  271. package/dist/ui/conversation-store.d.ts +55 -0
  272. package/dist/ui/conversation-store.js +107 -0
  273. package/dist/ui/conversation.js +11 -13
  274. package/dist/ui/diff.d.ts +7 -1
  275. package/dist/ui/diff.js +85 -48
  276. package/dist/ui/ephemeral.js +3 -9
  277. package/dist/ui/file-autocomplete.d.ts +24 -0
  278. package/dist/ui/file-autocomplete.js +56 -0
  279. package/dist/ui/footer-renderer.d.ts +69 -0
  280. package/dist/ui/footer-renderer.js +431 -0
  281. package/dist/ui/footer.d.ts +74 -7
  282. package/dist/ui/footer.js +173 -16
  283. package/dist/ui/input-controller.d.ts +51 -0
  284. package/dist/ui/input-controller.js +176 -0
  285. package/dist/ui/input-prompt.d.ts +19 -0
  286. package/dist/ui/input-prompt.js +206 -14
  287. package/dist/ui/keyboard-handler.d.ts +57 -0
  288. package/dist/ui/keyboard-handler.js +557 -0
  289. package/dist/ui/live-region-facade.d.ts +42 -0
  290. package/dist/ui/live-region-facade.js +205 -0
  291. package/dist/ui/live-region.d.ts +0 -4
  292. package/dist/ui/live-region.js +6 -14
  293. package/dist/ui/mascot/renderer.d.ts +1 -1
  294. package/dist/ui/mascot/renderer.js +37 -2
  295. package/dist/ui/overlay/data/tutorial-content.d.ts +9 -0
  296. package/dist/ui/overlay/data/tutorial-content.js +9 -0
  297. package/dist/ui/overlay/data/tutorial-registry.d.ts +12 -0
  298. package/dist/ui/overlay/data/tutorial-registry.js +116 -0
  299. package/dist/ui/overlay/data/tutorial-types.d.ts +35 -0
  300. package/dist/ui/overlay/data/tutorial-types.js +6 -0
  301. package/dist/ui/overlay/data/tutorials/basics/first-conversation.d.ts +7 -0
  302. package/dist/ui/overlay/data/tutorials/basics/first-conversation.js +220 -0
  303. package/dist/ui/overlay/data/tutorials/basics/first-project.d.ts +7 -0
  304. package/dist/ui/overlay/data/tutorials/basics/first-project.js +284 -0
  305. package/dist/ui/overlay/data/tutorials/basics/navigation.d.ts +8 -0
  306. package/dist/ui/overlay/data/tutorials/basics/navigation.js +22 -0
  307. package/dist/ui/overlay/data/tutorials/basics/welcome.d.ts +7 -0
  308. package/dist/ui/overlay/data/tutorials/basics/welcome.js +174 -0
  309. package/dist/ui/overlay/data/tutorials/config/context-management.d.ts +7 -0
  310. package/dist/ui/overlay/data/tutorials/config/context-management.js +158 -0
  311. package/dist/ui/overlay/data/tutorials/config/mcp-servers.d.ts +8 -0
  312. package/dist/ui/overlay/data/tutorials/config/mcp-servers.js +155 -0
  313. package/dist/ui/overlay/data/tutorials/config/model-selection.d.ts +7 -0
  314. package/dist/ui/overlay/data/tutorials/config/model-selection.js +162 -0
  315. package/dist/ui/overlay/data/tutorials/config/permissions-safety.d.ts +7 -0
  316. package/dist/ui/overlay/data/tutorials/config/permissions-safety.js +163 -0
  317. package/dist/ui/overlay/data/tutorials/config/settings-config.d.ts +7 -0
  318. package/dist/ui/overlay/data/tutorials/config/settings-config.js +166 -0
  319. package/dist/ui/overlay/data/tutorials/planning/arch.d.ts +7 -0
  320. package/dist/ui/overlay/data/tutorials/planning/arch.js +168 -0
  321. package/dist/ui/overlay/data/tutorials/planning/backlog.d.ts +7 -0
  322. package/dist/ui/overlay/data/tutorials/planning/backlog.js +103 -0
  323. package/dist/ui/overlay/data/tutorials/planning/build.d.ts +7 -0
  324. package/dist/ui/overlay/data/tutorials/planning/build.js +173 -0
  325. package/dist/ui/overlay/data/tutorials/planning/design.d.ts +7 -0
  326. package/dist/ui/overlay/data/tutorials/planning/design.js +205 -0
  327. package/dist/ui/overlay/data/tutorials/planning/docs.d.ts +7 -0
  328. package/dist/ui/overlay/data/tutorials/planning/docs.js +143 -0
  329. package/dist/ui/overlay/data/tutorials/planning/prd.d.ts +7 -0
  330. package/dist/ui/overlay/data/tutorials/planning/prd.js +173 -0
  331. package/dist/ui/overlay/data/tutorials/planning/scaffold.d.ts +7 -0
  332. package/dist/ui/overlay/data/tutorials/planning/scaffold.js +164 -0
  333. package/dist/ui/overlay/data/tutorials/planning/sketch.d.ts +7 -0
  334. package/dist/ui/overlay/data/tutorials/planning/sketch.js +58 -0
  335. package/dist/ui/overlay/data/tutorials/projects/anchors.d.ts +7 -0
  336. package/dist/ui/overlay/data/tutorials/projects/anchors.js +248 -0
  337. package/dist/ui/overlay/data/tutorials/projects/import-project.d.ts +7 -0
  338. package/dist/ui/overlay/data/tutorials/projects/import-project.js +172 -0
  339. package/dist/ui/overlay/data/tutorials/projects/managing-projects.d.ts +8 -0
  340. package/dist/ui/overlay/data/tutorials/projects/managing-projects.js +212 -0
  341. package/dist/ui/overlay/data/tutorials/projects/new-project.d.ts +7 -0
  342. package/dist/ui/overlay/data/tutorials/projects/new-project.js +251 -0
  343. package/dist/ui/overlay/data/tutorials/projects/session-management.d.ts +7 -0
  344. package/dist/ui/overlay/data/tutorials/projects/session-management.js +169 -0
  345. package/dist/ui/overlay/data/tutorials/teams/background-execution.d.ts +7 -0
  346. package/dist/ui/overlay/data/tutorials/teams/background-execution.js +171 -0
  347. package/dist/ui/overlay/data/tutorials/teams/multi-terminal.d.ts +8 -0
  348. package/dist/ui/overlay/data/tutorials/teams/multi-terminal.js +147 -0
  349. package/dist/ui/overlay/data/tutorials/teams/task-assignment.d.ts +7 -0
  350. package/dist/ui/overlay/data/tutorials/teams/task-assignment.js +204 -0
  351. package/dist/ui/overlay/data/tutorials/teams/team-overview.d.ts +7 -0
  352. package/dist/ui/overlay/data/tutorials/teams/team-overview.js +165 -0
  353. package/dist/ui/overlay/data/tutorials/teams/working-with-agents.d.ts +7 -0
  354. package/dist/ui/overlay/data/tutorials/teams/working-with-agents.js +172 -0
  355. package/dist/ui/overlay/impl/agents-overlay-v2.js +6 -17
  356. package/dist/ui/overlay/impl/anchors-overlay-v2.js +30 -64
  357. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.d.ts +43 -0
  358. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +232 -0
  359. package/dist/ui/overlay/impl/artifact-overlay-v2.d.ts +40 -0
  360. package/dist/ui/overlay/impl/artifact-overlay-v2.js +115 -0
  361. package/dist/ui/overlay/impl/ask-user-overlay-v2.js +2 -5
  362. package/dist/ui/overlay/impl/background-overlay-v2.d.ts +40 -0
  363. package/dist/ui/overlay/impl/background-overlay-v2.js +147 -0
  364. package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +4 -1
  365. package/dist/ui/overlay/impl/backlog-overlay-v2.js +55 -16
  366. package/dist/ui/overlay/impl/changelog-overlay-v2.d.ts +44 -0
  367. package/dist/ui/overlay/impl/changelog-overlay-v2.js +165 -0
  368. package/dist/ui/overlay/impl/commands-overlay-v2.js +4 -6
  369. package/dist/ui/overlay/impl/config-overlay-v2.d.ts +12 -1
  370. package/dist/ui/overlay/impl/config-overlay-v2.js +164 -100
  371. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +83 -0
  372. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +711 -0
  373. package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +2 -0
  374. package/dist/ui/overlay/impl/dashboard-overlay-v2.js +26 -3
  375. package/dist/ui/overlay/impl/delegations-overlay-v2.d.ts +28 -0
  376. package/dist/ui/overlay/impl/delegations-overlay-v2.js +279 -0
  377. package/dist/ui/overlay/impl/docs-overlay-v2.js +12 -9
  378. package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +7 -0
  379. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +119 -78
  380. package/dist/ui/overlay/impl/filter-overlay-v2.d.ts +41 -0
  381. package/dist/ui/overlay/impl/filter-overlay-v2.js +110 -0
  382. package/dist/ui/overlay/impl/games-overlay-v2.d.ts +31 -0
  383. package/dist/ui/overlay/impl/games-overlay-v2.js +135 -0
  384. package/dist/ui/overlay/impl/help-overlay-v2.d.ts +26 -3
  385. package/dist/ui/overlay/impl/help-overlay-v2.js +20 -42
  386. package/dist/ui/overlay/impl/login-overlay-v2.d.ts +49 -0
  387. package/dist/ui/overlay/impl/login-overlay-v2.js +277 -0
  388. package/dist/ui/overlay/impl/mcp-overlay-v2.d.ts +63 -0
  389. package/dist/ui/overlay/impl/mcp-overlay-v2.js +907 -0
  390. package/dist/ui/overlay/impl/model-overlay-v2.d.ts +57 -13
  391. package/dist/ui/overlay/impl/model-overlay-v2.js +1086 -61
  392. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +37 -6
  393. package/dist/ui/overlay/impl/new-overlay-v2.js +715 -65
  394. package/dist/ui/overlay/impl/notifications-overlay-v2.d.ts +20 -0
  395. package/dist/ui/overlay/impl/notifications-overlay-v2.js +116 -0
  396. package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.d.ts +76 -0
  397. package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.js +728 -0
  398. package/dist/ui/overlay/impl/pending-overlay-v2.d.ts +51 -0
  399. package/dist/ui/overlay/impl/pending-overlay-v2.js +445 -0
  400. package/dist/ui/overlay/impl/permission-overlay-v2.js +5 -5
  401. package/dist/ui/overlay/impl/permissions-overlay-v2.d.ts +85 -0
  402. package/dist/ui/overlay/impl/permissions-overlay-v2.js +820 -0
  403. package/dist/ui/overlay/impl/plan-approval-overlay-v2.d.ts +35 -0
  404. package/dist/ui/overlay/impl/plan-approval-overlay-v2.js +181 -0
  405. package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +36 -0
  406. package/dist/ui/overlay/impl/project-edit-overlay-v2.js +195 -0
  407. package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +1 -0
  408. package/dist/ui/overlay/impl/projects-overlay-v2.js +278 -44
  409. package/dist/ui/overlay/impl/reset-overlay-v2.d.ts +39 -0
  410. package/dist/ui/overlay/impl/reset-overlay-v2.js +107 -0
  411. package/dist/ui/overlay/impl/resume-overlay-v2.d.ts +60 -0
  412. package/dist/ui/overlay/impl/resume-overlay-v2.js +414 -0
  413. package/dist/ui/overlay/impl/session-mode-overlay-v2.d.ts +43 -0
  414. package/dist/ui/overlay/impl/session-mode-overlay-v2.js +124 -0
  415. package/dist/ui/overlay/impl/tasks-overlay-v2.d.ts +28 -0
  416. package/dist/ui/overlay/impl/tasks-overlay-v2.js +283 -0
  417. package/dist/ui/overlay/impl/team-overlay-v2.d.ts +86 -0
  418. package/dist/ui/overlay/impl/team-overlay-v2.js +692 -0
  419. package/dist/ui/overlay/impl/terminals-overlay-v2.d.ts +26 -0
  420. package/dist/ui/overlay/impl/terminals-overlay-v2.js +217 -0
  421. package/dist/ui/overlay/impl/tools-overlay-v2.js +3 -7
  422. package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +30 -16
  423. package/dist/ui/overlay/impl/tutorial-overlay-v2.js +133 -956
  424. package/dist/ui/overlay/impl/workflow-overlay-v2.js +5 -1
  425. package/dist/ui/overlay/index.d.ts +20 -1
  426. package/dist/ui/overlay/index.js +19 -0
  427. package/dist/ui/overlay/types.d.ts +5 -0
  428. package/dist/ui/overlay-manager.d.ts +43 -0
  429. package/dist/ui/overlay-manager.js +238 -0
  430. package/dist/ui/overlays.js +4 -16
  431. package/dist/ui/permission-overlay.js +6 -5
  432. package/dist/ui/status-bar-controller.d.ts +33 -0
  433. package/dist/ui/status-bar-controller.js +99 -0
  434. package/dist/ui/subagent-renderer.js +3 -19
  435. package/dist/ui/terminal-autocomplete-utils.d.ts +23 -0
  436. package/dist/ui/terminal-autocomplete-utils.js +83 -0
  437. package/dist/ui/terminal-line-builders.d.ts +17 -0
  438. package/dist/ui/terminal-line-builders.js +42 -0
  439. package/dist/ui/terminal-render-item.d.ts +16 -0
  440. package/dist/ui/terminal-render-item.js +267 -0
  441. package/dist/ui/terminal-renderer.d.ts +7 -8
  442. package/dist/ui/terminal-renderer.js +7 -8
  443. package/dist/ui/terminal-types.d.ts +179 -0
  444. package/dist/ui/terminal-types.js +34 -0
  445. package/dist/ui/terminal-ui.d.ts +144 -276
  446. package/dist/ui/terminal-ui.js +384 -1861
  447. package/dist/ui/todo-zone.d.ts +19 -1
  448. package/dist/ui/todo-zone.js +71 -13
  449. package/dist/ui/tool-formatters.js +512 -1
  450. package/dist/ui/turn-metrics.d.ts +56 -0
  451. package/dist/ui/turn-metrics.js +75 -0
  452. package/dist/ui/types.d.ts +28 -0
  453. package/dist/ui/types.js +1 -0
  454. package/dist/ui/vscode-diff-ipc.d.ts +102 -0
  455. package/dist/ui/vscode-diff-ipc.js +385 -0
  456. package/dist/utils/credentials.d.ts +24 -5
  457. package/dist/utils/credentials.js +123 -9
  458. package/dist/utils/format-tokens.d.ts +13 -0
  459. package/dist/utils/format-tokens.js +18 -0
  460. package/dist/utils/git-config.d.ts +26 -0
  461. package/dist/utils/git-config.js +54 -0
  462. package/dist/utils/message-utils.d.ts +61 -0
  463. package/dist/utils/message-utils.js +72 -0
  464. package/dist/utils/model-tiers.d.ts +8 -1
  465. package/dist/utils/model-tiers.js +38 -16
  466. package/dist/utils/open-browser.d.ts +5 -0
  467. package/dist/utils/open-browser.js +32 -0
  468. package/dist/utils/path-safety.js +3 -2
  469. package/dist/utils/project-detection.d.ts +58 -0
  470. package/dist/utils/project-detection.js +424 -0
  471. package/dist/utils/project-memory.js +2 -1
  472. package/dist/utils/project-status.d.ts +2 -2
  473. package/dist/utils/startup-perf.d.ts +18 -0
  474. package/dist/utils/startup-perf.js +60 -0
  475. package/dist/utils/token-tracker.d.ts +62 -0
  476. package/dist/utils/token-tracker.js +150 -0
  477. package/dist/utils/token-types.d.ts +23 -0
  478. package/dist/utils/token-types.js +18 -0
  479. package/dist/utils/types/config-types.d.ts +32 -0
  480. package/dist/utils/types/config-types.js +8 -0
  481. package/dist/utils/update-checker.d.ts +28 -0
  482. package/dist/utils/update-checker.js +106 -0
  483. package/dist/utils/version.d.ts +7 -0
  484. package/dist/utils/version.js +10 -0
  485. package/dist/utils/vscode-detect.d.ts +39 -0
  486. package/dist/utils/vscode-detect.js +137 -0
  487. package/package.json +25 -12
  488. package/dist/commands/handler-types.d.ts +0 -68
  489. package/dist/commands/handler-types.js +0 -8
  490. package/dist/commands/handlers/agent-commands.d.ts +0 -13
  491. package/dist/commands/handlers/agent-commands.js +0 -305
  492. package/dist/commands/handlers/design-commands.d.ts +0 -15
  493. package/dist/commands/handlers/design-commands.js +0 -334
  494. package/dist/commands/handlers/index.d.ts +0 -20
  495. package/dist/commands/handlers/index.js +0 -43
  496. package/dist/commands/handlers/overlay-commands.d.ts +0 -21
  497. package/dist/commands/handlers/overlay-commands.js +0 -287
  498. package/dist/commands/handlers/project-commands.d.ts +0 -11
  499. package/dist/commands/handlers/project-commands.js +0 -167
  500. package/dist/commands/handlers/simple-commands.d.ts +0 -19
  501. package/dist/commands/handlers/simple-commands.js +0 -144
  502. package/dist/commands/registry.d.ts +0 -50
  503. package/dist/commands/registry.js +0 -75
  504. package/dist/index.old.d.ts +0 -7
  505. package/dist/index.old.js +0 -1014
  506. package/dist/repl.d.ts +0 -149
  507. package/dist/repl.js +0 -1151
  508. package/dist/templates/claude-md.d.ts +0 -7
  509. package/dist/templates/claude-md.js +0 -189
  510. package/dist/test-autocomplete.d.ts +0 -7
  511. package/dist/test-autocomplete.js +0 -85
  512. package/dist/test-tabbed-menu.d.ts +0 -7
  513. package/dist/test-tabbed-menu.js +0 -25
  514. package/dist/tool-selector.d.ts +0 -71
  515. package/dist/tool-selector.js +0 -184
  516. package/dist/ui/agents-overlay-v2.d.ts +0 -43
  517. package/dist/ui/agents-overlay-v2.js +0 -809
  518. package/dist/ui/agents-overlay.d.ts +0 -12
  519. package/dist/ui/agents-overlay.js +0 -863
  520. package/dist/ui/anchors-overlay.d.ts +0 -12
  521. package/dist/ui/anchors-overlay.js +0 -775
  522. package/dist/ui/arch-type-overlay.d.ts +0 -15
  523. package/dist/ui/arch-type-overlay.js +0 -201
  524. package/dist/ui/ask-user-overlay-v2.d.ts +0 -26
  525. package/dist/ui/ask-user-overlay-v2.js +0 -555
  526. package/dist/ui/ask-user-simple-overlay-v2.d.ts +0 -25
  527. package/dist/ui/ask-user-simple-overlay-v2.js +0 -215
  528. package/dist/ui/backlog-overlay.d.ts +0 -32
  529. package/dist/ui/backlog-overlay.js +0 -652
  530. package/dist/ui/commands-overlay-v2.d.ts +0 -33
  531. package/dist/ui/commands-overlay-v2.js +0 -441
  532. package/dist/ui/commands-overlay.d.ts +0 -16
  533. package/dist/ui/commands-overlay.js +0 -439
  534. package/dist/ui/config-overlay.d.ts +0 -35
  535. package/dist/ui/config-overlay.js +0 -707
  536. package/dist/ui/docs-overlay.d.ts +0 -17
  537. package/dist/ui/docs-overlay.js +0 -303
  538. package/dist/ui/footer-v2.d.ts +0 -222
  539. package/dist/ui/footer-v2.js +0 -1349
  540. package/dist/ui/help-overlay-v2.d.ts +0 -34
  541. package/dist/ui/help-overlay-v2.js +0 -309
  542. package/dist/ui/help-overlay.d.ts +0 -16
  543. package/dist/ui/help-overlay.js +0 -316
  544. package/dist/ui/init-overlay-v2.d.ts +0 -34
  545. package/dist/ui/init-overlay-v2.js +0 -600
  546. package/dist/ui/init-overlay.d.ts +0 -34
  547. package/dist/ui/init-overlay.js +0 -604
  548. package/dist/ui/input-prompt-v2.d.ts +0 -180
  549. package/dist/ui/input-prompt-v2.js +0 -999
  550. package/dist/ui/iteration-limit-overlay-v2.d.ts +0 -21
  551. package/dist/ui/iteration-limit-overlay-v2.js +0 -114
  552. package/dist/ui/keys-overlay-v2.d.ts +0 -41
  553. package/dist/ui/keys-overlay-v2.js +0 -248
  554. package/dist/ui/mascot-overlay-v2.d.ts +0 -41
  555. package/dist/ui/mascot-overlay-v2.js +0 -138
  556. package/dist/ui/mascot-overlay.d.ts +0 -21
  557. package/dist/ui/mascot-overlay.js +0 -146
  558. package/dist/ui/model-overlay-v2.d.ts +0 -49
  559. package/dist/ui/model-overlay-v2.js +0 -118
  560. package/dist/ui/model-overlay.d.ts +0 -27
  561. package/dist/ui/model-overlay.js +0 -221
  562. package/dist/ui/model-warning-overlay.d.ts +0 -30
  563. package/dist/ui/model-warning-overlay.js +0 -169
  564. package/dist/ui/new-overlay.d.ts +0 -34
  565. package/dist/ui/new-overlay.js +0 -604
  566. package/dist/ui/overlay/impl/init-overlay-v2.d.ts +0 -77
  567. package/dist/ui/overlay/impl/init-overlay-v2.js +0 -593
  568. package/dist/ui/overlay/overlay-types.d.ts +0 -128
  569. package/dist/ui/overlay/overlay-types.js +0 -22
  570. package/dist/ui/overlays/help-overlay-v2.d.ts +0 -28
  571. package/dist/ui/overlays/help-overlay-v2.js +0 -198
  572. package/dist/ui/overlays/index.d.ts +0 -11
  573. package/dist/ui/overlays/index.js +0 -11
  574. package/dist/ui/permission-overlay-v2.d.ts +0 -36
  575. package/dist/ui/permission-overlay-v2.js +0 -380
  576. package/dist/ui/projects-overlay.d.ts +0 -19
  577. package/dist/ui/projects-overlay.js +0 -484
  578. package/dist/ui/theme-overlay-v2.d.ts +0 -42
  579. package/dist/ui/theme-overlay-v2.js +0 -135
  580. package/dist/ui/theme-overlay.d.ts +0 -24
  581. package/dist/ui/theme-overlay.js +0 -127
  582. package/dist/ui/tools-overlay-v2.d.ts +0 -47
  583. package/dist/ui/tools-overlay-v2.js +0 -218
  584. package/dist/ui/tools-overlay.d.ts +0 -34
  585. package/dist/ui/tools-overlay.js +0 -230
  586. package/dist/ui/tutorial-overlay-v2.d.ts +0 -31
  587. package/dist/ui/tutorial-overlay-v2.js +0 -1035
  588. package/dist/ui/tutorial-overlay.d.ts +0 -11
  589. package/dist/ui/tutorial-overlay.js +0 -1034
  590. package/dist/ui/workflow-overlay.d.ts +0 -22
  591. package/dist/ui/workflow-overlay.js +0 -636
package/dist/repl-v2.js CHANGED
@@ -9,35 +9,174 @@
9
9
  * Can be run standalone: npx tsx src/repl-v2.ts
10
10
  * Or imported and used with a real agent from index.ts
11
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';
12
24
  import { TerminalUI } from './ui/terminal-ui.js';
13
25
  import * as terminal from './ui/terminal.js';
14
26
  import { getStyles } from './themes/index.js';
15
- import { getSettings, getStartupMode } from './settings/index.js';
27
+ import { getSettings, getStartupMode, syncPermissionModeFromUI, getPermissionMode, permissionModeToAgentMode, getProjectSessionMode, isFirstRunComplete, getSessionRetentionDays, updateSettings } from './settings/index.js';
16
28
  import { renderMascotWithLogo } from './ui/mascot/renderer.js';
17
- import { registerCommands, executeCommand, allCommands, } from './commands-v2/index.js';
29
+ import { getStartupHighlights } from './changelog/index.js';
30
+ import { registerCommands, executeCommand, allCommands, getAutocompleteCommands, saveCurrentSession, saveCurrentTeam, loadProjectSession, archiveCurrentSession, convertMessagesToItems, } from './commands-v2/index.js';
18
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';
19
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';
20
38
  import { AskUserSimpleOverlayV2, } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
21
39
  import { GuardrailOverlayV2, } from './ui/overlay/impl/guardrail-overlay-v2.js';
22
40
  import { AskUserOverlayV2, } from './ui/overlay/impl/ask-user-overlay-v2.js';
23
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';
24
48
  import { formatToolResult } from './ui/tool-formatters.js';
49
+ import { formatTokens } from './ui/base/index.js';
25
50
  import { isMemoryInput } from './input-handlers/index.js';
26
51
  import { addAnchor } from './anchors/index.js';
27
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';
28
56
  // Version for display (when running standalone)
29
57
  const STANDALONE_VERSION = '0.4.0-v2';
30
58
  // Register all commands on module load
31
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
+ }
32
162
  // =============================================================================
33
163
  // REPL V2 Class
34
164
  // =============================================================================
35
165
  export class ReplV2 {
36
166
  agent;
167
+ team;
168
+ agentFactory;
37
169
  model;
38
170
  provider;
39
171
  version;
172
+ updateAvailable;
40
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)
41
180
  currentSession = null;
42
181
  ui;
43
182
  onUIReady;
@@ -50,6 +189,20 @@ export class ReplV2 {
50
189
  onSuggestionReady;
51
190
  onSubagentReady;
52
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;
53
206
  // Session stats
54
207
  startTime = new Date();
55
208
  sessionInputTokens = 0;
@@ -59,11 +212,17 @@ export class ReplV2 {
59
212
  textAccumulator = '';
60
213
  // Note: Subagent tracking is now simplified - callbacks receive toolUseId directly
61
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();
62
218
  constructor(options = {}) {
63
219
  this.agent = options.agent;
220
+ this.team = options.team;
221
+ this.agentFactory = options.agentFactory;
64
222
  this.model = options.model ?? 'simulation';
65
223
  this.provider = options.provider ?? 'simulation';
66
224
  this.version = options.version ?? STANDALONE_VERSION;
225
+ this.updateAvailable = options.updateAvailable ?? null;
67
226
  this.projectName = options.projectName ?? process.cwd().split('/').pop() ?? 'Project';
68
227
  this.onUIReady = options.onUIReady;
69
228
  this.onTextBufferReady = options.onTextBufferReady;
@@ -75,6 +234,15 @@ export class ReplV2 {
75
234
  this.onSuggestionReady = options.onSuggestionReady;
76
235
  this.onSubagentReady = options.onSubagentReady;
77
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;
78
246
  }
79
247
  /**
80
248
  * Flush accumulated agent text to the UI.
@@ -83,22 +251,582 @@ export class ReplV2 {
83
251
  */
84
252
  flushTextBuffer() {
85
253
  if (this.textAccumulator.trim()) {
86
- this.ui.print({ type: 'agent-text', text: this.textAccumulator.trim() });
254
+ this.ui.print({
255
+ type: 'agent-text',
256
+ text: this.textAccumulator.trim(),
257
+ expression: this.getActiveAgentMascot(),
258
+ });
87
259
  this.textAccumulator = '';
88
260
  }
89
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
+ }
90
745
  /**
91
746
  * Start the REPL.
92
747
  */
93
748
  start() {
94
- // Create TerminalUI
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);
95
797
  this.ui = new TerminalUI({
96
- initialMode: 'normal',
798
+ initialMode: initialUiMode,
97
799
  });
98
800
  // Print welcome screen
99
- this.printWelcome();
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
+ }
100
806
  // Set initial state
101
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
+ }
102
830
  // Notify caller that UI is ready and provide the V2 permission overlay function
103
831
  if (this.onUIReady) {
104
832
  const ui = this.ui;
@@ -149,6 +877,24 @@ export class ReplV2 {
149
877
  return result ?? { continue: false };
150
878
  });
151
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
+ }
152
898
  // Provide the suggestion setter function to the caller
153
899
  if (this.onSuggestionReady) {
154
900
  const ui = this.ui;
@@ -184,6 +930,53 @@ export class ReplV2 {
184
930
  },
185
931
  });
186
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
+ }
187
980
  // Event handlers
188
981
  this.ui.on('submit', (input) => {
189
982
  void this.processInput(input);
@@ -214,27 +1007,560 @@ export class ReplV2 {
214
1007
  });
215
1008
  this.ui.on('modeChange', () => {
216
1009
  const modes = ['normal', 'auto-accept', 'plan'];
217
- const currentIndex = modes.indexOf(this.ui.getMode());
1010
+ const currentMode = this.ui.getMode();
1011
+ const currentIndex = modes.indexOf(currentMode);
218
1012
  const nextMode = modes[(currentIndex + 1) % modes.length];
219
1013
  this.ui.setMode(nextMode);
220
- this.ui.print({ type: 'info', message: `Mode: ${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
+ }
221
1035
  });
222
- // Start UI
223
- this.ui.start();
224
- // If startup mode is 'menu', immediately execute /menu command
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.
225
1125
  const startupMode = getStartupMode();
226
1126
  if (startupMode === 'menu') {
227
- void this.handleCommand('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', '');
228
1523
  }
229
1524
  }
230
1525
  /**
231
1526
  * Stop the REPL.
1527
+ * Aborts all running sessions (foreground and background).
232
1528
  */
1529
+ /** Set MCP loading state on the footer status bar. */
1530
+ setMCPLoading(loading, toolCount) {
1531
+ this.ui.setMCPLoading(loading, toolCount);
1532
+ }
233
1533
  stop() {
234
- if (this.currentSession) {
235
- this.currentSession.abortController.abort();
236
- this.currentSession = null;
1534
+ // Stop notification polling before unregistering session
1535
+ try {
1536
+ const notifMgr = getNotificationManager();
1537
+ if (notifMgr) {
1538
+ notifMgr.stopPolling();
1539
+ }
237
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;
238
1564
  this.ui.stop();
239
1565
  }
240
1566
  // ===========================================================================
@@ -248,8 +1574,13 @@ export class ReplV2 {
248
1574
  const settings = getSettings();
249
1575
  const s = getStyles();
250
1576
  terminal.clearScreen();
251
- // Render mascot with logo (includes tagline)
252
- const logoLines = renderMascotWithLogo(settings.mascot, this.version, null);
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
+ }
253
1584
  console.log('');
254
1585
  for (const line of logoLines) {
255
1586
  console.log(line);
@@ -275,6 +1606,43 @@ export class ReplV2 {
275
1606
  const s = getStyles();
276
1607
  // Print logo with agent/model info
277
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
+ }
278
1646
  // Hints
279
1647
  console.log(s.muted('Type a message to start'));
280
1648
  console.log(s.muted('Type /help for commands, /exit to quit'));
@@ -287,10 +1655,36 @@ export class ReplV2 {
287
1655
  return {
288
1656
  ui: this.ui,
289
1657
  version: this.version,
290
- printWelcome: () => { this.printWelcome(); },
1658
+ updateAvailable: this.updateAvailable,
1659
+ printWelcome: () => { this.printWelcome(); return Promise.resolve(); },
291
1660
  printLogo: () => { this.printLogo(); },
1661
+ consumeRestoredSessionInfo: () => {
1662
+ const info = this.restoredSessionInfo;
1663
+ this.restoredSessionInfo = null;
1664
+ return info;
1665
+ },
292
1666
  // Agent integration
293
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
+ },
294
1688
  model: this.model,
295
1689
  provider: this.provider,
296
1690
  getContextManager: () => {
@@ -308,9 +1702,30 @@ export class ReplV2 {
308
1702
  sessionInputTokens: this.sessionInputTokens,
309
1703
  sessionOutputTokens: this.sessionOutputTokens,
310
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
+ },
311
1724
  };
312
1725
  }
313
1726
  async handleCommand(cmd, args) {
1727
+ // Record command usage for telemetry
1728
+ getAuthManager().recordCommand(cmd);
314
1729
  const ctx = this.createCommandContext();
315
1730
  const result = await executeCommand(cmd, args, ctx);
316
1731
  if (result === null) {
@@ -337,7 +1752,8 @@ export class ReplV2 {
337
1752
  this.ui.print({ type: 'info', message: 'Type /help to see available commands' });
338
1753
  }
339
1754
  else if (!result) {
340
- // result === false means exit
1755
+ // result === false means exit — clean up before exiting
1756
+ this.stop();
341
1757
  process.exit(0);
342
1758
  }
343
1759
  // Process any queued agent messages (from commands like /arch, /design)
@@ -366,33 +1782,285 @@ export class ReplV2 {
366
1782
  }
367
1783
  }
368
1784
  }
369
- // ===========================================================================
370
- // Input Processing
371
- // ===========================================================================
372
- async processInput(input) {
373
- if (input.startsWith('/')) {
374
- const spaceIndex = input.indexOf(' ');
375
- const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
376
- const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
377
- await this.handleCommand(cmd, args);
378
- await this.processQueue();
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;
379
2023
  }
380
- else if (isMemoryInput(input)) {
381
- // Handle memory note ("# note")
382
- this.handleMemoryInputV2(input);
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;
383
2031
  }
384
- else {
385
- // Print user message
386
- this.ui.print({ type: 'user-message', text: input });
387
- // Run agent (real or simulation)
388
- if (this.agent) {
389
- await this.runAgentReal(input);
390
- }
391
- else {
392
- await this.runAgentSimulation(input);
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;
393
2047
  }
394
- await this.processQueue();
395
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;
396
2064
  }
397
2065
  /**
398
2066
  * Handle memory note input ("# note") - V2 compatible version
@@ -422,7 +2090,7 @@ export class ReplV2 {
422
2090
  // Show confirmation
423
2091
  const scope = activeProject ? `project "${activeProject.displayName || activeProject.name}"` : 'global';
424
2092
  const priorityLabel = priority === 'info' ? '' : ` (${priority})`;
425
- const truncatedNote = note.length > 50 ? note.slice(0, 47) + '...' : note;
2093
+ const truncatedNote = truncate(note, 50);
426
2094
  this.ui.print({ type: 'success', message: `Saved${priorityLabel}: "${truncatedNote}" → ${scope}` });
427
2095
  // Also add to agent's anchor manager if enabled (for current session)
428
2096
  if (this.agent?.getAnchorManager()) {
@@ -435,6 +2103,67 @@ export class ReplV2 {
435
2103
  });
436
2104
  }
437
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
+ }
438
2167
  async processQueue() {
439
2168
  while (this.ui.hasQueuedInput()) {
440
2169
  const queued = this.ui.popQueuedInput();
@@ -461,24 +2190,71 @@ export class ReplV2 {
461
2190
  this.clearSubagentTracking?.();
462
2191
  // - LiveRegion entries (visual display of running tools)
463
2192
  this.ui.clearLiveRegion();
464
- this.currentSession = {
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
465
2199
  abortController,
466
2200
  currentAction: null,
467
2201
  promise: (async () => {
468
2202
  try {
469
2203
  this.ui.setAgentRunning(true);
2204
+ this.ui.resetTurnMetrics(); // Reset token tracking for this turn
470
2205
  setAction('Thinking...');
471
2206
  this.ui.setSpinnerText('Thinking...');
472
2207
  // Reset text accumulator for this run (class-level so it can be flushed externally)
473
2208
  this.textAccumulator = '';
474
2209
  let lastToolInput = null;
2210
+ let pendingDiffLines;
475
2211
  // Stream agent events (agent is guaranteed to exist in this method)
476
2212
  const agent = this.agent;
477
2213
  if (!agent)
478
2214
  return; // Type guard
479
- for await (const event of agent.stream(userMessage, {
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, {
480
2239
  signal,
481
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
+ },
482
2258
  })) {
483
2259
  if (signal.aborted)
484
2260
  break;
@@ -486,19 +2262,87 @@ export class ReplV2 {
486
2262
  if (event.chunk.type === 'text' && event.chunk.text) {
487
2263
  this.textAccumulator += event.chunk.text;
488
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
+ }
489
2273
  else if (event.chunk.type === 'done' && event.chunk.usage) {
490
2274
  this.sessionInputTokens += event.chunk.usage.inputTokens;
491
2275
  this.sessionOutputTokens += event.chunk.usage.outputTokens;
492
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
+ }
493
2298
  }
494
2299
  }
495
2300
  else if (event.type === 'tool_start') {
496
2301
  // Flush accumulated text before tool
497
2302
  if (this.textAccumulator.trim()) {
498
- this.ui.print({ type: 'agent-text', text: this.textAccumulator.trim() });
2303
+ this.ui.print({
2304
+ type: 'agent-text',
2305
+ text: this.textAccumulator.trim(),
2306
+ expression: this.getActiveAgentMascot(),
2307
+ agentId: activeAgentId,
2308
+ });
499
2309
  this.textAccumulator = '';
500
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
+ }
501
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
+ }
502
2346
  // For bash tool, add to live region for streaming output
503
2347
  if (event.name === 'bash' && event.toolUseId) {
504
2348
  const command = typeof lastToolInput.command === 'string'
@@ -506,8 +2350,8 @@ export class ReplV2 {
506
2350
  : '';
507
2351
  this.ui.addBashCommand(event.toolUseId, command);
508
2352
  }
509
- // Update spinner with tool name
510
- if (event.name !== 'todo_read' && event.name !== 'todo_write' && event.name !== 'suggest') {
2353
+ // Update spinner with tool name (skip silent tools)
2354
+ if (!this.agent?.isToolSilent(event.name)) {
511
2355
  if (event.name === 'task' && event.toolUseId) {
512
2356
  const subagentType = typeof lastToolInput.subagent_type === 'string'
513
2357
  ? lastToolInput.subagent_type
@@ -521,6 +2365,27 @@ export class ReplV2 {
521
2365
  // No queue needed - callbacks now receive toolUseId directly!
522
2366
  this.ui.addSubagent(event.toolUseId, subagentType, description);
523
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
+ }
524
2389
  else if (event.name !== 'task') {
525
2390
  setAction(event.name);
526
2391
  this.ui.setSpinnerText(`Running ${event.name}...`);
@@ -540,6 +2405,17 @@ export class ReplV2 {
540
2405
  lastToolInput = null;
541
2406
  // Complete bash command in live region
542
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
+ }
543
2419
  let exitCode = result.success ? 0 : 1;
544
2420
  if (result.success && typeof result.result === 'object' && result.result !== null) {
545
2421
  const res = result.result;
@@ -564,15 +2440,89 @@ export class ReplV2 {
564
2440
  ? t.status
565
2441
  : 'pending');
566
2442
  const activeForm = typeof t.activeForm === 'string' ? t.activeForm : undefined;
567
- return { content, status, activeForm };
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 };
568
2448
  });
569
2449
  this.ui.setTodos(todos);
570
2450
  this.ui.setCurrentTool(null);
571
2451
  continue;
572
2452
  }
573
2453
  // Skip silent tools (no output needed)
574
- if (toolName === 'todo_read' || toolName === 'suggest' ||
575
- toolName === 'ask_user' || toolName === 'ask_user_simple') {
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
+ });
576
2526
  this.ui.setCurrentTool(null);
577
2527
  continue;
578
2528
  }
@@ -588,39 +2538,517 @@ export class ReplV2 {
588
2538
  }
589
2539
  // Extract and format tool result
590
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
+ }
591
2561
  this.ui.print({
592
2562
  type: 'tool-result',
593
2563
  name: toolName,
594
2564
  params: this.formatToolParams(toolInput),
595
2565
  summary,
596
- content: content.length > 200 ? content : undefined,
2566
+ content: content || undefined,
2567
+ diffLines,
597
2568
  success,
2569
+ agentId: activeAgentId,
598
2570
  });
599
2571
  this.ui.setCurrentTool(null);
600
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
+ }
601
2636
  }
602
- // Flush remaining text
2637
+ // Flush remaining text and check for PLAN_READY marker
603
2638
  if (this.textAccumulator.trim()) {
604
- this.ui.print({ type: 'agent-text', text: this.textAccumulator.trim(), expression: 'success' });
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
+ }
605
2653
  }
2654
+ // Print per-turn summary before stopping agent
2655
+ this.printTurnSummary();
606
2656
  this.ui.setAgentRunning(false);
2657
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
607
2658
  // NOTE: Don't clear todos here - they should persist after agent finishes
608
2659
  // to show the final state. Only clear on explicit user action or new conversation.
609
2660
  // Notify that agent has finished (for applying deferred suggestions)
610
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
+ });
611
2667
  }
612
2668
  catch (err) {
613
2669
  if (!signal.aborted) {
614
2670
  this.ui.print({ type: 'error', message: String(err) });
615
2671
  }
616
2672
  this.ui.setAgentRunning(false);
2673
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
617
2674
  // Still notify on error so suggestions can be applied
618
2675
  this.onAgentFinish?.();
619
2676
  }
620
2677
  })(),
621
2678
  };
622
- await this.currentSession.promise;
623
- this.currentSession = null;
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
+ }
624
3052
  }
625
3053
  formatToolParams(input) {
626
3054
  if (!input)
@@ -633,12 +3061,12 @@ export class ReplV2 {
633
3061
  if ('pattern' in input)
634
3062
  return String(input.pattern);
635
3063
  if ('command' in input)
636
- return String(input.command).slice(0, 50);
3064
+ return truncate(String(input.command), 50);
637
3065
  // Default: show first param
638
3066
  const keys = Object.keys(input);
639
3067
  if (keys.length > 0) {
640
3068
  const val = String(input[keys[0]]);
641
- return val.length > 50 ? val.slice(0, 47) + '...' : val;
3069
+ return truncate(val, 50);
642
3070
  }
643
3071
  return '';
644
3072
  }
@@ -660,7 +3088,12 @@ export class ReplV2 {
660
3088
  this.currentSession.currentAction = action;
661
3089
  }
662
3090
  };
663
- this.currentSession = {
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
664
3097
  abortController,
665
3098
  currentAction: null,
666
3099
  promise: (async () => {
@@ -673,7 +3106,7 @@ export class ReplV2 {
673
3106
  if (signal.aborted)
674
3107
  return;
675
3108
  const response = `I understand you want me to help with "${userMessage}". Let me analyze this.`;
676
- this.ui.print({ type: 'agent-text', text: response });
3109
+ this.ui.print({ type: 'agent-text', text: response, expression: this.getActiveAgentMascot() });
677
3110
  // 2. Add todos
678
3111
  this.ui.setTodos([
679
3112
  { content: 'Analyze the request', status: 'completed' },
@@ -719,6 +3152,7 @@ export class ReplV2 {
719
3152
  expression: 'success',
720
3153
  });
721
3154
  this.ui.setAgentRunning(false);
3155
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
722
3156
  // NOTE: Don't clear todos - let them persist to show final state
723
3157
  // Notify that agent has finished (for applying deferred suggestions)
724
3158
  this.onAgentFinish?.();
@@ -728,12 +3162,16 @@ export class ReplV2 {
728
3162
  this.ui.print({ type: 'error', message: String(err) });
729
3163
  }
730
3164
  this.ui.setAgentRunning(false);
3165
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
731
3166
  this.onAgentFinish?.();
732
3167
  }
733
3168
  })(),
734
3169
  };
735
- await this.currentSession.promise;
736
- this.currentSession = null;
3170
+ // Register the session (foreground)
3171
+ this.startSession(session);
3172
+ // Wait for session to complete
3173
+ await session.promise;
3174
+ this.endSession(activeAgentId);
737
3175
  }
738
3176
  async sleep(ms, signal) {
739
3177
  return new Promise((resolve) => {
@@ -745,15 +3183,83 @@ export class ReplV2 {
745
3183
  });
746
3184
  }
747
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
+ // ===========================================================================
748
3249
  // Session Management
749
3250
  // ===========================================================================
3251
+ /**
3252
+ * Cancel the foreground session.
3253
+ * Background agents continue running.
3254
+ */
750
3255
  cancelSession() {
751
3256
  const action = this.currentSession?.currentAction ?? undefined;
752
- if (this.currentSession) {
3257
+ if (this.currentSession && this.foregroundAgentId) {
753
3258
  this.currentSession.abortController.abort();
754
- this.currentSession = null;
3259
+ this.endSession(this.foregroundAgentId);
755
3260
  }
756
3261
  this.ui.setAgentRunning(false);
3262
+ this.ui.setThinking(false); // Ensure thinking indicator is cleared
757
3263
  this.ui.setTodos([]);
758
3264
  this.ui.clearQueue();
759
3265
  this.ui.print({ type: 'interrupted', action, suggestion: 'what should I do instead?' });
@@ -765,10 +3271,16 @@ export class ReplV2 {
765
3271
  // Only run main() when executed directly (not when imported)
766
3272
  if (import.meta.url === `file://${process.argv[1]}`) {
767
3273
  const repl = new ReplV2();
768
- repl.start();
769
3274
  // Handle SIGINT
770
3275
  process.on('SIGINT', () => {
771
3276
  console.log('\n\nInterrupted\n');
772
3277
  process.exit(0);
773
3278
  });
3279
+ try {
3280
+ repl.start();
3281
+ }
3282
+ catch (err) {
3283
+ console.error('Failed to start REPL:', err);
3284
+ process.exit(1);
3285
+ }
774
3286
  }