@compilr-dev/cli 0.5.0 → 0.5.2

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 (587) hide show
  1. package/LICENSE +108 -0
  2. package/README.md +237 -69
  3. package/dist/.tsbuildinfo.app +1 -0
  4. package/dist/.tsbuildinfo.data +1 -0
  5. package/dist/.tsbuildinfo.domain +1 -0
  6. package/dist/.tsbuildinfo.foundation +1 -0
  7. package/dist/agent.d.ts +61 -4
  8. package/dist/agent.js +241 -245
  9. package/dist/anchors/index.d.ts +1 -1
  10. package/dist/anchors/index.js +1 -1
  11. package/dist/anchors/project-anchors.d.ts +2 -2
  12. package/dist/anchors/project-anchors.js +1 -1
  13. package/dist/auth/api-client.d.ts +124 -0
  14. package/dist/auth/api-client.js +261 -0
  15. package/dist/auth/index.d.ts +172 -0
  16. package/dist/auth/index.js +545 -0
  17. package/dist/auth/storage.d.ts +52 -0
  18. package/dist/auth/storage.js +118 -0
  19. package/dist/changelog/index.d.ts +16 -0
  20. package/dist/changelog/index.js +24 -0
  21. package/dist/changelog/releases.d.ts +17 -0
  22. package/dist/changelog/releases.js +63 -0
  23. package/dist/commands-v2/handlers/auth.d.ts +10 -0
  24. package/dist/commands-v2/handlers/auth.js +118 -0
  25. package/dist/commands-v2/handlers/background.d.ts +14 -0
  26. package/dist/commands-v2/handlers/background.js +276 -0
  27. package/dist/commands-v2/handlers/context.js +286 -81
  28. package/dist/commands-v2/handlers/core.d.ts +1 -0
  29. package/dist/commands-v2/handlers/core.js +133 -8
  30. package/dist/commands-v2/handlers/debug.js +18 -0
  31. package/dist/commands-v2/handlers/delegations.d.ts +8 -0
  32. package/dist/commands-v2/handlers/delegations.js +29 -0
  33. package/dist/commands-v2/handlers/files.d.ts +8 -0
  34. package/dist/commands-v2/handlers/files.js +162 -0
  35. package/dist/commands-v2/handlers/filter.d.ts +9 -0
  36. package/dist/commands-v2/handlers/filter.js +130 -0
  37. package/dist/commands-v2/handlers/games.d.ts +7 -0
  38. package/dist/commands-v2/handlers/games.js +57 -0
  39. package/dist/commands-v2/handlers/index.d.ts +13 -0
  40. package/dist/commands-v2/handlers/index.js +39 -0
  41. package/dist/commands-v2/handlers/mcp.d.ts +8 -0
  42. package/dist/commands-v2/handlers/mcp.js +39 -0
  43. package/dist/commands-v2/handlers/notifications.d.ts +9 -0
  44. package/dist/commands-v2/handlers/notifications.js +34 -0
  45. package/dist/commands-v2/handlers/project.js +295 -31
  46. package/dist/commands-v2/handlers/reset.d.ts +11 -0
  47. package/dist/commands-v2/handlers/reset.js +118 -0
  48. package/dist/commands-v2/handlers/session.d.ts +161 -0
  49. package/dist/commands-v2/handlers/session.js +805 -0
  50. package/dist/commands-v2/handlers/settings.d.ts +2 -0
  51. package/dist/commands-v2/handlers/settings.js +217 -35
  52. package/dist/commands-v2/handlers/tasks.d.ts +5 -0
  53. package/dist/commands-v2/handlers/tasks.js +36 -0
  54. package/dist/commands-v2/handlers/team.d.ts +9 -0
  55. package/dist/commands-v2/handlers/team.js +549 -0
  56. package/dist/commands-v2/handlers/terminals.d.ts +9 -0
  57. package/dist/commands-v2/handlers/terminals.js +34 -0
  58. package/dist/commands-v2/index.d.ts +3 -2
  59. package/dist/commands-v2/index.js +4 -1
  60. package/dist/commands-v2/registry.d.ts +15 -0
  61. package/dist/commands-v2/registry.js +34 -0
  62. package/dist/commands-v2/types.d.ts +81 -3
  63. package/dist/commands.js +13 -0
  64. package/dist/compilr-diff-companion.vsix +0 -0
  65. package/dist/db/index.js +98 -4
  66. package/dist/db/repositories/document-repository.d.ts +2 -0
  67. package/dist/db/repositories/document-repository.js +6 -1
  68. package/dist/db/repositories/index.d.ts +2 -0
  69. package/dist/db/repositories/index.js +1 -0
  70. package/dist/db/repositories/plan-repository.d.ts +101 -0
  71. package/dist/db/repositories/plan-repository.js +275 -0
  72. package/dist/db/repositories/project-repository.d.ts +6 -0
  73. package/dist/db/repositories/project-repository.js +41 -0
  74. package/dist/db/repositories/work-item-repository.d.ts +15 -0
  75. package/dist/db/repositories/work-item-repository.js +69 -4
  76. package/dist/db/schema.d.ts +40 -3
  77. package/dist/db/schema.js +66 -3
  78. package/dist/episodes/index.d.ts +20 -0
  79. package/dist/episodes/index.js +27 -0
  80. package/dist/episodes/recorder.d.ts +51 -0
  81. package/dist/episodes/recorder.js +195 -0
  82. package/dist/episodes/significant-work.d.ts +21 -0
  83. package/dist/episodes/significant-work.js +56 -0
  84. package/dist/episodes/store.d.ts +38 -0
  85. package/dist/episodes/store.js +199 -0
  86. package/dist/episodes/types.d.ts +35 -0
  87. package/dist/episodes/types.js +6 -0
  88. package/dist/episodes/work-at-risk.d.ts +12 -0
  89. package/dist/episodes/work-at-risk.js +38 -0
  90. package/dist/episodes/work-summary-anchor.d.ts +23 -0
  91. package/dist/episodes/work-summary-anchor.js +73 -0
  92. package/dist/games/coins.d.ts +66 -0
  93. package/dist/games/coins.js +165 -0
  94. package/dist/games/game-base.d.ts +84 -0
  95. package/dist/games/game-base.js +204 -0
  96. package/dist/games/index.d.ts +16 -0
  97. package/dist/games/index.js +49 -0
  98. package/dist/games/scores.d.ts +69 -0
  99. package/dist/games/scores.js +191 -0
  100. package/dist/games/tetris/board.d.ts +59 -0
  101. package/dist/games/tetris/board.js +170 -0
  102. package/dist/games/tetris/index.d.ts +109 -0
  103. package/dist/games/tetris/index.js +610 -0
  104. package/dist/games/tetris/pieces.d.ts +44 -0
  105. package/dist/games/tetris/pieces.js +271 -0
  106. package/dist/games/tetris/renderer.d.ts +26 -0
  107. package/dist/games/tetris/renderer.js +77 -0
  108. package/dist/guide/guide-content.d.ts +23 -0
  109. package/dist/guide/guide-content.js +196 -0
  110. package/dist/guide/index.d.ts +8 -0
  111. package/dist/guide/index.js +7 -0
  112. package/dist/guide/shared-content.d.ts +37 -0
  113. package/dist/guide/shared-content.js +1272 -0
  114. package/dist/guide/tutorial-helpers.d.ts +57 -0
  115. package/dist/guide/tutorial-helpers.js +147 -0
  116. package/dist/handlers/ask-user-handlers.d.ts +32 -0
  117. package/dist/handlers/ask-user-handlers.js +104 -0
  118. package/dist/handlers/delegation-handlers.d.ts +34 -0
  119. package/dist/handlers/delegation-handlers.js +291 -0
  120. package/dist/handlers/permission-handler.d.ts +30 -0
  121. package/dist/handlers/permission-handler.js +205 -0
  122. package/dist/index.d.ts +11 -1
  123. package/dist/index.js +448 -271
  124. package/dist/input-handlers/memory-handler.d.ts +1 -1
  125. package/dist/input-handlers/memory-handler.js +2 -1
  126. package/dist/models/index.d.ts +10 -0
  127. package/dist/models/index.js +12 -0
  128. package/dist/models/model-registry.d.ts +38 -0
  129. package/dist/models/model-registry.js +69 -0
  130. package/dist/models/model-tiers.d.ts +28 -0
  131. package/dist/models/model-tiers.js +71 -0
  132. package/dist/models/model-validation.d.ts +25 -0
  133. package/dist/models/model-validation.js +291 -0
  134. package/dist/models/ollama-models.d.ts +73 -0
  135. package/dist/models/ollama-models.js +178 -0
  136. package/dist/models/provider-types.d.ts +6 -0
  137. package/dist/models/provider-types.js +1 -0
  138. package/dist/models/providers.d.ts +35 -0
  139. package/dist/models/providers.js +58 -0
  140. package/dist/models/types.d.ts +4 -0
  141. package/dist/models/types.js +4 -0
  142. package/dist/multi-agent/activity.d.ts +21 -0
  143. package/dist/multi-agent/activity.js +34 -0
  144. package/dist/multi-agent/agent-selection.d.ts +55 -0
  145. package/dist/multi-agent/agent-selection.js +90 -0
  146. package/dist/multi-agent/artifacts.d.ts +197 -0
  147. package/dist/multi-agent/artifacts.js +379 -0
  148. package/dist/multi-agent/checkpointer.d.ts +138 -0
  149. package/dist/multi-agent/checkpointer.js +471 -0
  150. package/dist/multi-agent/collision-utils.d.ts +16 -0
  151. package/dist/multi-agent/collision-utils.js +28 -0
  152. package/dist/multi-agent/context-resolver.d.ts +97 -0
  153. package/dist/multi-agent/context-resolver.js +316 -0
  154. package/dist/multi-agent/custom-agents.d.ts +83 -0
  155. package/dist/multi-agent/custom-agents.js +227 -0
  156. package/dist/multi-agent/delegation-tracker.d.ts +157 -0
  157. package/dist/multi-agent/delegation-tracker.js +243 -0
  158. package/dist/multi-agent/file-lock-hook.d.ts +29 -0
  159. package/dist/multi-agent/file-lock-hook.js +97 -0
  160. package/dist/multi-agent/file-locks.d.ts +58 -0
  161. package/dist/multi-agent/file-locks.js +194 -0
  162. package/dist/multi-agent/index.d.ts +24 -0
  163. package/dist/multi-agent/index.js +30 -0
  164. package/dist/multi-agent/mention-parser.d.ts +64 -0
  165. package/dist/multi-agent/mention-parser.js +146 -0
  166. package/dist/multi-agent/notification-manager.d.ts +84 -0
  167. package/dist/multi-agent/notification-manager.js +224 -0
  168. package/dist/multi-agent/pending-requests.d.ts +122 -0
  169. package/dist/multi-agent/pending-requests.js +155 -0
  170. package/dist/multi-agent/session-registry.d.ts +139 -0
  171. package/dist/multi-agent/session-registry.js +514 -0
  172. package/dist/multi-agent/shared-context.d.ts +293 -0
  173. package/dist/multi-agent/shared-context.js +671 -0
  174. package/dist/multi-agent/skill-requirements.d.ts +66 -0
  175. package/dist/multi-agent/skill-requirements.js +178 -0
  176. package/dist/multi-agent/task-assignment.d.ts +69 -0
  177. package/dist/multi-agent/task-assignment.js +123 -0
  178. package/dist/multi-agent/task-suggestion.d.ts +31 -0
  179. package/dist/multi-agent/task-suggestion.js +72 -0
  180. package/dist/multi-agent/team-agent.d.ts +201 -0
  181. package/dist/multi-agent/team-agent.js +488 -0
  182. package/dist/multi-agent/team.d.ts +286 -0
  183. package/dist/multi-agent/team.js +610 -0
  184. package/dist/multi-agent/tool-config.d.ts +110 -0
  185. package/dist/multi-agent/tool-config.js +661 -0
  186. package/dist/multi-agent/types.d.ts +211 -0
  187. package/dist/multi-agent/types.js +617 -0
  188. package/dist/prompts/plan-mode-prompt.d.ts +11 -0
  189. package/dist/prompts/plan-mode-prompt.js +95 -0
  190. package/dist/repl-helpers.js +5 -2
  191. package/dist/repl-v2.d.ts +401 -2
  192. package/dist/repl-v2.js +2588 -65
  193. package/dist/session/index.d.ts +6 -0
  194. package/dist/session/index.js +6 -0
  195. package/dist/session/project-session-manager.d.ts +158 -0
  196. package/dist/session/project-session-manager.js +650 -0
  197. package/dist/settings/index.d.ts +133 -13
  198. package/dist/settings/index.js +329 -24
  199. package/dist/settings/mcp-config.d.ts +76 -0
  200. package/dist/settings/mcp-config.js +143 -0
  201. package/dist/settings/paths.d.ts +4 -0
  202. package/dist/settings/paths.js +6 -0
  203. package/dist/shared-handlers.d.ts +62 -0
  204. package/dist/shared-handlers.js +48 -0
  205. package/dist/system-prompt/builder.d.ts +5 -0
  206. package/dist/system-prompt/builder.js +4 -0
  207. package/dist/system-prompt/index.d.ts +18 -0
  208. package/dist/system-prompt/index.js +18 -0
  209. package/dist/system-prompt/modules.d.ts +5 -0
  210. package/dist/system-prompt/modules.js +4 -0
  211. package/dist/tabbed-menu.js +2 -1
  212. package/dist/templates/compilr-md-import.d.ts +16 -0
  213. package/dist/templates/compilr-md-import.js +241 -0
  214. package/dist/templates/compilr-md.js +10 -61
  215. package/dist/templates/config-json.d.ts +1 -25
  216. package/dist/templates/index.d.ts +2 -0
  217. package/dist/templates/index.js +34 -73
  218. package/dist/tool-names.d.ts +113 -0
  219. package/dist/tool-names.js +239 -0
  220. package/dist/tools/ask-user-simple.d.ts +1 -1
  221. package/dist/tools/ask-user-simple.js +2 -1
  222. package/dist/tools/ask-user.d.ts +1 -1
  223. package/dist/tools/ask-user.js +2 -1
  224. package/dist/tools/backlog.d.ts +2 -2
  225. package/dist/tools/backlog.js +1 -1
  226. package/dist/tools/db-tools.d.ts +13 -61
  227. package/dist/tools/db-tools.js +12 -13
  228. package/dist/tools/delegate-background.d.ts +27 -0
  229. package/dist/tools/delegate-background.js +115 -0
  230. package/dist/tools/delegate.d.ts +22 -0
  231. package/dist/tools/delegate.js +97 -0
  232. package/dist/tools/delegation-status.d.ts +16 -0
  233. package/dist/tools/delegation-status.js +128 -0
  234. package/dist/tools/guide-tool.d.ts +12 -0
  235. package/dist/tools/guide-tool.js +59 -0
  236. package/dist/tools/handoff.d.ts +25 -0
  237. package/dist/tools/handoff.js +99 -0
  238. package/dist/tools/meta-tools.d.ts +26 -0
  239. package/dist/tools/meta-tools.js +47 -0
  240. package/dist/tools/platform-adapter.d.ts +35 -0
  241. package/dist/tools/platform-adapter.js +404 -0
  242. package/dist/tools/project-db.d.ts +5 -73
  243. package/dist/tools/project-db.js +5 -336
  244. package/dist/tools.d.ts +67 -2
  245. package/dist/tools.js +240 -48
  246. package/dist/ui/autocomplete-controller.d.ts +42 -0
  247. package/dist/ui/autocomplete-controller.js +384 -0
  248. package/dist/ui/base/index.d.ts +1 -1
  249. package/dist/ui/base/index.js +1 -1
  250. package/dist/ui/base/overlay-base-v2.d.ts +10 -0
  251. package/dist/ui/base/overlay-base-v2.js +14 -0
  252. package/dist/ui/base/render-utils.d.ts +19 -0
  253. package/dist/ui/base/render-utils.js +25 -0
  254. package/dist/ui/base/tabbed-list-overlay-v2.d.ts +16 -1
  255. package/dist/ui/base/tabbed-list-overlay-v2.js +19 -1
  256. package/dist/ui/constants/labels.d.ts +14 -0
  257. package/dist/ui/constants/labels.js +52 -0
  258. package/dist/ui/conversation-store.d.ts +55 -0
  259. package/dist/ui/conversation-store.js +107 -0
  260. package/dist/ui/conversation.js +11 -13
  261. package/dist/ui/diff.d.ts +7 -1
  262. package/dist/ui/diff.js +85 -48
  263. package/dist/ui/ephemeral.js +3 -9
  264. package/dist/ui/file-autocomplete.d.ts +24 -0
  265. package/dist/ui/file-autocomplete.js +56 -0
  266. package/dist/ui/footer-renderer.d.ts +69 -0
  267. package/dist/ui/footer-renderer.js +431 -0
  268. package/dist/ui/footer.d.ts +74 -7
  269. package/dist/ui/footer.js +173 -16
  270. package/dist/ui/input-controller.d.ts +51 -0
  271. package/dist/ui/input-controller.js +176 -0
  272. package/dist/ui/input-prompt.d.ts +19 -0
  273. package/dist/ui/input-prompt.js +206 -14
  274. package/dist/ui/keyboard-handler.d.ts +57 -0
  275. package/dist/ui/keyboard-handler.js +557 -0
  276. package/dist/ui/live-region-facade.d.ts +42 -0
  277. package/dist/ui/live-region-facade.js +205 -0
  278. package/dist/ui/live-region.d.ts +0 -4
  279. package/dist/ui/live-region.js +6 -14
  280. package/dist/ui/mascot/renderer.d.ts +1 -1
  281. package/dist/ui/mascot/renderer.js +37 -2
  282. package/dist/ui/overlay/data/tutorial-content.d.ts +9 -0
  283. package/dist/ui/overlay/data/tutorial-content.js +9 -0
  284. package/dist/ui/overlay/data/tutorial-registry.d.ts +12 -0
  285. package/dist/ui/overlay/data/tutorial-registry.js +116 -0
  286. package/dist/ui/overlay/data/tutorial-types.d.ts +35 -0
  287. package/dist/ui/overlay/data/tutorial-types.js +6 -0
  288. package/dist/ui/overlay/data/tutorials/basics/first-conversation.d.ts +7 -0
  289. package/dist/ui/overlay/data/tutorials/basics/first-conversation.js +220 -0
  290. package/dist/ui/overlay/data/tutorials/basics/first-project.d.ts +7 -0
  291. package/dist/ui/overlay/data/tutorials/basics/first-project.js +284 -0
  292. package/dist/ui/overlay/data/tutorials/basics/navigation.d.ts +8 -0
  293. package/dist/ui/overlay/data/tutorials/basics/navigation.js +22 -0
  294. package/dist/ui/overlay/data/tutorials/basics/welcome.d.ts +7 -0
  295. package/dist/ui/overlay/data/tutorials/basics/welcome.js +174 -0
  296. package/dist/ui/overlay/data/tutorials/config/context-management.d.ts +7 -0
  297. package/dist/ui/overlay/data/tutorials/config/context-management.js +158 -0
  298. package/dist/ui/overlay/data/tutorials/config/mcp-servers.d.ts +8 -0
  299. package/dist/ui/overlay/data/tutorials/config/mcp-servers.js +155 -0
  300. package/dist/ui/overlay/data/tutorials/config/model-selection.d.ts +7 -0
  301. package/dist/ui/overlay/data/tutorials/config/model-selection.js +162 -0
  302. package/dist/ui/overlay/data/tutorials/config/permissions-safety.d.ts +7 -0
  303. package/dist/ui/overlay/data/tutorials/config/permissions-safety.js +163 -0
  304. package/dist/ui/overlay/data/tutorials/config/settings-config.d.ts +7 -0
  305. package/dist/ui/overlay/data/tutorials/config/settings-config.js +166 -0
  306. package/dist/ui/overlay/data/tutorials/planning/arch.d.ts +7 -0
  307. package/dist/ui/overlay/data/tutorials/planning/arch.js +168 -0
  308. package/dist/ui/overlay/data/tutorials/planning/backlog.d.ts +7 -0
  309. package/dist/ui/overlay/data/tutorials/planning/backlog.js +103 -0
  310. package/dist/ui/overlay/data/tutorials/planning/build.d.ts +7 -0
  311. package/dist/ui/overlay/data/tutorials/planning/build.js +173 -0
  312. package/dist/ui/overlay/data/tutorials/planning/design.d.ts +7 -0
  313. package/dist/ui/overlay/data/tutorials/planning/design.js +205 -0
  314. package/dist/ui/overlay/data/tutorials/planning/docs.d.ts +7 -0
  315. package/dist/ui/overlay/data/tutorials/planning/docs.js +143 -0
  316. package/dist/ui/overlay/data/tutorials/planning/prd.d.ts +7 -0
  317. package/dist/ui/overlay/data/tutorials/planning/prd.js +173 -0
  318. package/dist/ui/overlay/data/tutorials/planning/scaffold.d.ts +7 -0
  319. package/dist/ui/overlay/data/tutorials/planning/scaffold.js +164 -0
  320. package/dist/ui/overlay/data/tutorials/planning/sketch.d.ts +7 -0
  321. package/dist/ui/overlay/data/tutorials/planning/sketch.js +58 -0
  322. package/dist/ui/overlay/data/tutorials/projects/anchors.d.ts +7 -0
  323. package/dist/ui/overlay/data/tutorials/projects/anchors.js +248 -0
  324. package/dist/ui/overlay/data/tutorials/projects/import-project.d.ts +7 -0
  325. package/dist/ui/overlay/data/tutorials/projects/import-project.js +172 -0
  326. package/dist/ui/overlay/data/tutorials/projects/managing-projects.d.ts +8 -0
  327. package/dist/ui/overlay/data/tutorials/projects/managing-projects.js +212 -0
  328. package/dist/ui/overlay/data/tutorials/projects/new-project.d.ts +7 -0
  329. package/dist/ui/overlay/data/tutorials/projects/new-project.js +251 -0
  330. package/dist/ui/overlay/data/tutorials/projects/session-management.d.ts +7 -0
  331. package/dist/ui/overlay/data/tutorials/projects/session-management.js +169 -0
  332. package/dist/ui/overlay/data/tutorials/teams/background-execution.d.ts +7 -0
  333. package/dist/ui/overlay/data/tutorials/teams/background-execution.js +171 -0
  334. package/dist/ui/overlay/data/tutorials/teams/multi-terminal.d.ts +8 -0
  335. package/dist/ui/overlay/data/tutorials/teams/multi-terminal.js +147 -0
  336. package/dist/ui/overlay/data/tutorials/teams/task-assignment.d.ts +7 -0
  337. package/dist/ui/overlay/data/tutorials/teams/task-assignment.js +204 -0
  338. package/dist/ui/overlay/data/tutorials/teams/team-overview.d.ts +7 -0
  339. package/dist/ui/overlay/data/tutorials/teams/team-overview.js +165 -0
  340. package/dist/ui/overlay/data/tutorials/teams/working-with-agents.d.ts +7 -0
  341. package/dist/ui/overlay/data/tutorials/teams/working-with-agents.js +172 -0
  342. package/dist/ui/overlay/impl/agents-overlay-v2.js +6 -17
  343. package/dist/ui/overlay/impl/anchors-overlay-v2.js +30 -64
  344. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.d.ts +43 -0
  345. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +232 -0
  346. package/dist/ui/overlay/impl/artifact-overlay-v2.d.ts +40 -0
  347. package/dist/ui/overlay/impl/artifact-overlay-v2.js +115 -0
  348. package/dist/ui/overlay/impl/ask-user-overlay-v2.js +2 -5
  349. package/dist/ui/overlay/impl/background-overlay-v2.d.ts +40 -0
  350. package/dist/ui/overlay/impl/background-overlay-v2.js +147 -0
  351. package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +4 -1
  352. package/dist/ui/overlay/impl/backlog-overlay-v2.js +55 -16
  353. package/dist/ui/overlay/impl/changelog-overlay-v2.d.ts +44 -0
  354. package/dist/ui/overlay/impl/changelog-overlay-v2.js +165 -0
  355. package/dist/ui/overlay/impl/commands-overlay-v2.js +4 -6
  356. package/dist/ui/overlay/impl/config-overlay-v2.d.ts +12 -1
  357. package/dist/ui/overlay/impl/config-overlay-v2.js +164 -100
  358. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +83 -0
  359. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +711 -0
  360. package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +2 -0
  361. package/dist/ui/overlay/impl/dashboard-overlay-v2.js +26 -3
  362. package/dist/ui/overlay/impl/delegations-overlay-v2.d.ts +28 -0
  363. package/dist/ui/overlay/impl/delegations-overlay-v2.js +279 -0
  364. package/dist/ui/overlay/impl/docs-overlay-v2.js +12 -9
  365. package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +7 -0
  366. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +119 -78
  367. package/dist/ui/overlay/impl/filter-overlay-v2.d.ts +41 -0
  368. package/dist/ui/overlay/impl/filter-overlay-v2.js +110 -0
  369. package/dist/ui/overlay/impl/games-overlay-v2.d.ts +31 -0
  370. package/dist/ui/overlay/impl/games-overlay-v2.js +135 -0
  371. package/dist/ui/overlay/impl/help-overlay-v2.d.ts +26 -3
  372. package/dist/ui/overlay/impl/help-overlay-v2.js +20 -42
  373. package/dist/ui/overlay/impl/login-overlay-v2.d.ts +49 -0
  374. package/dist/ui/overlay/impl/login-overlay-v2.js +277 -0
  375. package/dist/ui/overlay/impl/mcp-overlay-v2.d.ts +63 -0
  376. package/dist/ui/overlay/impl/mcp-overlay-v2.js +907 -0
  377. package/dist/ui/overlay/impl/model-overlay-v2.d.ts +57 -13
  378. package/dist/ui/overlay/impl/model-overlay-v2.js +1086 -61
  379. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +37 -6
  380. package/dist/ui/overlay/impl/new-overlay-v2.js +715 -65
  381. package/dist/ui/overlay/impl/notifications-overlay-v2.d.ts +20 -0
  382. package/dist/ui/overlay/impl/notifications-overlay-v2.js +116 -0
  383. package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.d.ts +76 -0
  384. package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.js +728 -0
  385. package/dist/ui/overlay/impl/pending-overlay-v2.d.ts +51 -0
  386. package/dist/ui/overlay/impl/pending-overlay-v2.js +445 -0
  387. package/dist/ui/overlay/impl/permission-overlay-v2.js +5 -5
  388. package/dist/ui/overlay/impl/permissions-overlay-v2.d.ts +85 -0
  389. package/dist/ui/overlay/impl/permissions-overlay-v2.js +820 -0
  390. package/dist/ui/overlay/impl/plan-approval-overlay-v2.d.ts +35 -0
  391. package/dist/ui/overlay/impl/plan-approval-overlay-v2.js +181 -0
  392. package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +36 -0
  393. package/dist/ui/overlay/impl/project-edit-overlay-v2.js +195 -0
  394. package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +1 -0
  395. package/dist/ui/overlay/impl/projects-overlay-v2.js +278 -44
  396. package/dist/ui/overlay/impl/reset-overlay-v2.d.ts +39 -0
  397. package/dist/ui/overlay/impl/reset-overlay-v2.js +107 -0
  398. package/dist/ui/overlay/impl/resume-overlay-v2.d.ts +60 -0
  399. package/dist/ui/overlay/impl/resume-overlay-v2.js +414 -0
  400. package/dist/ui/overlay/impl/session-mode-overlay-v2.d.ts +43 -0
  401. package/dist/ui/overlay/impl/session-mode-overlay-v2.js +124 -0
  402. package/dist/ui/overlay/impl/tasks-overlay-v2.d.ts +28 -0
  403. package/dist/ui/overlay/impl/tasks-overlay-v2.js +283 -0
  404. package/dist/ui/overlay/impl/team-overlay-v2.d.ts +86 -0
  405. package/dist/ui/overlay/impl/team-overlay-v2.js +692 -0
  406. package/dist/ui/overlay/impl/terminals-overlay-v2.d.ts +26 -0
  407. package/dist/ui/overlay/impl/terminals-overlay-v2.js +217 -0
  408. package/dist/ui/overlay/impl/tools-overlay-v2.js +3 -7
  409. package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +30 -16
  410. package/dist/ui/overlay/impl/tutorial-overlay-v2.js +133 -956
  411. package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +1 -0
  412. package/dist/ui/overlay/impl/workflow-overlay-v2.js +10 -4
  413. package/dist/ui/overlay/index.d.ts +20 -1
  414. package/dist/ui/overlay/index.js +19 -0
  415. package/dist/ui/overlay/types.d.ts +5 -0
  416. package/dist/ui/overlay-manager.d.ts +43 -0
  417. package/dist/ui/overlay-manager.js +238 -0
  418. package/dist/ui/overlays.js +4 -16
  419. package/dist/ui/permission-overlay.js +6 -5
  420. package/dist/ui/status-bar-controller.d.ts +33 -0
  421. package/dist/ui/status-bar-controller.js +99 -0
  422. package/dist/ui/subagent-renderer.js +3 -19
  423. package/dist/ui/terminal-autocomplete-utils.d.ts +23 -0
  424. package/dist/ui/terminal-autocomplete-utils.js +83 -0
  425. package/dist/ui/terminal-line-builders.d.ts +17 -0
  426. package/dist/ui/terminal-line-builders.js +42 -0
  427. package/dist/ui/terminal-render-item.d.ts +16 -0
  428. package/dist/ui/terminal-render-item.js +267 -0
  429. package/dist/ui/terminal-renderer.d.ts +7 -8
  430. package/dist/ui/terminal-renderer.js +7 -8
  431. package/dist/ui/terminal-types.d.ts +179 -0
  432. package/dist/ui/terminal-types.js +34 -0
  433. package/dist/ui/terminal-ui.d.ts +144 -276
  434. package/dist/ui/terminal-ui.js +384 -1861
  435. package/dist/ui/todo-zone.d.ts +19 -1
  436. package/dist/ui/todo-zone.js +71 -13
  437. package/dist/ui/tool-formatters.js +696 -1
  438. package/dist/ui/turn-metrics.d.ts +56 -0
  439. package/dist/ui/turn-metrics.js +75 -0
  440. package/dist/ui/types.d.ts +28 -0
  441. package/dist/ui/types.js +1 -0
  442. package/dist/ui/vscode-diff-ipc.d.ts +102 -0
  443. package/dist/ui/vscode-diff-ipc.js +385 -0
  444. package/dist/utils/credentials.d.ts +24 -5
  445. package/dist/utils/credentials.js +123 -9
  446. package/dist/utils/format-tokens.d.ts +13 -0
  447. package/dist/utils/format-tokens.js +18 -0
  448. package/dist/utils/git-config.d.ts +26 -0
  449. package/dist/utils/git-config.js +54 -0
  450. package/dist/utils/message-utils.d.ts +61 -0
  451. package/dist/utils/message-utils.js +72 -0
  452. package/dist/utils/model-tiers.d.ts +8 -1
  453. package/dist/utils/model-tiers.js +38 -16
  454. package/dist/utils/open-browser.d.ts +5 -0
  455. package/dist/utils/open-browser.js +32 -0
  456. package/dist/utils/path-safety.js +3 -2
  457. package/dist/utils/project-detection.d.ts +58 -0
  458. package/dist/utils/project-detection.js +424 -0
  459. package/dist/utils/project-memory.js +2 -1
  460. package/dist/utils/project-status.d.ts +2 -2
  461. package/dist/utils/startup-perf.d.ts +18 -0
  462. package/dist/utils/startup-perf.js +60 -0
  463. package/dist/utils/token-tracker.d.ts +62 -0
  464. package/dist/utils/token-tracker.js +150 -0
  465. package/dist/utils/token-types.d.ts +23 -0
  466. package/dist/utils/token-types.js +18 -0
  467. package/dist/utils/types/config-types.d.ts +32 -0
  468. package/dist/utils/types/config-types.js +8 -0
  469. package/dist/utils/update-checker.d.ts +28 -0
  470. package/dist/utils/update-checker.js +106 -0
  471. package/dist/utils/version.d.ts +7 -0
  472. package/dist/utils/version.js +10 -0
  473. package/dist/utils/vscode-detect.d.ts +39 -0
  474. package/dist/utils/vscode-detect.js +137 -0
  475. package/package.json +27 -13
  476. package/dist/commands/handler-types.d.ts +0 -68
  477. package/dist/commands/handler-types.js +0 -8
  478. package/dist/commands/handlers/agent-commands.d.ts +0 -13
  479. package/dist/commands/handlers/agent-commands.js +0 -305
  480. package/dist/commands/handlers/design-commands.d.ts +0 -15
  481. package/dist/commands/handlers/design-commands.js +0 -334
  482. package/dist/commands/handlers/index.d.ts +0 -20
  483. package/dist/commands/handlers/index.js +0 -43
  484. package/dist/commands/handlers/overlay-commands.d.ts +0 -21
  485. package/dist/commands/handlers/overlay-commands.js +0 -287
  486. package/dist/commands/handlers/project-commands.d.ts +0 -11
  487. package/dist/commands/handlers/project-commands.js +0 -167
  488. package/dist/commands/handlers/simple-commands.d.ts +0 -19
  489. package/dist/commands/handlers/simple-commands.js +0 -144
  490. package/dist/commands/registry.d.ts +0 -50
  491. package/dist/commands/registry.js +0 -75
  492. package/dist/index.old.d.ts +0 -7
  493. package/dist/index.old.js +0 -1014
  494. package/dist/repl.d.ts +0 -149
  495. package/dist/repl.js +0 -1151
  496. package/dist/templates/claude-md.d.ts +0 -7
  497. package/dist/templates/claude-md.js +0 -189
  498. package/dist/test-autocomplete.d.ts +0 -7
  499. package/dist/test-autocomplete.js +0 -85
  500. package/dist/test-tabbed-menu.d.ts +0 -7
  501. package/dist/test-tabbed-menu.js +0 -25
  502. package/dist/tool-selector.d.ts +0 -71
  503. package/dist/tool-selector.js +0 -184
  504. package/dist/tools/anchor-tools.d.ts +0 -31
  505. package/dist/tools/anchor-tools.js +0 -255
  506. package/dist/tools/backlog-wrappers.d.ts +0 -54
  507. package/dist/tools/backlog-wrappers.js +0 -338
  508. package/dist/tools/document-db.d.ts +0 -43
  509. package/dist/tools/document-db.js +0 -220
  510. package/dist/tools/workitem-db.d.ts +0 -103
  511. package/dist/tools/workitem-db.js +0 -549
  512. package/dist/ui/agents-overlay-v2.d.ts +0 -43
  513. package/dist/ui/agents-overlay-v2.js +0 -809
  514. package/dist/ui/agents-overlay.d.ts +0 -12
  515. package/dist/ui/agents-overlay.js +0 -863
  516. package/dist/ui/anchors-overlay.d.ts +0 -12
  517. package/dist/ui/anchors-overlay.js +0 -775
  518. package/dist/ui/arch-type-overlay.d.ts +0 -15
  519. package/dist/ui/arch-type-overlay.js +0 -201
  520. package/dist/ui/ask-user-overlay-v2.d.ts +0 -26
  521. package/dist/ui/ask-user-overlay-v2.js +0 -555
  522. package/dist/ui/ask-user-simple-overlay-v2.d.ts +0 -25
  523. package/dist/ui/ask-user-simple-overlay-v2.js +0 -215
  524. package/dist/ui/backlog-overlay.d.ts +0 -32
  525. package/dist/ui/backlog-overlay.js +0 -652
  526. package/dist/ui/commands-overlay-v2.d.ts +0 -33
  527. package/dist/ui/commands-overlay-v2.js +0 -441
  528. package/dist/ui/commands-overlay.d.ts +0 -16
  529. package/dist/ui/commands-overlay.js +0 -439
  530. package/dist/ui/config-overlay.d.ts +0 -35
  531. package/dist/ui/config-overlay.js +0 -707
  532. package/dist/ui/docs-overlay.d.ts +0 -17
  533. package/dist/ui/docs-overlay.js +0 -303
  534. package/dist/ui/footer-v2.d.ts +0 -222
  535. package/dist/ui/footer-v2.js +0 -1349
  536. package/dist/ui/help-overlay-v2.d.ts +0 -34
  537. package/dist/ui/help-overlay-v2.js +0 -309
  538. package/dist/ui/help-overlay.d.ts +0 -16
  539. package/dist/ui/help-overlay.js +0 -316
  540. package/dist/ui/init-overlay-v2.d.ts +0 -34
  541. package/dist/ui/init-overlay-v2.js +0 -600
  542. package/dist/ui/init-overlay.d.ts +0 -34
  543. package/dist/ui/init-overlay.js +0 -604
  544. package/dist/ui/input-prompt-v2.d.ts +0 -180
  545. package/dist/ui/input-prompt-v2.js +0 -999
  546. package/dist/ui/iteration-limit-overlay-v2.d.ts +0 -21
  547. package/dist/ui/iteration-limit-overlay-v2.js +0 -114
  548. package/dist/ui/keys-overlay-v2.d.ts +0 -41
  549. package/dist/ui/keys-overlay-v2.js +0 -248
  550. package/dist/ui/mascot-overlay-v2.d.ts +0 -41
  551. package/dist/ui/mascot-overlay-v2.js +0 -138
  552. package/dist/ui/mascot-overlay.d.ts +0 -21
  553. package/dist/ui/mascot-overlay.js +0 -146
  554. package/dist/ui/model-overlay-v2.d.ts +0 -49
  555. package/dist/ui/model-overlay-v2.js +0 -118
  556. package/dist/ui/model-overlay.d.ts +0 -27
  557. package/dist/ui/model-overlay.js +0 -221
  558. package/dist/ui/model-warning-overlay.d.ts +0 -30
  559. package/dist/ui/model-warning-overlay.js +0 -169
  560. package/dist/ui/new-overlay.d.ts +0 -34
  561. package/dist/ui/new-overlay.js +0 -604
  562. package/dist/ui/overlay/impl/init-overlay-v2.d.ts +0 -77
  563. package/dist/ui/overlay/impl/init-overlay-v2.js +0 -593
  564. package/dist/ui/overlay/overlay-types.d.ts +0 -128
  565. package/dist/ui/overlay/overlay-types.js +0 -22
  566. package/dist/ui/overlays/help-overlay-v2.d.ts +0 -28
  567. package/dist/ui/overlays/help-overlay-v2.js +0 -198
  568. package/dist/ui/overlays/index.d.ts +0 -11
  569. package/dist/ui/overlays/index.js +0 -11
  570. package/dist/ui/permission-overlay-v2.d.ts +0 -36
  571. package/dist/ui/permission-overlay-v2.js +0 -380
  572. package/dist/ui/projects-overlay.d.ts +0 -19
  573. package/dist/ui/projects-overlay.js +0 -484
  574. package/dist/ui/theme-overlay-v2.d.ts +0 -42
  575. package/dist/ui/theme-overlay-v2.js +0 -135
  576. package/dist/ui/theme-overlay.d.ts +0 -24
  577. package/dist/ui/theme-overlay.js +0 -127
  578. package/dist/ui/tools-overlay-v2.d.ts +0 -47
  579. package/dist/ui/tools-overlay-v2.js +0 -218
  580. package/dist/ui/tools-overlay.d.ts +0 -34
  581. package/dist/ui/tools-overlay.js +0 -230
  582. package/dist/ui/tutorial-overlay-v2.d.ts +0 -31
  583. package/dist/ui/tutorial-overlay-v2.js +0 -1035
  584. package/dist/ui/tutorial-overlay.d.ts +0 -11
  585. package/dist/ui/tutorial-overlay.js +0 -1034
  586. package/dist/ui/workflow-overlay.d.ts +0 -22
  587. package/dist/ui/workflow-overlay.js +0 -636
@@ -14,93 +14,23 @@
14
14
  * - All output goes through TerminalUI (no external console.log)
15
15
  */
16
16
  import { EventEmitter } from 'events';
17
- import * as readline from 'readline';
18
- import * as terminal from './terminal.js';
19
- import { getPhysicalLineCount, getVisibleLength } from './line-utils.js';
17
+ import { getVisibleLength } from './line-utils.js';
20
18
  import { getStyles } from '../themes/index.js';
21
- import { MODE_INFO } from './types.js';
22
- import { getAutocompleteCommands } from '../commands.js';
23
- import { getFileMatches, extractAtMention, replaceAtMention, } from './file-autocomplete.js';
24
- import { LiveRegion } from './live-region.js';
25
- import { renderMarkdown } from './conversation.js';
26
- export const DEFAULT_TERMINAL_UI_CONFIG = {
27
- verbose: false,
28
- theme: 'default',
29
- showMascot: true,
30
- };
31
- // =============================================================================
32
- // Constants
33
- // =============================================================================
34
- const MAX_VISIBLE_COMMANDS = 10;
35
- // =============================================================================
36
- // Mascot Expressions
37
- // =============================================================================
38
- /**
39
- * Inline mascot expressions for agent identity and state feedback.
40
- * Used to prefix agent messages and in spinner animations.
41
- */
42
- export const MASCOT = {
43
- // Core expressions
44
- neutral: '[•_•]', // Default state - regular messages
45
- thinking: '[°_°]', // Processing/thinking
46
- searching: '[◐_◐]', // Searching/scanning files
47
- success: '[^_^]', // Task completed successfully
48
- error: '[×_×]', // Error occurred
49
- confused: '[?_?]', // Needs clarification
50
- working: '[•̀_•́]', // Actively working on task
51
- // CRT monitor animation frames (subtle scanner effect)
52
- crt: ['[░░░]', '[▒░░]', '[░▒░]', '[░░▒]', '[░▒░]', '[▒░░]'],
53
- };
54
- // =============================================================================
55
- // Fuzzy Matching
56
- // =============================================================================
57
- /**
58
- * Calculate fuzzy match score for a query against a target string.
59
- * Higher score = better match. Returns -1 if no match.
60
- */
61
- function fuzzyMatchScore(query, target) {
62
- const queryLower = query.toLowerCase();
63
- const targetLower = target.toLowerCase();
64
- // Exact prefix match - highest priority (score 1000+)
65
- if (targetLower.startsWith(queryLower)) {
66
- return 1000 + (100 - target.length); // Shorter commands rank higher
67
- }
68
- // Contiguous substring match - high priority (score 500+)
69
- if (targetLower.includes(queryLower)) {
70
- const index = targetLower.indexOf(queryLower);
71
- return 500 + (100 - index); // Earlier matches rank higher
72
- }
73
- // Fuzzy match - characters appear in order (score 100+)
74
- let queryIdx = 0;
75
- let consecutiveBonus = 0;
76
- let lastMatchIdx = -1;
77
- for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
78
- if (targetLower[i] === queryLower[queryIdx]) {
79
- if (lastMatchIdx === i - 1) {
80
- consecutiveBonus += 10;
81
- }
82
- lastMatchIdx = i;
83
- queryIdx++;
84
- }
85
- }
86
- if (queryIdx === queryLower.length) {
87
- return 100 + consecutiveBonus + (100 - target.length);
88
- }
89
- return -1;
90
- }
91
- /**
92
- * Filter and rank commands matching input using fuzzy matching
93
- */
94
- function filterCommands(input, commands) {
95
- const scored = commands
96
- .map((cmd) => ({
97
- cmd,
98
- score: fuzzyMatchScore(input, cmd.command),
99
- }))
100
- .filter((item) => item.score >= 0);
101
- scored.sort((a, b) => b.score - a.score);
102
- return scored.map((item) => item.cmd);
103
- }
19
+ import { renderItem } from './terminal-render-item.js';
20
+ import { TurnMetrics } from './turn-metrics.js';
21
+ import { StatusBarController } from './status-bar-controller.js';
22
+ import { ConversationStore } from './conversation-store.js';
23
+ import { AutocompleteController } from './autocomplete-controller.js';
24
+ import { InputController } from './input-controller.js';
25
+ import { LiveRegionFacade } from './live-region-facade.js';
26
+ import { OverlayManager } from './overlay-manager.js';
27
+ import { FooterRenderer } from './footer-renderer.js';
28
+ import { KeyboardHandler } from './keyboard-handler.js';
29
+ import { DEFAULT_TERMINAL_UI_CONFIG, } from './terminal-types.js';
30
+ export { DEFAULT_TERMINAL_UI_CONFIG, MASCOT } from './terminal-types.js';
31
+ import { getPendingRequestsManager } from '../multi-agent/index.js';
32
+ import { getSessionRegistry } from '../multi-agent/session-registry.js';
33
+ import { getNotificationManager } from '../multi-agent/notification-manager.js';
104
34
  // =============================================================================
105
35
  // Footer V2 Class
106
36
  // =============================================================================
@@ -109,68 +39,120 @@ export class TerminalUI extends EventEmitter {
109
39
  promptPrefix;
110
40
  promptPrefixLen;
111
41
  config;
112
- // Conversation history (for re-rendering on config change)
113
- conversationHistory = [];
114
- // Input state (multiline)
115
- lines = [''];
116
- currentLine = 0;
117
- cursorPos = 0;
42
+ // Conversation history, restored history, and filtering
43
+ conversation = new ConversationStore();
44
+ // Input state (delegated to InputController)
45
+ input = new InputController();
46
+ get lines() { return this.input.lines; }
47
+ set lines(v) { this.input.lines = v; }
48
+ get currentLine() { return this.input.currentLine; }
49
+ set currentLine(v) { this.input.currentLine = v; }
50
+ get cursorPos() { return this.input.cursorPos; }
51
+ set cursorPos(v) { this.input.cursorPos = v; }
52
+ get suggestion() { return this.input.suggestion; }
53
+ set suggestion(v) { this.input.suggestion = v; }
54
+ get queuedInputs() { return this.input.queuedInputs; }
55
+ set queuedInputs(v) { this.input.queuedInputs = v; }
118
56
  mode;
119
57
  projectName = null;
120
58
  todos = [];
59
+ // Plan mode state
60
+ activePlanId = null;
61
+ activePlanName = null;
121
62
  spinnerText = null;
122
- spinnerFrame = 0;
123
63
  agentRunning = false;
124
- queuedInputs = [];
125
64
  agentMessageQueue = [];
126
65
  currentTool = null;
66
+ // Per-turn token and tool call tracking
67
+ turnMetrics = new TurnMetrics();
127
68
  // LiveRegion for running tools (bash, subagents)
128
- liveRegion = new LiveRegion();
129
- // Command autocomplete state (for /commands)
130
- autocomplete = {
131
- active: false,
132
- matches: [],
133
- selectedIndex: 0,
134
- scrollOffset: 0,
135
- };
136
- // File autocomplete state (for @paths)
137
- fileAutocomplete = {
138
- active: false,
139
- matches: [],
140
- selectedIndex: 0,
141
- scrollOffset: 0,
142
- partial: '',
143
- };
144
- // History state
145
- history = [];
146
- historyIndex = -1;
147
- savedInput = '';
69
+ live = new LiveRegionFacade({
70
+ requestRender: () => { this.needsRender = true; },
71
+ print: (item) => { this.print(item); },
72
+ emit: (event, ...args) => { this.emit(event, ...args); },
73
+ setSpinnerText: (text) => { this.spinnerText = text; },
74
+ });
75
+ // Autocomplete controller (command, file, agent)
76
+ ac = new AutocompleteController();
148
77
  // Render tracking
149
- lastRenderHeight = 0;
150
- cursorLineFromBottom = 0;
151
78
  isRunning = false;
152
79
  isPaused = false;
153
80
  renderTimer = null;
154
81
  needsRender = false;
155
- // Double Esc detection
156
- lastEscapeTime = 0;
157
- // Ghost text suggestion
158
- suggestion = null;
82
+ // Active team agent (for multi-agent footer display)
83
+ activeTeamAgent = null;
159
84
  // Todo visibility (Ctrl+T to toggle)
160
85
  showTodos = true;
161
86
  // View mode (Ctrl+O to toggle verbose view)
162
87
  // normal: compact output
163
88
  // verbose-temp: temporarily show last N items verbose (any key returns to normal)
164
89
  viewMode = 'normal';
165
- // Spinner animation - CRT monitor fill effect
166
- spinnerFrames = MASCOT.crt;
167
- spinnerTimer = null;
168
- // Overlay stack (supports nested overlays)
169
- overlayStack = [];
170
- overlayResolvers = new Map();
171
- overlayRenderState = { lineCount: 0, maxLineCount: 0 };
172
- // Buffer for items printed while overlay is active (to prevent cursor corruption)
173
- overlayPrintBuffer = [];
90
+ // Status bar navigation (down arrow into footer indicators)
91
+ statusBar = new StatusBarController({
92
+ requestRender: () => { this.needsRender = true; },
93
+ emit: (event, ...args) => { this.emit(event, ...args); },
94
+ });
95
+ // Overlay lifecycle (stack, resolvers, render state, print buffer)
96
+ overlay = new OverlayManager({
97
+ clearFooter: () => { this.footer.clear(); },
98
+ renderFooter: () => { this.footer.render(); },
99
+ print: (item) => { this.print(item); },
100
+ renderItemToConsole: (item) => { this.renderItemToConsole(item); },
101
+ });
102
+ // Footer renderer (clear, render, spinner animation, line builders)
103
+ footer = new FooterRenderer({
104
+ isFooterRenderAllowed: () => this.isRunning && !this.isPaused && !this.overlay.hasActiveOverlay(),
105
+ getRenderData: () => ({
106
+ liveRegion: this.live.region,
107
+ liveVerbose: this.config.verbose,
108
+ agentRunning: this.agentRunning,
109
+ currentTool: this.currentTool,
110
+ spinnerText: this.spinnerText,
111
+ activeBashInfo: this.live.getActiveBashInfo(),
112
+ turnMetrics: this.turnMetrics,
113
+ todos: this.todos,
114
+ showTodos: this.showTodos,
115
+ lines: this.lines,
116
+ currentLine: this.currentLine,
117
+ cursorPos: this.cursorPos,
118
+ queuedInputs: this.queuedInputs,
119
+ suggestion: this.suggestion,
120
+ promptPrefix: this.promptPrefix,
121
+ promptPrefixLen: this.promptPrefixLen,
122
+ ac: this.ac,
123
+ statusBar: this.statusBar,
124
+ mode: this.mode,
125
+ activePlanName: this.activePlanName,
126
+ activeTeamAgent: this.activeTeamAgent,
127
+ projectName: this.projectName,
128
+ conversationFilterActive: this.conversation.isFilterActive,
129
+ hasRestoredHistory: this.conversation.hasRestoredHistory(),
130
+ config: this.config,
131
+ }),
132
+ });
133
+ // Keyboard input handler (capture, escape, overlay routing, keypress dispatch)
134
+ keyboard = new KeyboardHandler({
135
+ isGuardActive: () => !this.isRunning || this.isPaused,
136
+ isAgentRunning: () => this.agentRunning,
137
+ getViewMode: () => this.viewMode,
138
+ setViewMode: (mode) => { this.viewMode = mode; },
139
+ requestRender: () => { this.needsRender = true; },
140
+ emit: (event, ...args) => { this.emit(event, ...args); },
141
+ input: this.input,
142
+ ac: this.ac,
143
+ overlay: this.overlay,
144
+ statusBar: this.statusBar,
145
+ getActiveOverlay: () => this.overlay.getActiveOverlay(),
146
+ hasActiveOverlay: () => this.overlay.hasActiveOverlay(),
147
+ hasActiveBash: () => this.live.hasActiveBash(),
148
+ backgroundBashCommand: () => { this.backgroundBashCommand(); },
149
+ hasRestoredHistory: () => this.conversation.hasRestoredHistory(),
150
+ showRestoredHistory: () => { this.showRestoredHistory(); },
151
+ toggleVerboseView: () => { this.toggleVerboseView(); },
152
+ toggleLiveRegionExpanded: () => { this.toggleLiveRegionExpanded(); },
153
+ toggleTodos: () => { this.toggleTodos(); },
154
+ reRenderConversationVerbose: (verbose) => { this.reRenderConversationVerbose(verbose); },
155
+ });
174
156
  constructor(options = {}) {
175
157
  super();
176
158
  const s = getStyles();
@@ -180,38 +162,53 @@ export class TerminalUI extends EventEmitter {
180
162
  this.config = { ...DEFAULT_TERMINAL_UI_CONFIG, ...options.config };
181
163
  }
182
164
  // ===========================================================================
183
- // Input value helpers
165
+ // Status bar navigation (delegated to StatusBarController)
184
166
  // ===========================================================================
185
- /** Get the full input value (all lines joined with newlines) */
186
- getInputValue() {
187
- return this.lines.join('\n');
188
- }
189
- /** Get the current line content */
190
- getCurrentLineContent() {
191
- return this.lines[this.currentLine];
167
+ /** Set background agent count (called by REPL to keep UI in sync). */
168
+ setBackgroundAgentCount(count) {
169
+ this.statusBar.setBackgroundAgentCount(count);
192
170
  }
193
- /** Clear input and reset to single empty line */
194
- clearInput() {
195
- this.lines = [''];
196
- this.currentLine = 0;
197
- this.cursorPos = 0;
171
+ /** Set MCP loading state for status bar indicator. */
172
+ setMCPLoading(loading, toolCount) {
173
+ this.statusBar.setMCPLoading(loading, toolCount);
198
174
  }
199
175
  // ===========================================================================
200
176
  // Lifecycle
201
177
  // ===========================================================================
202
- start() {
178
+ start(skipInitialRender) {
203
179
  if (this.isRunning)
204
180
  return;
205
181
  this.isRunning = true;
206
182
  this.isPaused = false;
207
183
  // Start keyboard input handling
208
- this.startKeyboardCapture();
209
- // Initial render
210
- this.render();
184
+ this.keyboard.startCapture();
185
+ // Initial render (skip when a startup overlay will be shown immediately)
186
+ if (!skipInitialRender) {
187
+ this.footer.render();
188
+ }
189
+ // Phase 3b: Listen for pending requests changes to trigger re-render
190
+ const pendingManager = getPendingRequestsManager();
191
+ pendingManager.on('count-changed', () => {
192
+ this.needsRender = true;
193
+ });
194
+ // Multi-terminal: Listen for session count changes (other terminals joining/leaving)
195
+ const registry = getSessionRegistry();
196
+ if (registry) {
197
+ registry.onCountChange(() => {
198
+ this.needsRender = true;
199
+ });
200
+ }
201
+ // Cross-session notifications: re-render when count changes
202
+ const notifMgr = getNotificationManager();
203
+ if (notifMgr) {
204
+ notifMgr.on('count-changed', () => {
205
+ this.needsRender = true;
206
+ });
207
+ }
211
208
  // Start render loop (60ms = ~16fps)
212
209
  this.renderTimer = setInterval(() => {
213
210
  if (this.needsRender && !this.isPaused) {
214
- this.render();
211
+ this.footer.render();
215
212
  this.needsRender = false;
216
213
  }
217
214
  }, 60);
@@ -226,11 +223,11 @@ export class TerminalUI extends EventEmitter {
226
223
  this.renderTimer = null;
227
224
  }
228
225
  // Stop spinner
229
- this.stopSpinnerAnimation();
226
+ this.footer.stopSpinnerAnimation();
230
227
  // Stop keyboard capture
231
- this.stopKeyboardCapture();
228
+ this.keyboard.stopCapture();
232
229
  // Clear footer
233
- this.clear();
230
+ this.footer.clear();
234
231
  }
235
232
  // ===========================================================================
236
233
  // Public API - Output (THE KEY METHODS)
@@ -245,222 +242,38 @@ export class TerminalUI extends EventEmitter {
245
242
  */
246
243
  print(item) {
247
244
  // Store in history for re-render capability
248
- this.conversationHistory.push(item);
245
+ this.conversation.addItem(item);
246
+ // Check if item should be shown based on current filter
247
+ if (!this.conversation.shouldShowItem(item)) {
248
+ return; // Filtered out - stored but not displayed
249
+ }
249
250
  // If overlay is active, buffer the item to render later
250
251
  // This prevents output from corrupting overlay cursor tracking
251
- if (this.hasActiveOverlay()) {
252
- this.overlayPrintBuffer.push(item);
252
+ if (this.overlay.hasActiveOverlay()) {
253
+ this.overlay.bufferItem(item);
253
254
  return;
254
255
  }
255
256
  // IMPORTANT: If needsRender is true, render FIRST to update lastRenderHeight.
256
257
  // This prevents ghost lines when footer height changed (e.g., spinner started)
257
258
  // but render loop hasn't fired yet. Without this, clear() uses stale height.
258
259
  if (this.needsRender) {
259
- this.render();
260
+ this.footer.render();
260
261
  this.needsRender = false;
261
262
  }
262
263
  // Clear footer, render item, re-render footer
263
- this.clear();
264
- this.renderItem(item);
265
- this.render();
264
+ this.footer.clear();
265
+ this.renderItemToConsole(item);
266
+ this.footer.render();
266
267
  }
267
268
  /**
268
269
  * Render a single item to the console.
269
270
  * Config-aware: respects verbose, showMascot settings.
271
+ * Delegates to standalone renderItem() function.
270
272
  */
271
- renderItem(item) {
272
- const s = getStyles();
273
- switch (item.type) {
274
- case 'user-message':
275
- console.log('');
276
- console.log(s.primaryBold('> ') + item.text);
277
- console.log(''); // Trailing blank for separation
278
- break;
279
- case 'agent-text': {
280
- // Render markdown for proper formatting (headers, lists, bold, code, etc.)
281
- const rendered = renderMarkdown(item.text);
282
- // Prefix agent messages with mascot expression (if enabled)
283
- if (this.config.showMascot) {
284
- const expr = item.expression ? MASCOT[item.expression] : MASCOT.neutral;
285
- // Only prefix the first line with mascot
286
- const lines = rendered.split('\n');
287
- if (lines.length > 0) {
288
- console.log(s.primary(expr) + s.muted(' > ') + lines[0]);
289
- for (let i = 1; i < lines.length; i++) {
290
- console.log(lines[i]);
291
- }
292
- }
293
- }
294
- else {
295
- console.log(rendered);
296
- }
297
- console.log(''); // Trailing blank for separation
298
- break;
299
- }
300
- case 'thinking':
301
- // Only show thinking in verbose mode
302
- if (this.config.verbose) {
303
- console.log(s.muted(`∴ Thinking…`));
304
- console.log('');
305
- // Indent thinking text
306
- const lines = item.text.split('\n');
307
- for (const line of lines) {
308
- console.log(s.muted(` ${line}`));
309
- }
310
- console.log('');
311
- }
312
- // If not verbose, skip entirely (but still stored in history)
313
- break;
314
- case 'tool-start': {
315
- // Truncate long params (e.g., long bash commands)
316
- const maxLen = Math.min(60, terminal.getTerminalWidth() - 15);
317
- let params = item.params;
318
- // Handle multi-line params
319
- const nlIdx = params.indexOf('\n');
320
- if (nlIdx !== -1) {
321
- const first = params.slice(0, nlIdx).trim();
322
- const count = params.split('\n').length;
323
- params = first.length > 0 ? `${first}… (${String(count)} lines)` : `(${String(count)} line script)`;
324
- }
325
- // Truncate if still too long
326
- if (params.length > maxLen) {
327
- params = params.slice(0, maxLen) + '…';
328
- }
329
- console.log(s.info(`● ${item.name}`) + s.muted(`(${params})`));
330
- break;
331
- }
332
- case 'tool-result': {
333
- // Truncate long params (e.g., long bash commands)
334
- const maxParamsLen = Math.min(60, terminal.getTerminalWidth() - 15);
335
- let paramsDisplay = item.params;
336
- // Handle multi-line params (show first line + count)
337
- const newlineIdx = paramsDisplay.indexOf('\n');
338
- if (newlineIdx !== -1) {
339
- const firstLine = paramsDisplay.slice(0, newlineIdx).trim();
340
- const lineCount = paramsDisplay.split('\n').length;
341
- paramsDisplay = firstLine.length > 0
342
- ? `${firstLine}… (${String(lineCount)} lines)`
343
- : `(${String(lineCount)} line script)`;
344
- }
345
- // Truncate if still too long
346
- if (paramsDisplay.length > maxParamsLen) {
347
- paramsDisplay = paramsDisplay.slice(0, maxParamsLen) + '…';
348
- }
349
- console.log(s.info(`● ${item.name}`) + s.muted(`(${paramsDisplay})`));
350
- if (item.content) {
351
- const lines = item.content.split('\n').filter((l) => l.length > 0);
352
- const maxPreviewLines = 3;
353
- if (this.config.verbose) {
354
- // Verbose: show all lines
355
- for (const line of lines) {
356
- console.log(s.muted(` ⎿ ${line}`));
357
- }
358
- }
359
- else if (lines.length > 0) {
360
- // Compact: show first few lines + hidden count
361
- const previewLines = lines.slice(0, maxPreviewLines);
362
- const hiddenCount = lines.length - maxPreviewLines;
363
- for (let i = 0; i < previewLines.length; i++) {
364
- if (i === 0) {
365
- console.log(s.muted(` ⎿ ${previewLines[i]}`));
366
- }
367
- else {
368
- console.log(s.muted(` ${previewLines[i]}`));
369
- }
370
- }
371
- if (hiddenCount > 0) {
372
- console.log(s.muted(` … +${String(hiddenCount)} lines`) + s.muted(' (ctrl+o to expand)'));
373
- }
374
- }
375
- else {
376
- // No output lines
377
- const expandHint = s.muted(' (ctrl+o to expand)');
378
- if (item.success === false) {
379
- console.log(s.error(` ⎿ ${item.summary}`) + expandHint);
380
- }
381
- else {
382
- console.log(s.muted(` ⎿ ${item.summary}`) + expandHint);
383
- }
384
- }
385
- }
386
- else {
387
- // No content, just show summary
388
- if (item.success === false) {
389
- console.log(s.error(` ⎿ ${item.summary}`));
390
- }
391
- else {
392
- console.log(s.muted(` ⎿ ${item.summary}`));
393
- }
394
- }
395
- console.log('');
396
- break;
397
- }
398
- case 'tool-error': {
399
- // Truncate long params
400
- const maxErrLen = Math.min(60, terminal.getTerminalWidth() - 15);
401
- let errParams = item.params;
402
- const errNlIdx = errParams.indexOf('\n');
403
- if (errNlIdx !== -1) {
404
- const errFirst = errParams.slice(0, errNlIdx).trim();
405
- const errCount = errParams.split('\n').length;
406
- errParams = errFirst.length > 0 ? `${errFirst}… (${String(errCount)} lines)` : `(${String(errCount)} line script)`;
407
- }
408
- if (errParams.length > maxErrLen) {
409
- errParams = errParams.slice(0, maxErrLen) + '…';
410
- }
411
- console.log(s.info(`● ${item.name}`) + s.muted(`(${errParams})`));
412
- console.log(s.error(` ⎿ Error: ${item.error}`));
413
- console.log('');
414
- break;
415
- }
416
- case 'interrupted': {
417
- // Show what was ongoing (if provided)
418
- if (item.action) {
419
- console.log(s.info(`● ${item.action}`));
420
- }
421
- else {
422
- // No action means interrupted right after user message.
423
- // Move cursor up to consume the trailing blank from user-message.
424
- //
425
- // ⚠️ WARNING: This cursor manipulation is fragile!
426
- // It assumes the previous item was user-message which adds a trailing blank.
427
- // If we add items that don't add trailing blanks, or if the rendering
428
- // order changes, this could cause visual artifacts (overwriting content).
429
- // If issues arise, consider tracking the last printed item type instead.
430
- process.stdout.write('\x1b[1A'); // Move up one line
431
- }
432
- // Show interrupted line with mascot
433
- const suggestion = item.suggestion ?? 'What should I do instead?';
434
- console.log(s.warning(` ⎿ ${MASCOT.confused} Interrupted - ${suggestion}`));
435
- console.log('');
436
- // Also set as ghost text suggestion
437
- this.setSuggestion(suggestion);
438
- break;
439
- }
440
- case 'error':
441
- console.log(s.error(`${MASCOT.error} ${item.message}`));
442
- console.log('');
443
- break;
444
- case 'success':
445
- console.log(s.success(`${MASCOT.success} ${item.message}`));
446
- console.log('');
447
- break;
448
- case 'info':
449
- console.log(s.info(item.message));
450
- console.log('');
451
- break;
452
- case 'warning':
453
- console.log(s.warning(item.message));
454
- console.log('');
455
- break;
456
- case 'raw':
457
- console.log(item.text);
458
- break;
459
- case 'raw-lines':
460
- for (const line of item.lines) {
461
- console.log(line);
462
- }
463
- break;
273
+ renderItemToConsole(item) {
274
+ const suggestion = renderItem(item, this.config);
275
+ if (suggestion !== null) {
276
+ this.setSuggestion(suggestion);
464
277
  }
465
278
  }
466
279
  /**
@@ -470,9 +283,9 @@ export class TerminalUI extends EventEmitter {
470
283
  * @deprecated Prefer using print() with PrintableItem
471
284
  */
472
285
  output(callback) {
473
- this.clear();
286
+ this.footer.clear();
474
287
  callback();
475
- this.render();
288
+ this.footer.render();
476
289
  }
477
290
  // ===========================================================================
478
291
  // Public API - State setters
@@ -481,11 +294,11 @@ export class TerminalUI extends EventEmitter {
481
294
  const wasRunning = this.agentRunning;
482
295
  this.agentRunning = running;
483
296
  if (running && !wasRunning) {
484
- this.startSpinnerAnimation();
297
+ this.footer.startSpinnerAnimation(() => { this.needsRender = true; });
485
298
  // Let render loop handle it
486
299
  }
487
300
  else if (!running && wasRunning) {
488
- this.stopSpinnerAnimation();
301
+ this.footer.stopSpinnerAnimation();
489
302
  this.currentTool = null;
490
303
  }
491
304
  this.needsRender = true;
@@ -511,6 +324,51 @@ export class TerminalUI extends EventEmitter {
511
324
  }
512
325
  this.needsRender = true;
513
326
  }
327
+ /**
328
+ * Set whether extended thinking mode is active
329
+ * Shows "thinking" indicator in status line
330
+ */
331
+ setThinking(thinking) {
332
+ this.turnMetrics.isThinking = thinking;
333
+ this.needsRender = true;
334
+ }
335
+ // ===========================================================================
336
+ // Token Tracking (per-turn metrics)
337
+ // ===========================================================================
338
+ /**
339
+ * Add input/output tokens for live display
340
+ * @param thinkingTokens - Optional thinking tokens (Gemini 2.5+ models)
341
+ * @param cacheReadTokens - Optional cache read tokens (Anthropic/Gemini)
342
+ * @param debugPayload - Optional debug payload sizes (char counts)
343
+ */
344
+ addTokens(inputTokens, outputTokens, thinkingTokens, cacheReadTokens, debugPayload) {
345
+ this.turnMetrics.addTokens(inputTokens, outputTokens, thinkingTokens, cacheReadTokens, debugPayload);
346
+ this.needsRender = true;
347
+ }
348
+ /**
349
+ * Increment tool call counter
350
+ */
351
+ incrementToolCalls() {
352
+ this.turnMetrics.incrementToolCalls();
353
+ }
354
+ /**
355
+ * Increment API call counter (called on each LLM request completion)
356
+ */
357
+ incrementApiCalls() {
358
+ this.turnMetrics.incrementApiCalls();
359
+ }
360
+ /**
361
+ * Reset turn metrics (call when agent starts a new turn)
362
+ */
363
+ resetTurnMetrics() {
364
+ this.turnMetrics.reset();
365
+ }
366
+ /**
367
+ * Get current turn metrics
368
+ */
369
+ getTurnMetrics() {
370
+ return this.turnMetrics.getSnapshot();
371
+ }
514
372
  setMode(mode) {
515
373
  this.mode = mode;
516
374
  this.needsRender = true;
@@ -526,215 +384,117 @@ export class TerminalUI extends EventEmitter {
526
384
  return this.projectName;
527
385
  }
528
386
  /**
529
- * Set ghost text suggestion (shown when input is empty)
387
+ * Set the active team agent (displayed in footer)
388
+ * Pass null to clear (default agent or no team)
530
389
  */
531
- setSuggestion(text) {
532
- this.suggestion = text;
533
- this.needsRender = true;
534
- }
535
- getSuggestion() {
536
- return this.suggestion;
537
- }
538
- clearSuggestion() {
539
- this.suggestion = null;
390
+ setActiveTeamAgent(agent) {
391
+ this.activeTeamAgent = agent;
540
392
  this.needsRender = true;
541
393
  }
542
- // ===========================================================================
543
- // Public API - LiveRegion (subagents, bash commands)
544
- // ===========================================================================
545
394
  /**
546
- * Add a subagent to the live region.
395
+ * Get the active team agent
547
396
  */
548
- addSubagent(id, agentType, description) {
549
- const item = {
550
- id,
551
- type: 'subagent',
552
- status: 'running',
553
- startTime: Date.now(),
554
- agentType,
555
- description,
556
- toolCount: 0,
557
- tokenCount: 0,
558
- lastAction: '',
559
- lastActionDetails: [],
560
- };
561
- this.liveRegion.addItem(item);
562
- // Let render loop handle it (same as addBashCommand)
563
- this.needsRender = true;
397
+ getActiveTeamAgent() {
398
+ return this.activeTeamAgent;
564
399
  }
565
400
  /**
566
- * Update a subagent's tool usage.
401
+ * Set the callback to get available team agents for $ autocomplete
567
402
  */
568
- updateSubagentTool(id, toolName, summary) {
569
- const item = this.liveRegion.getItem(id);
570
- if (item && item.type === 'subagent') {
571
- const newAction = `${toolName}: ${summary}`;
572
- // Add previous action to history (for expanded view)
573
- const updatedDetails = item.lastAction
574
- ? [...item.lastActionDetails, item.lastAction]
575
- : [...item.lastActionDetails];
576
- this.liveRegion.updateItem(id, {
577
- toolCount: item.toolCount + 1,
578
- lastAction: newAction,
579
- lastActionDetails: updatedDetails,
580
- });
581
- this.needsRender = true;
582
- }
403
+ setAgentSuggestionsCallback(callback) {
404
+ this.ac.getAgentSuggestions = callback;
583
405
  }
584
406
  /**
585
- * Mark a subagent as completed.
407
+ * Set the callback to get available commands for / autocomplete
586
408
  */
587
- completeSubagent(id, success, tokenCount, _error) {
588
- const item = this.liveRegion.getItem(id);
589
- if (item && item.type === 'subagent') {
590
- this.liveRegion.updateItem(id, {
591
- status: success ? 'done' : 'error',
592
- tokenCount: tokenCount ?? item.tokenCount,
593
- endTime: Date.now(),
594
- });
595
- this.liveRegion.completeItem(id, success ? 'done' : 'error');
596
- this.needsRender = true;
597
- }
409
+ setCommandsCallback(callback) {
410
+ this.ac.getCommands = callback;
598
411
  }
599
412
  /**
600
- * Remove a subagent and return it for committing to scrolling zone.
413
+ * Set the active plan (for Plan Mode display in footer)
601
414
  */
602
- removeSubagent(id) {
603
- const item = this.liveRegion.removeItem(id);
415
+ setActivePlan(planId, planName) {
416
+ this.activePlanId = planId;
417
+ this.activePlanName = planName;
604
418
  this.needsRender = true;
605
- return item;
606
419
  }
607
420
  /**
608
- * Clear all subagents from live region.
421
+ * Get the active plan info
609
422
  */
610
- clearLiveRegion() {
611
- this.liveRegion.clear();
612
- this.needsRender = true;
423
+ getActivePlan() {
424
+ return { id: this.activePlanId, name: this.activePlanName };
613
425
  }
614
426
  /**
615
- * Toggle expanded view in live region (Ctrl+O).
427
+ * Clear the active plan
616
428
  */
617
- toggleLiveRegionExpanded() {
618
- this.liveRegion.toggleExpanded();
429
+ clearActivePlan() {
430
+ this.activePlanId = null;
431
+ this.activePlanName = null;
619
432
  this.needsRender = true;
620
433
  }
621
434
  /**
622
- * Check if live region has items.
435
+ * Set ghost text suggestion (shown when input is empty)
623
436
  */
624
- hasLiveItems() {
625
- return this.liveRegion.hasItems();
437
+ setSuggestion(text) {
438
+ this.input.suggestion = text;
439
+ this.needsRender = true;
626
440
  }
627
- /**
628
- * Get live region for direct access if needed.
629
- */
630
- getLiveRegion() {
631
- return this.liveRegion;
441
+ getSuggestion() {
442
+ return this.input.suggestion;
443
+ }
444
+ clearSuggestion() {
445
+ this.input.suggestion = null;
446
+ this.needsRender = true;
632
447
  }
633
448
  // ===========================================================================
634
- // Live Region - Bash Commands
449
+ // Public API - LiveRegion (subagents, bash commands)
635
450
  // ===========================================================================
636
- /**
637
- * Add a bash command to the live region.
638
- */
451
+ addSubagent(id, agentType, description) {
452
+ this.live.addSubagent(id, agentType, description);
453
+ }
454
+ updateSubagentTool(id, toolName, summary) {
455
+ this.live.updateSubagentTool(id, toolName, summary);
456
+ }
457
+ completeSubagent(id, success, tokenCount, _error) {
458
+ this.live.completeSubagent(id, success, tokenCount, _error);
459
+ }
460
+ removeSubagent(id) {
461
+ return this.live.removeSubagent(id);
462
+ }
463
+ clearLiveRegion() {
464
+ this.live.clearLiveRegion();
465
+ }
466
+ toggleLiveRegionExpanded() {
467
+ this.live.toggleLiveRegionExpanded();
468
+ }
469
+ hasLiveItems() {
470
+ return this.live.hasLiveItems();
471
+ }
472
+ getLiveRegion() {
473
+ return this.live.getLiveRegion();
474
+ }
639
475
  addBashCommand(id, command) {
640
- const item = {
641
- id,
642
- type: 'bash',
643
- status: 'running',
644
- startTime: Date.now(),
645
- command,
646
- output: [],
647
- };
648
- this.liveRegion.addItem(item);
649
- this.needsRender = true;
476
+ this.live.addBashCommand(id, command);
650
477
  }
651
- /**
652
- * Add output line to a bash command.
653
- */
654
478
  updateBashOutput(id, line) {
655
- const item = this.liveRegion.getItem(id);
656
- if (item && item.type === 'bash') {
657
- item.output.push(line);
658
- this.liveRegion.updateItem(id, { output: [...item.output] });
659
- this.needsRender = true;
660
- }
479
+ this.live.updateBashOutput(id, line);
661
480
  }
662
- /**
663
- * Mark a bash command as completed.
664
- */
665
481
  completeBashCommand(id, exitCode) {
666
- const item = this.liveRegion.getItem(id);
667
- if (item && item.type === 'bash') {
668
- this.liveRegion.updateItem(id, {
669
- status: exitCode === 0 ? 'done' : 'error',
670
- exitCode,
671
- endTime: Date.now(),
672
- });
673
- this.liveRegion.completeItem(id, exitCode === 0 ? 'done' : 'error');
674
- this.needsRender = true;
675
- }
482
+ this.live.completeBashCommand(id, exitCode);
676
483
  }
677
- /**
678
- * Remove a bash command and commit it to the scrolling zone.
679
- */
680
484
  commitBashCommand(id) {
681
- const item = this.liveRegion.removeItem(id);
682
- if (item && item.type === 'bash') {
683
- const output = item.output.join('\n') || '(no output)';
684
- const exitCode = item.exitCode ?? 0;
685
- // Print the bash output to scrolling zone
686
- this.print({
687
- type: 'tool-result',
688
- name: 'Bash',
689
- params: item.command,
690
- summary: exitCode === 0 ? 'Completed' : `Exit code ${String(exitCode)}`,
691
- content: output,
692
- success: exitCode === 0,
693
- });
694
- }
695
- this.needsRender = true;
485
+ this.live.commitBashCommand(id);
696
486
  }
697
- /**
698
- * Remove a subagent and commit it to the scrolling zone.
699
- * This is the atomic equivalent of commitBashCommand for subagents.
700
- * The result content comes from tool_end, not from the LiveRegion item.
701
- */
702
- commitSubagent(id, resultContent, success) {
703
- // Remove from LiveRegion FIRST (no needsRender yet - that's the key!)
704
- const item = this.liveRegion.removeItem(id);
705
- if (item && item.type === 'subagent') {
706
- // Build summary from item stats
707
- const toolCount = item.toolCount;
708
- const tokenCount = item.tokenCount;
709
- const duration = item.endTime
710
- ? Math.round((item.endTime - item.startTime) / 1000)
711
- : Math.round((Date.now() - item.startTime) / 1000);
712
- const tokenStr = tokenCount > 0 ? `, ${this.formatTokens(tokenCount)} tokens` : '';
713
- const summary = `${String(toolCount)} tool calls${tokenStr}, ${String(duration)}s`;
714
- // Print atomically (uses current lastRenderHeight before it changes)
715
- this.print({
716
- type: 'tool-result',
717
- name: item.agentType,
718
- params: item.description,
719
- summary,
720
- content: resultContent.length > 200 ? resultContent : undefined,
721
- success,
722
- });
723
- // Reset spinner text
724
- this.spinnerText = null;
725
- }
726
- // Set needsRender AFTER print (same pattern as commitBashCommand)
727
- this.needsRender = true;
728
- return item;
487
+ backgroundBashCommand() {
488
+ this.live.backgroundBashCommand();
729
489
  }
730
- /**
731
- * Format token count (e.g., 5000 -> "5.0k")
732
- */
733
- formatTokens(tokens) {
734
- if (tokens >= 1000) {
735
- return `${(tokens / 1000).toFixed(1)}k`;
736
- }
737
- return String(tokens);
490
+ hasActiveBash() {
491
+ return this.live.hasActiveBash();
492
+ }
493
+ getActiveBashInfo() {
494
+ return this.live.getActiveBashInfo();
495
+ }
496
+ commitSubagent(id, resultContent, success) {
497
+ return this.live.commitSubagent(id, resultContent, success);
738
498
  }
739
499
  /**
740
500
  * Toggle todo list visibility (Ctrl+T)
@@ -779,8 +539,7 @@ export class TerminalUI extends EventEmitter {
779
539
  // Clear screen and scrollback buffer
780
540
  process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
781
541
  // Reset footer state
782
- this.lastRenderHeight = 0;
783
- this.cursorLineFromBottom = 0;
542
+ this.footer.resetRenderState();
784
543
  // Show mode indicator at top
785
544
  if (verbose) {
786
545
  console.log(s.info(`[Verbose View Mode] Press any key to return to normal view`));
@@ -789,8 +548,8 @@ export class TerminalUI extends EventEmitter {
789
548
  // Re-render all items with temporary verbose override
790
549
  const originalVerbose = this.config.verbose;
791
550
  this.config.verbose = verbose;
792
- for (const item of this.conversationHistory) {
793
- this.renderItem(item);
551
+ for (const item of this.conversation.history) {
552
+ this.renderItemToConsole(item);
794
553
  }
795
554
  // Restore original config
796
555
  this.config.verbose = originalVerbose;
@@ -798,6 +557,46 @@ export class TerminalUI extends EventEmitter {
798
557
  this.needsRender = true;
799
558
  }
800
559
  // ===========================================================================
560
+ // Public API - Restored History (Ctrl+R)
561
+ // ===========================================================================
562
+ /**
563
+ * Store restored conversation items for Ctrl+R history view.
564
+ * Called when a session is resumed/restored.
565
+ */
566
+ setRestoredHistory(items) {
567
+ this.conversation.setRestoredHistory(items);
568
+ }
569
+ /**
570
+ * Check if there is restored history available.
571
+ */
572
+ hasRestoredHistory() {
573
+ return this.conversation.hasRestoredHistory();
574
+ }
575
+ /**
576
+ * Show restored conversation history (Ctrl+R).
577
+ * Renders items permanently into the scrollback buffer.
578
+ * Stays in normal mode — the output persists like any other printed content.
579
+ */
580
+ showRestoredHistory() {
581
+ const s = getStyles();
582
+ // Clear existing footer to make room for output
583
+ this.footer.clear();
584
+ // Print header
585
+ console.log('');
586
+ console.log(s.info(`── Conversation History (${String(this.conversation.restoredHistory.length)} items) ──`));
587
+ console.log('');
588
+ // Render each restored item into the scrollback
589
+ for (const item of this.conversation.restoredHistory) {
590
+ this.renderItemToConsole(item);
591
+ }
592
+ console.log('');
593
+ console.log(s.muted(`── End of history ──`));
594
+ console.log('');
595
+ // Reset footer state so it re-renders cleanly below the output
596
+ this.footer.resetRenderState();
597
+ this.needsRender = true;
598
+ }
599
+ // ===========================================================================
801
600
  // Public API - Config
802
601
  // ===========================================================================
803
602
  /**
@@ -829,16 +628,21 @@ export class TerminalUI extends EventEmitter {
829
628
  */
830
629
  reRenderConversation() {
831
630
  // Clear screen AND scrollback buffer, then move to home
832
- // \x1b[2J - clear entire screen
833
- // \x1b[3J - clear scrollback buffer (prevents accumulation)
834
- // \x1b[H - move cursor to home position
835
631
  process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
836
632
  // Reset footer state since we cleared everything
837
- this.lastRenderHeight = 0;
838
- this.cursorLineFromBottom = 0;
839
- // Re-render all items with new config
840
- for (const item of this.conversationHistory) {
841
- this.renderItem(item);
633
+ this.footer.resetRenderState();
634
+ // Show filter indicator if active
635
+ if (this.conversation.isFilterActive) {
636
+ const agentList = [...this.conversation.filterAgentIds].map(id => `$${id}`).join(', ');
637
+ const s = getStyles();
638
+ console.log(s.muted(`── Showing messages from: ${agentList} ──`));
639
+ console.log('');
640
+ }
641
+ // Re-render all items with new config (applying filter)
642
+ for (const item of this.conversation.history) {
643
+ if (this.conversation.shouldShowItem(item)) {
644
+ this.renderItemToConsole(item);
645
+ }
842
646
  }
843
647
  // Footer will be re-rendered by the render loop
844
648
  this.needsRender = true;
@@ -851,336 +655,66 @@ export class TerminalUI extends EventEmitter {
851
655
  * Called by /clear command.
852
656
  */
853
657
  clearConversationHistory() {
854
- this.conversationHistory = [];
658
+ this.conversation.clearHistory();
855
659
  }
856
660
  /**
857
661
  * Get conversation history (copy).
858
662
  */
859
663
  getConversationHistory() {
860
- return [...this.conversationHistory];
664
+ return this.conversation.getHistory();
861
665
  }
862
666
  // ===========================================================================
863
- // Public API - Overlays
667
+ // Public API - Conversation Filter (delegated to ConversationStore)
864
668
  // ===========================================================================
865
- /**
866
- * Check if an overlay is currently active.
867
- */
868
- hasActiveOverlay() {
869
- return this.overlayStack.length > 0;
669
+ setConversationFilter(agentIds) {
670
+ this.conversation.setFilter(agentIds);
671
+ this.reRenderConversation();
870
672
  }
871
- /**
872
- * Get the currently active overlay (if any).
873
- */
874
- getActiveOverlay() {
875
- return this.overlayStack.length > 0
876
- ? this.overlayStack[this.overlayStack.length - 1]
877
- : null;
673
+ clearConversationFilter() {
674
+ this.conversation.clearFilter();
675
+ this.reRenderConversation();
878
676
  }
879
- /**
880
- * Show an overlay and wait for result.
881
- * Returns promise that resolves when overlay closes.
882
- */
883
- async showOverlay(overlay) {
884
- // Check if we're pushing onto an existing overlay (stack has items before this push)
885
- const isPushingOntoExisting = this.overlayStack.length > 0;
886
- // 1. If pushing onto existing overlay, clear its render first
887
- if (isPushingOntoExisting) {
888
- this.clearOverlayRender();
889
- }
890
- // 2. Push overlay onto stack
891
- this.overlayStack.push(overlay);
892
- // 3. Call onMount lifecycle (alternate screen overlays enter alternate screen here)
893
- await overlay.onMount?.();
894
- // 4. Switch render mode based on overlay type
895
- // Skip for alternate screen overlays - they manage their own screen buffer
896
- if (!overlay.usesAlternateScreen) {
897
- if (overlay.type === 'fullscreen') {
898
- this.enterFullscreenOverlayMode();
899
- }
900
- else {
901
- this.enterInlineOverlayMode();
902
- }
903
- }
904
- // 5. Initial render
905
- this.renderOverlay();
906
- // 6. Wait for close
907
- return new Promise((resolve) => {
908
- this.overlayResolvers.set(overlay.id, resolve);
909
- });
677
+ getConversationFilter() {
678
+ return this.conversation.getFilter();
910
679
  }
911
- /**
912
- * Close current overlay with result.
913
- * Called internally when overlay returns close action.
914
- */
915
- closeCurrentOverlay(result, cancelled) {
916
- const overlay = this.overlayStack.pop();
917
- if (!overlay)
918
- return;
919
- // Check if overlay uses alternate screen (manages its own screen buffer)
920
- const usesAlternateScreen = overlay.usesAlternateScreen === true;
921
- // 1. Call onUnmount lifecycle (this exits alternate screen if used)
922
- overlay.onUnmount?.();
923
- // 2. Get close summary before clearing (needs overlay reference)
924
- const summary = (!cancelled && result !== null)
925
- ? overlay.getCloseSummary?.(result)
926
- : null;
927
- // 3. Restore render mode FIRST (clears overlay from screen)
928
- if (this.overlayStack.length === 0) {
929
- // Skip exitOverlayMode for alternate screen overlays - exiting alternate
930
- // screen already restored the main screen, clearing would corrupt it
931
- if (usesAlternateScreen) {
932
- // Just reset state, skip clearing AND skip re-rendering footer
933
- // (alternate screen restore already put the screen back to its previous state)
934
- this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
935
- this.flushOverlayPrintBuffer();
936
- // Don't call render() - the footer was restored by alternate screen exit
937
- }
938
- else {
939
- this.exitOverlayMode();
940
- }
941
- }
942
- else {
943
- // Popping back to parent overlay:
944
- // 1. Clear the child overlay's render (skip for alternate screen)
945
- if (!usesAlternateScreen) {
946
- this.clearOverlayRender();
947
- }
948
- // 2. Reset render state for parent
949
- this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
950
- // 3. Re-render parent overlay
951
- this.renderOverlay();
952
- }
953
- // 4. Show close summary AFTER clearing overlay
954
- if (summary) {
955
- this.print({ type: 'info', message: summary });
956
- }
957
- // 5. Resolve promise
958
- const resolver = this.overlayResolvers.get(overlay.id);
959
- resolver?.(result);
960
- this.overlayResolvers.delete(overlay.id);
680
+ addToConversationFilter(agentId) {
681
+ return this.conversation.addToFilter(agentId);
961
682
  }
962
- /**
963
- * Enter fullscreen overlay mode.
964
- * Clears screen and prepares for overlay rendering.
965
- */
966
- enterFullscreenOverlayMode() {
967
- // Clear footer first
968
- this.clear();
969
- // Clear screen for fullscreen overlay
970
- process.stdout.write('\x1b[2J\x1b[H');
971
- // Reset overlay render state (both lineCount and maxLineCount)
972
- this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
973
- }
974
- /**
975
- * Enter inline overlay mode.
976
- * Clears footer but keeps conversation visible.
977
- */
978
- enterInlineOverlayMode() {
979
- // Clear footer to make room for inline overlay
980
- this.clear();
981
- // Reset overlay render state (both lineCount and maxLineCount)
982
- this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
983
- }
984
- /**
985
- * Exit overlay mode and restore normal rendering.
986
- */
987
- exitOverlayMode() {
988
- // Clear overlay content (uses maxLineCount to ensure all lines are cleared)
989
- this.clearOverlayRender();
990
- // Reset state
991
- this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
992
- // Flush any items that were buffered while overlay was active
993
- this.flushOverlayPrintBuffer();
994
- // Re-render footer
995
- this.render();
683
+ isAgentInFilter(agentId) {
684
+ return this.conversation.isAgentInFilter(agentId);
996
685
  }
997
- /**
998
- * Flush items that were buffered while an overlay was active.
999
- * Called after overlay closes to render any pending output.
1000
- */
1001
- flushOverlayPrintBuffer() {
1002
- if (this.overlayPrintBuffer.length === 0)
1003
- return;
1004
- // Render all buffered items
1005
- for (const item of this.overlayPrintBuffer) {
1006
- this.renderItem(item);
1007
- }
1008
- // Clear the buffer
1009
- this.overlayPrintBuffer = [];
1010
- }
1011
- /**
1012
- * Clear previous overlay render.
1013
- * Uses maxLineCount to ensure all lines are cleared even if content shrank between renders.
1014
- */
1015
- clearOverlayRender() {
1016
- // Use maxLineCount to handle cases where overlay content varied between renders
1017
- const linesToClear = this.overlayRenderState.maxLineCount;
1018
- if (linesToClear > 0) {
1019
- // Move cursor up to start of overlay
1020
- process.stdout.write(`\x1b[${String(linesToClear)}A`);
1021
- // Move to column 1
1022
- process.stdout.write('\r');
1023
- // Clear from cursor to end of screen
1024
- process.stdout.write('\x1b[J');
1025
- }
1026
- }
1027
- /**
1028
- * Render the active overlay.
1029
- */
1030
- renderOverlay() {
1031
- const overlay = this.getActiveOverlay();
1032
- if (!overlay)
1033
- return;
1034
- const termWidth = terminal.getTerminalWidth();
1035
- const termHeight = terminal.getTerminalHeight();
1036
- const s = getStyles();
1037
- const context = {
1038
- width: termWidth,
1039
- height: termHeight,
1040
- styles: s,
1041
- };
1042
- const content = overlay.render(context);
1043
- if (overlay.type === 'fullscreen') {
1044
- this.renderFullscreenOverlay(content, termWidth);
1045
- }
1046
- else {
1047
- this.renderInlineOverlay(content, termWidth);
1048
- }
686
+ // ===========================================================================
687
+ // Public API - Overlays (delegated to OverlayManager)
688
+ // ===========================================================================
689
+ hasActiveOverlay() {
690
+ return this.overlay.hasActiveOverlay();
1049
691
  }
1050
- /**
1051
- * Render fullscreen overlay.
1052
- *
1053
- * CRITICAL: Cursor positioning must be consistent between renders.
1054
- * Same logic as renderInlineOverlay - pad to maxLineCount for consistency.
1055
- */
1056
- renderFullscreenOverlay(content, termWidth) {
1057
- // Clear previous render
1058
- this.clearOverlayRender();
1059
- // Calculate physical lines
1060
- let physicalLines = 0;
1061
- for (const line of content.lines) {
1062
- const visibleLen = getVisibleLength(line);
1063
- physicalLines += Math.max(1, Math.ceil(visibleLen / termWidth));
1064
- }
1065
- // Update maxLineCount BEFORE padding (track the natural maximum)
1066
- const contentMinHeight = content.minHeight ?? 0;
1067
- const naturalHeight = Math.max(physicalLines, contentMinHeight);
1068
- this.overlayRenderState.maxLineCount = Math.max(this.overlayRenderState.maxLineCount, naturalHeight);
1069
- // CRITICAL: Pad to maxLineCount for consistent cursor positioning
1070
- const targetHeight = this.overlayRenderState.maxLineCount;
1071
- const paddedLines = [...content.lines];
1072
- while (physicalLines < targetHeight) {
1073
- paddedLines.push('');
1074
- physicalLines++;
1075
- }
1076
- // Update lineCount to actual rendered height
1077
- this.overlayRenderState.lineCount = physicalLines;
1078
- // Render lines
1079
- for (let i = 0; i < paddedLines.length; i++) {
1080
- process.stdout.write(paddedLines[i]);
1081
- if (i < paddedLines.length - 1) {
1082
- process.stdout.write('\n');
1083
- }
1084
- }
1085
- // Position cursor if specified
1086
- if (content.cursorPosition) {
1087
- const { line, column } = content.cursorPosition;
1088
- // Move to the correct line (relative from end)
1089
- const linesFromEnd = paddedLines.length - 1 - line;
1090
- if (linesFromEnd > 0) {
1091
- process.stdout.write(`\x1b[${String(linesFromEnd)}A`);
1092
- }
1093
- process.stdout.write(`\x1b[${String(column)}G`);
1094
- }
1095
- terminal.showCursor();
692
+ getActiveOverlay() {
693
+ return this.overlay.getActiveOverlay();
1096
694
  }
1097
- /**
1098
- * Render inline overlay (above footer).
1099
- *
1100
- * CRITICAL: Cursor positioning must be consistent between renders.
1101
- * After rendering, cursor MUST be at maxLineCount position, not at
1102
- * actual content height. Otherwise, the next clear will move cursor
1103
- * up by maxLineCount from the wrong position, causing "ghost lines".
1104
- */
1105
- renderInlineOverlay(content, termWidth) {
1106
- // Clear previous render
1107
- this.clearOverlayRender();
1108
- // Calculate physical lines
1109
- let physicalLines = 0;
1110
- for (const line of content.lines) {
1111
- const visibleLen = getVisibleLength(line);
1112
- physicalLines += Math.max(1, Math.ceil(visibleLen / termWidth));
1113
- }
1114
- // Update maxLineCount BEFORE padding (track the natural maximum)
1115
- const contentMinHeight = content.minHeight ?? 0;
1116
- const naturalHeight = Math.max(physicalLines, contentMinHeight);
1117
- this.overlayRenderState.maxLineCount = Math.max(this.overlayRenderState.maxLineCount, naturalHeight);
1118
- // CRITICAL: Pad to maxLineCount, not just minHeight!
1119
- // This ensures cursor position is consistent between renders.
1120
- // Without this, going from tall content (e.g., detail screen with 25 lines)
1121
- // to short content (e.g., main list with 20 lines) leaves cursor at line 20,
1122
- // but next clear moves up 25 lines → cursor goes to line -5 → ghost lines!
1123
- const targetHeight = this.overlayRenderState.maxLineCount;
1124
- const paddedLines = [...content.lines];
1125
- while (physicalLines < targetHeight) {
1126
- paddedLines.push('');
1127
- physicalLines++;
1128
- }
1129
- // Update lineCount to actual rendered height
1130
- this.overlayRenderState.lineCount = physicalLines;
1131
- // Render lines (each line ends with \n for consistent cursor positioning)
1132
- for (const line of paddedLines) {
1133
- process.stdout.write(line + '\n');
1134
- }
1135
- terminal.showCursor();
695
+ async showOverlay(overlay) {
696
+ return this.overlay.showOverlay(overlay);
1136
697
  }
1137
- /**
1138
- * Process overlay action returned by handleKey.
1139
- */
1140
- processOverlayAction(action) {
1141
- switch (action.type) {
1142
- case 'none':
1143
- // Do nothing
1144
- break;
1145
- case 'render':
1146
- this.renderOverlay();
1147
- break;
1148
- case 'close':
1149
- if (action.cancelled) {
1150
- this.closeCurrentOverlay(null, true);
1151
- }
1152
- else {
1153
- this.closeCurrentOverlay(action.result ?? null, false);
1154
- }
1155
- break;
1156
- case 'push':
1157
- if (action.overlay) {
1158
- void this.showOverlay(action.overlay);
1159
- }
1160
- break;
1161
- case 'pop':
1162
- this.closeCurrentOverlay(null, true);
1163
- break;
1164
- }
698
+ requestOverlayRender() {
699
+ this.overlay.requestOverlayRender();
1165
700
  }
1166
701
  // ===========================================================================
1167
702
  // Public API - Input queue
1168
703
  // ===========================================================================
1169
704
  getQueuedInputs() {
1170
- return [...this.queuedInputs];
705
+ return this.input.getQueuedInputs();
1171
706
  }
1172
707
  popQueuedInput() {
1173
- if (this.queuedInputs.length === 0)
1174
- return null;
1175
- const input = this.queuedInputs.shift();
1176
- this.needsRender = true;
1177
- return input ?? null;
708
+ const result = this.input.popQueuedInput();
709
+ if (result !== null)
710
+ this.needsRender = true;
711
+ return result;
1178
712
  }
1179
713
  hasQueuedInput() {
1180
- return this.queuedInputs.length > 0;
714
+ return this.input.hasQueuedInput();
1181
715
  }
1182
716
  clearQueue() {
1183
- this.queuedInputs = [];
717
+ this.input.clearQueue();
1184
718
  this.needsRender = true;
1185
719
  }
1186
720
  // ===========================================================================
@@ -1225,11 +759,11 @@ export class TerminalUI extends EventEmitter {
1225
759
  this.renderTimer = null;
1226
760
  }
1227
761
  // Stop spinner
1228
- this.stopSpinnerAnimation();
762
+ this.footer.stopSpinnerAnimation();
1229
763
  // Stop keyboard capture
1230
- this.stopKeyboardCapture();
764
+ this.keyboard.stopCapture();
1231
765
  // Clear footer from screen
1232
- this.clear();
766
+ this.footer.clear();
1233
767
  }
1234
768
  /**
1235
769
  * Resume footer after pause
@@ -1238,15 +772,15 @@ export class TerminalUI extends EventEmitter {
1238
772
  this.isPaused = false;
1239
773
  // Resume spinner if agent running
1240
774
  if (this.agentRunning) {
1241
- this.startSpinnerAnimation();
775
+ this.footer.startSpinnerAnimation(() => { this.needsRender = true; });
1242
776
  }
1243
777
  // Restart keyboard capture
1244
- this.startKeyboardCapture();
778
+ this.keyboard.startCapture();
1245
779
  // Restart render loop
1246
- this.render();
780
+ this.footer.render();
1247
781
  this.renderTimer = setInterval(() => {
1248
782
  if (this.needsRender && !this.isPaused) {
1249
- this.render();
783
+ this.footer.render();
1250
784
  this.needsRender = false;
1251
785
  }
1252
786
  }, 60);
@@ -1255,7 +789,7 @@ export class TerminalUI extends EventEmitter {
1255
789
  * Restart input after command
1256
790
  */
1257
791
  restartInput() {
1258
- this.render();
792
+ this.footer.render();
1259
793
  }
1260
794
  /**
1261
795
  * Refresh prompt with theme colors
@@ -1273,1024 +807,13 @@ export class TerminalUI extends EventEmitter {
1273
807
  * @deprecated Use print() or output() instead
1274
808
  */
1275
809
  clearForOutput() {
1276
- this.clear();
810
+ this.footer.clear();
1277
811
  }
1278
812
  /**
1279
813
  * @deprecated Use print() or output() instead
1280
814
  */
1281
815
  forceRender() {
1282
- this.render();
816
+ this.footer.render();
1283
817
  this.needsRender = false;
1284
818
  }
1285
- // ===========================================================================
1286
- // Private - Rendering
1287
- // ===========================================================================
1288
- clear() {
1289
- if (this.lastRenderHeight === 0) {
1290
- return;
1291
- }
1292
- terminal.moveCursorToLineStart();
1293
- // Move cursor to TOP of footer
1294
- // cursorRowFromTop = which row the cursor is on (1-indexed from top of footer)
1295
- const cursorRowFromTop = this.lastRenderHeight - this.cursorLineFromBottom;
1296
- const rowsToMoveUp = cursorRowFromTop - 1;
1297
- if (rowsToMoveUp > 0) {
1298
- terminal.moveCursorUp(rowsToMoveUp);
1299
- }
1300
- terminal.clearToEndOfScreen();
1301
- this.lastRenderHeight = 0;
1302
- this.cursorLineFromBottom = 0;
1303
- }
1304
- render() {
1305
- if (!this.isRunning || this.isPaused) {
1306
- return;
1307
- }
1308
- // Don't render footer when overlay is active
1309
- if (this.hasActiveOverlay()) {
1310
- return;
1311
- }
1312
- const termWidth = terminal.getTerminalWidth();
1313
- const s = getStyles();
1314
- // Build all lines in order
1315
- const lines = [];
1316
- // 0. LiveRegion (if has items) - rendered at top of footer
1317
- if (this.liveRegion.hasItems()) {
1318
- const liveLines = this.liveRegion.render({
1319
- width: termWidth,
1320
- verbose: this.config.verbose,
1321
- showTokens: true,
1322
- });
1323
- for (const line of liveLines) {
1324
- lines.push({
1325
- content: line,
1326
- physicalLines: getPhysicalLineCount(line, termWidth),
1327
- });
1328
- }
1329
- // Blank line after live region for spacing
1330
- if (liveLines.length > 0) {
1331
- lines.push({ content: '', physicalLines: 1 });
1332
- }
1333
- }
1334
- // 1. Header: Spinner (when running) or "Todos" (when idle with todos)
1335
- if (this.agentRunning) {
1336
- const spinnerLine = this.buildSpinnerLine();
1337
- lines.push({
1338
- content: spinnerLine,
1339
- physicalLines: getPhysicalLineCount(spinnerLine, termWidth),
1340
- });
1341
- }
1342
- else if (this.todos.length > 0) {
1343
- const headerLine = s.muted('Todos');
1344
- lines.push({
1345
- content: headerLine,
1346
- physicalLines: 1,
1347
- });
1348
- }
1349
- // 2. Todo list (show if there are todos AND showTodos is true)
1350
- if (this.todos.length > 0 && this.showTodos) {
1351
- for (const todo of this.todos) {
1352
- const todoLine = this.buildTodoLine(todo);
1353
- lines.push({
1354
- content: todoLine,
1355
- physicalLines: getPhysicalLineCount(todoLine, termWidth),
1356
- });
1357
- }
1358
- // Blank line after todos for spacing
1359
- lines.push({ content: '', physicalLines: 1 });
1360
- }
1361
- // 3. Queued inputs (show multiline as single line with ↵ indicator)
1362
- for (const queued of this.queuedInputs) {
1363
- const displayText = queued.replace(/\n/g, ' ↵ ');
1364
- const queuedLine = s.muted(`queued: "${displayText}"`);
1365
- lines.push({
1366
- content: queuedLine,
1367
- physicalLines: getPhysicalLineCount(queuedLine, termWidth),
1368
- });
1369
- }
1370
- // 4. Top separator
1371
- const separator = '─'.repeat(termWidth);
1372
- lines.push({ content: separator, physicalLines: 1 });
1373
- // 5. Input prompt (multiline support)
1374
- const continuationPrompt = s.muted(' \\ ');
1375
- const continuationPromptLen = getVisibleLength(continuationPrompt);
1376
- const cursorLineIndex = lines.length + this.currentLine; // Index where cursor is
1377
- for (let i = 0; i < this.lines.length; i++) {
1378
- const linePrompt = i === 0 ? this.promptPrefix : continuationPrompt;
1379
- const linePromptLen = i === 0 ? this.promptPrefixLen : continuationPromptLen;
1380
- const lineContent = this.lines[i];
1381
- // Show ghost text on first line when input is empty and no autocomplete
1382
- let fullLine;
1383
- let totalLen;
1384
- if (i === 0 &&
1385
- lineContent === '' &&
1386
- this.lines.length === 1 &&
1387
- this.suggestion &&
1388
- !this.autocomplete.active &&
1389
- !this.fileAutocomplete.active) {
1390
- // Build: [prompt][suggestion]...[↵ send]
1391
- const rightHint = '↵ send';
1392
- const leftContent = linePrompt + s.muted(this.suggestion);
1393
- const leftLen = linePromptLen + this.suggestion.length;
1394
- const rightLen = rightHint.length;
1395
- const padding = Math.max(1, termWidth - leftLen - rightLen);
1396
- fullLine = leftContent + ' '.repeat(padding) + s.muted(rightHint);
1397
- totalLen = termWidth; // Full width
1398
- }
1399
- else {
1400
- fullLine = linePrompt + lineContent;
1401
- totalLen = linePromptLen + getVisibleLength(lineContent);
1402
- }
1403
- const physicalLines = totalLen === 0 ? 1 : Math.ceil(totalLen / termWidth) || 1;
1404
- lines.push({
1405
- content: fullLine,
1406
- physicalLines,
1407
- });
1408
- }
1409
- // 6. Bottom separator
1410
- lines.push({ content: separator, physicalLines: 1 });
1411
- // 7. Mode indicator
1412
- const modeLine = this.buildModeLine(termWidth);
1413
- lines.push({
1414
- content: modeLine,
1415
- physicalLines: getPhysicalLineCount(modeLine, termWidth),
1416
- });
1417
- // 8. Autocomplete dropdown (if active) - file autocomplete takes priority
1418
- const fileAutocompleteLines = this.buildFileAutocompleteLines(termWidth);
1419
- const autocompleteLines = fileAutocompleteLines.length > 0
1420
- ? fileAutocompleteLines
1421
- : this.buildAutocompleteLines(termWidth);
1422
- for (const line of autocompleteLines) {
1423
- lines.push({
1424
- content: line,
1425
- physicalLines: getPhysicalLineCount(line, termWidth),
1426
- });
1427
- }
1428
- // Calculate total physical height
1429
- let totalPhysicalLines = 0;
1430
- for (const line of lines) {
1431
- totalPhysicalLines += line.physicalLines;
1432
- }
1433
- // Clear existing footer
1434
- this.clear();
1435
- // Write all lines
1436
- for (let i = 0; i < lines.length; i++) {
1437
- terminal.write(lines[i].content);
1438
- if (i < lines.length - 1) {
1439
- terminal.write('\n');
1440
- }
1441
- }
1442
- // Calculate cursor position (for multiline, position on the line where cursor is)
1443
- let rowsAfterCursor = 0;
1444
- for (let i = cursorLineIndex + 1; i < lines.length; i++) {
1445
- rowsAfterCursor += lines[i].physicalLines;
1446
- }
1447
- if (rowsAfterCursor > 0) {
1448
- terminal.moveCursorUp(rowsAfterCursor);
1449
- }
1450
- // Calculate column within current line
1451
- const currentLinePromptLen = this.currentLine === 0 ? this.promptPrefixLen : continuationPromptLen;
1452
- const totalPos = currentLinePromptLen + this.cursorPos;
1453
- const cursorCol = (totalPos % termWidth) + 1;
1454
- terminal.moveCursorToColumn(cursorCol);
1455
- terminal.showCursor();
1456
- this.lastRenderHeight = totalPhysicalLines;
1457
- this.cursorLineFromBottom = rowsAfterCursor;
1458
- }
1459
- // ===========================================================================
1460
- // Private - Line builders
1461
- // ===========================================================================
1462
- buildSpinnerLine() {
1463
- const s = getStyles();
1464
- const frame = this.spinnerFrames[this.spinnerFrame % this.spinnerFrames.length];
1465
- const text = this.currentTool ?? this.spinnerText ?? 'Thinking...';
1466
- // Build hints
1467
- const hints = ['esc to interrupt'];
1468
- if (this.todos.length > 0 && this.showTodos) {
1469
- hints.push('ctrl+t to hide todos');
1470
- }
1471
- else if (this.todos.length > 0 && !this.showTodos) {
1472
- hints.push('ctrl+t to show todos');
1473
- }
1474
- const hintsText = hints.length > 0 ? ` (${hints.join(' · ')})` : '';
1475
- // Format: [CRT scanner] text (hints)
1476
- return s.muted(`${frame} ${text}${hintsText}`);
1477
- }
1478
- buildTodoLine(todo) {
1479
- const s = getStyles();
1480
- const icon = todo.status === 'completed' ? '✓' :
1481
- todo.status === 'in_progress' ? '→' : '☐';
1482
- const style = todo.status === 'completed' ? s.muted :
1483
- todo.status === 'in_progress' ? s.info : s.muted;
1484
- return style(`${icon} ${todo.content}`);
1485
- }
1486
- buildModeLine(termWidth) {
1487
- const s = getStyles();
1488
- const modeInfo = MODE_INFO[this.mode];
1489
- let leftPart;
1490
- switch (this.mode) {
1491
- case 'normal':
1492
- leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
1493
- break;
1494
- case 'auto-accept':
1495
- leftPart = s.warning(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
1496
- break;
1497
- case 'plan':
1498
- leftPart = s.info(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
1499
- break;
1500
- default:
1501
- leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
1502
- }
1503
- if (!this.projectName) {
1504
- return leftPart;
1505
- }
1506
- const projectText = `Project: ${this.projectName}`;
1507
- const rightPart = s.muted(projectText);
1508
- const leftVisible = getVisibleLength(leftPart);
1509
- const padding = Math.max(2, termWidth - leftVisible - projectText.length);
1510
- return leftPart + ' '.repeat(padding) + rightPart;
1511
- }
1512
- buildAutocompleteLines(termWidth) {
1513
- if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
1514
- return [];
1515
- }
1516
- const s = getStyles();
1517
- const lines = [];
1518
- const { matches, selectedIndex, scrollOffset } = this.autocomplete;
1519
- const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
1520
- const total = matches.length;
1521
- // Show scroll indicator if there are more items above
1522
- if (scrollOffset > 0) {
1523
- lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
1524
- }
1525
- for (let i = 0; i < visible.length; i++) {
1526
- const cmd = visible[i];
1527
- const actualIndex = scrollOffset + i;
1528
- const isSelected = actualIndex === selectedIndex;
1529
- const prefix = isSelected ? s.primary('❯ ') : ' ';
1530
- const name = isSelected ? s.primaryBold(cmd.command) : cmd.command;
1531
- // Truncate if too long
1532
- const baseLen = getVisibleLength(prefix) + getVisibleLength(name) + 3; // 3 for ' - '
1533
- const maxDescLen = termWidth - baseLen - 2;
1534
- const truncatedDesc = cmd.description.length > maxDescLen
1535
- ? cmd.description.slice(0, maxDescLen - 1) + '…'
1536
- : cmd.description;
1537
- lines.push(`${prefix}${name} ${s.muted('- ' + truncatedDesc)}`);
1538
- }
1539
- // Show scroll indicator if there are more items below
1540
- const belowCount = total - scrollOffset - visible.length;
1541
- if (belowCount > 0) {
1542
- lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
1543
- }
1544
- return lines;
1545
- }
1546
- buildFileAutocompleteLines(termWidth) {
1547
- if (!this.fileAutocomplete.active || this.fileAutocomplete.matches.length === 0) {
1548
- return [];
1549
- }
1550
- const s = getStyles();
1551
- const lines = [];
1552
- const { matches, selectedIndex, scrollOffset } = this.fileAutocomplete;
1553
- const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
1554
- const total = matches.length;
1555
- // Show scroll indicator if there are more items above
1556
- if (scrollOffset > 0) {
1557
- lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
1558
- }
1559
- for (let i = 0; i < visible.length; i++) {
1560
- const file = visible[i];
1561
- const actualIndex = scrollOffset + i;
1562
- const isSelected = actualIndex === selectedIndex;
1563
- const prefix = isSelected ? s.primary('❯ ') : ' ';
1564
- const icon = file.isDirectory ? '📁 ' : '📄 ';
1565
- const name = isSelected ? s.primaryBold(file.path) : file.path;
1566
- // Truncate if too long
1567
- const baseLen = getVisibleLength(prefix) + 3 + getVisibleLength(file.path); // 3 for icon
1568
- if (baseLen > termWidth - 2) {
1569
- const maxLen = termWidth - getVisibleLength(prefix) - 5; // 3 for icon, 2 for padding
1570
- const truncatedPath = file.path.length > maxLen
1571
- ? '…' + file.path.slice(-(maxLen - 1))
1572
- : file.path;
1573
- const truncatedName = isSelected ? s.primaryBold(truncatedPath) : truncatedPath;
1574
- lines.push(`${prefix}${icon}${truncatedName}`);
1575
- }
1576
- else {
1577
- lines.push(`${prefix}${icon}${name}`);
1578
- }
1579
- }
1580
- // Show scroll indicator if there are more items below
1581
- const belowCount = total - scrollOffset - visible.length;
1582
- if (belowCount > 0) {
1583
- lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
1584
- }
1585
- return lines;
1586
- }
1587
- // ===========================================================================
1588
- // Private - Cursor calculations
1589
- // ===========================================================================
1590
- // ===========================================================================
1591
- // Private - Autocomplete
1592
- // ===========================================================================
1593
- updateAutocomplete() {
1594
- const currentLine = this.getCurrentLineContent();
1595
- // Check for @ file path autocomplete first (works on any line)
1596
- const atMention = extractAtMention(currentLine, this.cursorPos);
1597
- if (atMention !== null) {
1598
- // File autocomplete mode - disable command autocomplete
1599
- this.autocomplete.active = false;
1600
- this.autocomplete.matches = [];
1601
- this.autocomplete.selectedIndex = 0;
1602
- this.autocomplete.scrollOffset = 0;
1603
- // Enable file autocomplete
1604
- this.fileAutocomplete.active = true;
1605
- this.fileAutocomplete.partial = atMention;
1606
- this.fileAutocomplete.matches = getFileMatches(atMention);
1607
- if (this.fileAutocomplete.selectedIndex >= this.fileAutocomplete.matches.length) {
1608
- this.fileAutocomplete.selectedIndex = Math.max(0, this.fileAutocomplete.matches.length - 1);
1609
- this.fileAutocomplete.scrollOffset = 0;
1610
- }
1611
- return;
1612
- }
1613
- // Reset file autocomplete
1614
- this.fileAutocomplete.active = false;
1615
- this.fileAutocomplete.matches = [];
1616
- this.fileAutocomplete.selectedIndex = 0;
1617
- this.fileAutocomplete.scrollOffset = 0;
1618
- this.fileAutocomplete.partial = '';
1619
- // Activate command autocomplete when first line starts with /
1620
- const firstLine = this.lines[0];
1621
- if (this.currentLine === 0 && firstLine.startsWith('/')) {
1622
- this.autocomplete.active = true;
1623
- const freshCommands = getAutocompleteCommands();
1624
- this.autocomplete.matches = filterCommands(firstLine, freshCommands);
1625
- // Reset selection if it's out of bounds
1626
- if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
1627
- this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
1628
- this.autocomplete.scrollOffset = 0;
1629
- }
1630
- }
1631
- else {
1632
- this.autocomplete.active = false;
1633
- this.autocomplete.matches = [];
1634
- this.autocomplete.selectedIndex = 0;
1635
- this.autocomplete.scrollOffset = 0;
1636
- }
1637
- }
1638
- acceptAutocomplete() {
1639
- // File autocomplete takes priority (works on current line)
1640
- if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
1641
- const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
1642
- const currentLine = this.getCurrentLineContent();
1643
- const result = replaceAtMention(currentLine, this.cursorPos, selectedFile.path);
1644
- this.lines[this.currentLine] = result.input;
1645
- this.cursorPos = result.cursorPos;
1646
- this.fileAutocomplete.active = false;
1647
- this.fileAutocomplete.matches = [];
1648
- this.fileAutocomplete.selectedIndex = 0;
1649
- this.fileAutocomplete.scrollOffset = 0;
1650
- this.fileAutocomplete.partial = '';
1651
- // Check if still in @ context (e.g., directory selected, might want to continue)
1652
- this.updateAutocomplete();
1653
- return;
1654
- }
1655
- // Command autocomplete (works on first line only)
1656
- if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
1657
- const selected = this.autocomplete.matches[this.autocomplete.selectedIndex];
1658
- this.lines[0] = selected.command;
1659
- this.currentLine = 0;
1660
- this.cursorPos = this.lines[0].length;
1661
- this.autocomplete.active = false;
1662
- this.autocomplete.matches = [];
1663
- this.autocomplete.selectedIndex = 0;
1664
- this.autocomplete.scrollOffset = 0;
1665
- }
1666
- }
1667
- closeAutocomplete() {
1668
- // Close file autocomplete
1669
- this.fileAutocomplete.active = false;
1670
- this.fileAutocomplete.matches = [];
1671
- this.fileAutocomplete.selectedIndex = 0;
1672
- this.fileAutocomplete.scrollOffset = 0;
1673
- this.fileAutocomplete.partial = '';
1674
- // Close command autocomplete
1675
- this.autocomplete.active = false;
1676
- this.autocomplete.matches = [];
1677
- this.autocomplete.selectedIndex = 0;
1678
- this.autocomplete.scrollOffset = 0;
1679
- }
1680
- navigateAutocompleteUp() {
1681
- // File autocomplete navigation takes priority
1682
- if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
1683
- if (this.fileAutocomplete.selectedIndex > 0) {
1684
- this.fileAutocomplete.selectedIndex--;
1685
- if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.scrollOffset) {
1686
- this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex;
1687
- }
1688
- return true;
1689
- }
1690
- return false;
1691
- }
1692
- // Command autocomplete navigation
1693
- if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
1694
- return false;
1695
- }
1696
- if (this.autocomplete.selectedIndex > 0) {
1697
- this.autocomplete.selectedIndex--;
1698
- // Adjust scroll if selection goes above visible area
1699
- if (this.autocomplete.selectedIndex < this.autocomplete.scrollOffset) {
1700
- this.autocomplete.scrollOffset = this.autocomplete.selectedIndex;
1701
- }
1702
- return true;
1703
- }
1704
- return false;
1705
- }
1706
- navigateAutocompleteDown() {
1707
- // File autocomplete navigation takes priority
1708
- if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
1709
- if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.matches.length - 1) {
1710
- this.fileAutocomplete.selectedIndex++;
1711
- const maxVisibleIndex = this.fileAutocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
1712
- if (this.fileAutocomplete.selectedIndex > maxVisibleIndex) {
1713
- this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
1714
- }
1715
- return true;
1716
- }
1717
- return false;
1718
- }
1719
- // Command autocomplete navigation
1720
- if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
1721
- return false;
1722
- }
1723
- if (this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1) {
1724
- this.autocomplete.selectedIndex++;
1725
- // Adjust scroll if selection goes below visible area
1726
- const maxVisibleIndex = this.autocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
1727
- if (this.autocomplete.selectedIndex > maxVisibleIndex) {
1728
- this.autocomplete.scrollOffset = this.autocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
1729
- }
1730
- return true;
1731
- }
1732
- return false;
1733
- }
1734
- // ===========================================================================
1735
- // Private - History
1736
- // ===========================================================================
1737
- addToHistory(input) {
1738
- const trimmed = input.trim();
1739
- // Don't add empty or duplicate consecutive entries
1740
- if (trimmed && (this.history.length === 0 || this.history[this.history.length - 1] !== trimmed)) {
1741
- this.history.push(trimmed);
1742
- }
1743
- }
1744
- navigateHistoryUp() {
1745
- if (this.autocomplete.active)
1746
- return false;
1747
- if (this.history.length === 0)
1748
- return false;
1749
- // Save current input when starting to navigate
1750
- if (this.historyIndex === -1) {
1751
- this.savedInput = this.getInputValue();
1752
- }
1753
- // Navigate to older entry
1754
- if (this.historyIndex < this.history.length - 1) {
1755
- this.historyIndex++;
1756
- const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
1757
- // History entries are single-line (newlines stripped on save)
1758
- this.lines = [historyEntry];
1759
- this.currentLine = 0;
1760
- this.cursorPos = historyEntry.length;
1761
- return true;
1762
- }
1763
- return false;
1764
- }
1765
- navigateHistoryDown() {
1766
- if (this.autocomplete.active)
1767
- return false;
1768
- if (this.historyIndex < 0)
1769
- return false;
1770
- this.historyIndex--;
1771
- if (this.historyIndex === -1) {
1772
- // Restore saved input (may be multiline)
1773
- this.lines = this.savedInput.split('\n');
1774
- if (this.lines.length === 0)
1775
- this.lines = [''];
1776
- this.currentLine = this.lines.length - 1;
1777
- this.cursorPos = this.lines[this.currentLine].length;
1778
- }
1779
- else {
1780
- // Navigate to newer entry
1781
- const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
1782
- this.lines = [historyEntry];
1783
- this.currentLine = 0;
1784
- this.cursorPos = historyEntry.length;
1785
- }
1786
- return true;
1787
- }
1788
- resetHistoryNavigation() {
1789
- this.historyIndex = -1;
1790
- this.savedInput = '';
1791
- }
1792
- // ===========================================================================
1793
- // Private - Spinner animation
1794
- // ===========================================================================
1795
- startSpinnerAnimation() {
1796
- if (this.spinnerTimer)
1797
- return;
1798
- this.spinnerTimer = setInterval(() => {
1799
- this.spinnerFrame++;
1800
- this.needsRender = true;
1801
- }, 200); // Slower, more relaxed animation
1802
- }
1803
- stopSpinnerAnimation() {
1804
- if (this.spinnerTimer) {
1805
- clearInterval(this.spinnerTimer);
1806
- this.spinnerTimer = null;
1807
- }
1808
- this.spinnerFrame = 0;
1809
- }
1810
- // ===========================================================================
1811
- // Private - Keyboard handling
1812
- // ===========================================================================
1813
- keyHandler = null;
1814
- dataHandler = null;
1815
- startKeyboardCapture() {
1816
- if (this.keyHandler)
1817
- return; // Already capturing
1818
- readline.emitKeypressEvents(process.stdin);
1819
- if (process.stdin.isTTY) {
1820
- process.stdin.setRawMode(true);
1821
- }
1822
- // Handle raw data for reliable Escape and Option+Arrow detection
1823
- this.dataHandler = (data) => {
1824
- if (!this.isRunning || this.isPaused)
1825
- return;
1826
- // Pure Escape key is a single byte 0x1B
1827
- if (data.length === 1 && data[0] === 0x1b) {
1828
- this.handleEscape();
1829
- return;
1830
- }
1831
- // Mac Option+Left (word left): ESC b or ESC [ 1 ; 9 D
1832
- const isOptionLeft = (data.length === 2 && data[0] === 0x1b && data[1] === 0x62) ||
1833
- (data.length === 6 &&
1834
- data[0] === 0x1b &&
1835
- data[1] === 0x5b &&
1836
- data[2] === 0x31 &&
1837
- data[3] === 0x3b &&
1838
- data[4] === 0x39 &&
1839
- data[5] === 0x44);
1840
- // Mac Option+Right (word right): ESC f or ESC [ 1 ; 9 C
1841
- const isOptionRight = (data.length === 2 && data[0] === 0x1b && data[1] === 0x66) ||
1842
- (data.length === 6 &&
1843
- data[0] === 0x1b &&
1844
- data[1] === 0x5b &&
1845
- data[2] === 0x31 &&
1846
- data[3] === 0x3b &&
1847
- data[4] === 0x39 &&
1848
- data[5] === 0x43);
1849
- if (isOptionLeft) {
1850
- this.handleWordLeft();
1851
- return;
1852
- }
1853
- if (isOptionRight) {
1854
- this.handleWordRight();
1855
- return;
1856
- }
1857
- // Home/Cmd+Left: Ctrl+A (\x01) or ESC [ H
1858
- const isHome = (data.length === 1 && data[0] === 0x01) ||
1859
- (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x48);
1860
- // End/Cmd+Right: Ctrl+E (\x05) or ESC [ F
1861
- const isEnd = (data.length === 1 && data[0] === 0x05) ||
1862
- (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
1863
- if (isHome) {
1864
- this.cursorPos = 0;
1865
- this.needsRender = true;
1866
- return;
1867
- }
1868
- if (isEnd) {
1869
- this.cursorPos = this.lines[this.currentLine].length;
1870
- this.needsRender = true;
1871
- return;
1872
- }
1873
- };
1874
- this.keyHandler = (str, key) => {
1875
- if (!this.isRunning || this.isPaused)
1876
- return;
1877
- // Skip escape here - handled by dataHandler
1878
- if (key.name === 'escape')
1879
- return;
1880
- this.handleKeypress(str, key);
1881
- };
1882
- process.stdin.on('data', this.dataHandler);
1883
- process.stdin.on('keypress', this.keyHandler);
1884
- process.stdin.resume();
1885
- }
1886
- stopKeyboardCapture() {
1887
- if (this.dataHandler) {
1888
- process.stdin.removeListener('data', this.dataHandler);
1889
- this.dataHandler = null;
1890
- }
1891
- if (this.keyHandler) {
1892
- process.stdin.removeListener('keypress', this.keyHandler);
1893
- this.keyHandler = null;
1894
- }
1895
- if (process.stdin.isTTY) {
1896
- process.stdin.setRawMode(false);
1897
- }
1898
- }
1899
- /**
1900
- * Handle word left (Option+Left) - move cursor to previous word boundary
1901
- */
1902
- handleWordLeft() {
1903
- const line = this.lines[this.currentLine];
1904
- if (this.cursorPos > 0) {
1905
- let pos = this.cursorPos;
1906
- // Skip spaces
1907
- while (pos > 0 && line[pos - 1] === ' ')
1908
- pos--;
1909
- // Skip word
1910
- while (pos > 0 && line[pos - 1] !== ' ')
1911
- pos--;
1912
- this.cursorPos = pos;
1913
- this.needsRender = true;
1914
- }
1915
- else if (this.currentLine > 0) {
1916
- // Move to end of previous line
1917
- this.currentLine--;
1918
- this.cursorPos = this.lines[this.currentLine].length;
1919
- this.needsRender = true;
1920
- }
1921
- }
1922
- /**
1923
- * Handle word right (Option+Right) - move cursor to next word boundary
1924
- */
1925
- handleWordRight() {
1926
- const line = this.lines[this.currentLine];
1927
- if (this.cursorPos < line.length) {
1928
- let pos = this.cursorPos;
1929
- // Skip word
1930
- while (pos < line.length && line[pos] !== ' ')
1931
- pos++;
1932
- // Skip spaces
1933
- while (pos < line.length && line[pos] === ' ')
1934
- pos++;
1935
- this.cursorPos = pos;
1936
- this.needsRender = true;
1937
- }
1938
- else if (this.currentLine < this.lines.length - 1) {
1939
- // Move to start of next line
1940
- this.currentLine++;
1941
- this.cursorPos = 0;
1942
- this.needsRender = true;
1943
- }
1944
- }
1945
- /**
1946
- * Handle Escape key - called from raw data handler for reliable detection
1947
- */
1948
- handleEscape() {
1949
- // In verbose-temp mode, escape returns to normal
1950
- if (this.viewMode === 'verbose-temp') {
1951
- this.viewMode = 'normal';
1952
- this.reRenderConversationVerbose(false);
1953
- return;
1954
- }
1955
- // Route escape to active overlay if present
1956
- const overlay = this.getActiveOverlay();
1957
- if (overlay) {
1958
- const keyEvent = {
1959
- raw: Buffer.from([0x1b]),
1960
- name: 'escape',
1961
- ctrl: false,
1962
- shift: false,
1963
- meta: false,
1964
- };
1965
- const actionOrPromise = overlay.handleKey(keyEvent);
1966
- // Handle both sync and async handleKey
1967
- if (actionOrPromise instanceof Promise) {
1968
- actionOrPromise
1969
- .then((action) => { this.processOverlayAction(action); })
1970
- .catch((err) => { console.error('Overlay handleKey error:', err); });
1971
- }
1972
- else {
1973
- this.processOverlayAction(actionOrPromise);
1974
- }
1975
- return;
1976
- }
1977
- const now = Date.now();
1978
- const timeSinceLastEsc = now - this.lastEscapeTime;
1979
- const isDoubleEsc = timeSinceLastEsc < 500;
1980
- this.lastEscapeTime = now;
1981
- if (this.fileAutocomplete.active) {
1982
- // 1. Close file autocomplete first
1983
- this.closeAutocomplete();
1984
- this.needsRender = true;
1985
- }
1986
- else if (this.autocomplete.active) {
1987
- // 2. Close command autocomplete
1988
- this.closeAutocomplete();
1989
- this.needsRender = true;
1990
- }
1991
- else if (isDoubleEsc && this.getInputValue().length > 0) {
1992
- // 3. Double Esc clears input (if there's content)
1993
- this.clearInput();
1994
- this.resetHistoryNavigation();
1995
- this.needsRender = true;
1996
- }
1997
- else if (this.agentRunning) {
1998
- // 4. Single Esc cancels agent
1999
- this.emit('cancel');
2000
- }
2001
- else {
2002
- // 5. Single Esc emits escape
2003
- this.emit('escape');
2004
- }
2005
- }
2006
- /**
2007
- * Handle keypress when overlay is active.
2008
- * Converts keyboard event to KeyEvent and routes to overlay.
2009
- */
2010
- handleOverlayKeypress(str, key) {
2011
- const overlay = this.getActiveOverlay();
2012
- if (!overlay)
2013
- return;
2014
- // Build KeyEvent from keypress data
2015
- const rawData = key.sequence ?? str;
2016
- const keyName = key.name ?? (str ? str.toLowerCase() : '');
2017
- const keyEvent = {
2018
- raw: Buffer.from(rawData),
2019
- name: keyName,
2020
- char: str && str.length === 1 && str.charCodeAt(0) >= 32 ? str : undefined,
2021
- ctrl: key.ctrl ?? false,
2022
- shift: key.shift ?? false,
2023
- meta: key.meta ?? false,
2024
- };
2025
- const actionOrPromise = overlay.handleKey(keyEvent);
2026
- // Handle both sync and async handleKey
2027
- if (actionOrPromise instanceof Promise) {
2028
- actionOrPromise
2029
- .then((action) => { this.processOverlayAction(action); })
2030
- .catch((err) => { console.error('Overlay handleKey error:', err); });
2031
- }
2032
- else {
2033
- this.processOverlayAction(actionOrPromise);
2034
- }
2035
- }
2036
- handleKeypress(str, key) {
2037
- // Route to active overlay if present
2038
- if (this.hasActiveOverlay()) {
2039
- this.handleOverlayKeypress(str, key);
2040
- return;
2041
- }
2042
- // In verbose-temp mode, any key (except Ctrl+O) returns to normal
2043
- if (this.viewMode === 'verbose-temp') {
2044
- // Ctrl+O toggles back
2045
- if (key.ctrl && key.name === 'o') {
2046
- this.toggleVerboseView();
2047
- this.toggleLiveRegionExpanded();
2048
- return;
2049
- }
2050
- // Any other key returns to normal
2051
- this.viewMode = 'normal';
2052
- this.reRenderConversationVerbose(false);
2053
- // Don't consume the key - let it be processed normally
2054
- }
2055
- // Ctrl+C - interrupt/exit
2056
- if (key.ctrl && key.name === 'c') {
2057
- this.emit('interrupt');
2058
- return;
2059
- }
2060
- // Ctrl+T - toggle todo list visibility
2061
- if (key.ctrl && key.name === 't') {
2062
- this.toggleTodos();
2063
- return;
2064
- }
2065
- // Ctrl+O - toggle verbose view mode AND LiveRegion expansion
2066
- if (key.ctrl && key.name === 'o') {
2067
- this.toggleVerboseView();
2068
- this.toggleLiveRegionExpanded();
2069
- return;
2070
- }
2071
- // Tab - accept suggestion, or autocomplete
2072
- if (key.name === 'tab' && !key.shift) {
2073
- // Accept ghost text suggestion if input is empty
2074
- if (this.suggestion && this.getInputValue() === '') {
2075
- this.lines[0] = this.suggestion;
2076
- this.cursorPos = this.suggestion.length;
2077
- this.suggestion = null;
2078
- this.needsRender = true;
2079
- return;
2080
- }
2081
- if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
2082
- this.acceptAutocomplete();
2083
- this.needsRender = true;
2084
- return;
2085
- }
2086
- if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
2087
- this.acceptAutocomplete();
2088
- this.needsRender = true;
2089
- return;
2090
- }
2091
- // Otherwise ignore tab (or could insert spaces)
2092
- return;
2093
- }
2094
- // Enter - handle multiline continuation, autocomplete, then execute
2095
- if (key.name === 'return') {
2096
- // Check for backslash continuation at end of current line
2097
- const currentLine = this.getCurrentLineContent();
2098
- if (currentLine.endsWith('\\')) {
2099
- // Remove backslash and add new line
2100
- this.lines[this.currentLine] = currentLine.slice(0, -1);
2101
- this.lines.push('');
2102
- this.currentLine++;
2103
- this.cursorPos = 0;
2104
- this.closeAutocomplete();
2105
- this.resetHistoryNavigation();
2106
- this.needsRender = true;
2107
- return;
2108
- }
2109
- // If file autocomplete is active, accept the selection and submit
2110
- if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
2111
- this.acceptAutocomplete();
2112
- // Fall through to submit the message
2113
- }
2114
- // If command autocomplete is active, accept the selection first
2115
- if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
2116
- this.acceptAutocomplete();
2117
- // Fall through to execute the command
2118
- }
2119
- let input = this.getInputValue().trim();
2120
- // If input is empty but we have a ghost text suggestion, accept it
2121
- if (!input && this.suggestion) {
2122
- input = this.suggestion;
2123
- this.suggestion = null;
2124
- }
2125
- if (input) {
2126
- // Add to history before processing (store as single line for history)
2127
- this.addToHistory(input.replace(/\n/g, ' '));
2128
- this.resetHistoryNavigation();
2129
- if (this.agentRunning) {
2130
- this.queuedInputs.push(input);
2131
- this.needsRender = true;
2132
- }
2133
- else if (input.startsWith('/')) {
2134
- const spaceIndex = input.indexOf(' ');
2135
- const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
2136
- const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
2137
- this.closeAutocomplete();
2138
- this.emit('command', cmd, args);
2139
- }
2140
- else {
2141
- this.emit('submit', input);
2142
- }
2143
- this.clearInput();
2144
- this.closeAutocomplete();
2145
- this.needsRender = true;
2146
- }
2147
- return;
2148
- }
2149
- // Arrow Up - autocomplete > multiline navigation > history
2150
- if (key.name === 'up') {
2151
- // 1. Autocomplete navigation takes priority
2152
- if (this.navigateAutocompleteUp()) {
2153
- this.needsRender = true;
2154
- return;
2155
- }
2156
- // 2. Navigate to previous line in multiline input
2157
- if (this.currentLine > 0) {
2158
- this.currentLine--;
2159
- // Try to keep same cursor position, but clamp to line length
2160
- this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
2161
- this.needsRender = true;
2162
- return;
2163
- }
2164
- // 3. History navigation (only when at first line)
2165
- if (this.navigateHistoryUp()) {
2166
- this.closeAutocomplete();
2167
- this.needsRender = true;
2168
- }
2169
- return;
2170
- }
2171
- // Arrow Down - autocomplete > multiline navigation > history
2172
- if (key.name === 'down') {
2173
- // 1. Autocomplete navigation takes priority
2174
- if (this.navigateAutocompleteDown()) {
2175
- this.needsRender = true;
2176
- return;
2177
- }
2178
- // 2. Navigate to next line in multiline input
2179
- if (this.currentLine < this.lines.length - 1) {
2180
- this.currentLine++;
2181
- // Try to keep same cursor position, but clamp to line length
2182
- this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
2183
- this.needsRender = true;
2184
- return;
2185
- }
2186
- // 3. History forward (only when at last line)
2187
- if (this.historyIndex >= 0 && this.navigateHistoryDown()) {
2188
- this.needsRender = true;
2189
- }
2190
- return;
2191
- }
2192
- // Backspace
2193
- if (key.name === 'backspace') {
2194
- if (this.cursorPos > 0) {
2195
- // Delete character in current line
2196
- const line = this.lines[this.currentLine];
2197
- this.lines[this.currentLine] = line.slice(0, this.cursorPos - 1) + line.slice(this.cursorPos);
2198
- this.cursorPos--;
2199
- this.resetHistoryNavigation();
2200
- this.updateAutocomplete();
2201
- this.needsRender = true;
2202
- }
2203
- else if (this.currentLine > 0) {
2204
- // At start of line - merge with previous line
2205
- const currentLine = this.lines[this.currentLine];
2206
- const prevLine = this.lines[this.currentLine - 1];
2207
- this.lines[this.currentLine - 1] = prevLine + currentLine;
2208
- this.lines.splice(this.currentLine, 1);
2209
- this.currentLine--;
2210
- this.cursorPos = prevLine.length;
2211
- this.resetHistoryNavigation();
2212
- this.updateAutocomplete();
2213
- this.needsRender = true;
2214
- }
2215
- return;
2216
- }
2217
- // Delete
2218
- if (key.name === 'delete') {
2219
- const line = this.lines[this.currentLine];
2220
- if (this.cursorPos < line.length) {
2221
- // Delete character in current line
2222
- this.lines[this.currentLine] = line.slice(0, this.cursorPos) + line.slice(this.cursorPos + 1);
2223
- this.resetHistoryNavigation();
2224
- this.updateAutocomplete();
2225
- this.needsRender = true;
2226
- }
2227
- else if (this.currentLine < this.lines.length - 1) {
2228
- // At end of line - merge with next line
2229
- const nextLine = this.lines[this.currentLine + 1];
2230
- this.lines[this.currentLine] = line + nextLine;
2231
- this.lines.splice(this.currentLine + 1, 1);
2232
- this.resetHistoryNavigation();
2233
- this.updateAutocomplete();
2234
- this.needsRender = true;
2235
- }
2236
- return;
2237
- }
2238
- // Arrow keys (left/right for cursor movement with multiline support)
2239
- if (key.name === 'left') {
2240
- if (this.cursorPos > 0) {
2241
- this.cursorPos--;
2242
- this.needsRender = true;
2243
- }
2244
- else if (this.currentLine > 0) {
2245
- // Move to end of previous line
2246
- this.currentLine--;
2247
- this.cursorPos = this.lines[this.currentLine].length;
2248
- this.needsRender = true;
2249
- }
2250
- return;
2251
- }
2252
- if (key.name === 'right') {
2253
- const lineLen = this.lines[this.currentLine].length;
2254
- if (this.cursorPos < lineLen) {
2255
- this.cursorPos++;
2256
- this.needsRender = true;
2257
- }
2258
- else if (this.currentLine < this.lines.length - 1) {
2259
- // Move to start of next line
2260
- this.currentLine++;
2261
- this.cursorPos = 0;
2262
- this.needsRender = true;
2263
- }
2264
- return;
2265
- }
2266
- if (key.name === 'home') {
2267
- this.cursorPos = 0;
2268
- this.needsRender = true;
2269
- return;
2270
- }
2271
- if (key.name === 'end') {
2272
- this.cursorPos = this.lines[this.currentLine].length;
2273
- this.needsRender = true;
2274
- return;
2275
- }
2276
- // Shift+Tab - mode change
2277
- if (key.shift && key.name === 'tab') {
2278
- this.emit('modeChange');
2279
- return;
2280
- }
2281
- // Regular character
2282
- if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
2283
- // Clear ghost text suggestion when user starts typing
2284
- if (this.suggestion) {
2285
- this.suggestion = null;
2286
- }
2287
- const line = this.lines[this.currentLine];
2288
- this.lines[this.currentLine] =
2289
- line.slice(0, this.cursorPos) + str + line.slice(this.cursorPos);
2290
- this.cursorPos++;
2291
- this.resetHistoryNavigation();
2292
- this.updateAutocomplete();
2293
- this.needsRender = true;
2294
- }
2295
- }
2296
819
  }