@pentoshi/clai 2.0.62 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. package/README.md +262 -368
  2. package/dist/agent/classic-renderer.js +9 -1
  3. package/dist/agent/classic-renderer.js.map +1 -1
  4. package/dist/agent/context-manager.d.ts +5 -0
  5. package/dist/agent/context-manager.js +14 -2
  6. package/dist/agent/context-manager.js.map +1 -1
  7. package/dist/agent/events.d.ts +9 -1
  8. package/dist/agent/plan-tool.js +80 -8
  9. package/dist/agent/plan-tool.js.map +1 -1
  10. package/dist/agent/runner.js +261 -122
  11. package/dist/agent/runner.js.map +1 -1
  12. package/dist/agent/tool-call-parser.d.ts +20 -0
  13. package/dist/agent/tool-call-parser.js +186 -2
  14. package/dist/agent/tool-call-parser.js.map +1 -1
  15. package/dist/app/adapters/agent-event-adapter.d.ts +29 -0
  16. package/dist/app/adapters/agent-event-adapter.js +148 -0
  17. package/dist/app/adapters/agent-event-adapter.js.map +1 -0
  18. package/dist/app/adapters/current-agent-adapter.d.ts +7 -0
  19. package/dist/app/adapters/current-agent-adapter.js +27 -0
  20. package/dist/app/adapters/current-agent-adapter.js.map +1 -0
  21. package/dist/app/adapters/current-jobs-adapter.d.ts +4 -0
  22. package/dist/app/adapters/current-jobs-adapter.js +13 -0
  23. package/dist/app/adapters/current-jobs-adapter.js.map +1 -0
  24. package/dist/app/adapters/current-store-adapter.d.ts +3 -0
  25. package/dist/app/adapters/current-store-adapter.js +22 -0
  26. package/dist/app/adapters/current-store-adapter.js.map +1 -0
  27. package/dist/app/adapters/current-terminal-adapter.d.ts +9 -0
  28. package/dist/app/adapters/current-terminal-adapter.js +31 -0
  29. package/dist/app/adapters/current-terminal-adapter.js.map +1 -0
  30. package/dist/app/adapters/current-updates-adapter.d.ts +7 -0
  31. package/dist/app/adapters/current-updates-adapter.js +31 -0
  32. package/dist/app/adapters/current-updates-adapter.js.map +1 -0
  33. package/dist/app/adapters/in-memory-clipboard-adapter.d.ts +10 -0
  34. package/dist/app/adapters/in-memory-clipboard-adapter.js +20 -0
  35. package/dist/app/adapters/in-memory-clipboard-adapter.js.map +1 -0
  36. package/dist/app/commands/command.d.ts +24 -0
  37. package/dist/app/commands/command.js +4 -0
  38. package/dist/app/commands/command.js.map +1 -0
  39. package/dist/app/commands/registry.d.ts +45 -0
  40. package/dist/app/commands/registry.js +164 -0
  41. package/dist/app/commands/registry.js.map +1 -0
  42. package/dist/app/controllers/disposable.d.ts +15 -0
  43. package/dist/app/controllers/disposable.js +38 -0
  44. package/dist/app/controllers/disposable.js.map +1 -0
  45. package/dist/app/controllers/job-controller.d.ts +22 -0
  46. package/dist/app/controllers/job-controller.js +35 -0
  47. package/dist/app/controllers/job-controller.js.map +1 -0
  48. package/dist/app/controllers/plan-controller.d.ts +31 -0
  49. package/dist/app/controllers/plan-controller.js +69 -0
  50. package/dist/app/controllers/plan-controller.js.map +1 -0
  51. package/dist/app/controllers/session-controller.d.ts +167 -0
  52. package/dist/app/controllers/session-controller.js +467 -0
  53. package/dist/app/controllers/session-controller.js.map +1 -0
  54. package/dist/app/controllers/turn-controller.d.ts +52 -0
  55. package/dist/app/controllers/turn-controller.js +118 -0
  56. package/dist/app/controllers/turn-controller.js.map +1 -0
  57. package/dist/app/events/app-event.d.ts +122 -0
  58. package/dist/app/events/app-event.js +25 -0
  59. package/dist/app/events/app-event.js.map +1 -0
  60. package/dist/app/events/event-buffer.d.ts +51 -0
  61. package/dist/app/events/event-buffer.js +109 -0
  62. package/dist/app/events/event-buffer.js.map +1 -0
  63. package/dist/app/events/sequencer.d.ts +32 -0
  64. package/dist/app/events/sequencer.js +55 -0
  65. package/dist/app/events/sequencer.js.map +1 -0
  66. package/dist/app/ports/agent-port.d.ts +29 -0
  67. package/dist/app/ports/agent-port.js +2 -0
  68. package/dist/app/ports/agent-port.js.map +1 -0
  69. package/dist/app/ports/clipboard-port.d.ts +9 -0
  70. package/dist/app/ports/clipboard-port.js +2 -0
  71. package/dist/app/ports/clipboard-port.js.map +1 -0
  72. package/dist/app/ports/confirm-port.d.ts +17 -0
  73. package/dist/app/ports/confirm-port.js +2 -0
  74. package/dist/app/ports/confirm-port.js.map +1 -0
  75. package/dist/app/ports/jobs-port.d.ts +18 -0
  76. package/dist/app/ports/jobs-port.js +2 -0
  77. package/dist/app/ports/jobs-port.js.map +1 -0
  78. package/dist/app/ports/persistence-port.d.ts +21 -0
  79. package/dist/app/ports/persistence-port.js +2 -0
  80. package/dist/app/ports/persistence-port.js.map +1 -0
  81. package/dist/app/ports/secret-port.d.ts +12 -0
  82. package/dist/app/ports/secret-port.js +2 -0
  83. package/dist/app/ports/secret-port.js.map +1 -0
  84. package/dist/app/ports/terminal-port.d.ts +18 -0
  85. package/dist/app/ports/terminal-port.js +2 -0
  86. package/dist/app/ports/terminal-port.js.map +1 -0
  87. package/dist/app/ports/updates-port.d.ts +12 -0
  88. package/dist/app/ports/updates-port.js +2 -0
  89. package/dist/app/ports/updates-port.js.map +1 -0
  90. package/dist/commands/doctor.js +19 -0
  91. package/dist/commands/doctor.js.map +1 -1
  92. package/dist/commands/update.js +1 -1
  93. package/dist/commands/update.js.map +1 -1
  94. package/dist/index.js +47 -12
  95. package/dist/index.js.map +1 -1
  96. package/dist/llm/router.js +4 -2
  97. package/dist/llm/router.js.map +1 -1
  98. package/dist/prompts/index.d.ts +1 -1
  99. package/dist/prompts/index.js +11 -8
  100. package/dist/prompts/index.js.map +1 -1
  101. package/dist/repl/slash-commands.js +1 -1
  102. package/dist/repl/slash-commands.js.map +1 -1
  103. package/dist/repl.js +35 -107
  104. package/dist/repl.js.map +1 -1
  105. package/dist/store/config.js +3 -1
  106. package/dist/store/config.js.map +1 -1
  107. package/dist/store/history.d.ts +26 -2
  108. package/dist/store/history.js +383 -72
  109. package/dist/store/history.js.map +1 -1
  110. package/dist/store/logs.js +3 -3
  111. package/dist/store/logs.js.map +1 -1
  112. package/dist/store/paths.d.ts +6 -0
  113. package/dist/store/paths.js +38 -0
  114. package/dist/store/paths.js.map +1 -0
  115. package/dist/store/plan.js +8 -5
  116. package/dist/store/plan.js.map +1 -1
  117. package/dist/tools/fs.d.ts +6 -0
  118. package/dist/tools/fs.js +56 -6
  119. package/dist/tools/fs.js.map +1 -1
  120. package/dist/tools/jobs.js +2 -2
  121. package/dist/tools/jobs.js.map +1 -1
  122. package/dist/tools/registry.js +189 -67
  123. package/dist/tools/registry.js.map +1 -1
  124. package/dist/tools/shell.js +2 -2
  125. package/dist/tools/shell.js.map +1 -1
  126. package/dist/tui/runtime.d.ts +19 -0
  127. package/dist/tui/runtime.js +92 -0
  128. package/dist/tui/runtime.js.map +1 -0
  129. package/dist/tui/state.js +22 -17
  130. package/dist/tui/state.js.map +1 -1
  131. package/dist/tui-v2/actions/action-id.d.ts +14 -0
  132. package/dist/tui-v2/actions/action-id.js +109 -0
  133. package/dist/tui-v2/actions/action-id.js.map +1 -0
  134. package/dist/tui-v2/actions/action-router.d.ts +18 -0
  135. package/dist/tui-v2/actions/action-router.js +62 -0
  136. package/dist/tui-v2/actions/action-router.js.map +1 -0
  137. package/dist/tui-v2/actions/chord-from-key.d.ts +26 -0
  138. package/dist/tui-v2/actions/chord-from-key.js +45 -0
  139. package/dist/tui-v2/actions/chord-from-key.js.map +1 -0
  140. package/dist/tui-v2/actions/keymap.d.ts +24 -0
  141. package/dist/tui-v2/actions/keymap.js +134 -0
  142. package/dist/tui-v2/actions/keymap.js.map +1 -0
  143. package/dist/tui-v2/app/App.d.ts +13 -0
  144. package/dist/tui-v2/app/App.js +332 -0
  145. package/dist/tui-v2/app/App.js.map +1 -0
  146. package/dist/tui-v2/app/command-handlers.d.ts +8 -0
  147. package/dist/tui-v2/app/command-handlers.js +66 -0
  148. package/dist/tui-v2/app/command-handlers.js.map +1 -0
  149. package/dist/tui-v2/app/commands/config-commands.d.ts +14 -0
  150. package/dist/tui-v2/app/commands/config-commands.js +161 -0
  151. package/dist/tui-v2/app/commands/config-commands.js.map +1 -0
  152. package/dist/tui-v2/app/commands/key-commands.d.ts +9 -0
  153. package/dist/tui-v2/app/commands/key-commands.js +263 -0
  154. package/dist/tui-v2/app/commands/key-commands.js.map +1 -0
  155. package/dist/tui-v2/app/commands/picker-commands.d.ts +24 -0
  156. package/dist/tui-v2/app/commands/picker-commands.js +423 -0
  157. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -0
  158. package/dist/tui-v2/app/commands/session-commands.d.ts +19 -0
  159. package/dist/tui-v2/app/commands/session-commands.js +162 -0
  160. package/dist/tui-v2/app/commands/session-commands.js.map +1 -0
  161. package/dist/tui-v2/app/plan-lifecycle.d.ts +22 -0
  162. package/dist/tui-v2/app/plan-lifecycle.js +95 -0
  163. package/dist/tui-v2/app/plan-lifecycle.js.map +1 -0
  164. package/dist/tui-v2/app/providers.d.ts +17 -0
  165. package/dist/tui-v2/app/providers.js +32 -0
  166. package/dist/tui-v2/app/providers.js.map +1 -0
  167. package/dist/tui-v2/bootstrap/capabilities.d.ts +35 -0
  168. package/dist/tui-v2/bootstrap/capabilities.js +93 -0
  169. package/dist/tui-v2/bootstrap/capabilities.js.map +1 -0
  170. package/dist/tui-v2/bootstrap/composition-root.d.ts +85 -0
  171. package/dist/tui-v2/bootstrap/composition-root.js +131 -0
  172. package/dist/tui-v2/bootstrap/composition-root.js.map +1 -0
  173. package/dist/tui-v2/bootstrap/disable-native-selection.d.ts +11 -0
  174. package/dist/tui-v2/bootstrap/disable-native-selection.js +13 -0
  175. package/dist/tui-v2/bootstrap/disable-native-selection.js.map +1 -0
  176. package/dist/tui-v2/bootstrap/lifecycle.d.ts +62 -0
  177. package/dist/tui-v2/bootstrap/lifecycle.js +123 -0
  178. package/dist/tui-v2/bootstrap/lifecycle.js.map +1 -0
  179. package/dist/tui-v2/bootstrap/osc52-clipboard.d.ts +28 -0
  180. package/dist/tui-v2/bootstrap/osc52-clipboard.js +44 -0
  181. package/dist/tui-v2/bootstrap/osc52-clipboard.js.map +1 -0
  182. package/dist/tui-v2/bootstrap/overlay-ports.d.ts +12 -0
  183. package/dist/{tui/confirm.js → tui-v2/bootstrap/overlay-ports.js} +17 -25
  184. package/dist/tui-v2/bootstrap/overlay-ports.js.map +1 -0
  185. package/dist/tui-v2/bootstrap/pager-export.d.ts +24 -0
  186. package/dist/tui-v2/bootstrap/pager-export.js +87 -0
  187. package/dist/tui-v2/bootstrap/pager-export.js.map +1 -0
  188. package/dist/tui-v2/bootstrap/start-tui-v2.d.ts +15 -0
  189. package/dist/tui-v2/bootstrap/start-tui-v2.js +95 -0
  190. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -0
  191. package/dist/tui-v2/bootstrap/ui-selection.d.ts +30 -0
  192. package/dist/tui-v2/bootstrap/ui-selection.js +59 -0
  193. package/dist/tui-v2/bootstrap/ui-selection.js.map +1 -0
  194. package/dist/tui-v2/components/completion/completion-menu.d.ts +23 -0
  195. package/dist/tui-v2/components/completion/completion-menu.js +84 -0
  196. package/dist/tui-v2/components/completion/completion-menu.js.map +1 -0
  197. package/dist/tui-v2/components/jobs/jobs-panel.d.ts +16 -0
  198. package/dist/tui-v2/components/jobs/jobs-panel.js +92 -0
  199. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -0
  200. package/dist/tui-v2/components/modal/confirm-modal.d.ts +20 -0
  201. package/dist/tui-v2/components/modal/confirm-modal.js +110 -0
  202. package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -0
  203. package/dist/tui-v2/components/modal/prompt-actions-modal.d.ts +15 -0
  204. package/dist/tui-v2/components/modal/prompt-actions-modal.js +80 -0
  205. package/dist/tui-v2/components/modal/prompt-actions-modal.js.map +1 -0
  206. package/dist/tui-v2/components/modal/secret-modal.d.ts +18 -0
  207. package/dist/tui-v2/components/modal/secret-modal.js +50 -0
  208. package/dist/tui-v2/components/modal/secret-modal.js.map +1 -0
  209. package/dist/tui-v2/components/overlay/overlay-host.d.ts +26 -0
  210. package/dist/tui-v2/components/overlay/overlay-host.js +43 -0
  211. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -0
  212. package/dist/tui-v2/components/pager/pager.d.ts +20 -0
  213. package/dist/tui-v2/components/pager/pager.js +396 -0
  214. package/dist/tui-v2/components/pager/pager.js.map +1 -0
  215. package/dist/tui-v2/components/picker/picker.d.ts +17 -0
  216. package/dist/tui-v2/components/picker/picker.js +222 -0
  217. package/dist/tui-v2/components/picker/picker.js.map +1 -0
  218. package/dist/tui-v2/components/plan/plan-view.d.ts +20 -0
  219. package/dist/tui-v2/components/plan/plan-view.js +120 -0
  220. package/dist/tui-v2/components/plan/plan-view.js.map +1 -0
  221. package/dist/tui-v2/components/queue/queue-panel.d.ts +23 -0
  222. package/dist/tui-v2/components/queue/queue-panel.js +58 -0
  223. package/dist/tui-v2/components/queue/queue-panel.js.map +1 -0
  224. package/dist/tui-v2/components/status/status-line.d.ts +27 -0
  225. package/dist/tui-v2/components/status/status-line.js +167 -0
  226. package/dist/tui-v2/components/status/status-line.js.map +1 -0
  227. package/dist/tui-v2/components/toast/toast-host.d.ts +15 -0
  228. package/dist/tui-v2/components/toast/toast-host.js +48 -0
  229. package/dist/tui-v2/components/toast/toast-host.js.map +1 -0
  230. package/dist/tui-v2/components/transcript/assistant-message.d.ts +20 -0
  231. package/dist/tui-v2/components/transcript/assistant-message.js +41 -0
  232. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -0
  233. package/dist/tui-v2/components/transcript/compacted-row.d.ts +19 -0
  234. package/dist/tui-v2/components/transcript/compacted-row.js +69 -0
  235. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -0
  236. package/dist/tui-v2/components/transcript/intro-card.d.ts +19 -0
  237. package/dist/tui-v2/components/transcript/intro-card.js +50 -0
  238. package/dist/tui-v2/components/transcript/intro-card.js.map +1 -0
  239. package/dist/tui-v2/components/transcript/linkable-text.d.ts +21 -0
  240. package/dist/tui-v2/components/transcript/linkable-text.js +46 -0
  241. package/dist/tui-v2/components/transcript/linkable-text.js.map +1 -0
  242. package/dist/tui-v2/components/transcript/notice-row.d.ts +14 -0
  243. package/dist/tui-v2/components/transcript/notice-row.js +40 -0
  244. package/dist/tui-v2/components/transcript/notice-row.js.map +1 -0
  245. package/dist/tui-v2/components/transcript/search-bar.d.ts +19 -0
  246. package/dist/tui-v2/components/transcript/search-bar.js +7 -0
  247. package/dist/tui-v2/components/transcript/search-bar.js.map +1 -0
  248. package/dist/tui-v2/components/transcript/thinking-block.d.ts +22 -0
  249. package/dist/tui-v2/components/transcript/thinking-block.js +27 -0
  250. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -0
  251. package/dist/tui-v2/components/transcript/tool-card.d.ts +26 -0
  252. package/dist/tui-v2/components/transcript/tool-card.js +205 -0
  253. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -0
  254. package/dist/tui-v2/components/transcript/transcript-row.d.ts +25 -0
  255. package/dist/tui-v2/components/transcript/transcript-row.js +37 -0
  256. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -0
  257. package/dist/tui-v2/components/transcript/transcript-scroll-port.d.ts +52 -0
  258. package/dist/tui-v2/components/transcript/transcript-scroll-port.js +86 -0
  259. package/dist/tui-v2/components/transcript/transcript-scroll-port.js.map +1 -0
  260. package/dist/tui-v2/components/transcript/transcript-view.d.ts +24 -0
  261. package/dist/tui-v2/components/transcript/transcript-view.js +403 -0
  262. package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -0
  263. package/dist/tui-v2/components/transcript/use-click-without-drag.d.ts +13 -0
  264. package/dist/tui-v2/components/transcript/use-click-without-drag.js +35 -0
  265. package/dist/tui-v2/components/transcript/use-click-without-drag.js.map +1 -0
  266. package/dist/tui-v2/components/transcript/use-native-selection-copy.d.ts +10 -0
  267. package/dist/tui-v2/components/transcript/use-native-selection-copy.js +37 -0
  268. package/dist/tui-v2/components/transcript/use-native-selection-copy.js.map +1 -0
  269. package/dist/tui-v2/components/transcript/use-transcript-selection.d.ts +23 -0
  270. package/dist/tui-v2/components/transcript/use-transcript-selection.js +136 -0
  271. package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -0
  272. package/dist/tui-v2/components/transcript/user-message.d.ts +10 -0
  273. package/dist/tui-v2/components/transcript/user-message.js +23 -0
  274. package/dist/tui-v2/components/transcript/user-message.js.map +1 -0
  275. package/dist/tui-v2/composer/arrow-intent.d.ts +23 -0
  276. package/dist/tui-v2/composer/arrow-intent.js +35 -0
  277. package/dist/tui-v2/composer/arrow-intent.js.map +1 -0
  278. package/dist/tui-v2/composer/completion.d.ts +37 -0
  279. package/dist/tui-v2/composer/completion.js +67 -0
  280. package/dist/tui-v2/composer/completion.js.map +1 -0
  281. package/dist/tui-v2/composer/composer-editor.d.ts +33 -0
  282. package/dist/tui-v2/composer/composer-editor.js +620 -0
  283. package/dist/tui-v2/composer/composer-editor.js.map +1 -0
  284. package/dist/tui-v2/composer/composer-height.d.ts +21 -0
  285. package/dist/tui-v2/composer/composer-height.js +29 -0
  286. package/dist/tui-v2/composer/composer-height.js.map +1 -0
  287. package/dist/tui-v2/composer/composer-meta.d.ts +9 -0
  288. package/dist/tui-v2/composer/composer-meta.js +38 -0
  289. package/dist/tui-v2/composer/composer-meta.js.map +1 -0
  290. package/dist/tui-v2/composer/history-nav.d.ts +15 -0
  291. package/dist/tui-v2/composer/history-nav.js +16 -0
  292. package/dist/tui-v2/composer/history-nav.js.map +1 -0
  293. package/dist/tui-v2/composer/newline-hint.d.ts +13 -0
  294. package/dist/tui-v2/composer/newline-hint.js +13 -0
  295. package/dist/tui-v2/composer/newline-hint.js.map +1 -0
  296. package/dist/tui-v2/composer/paste-placeholder.d.ts +35 -0
  297. package/dist/tui-v2/composer/paste-placeholder.js +55 -0
  298. package/dist/tui-v2/composer/paste-placeholder.js.map +1 -0
  299. package/dist/tui-v2/composer/prompt-history.d.ts +22 -0
  300. package/dist/tui-v2/composer/prompt-history.js +51 -0
  301. package/dist/tui-v2/composer/prompt-history.js.map +1 -0
  302. package/dist/tui-v2/composer/secret-buffer.d.ts +20 -0
  303. package/dist/tui-v2/composer/secret-buffer.js +42 -0
  304. package/dist/tui-v2/composer/secret-buffer.js.map +1 -0
  305. package/dist/tui-v2/composer/textarea-keybindings.d.ts +22 -0
  306. package/dist/tui-v2/composer/textarea-keybindings.js +27 -0
  307. package/dist/tui-v2/composer/textarea-keybindings.js.map +1 -0
  308. package/dist/tui-v2/controllers/focus-controller.d.ts +30 -0
  309. package/dist/tui-v2/controllers/focus-controller.js +80 -0
  310. package/dist/tui-v2/controllers/focus-controller.js.map +1 -0
  311. package/dist/tui-v2/controllers/overlay-controller.d.ts +99 -0
  312. package/dist/tui-v2/controllers/overlay-controller.js +195 -0
  313. package/dist/tui-v2/controllers/overlay-controller.js.map +1 -0
  314. package/dist/tui-v2/controllers/selection-controller.d.ts +76 -0
  315. package/dist/tui-v2/controllers/selection-controller.js +238 -0
  316. package/dist/tui-v2/controllers/selection-controller.js.map +1 -0
  317. package/dist/tui-v2/controllers/toast-controller.d.ts +35 -0
  318. package/dist/tui-v2/controllers/toast-controller.js +88 -0
  319. package/dist/tui-v2/controllers/toast-controller.js.map +1 -0
  320. package/dist/tui-v2/layout/compute-layout.d.ts +59 -0
  321. package/dist/tui-v2/layout/compute-layout.js +170 -0
  322. package/dist/tui-v2/layout/compute-layout.js.map +1 -0
  323. package/dist/tui-v2/rendering/ansi-to-styled.d.ts +17 -0
  324. package/dist/tui-v2/rendering/ansi-to-styled.js +217 -0
  325. package/dist/tui-v2/rendering/ansi-to-styled.js.map +1 -0
  326. package/dist/tui-v2/rendering/batch-sections.d.ts +57 -0
  327. package/dist/tui-v2/rendering/batch-sections.js +177 -0
  328. package/dist/tui-v2/rendering/batch-sections.js.map +1 -0
  329. package/dist/tui-v2/rendering/link-detector.d.ts +16 -0
  330. package/dist/tui-v2/rendering/link-detector.js +43 -0
  331. package/dist/tui-v2/rendering/link-detector.js.map +1 -0
  332. package/dist/tui-v2/rendering/open-tool-output.d.ts +28 -0
  333. package/dist/tui-v2/rendering/open-tool-output.js +138 -0
  334. package/dist/tui-v2/rendering/open-tool-output.js.map +1 -0
  335. package/dist/tui-v2/rendering/picker-filter.d.ts +22 -0
  336. package/dist/tui-v2/rendering/picker-filter.js +68 -0
  337. package/dist/tui-v2/rendering/picker-filter.js.map +1 -0
  338. package/dist/tui-v2/rendering/plan-view.d.ts +32 -0
  339. package/dist/tui-v2/rendering/plan-view.js +196 -0
  340. package/dist/tui-v2/rendering/plan-view.js.map +1 -0
  341. package/dist/tui-v2/rendering/render-markdown-lines.d.ts +29 -0
  342. package/dist/tui-v2/rendering/render-markdown-lines.js +46 -0
  343. package/dist/tui-v2/rendering/render-markdown-lines.js.map +1 -0
  344. package/dist/tui-v2/rendering/sanitize-display.d.ts +13 -0
  345. package/dist/tui-v2/rendering/sanitize-display.js +24 -0
  346. package/dist/tui-v2/rendering/sanitize-display.js.map +1 -0
  347. package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +10 -0
  348. package/dist/tui-v2/rendering/strip-tool-surfaces.js +32 -0
  349. package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -0
  350. package/dist/tui-v2/rendering/theme.d.ts +67 -0
  351. package/dist/tui-v2/rendering/theme.js +74 -0
  352. package/dist/tui-v2/rendering/theme.js.map +1 -0
  353. package/dist/tui-v2/rendering/tool-presenter.d.ts +39 -0
  354. package/dist/tui-v2/rendering/tool-presenter.js +209 -0
  355. package/dist/tui-v2/rendering/tool-presenter.js.map +1 -0
  356. package/dist/tui-v2/rendering/transcript-export.d.ts +11 -0
  357. package/dist/tui-v2/rendering/transcript-export.js +18 -0
  358. package/dist/tui-v2/rendering/transcript-export.js.map +1 -0
  359. package/dist/tui-v2/rendering/transcript-semantic.d.ts +17 -0
  360. package/dist/tui-v2/rendering/transcript-semantic.js +67 -0
  361. package/dist/tui-v2/rendering/transcript-semantic.js.map +1 -0
  362. package/dist/tui-v2/state/pager-search.d.ts +22 -0
  363. package/dist/tui-v2/state/pager-search.js +78 -0
  364. package/dist/tui-v2/state/pager-search.js.map +1 -0
  365. package/dist/tui-v2/state/semantic-document.d.ts +34 -0
  366. package/dist/tui-v2/state/semantic-document.js +201 -0
  367. package/dist/tui-v2/state/semantic-document.js.map +1 -0
  368. package/dist/tui-v2/state/transcript-compaction.d.ts +24 -0
  369. package/dist/tui-v2/state/transcript-compaction.js +79 -0
  370. package/dist/tui-v2/state/transcript-compaction.js.map +1 -0
  371. package/dist/tui-v2/state/transcript-hydrate.d.ts +27 -0
  372. package/dist/tui-v2/state/transcript-hydrate.js +276 -0
  373. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -0
  374. package/dist/tui-v2/state/transcript-reducer.d.ts +10 -0
  375. package/dist/tui-v2/state/transcript-reducer.js +339 -0
  376. package/dist/tui-v2/state/transcript-reducer.js.map +1 -0
  377. package/dist/tui-v2/state/transcript-search.d.ts +17 -0
  378. package/dist/tui-v2/state/transcript-search.js +38 -0
  379. package/dist/tui-v2/state/transcript-search.js.map +1 -0
  380. package/dist/tui-v2/state/transcript-store.d.ts +32 -0
  381. package/dist/tui-v2/state/transcript-store.js +66 -0
  382. package/dist/tui-v2/state/transcript-store.js.map +1 -0
  383. package/dist/tui-v2/state/transcript-struct.d.ts +15 -0
  384. package/dist/tui-v2/state/transcript-struct.js +133 -0
  385. package/dist/tui-v2/state/transcript-struct.js.map +1 -0
  386. package/dist/tui-v2/state/transcript-types.d.ts +76 -0
  387. package/dist/tui-v2/state/transcript-types.js +65 -0
  388. package/dist/tui-v2/state/transcript-types.js.map +1 -0
  389. package/dist/tui-v2/state/use-overlay.d.ts +3 -0
  390. package/dist/tui-v2/state/use-overlay.js +6 -0
  391. package/dist/tui-v2/state/use-overlay.js.map +1 -0
  392. package/dist/tui-v2/state/use-plan.d.ts +4 -0
  393. package/dist/tui-v2/state/use-plan.js +6 -0
  394. package/dist/tui-v2/state/use-plan.js.map +1 -0
  395. package/dist/tui-v2/state/use-session-state.d.ts +2 -0
  396. package/dist/tui-v2/state/use-session-state.js +7 -0
  397. package/dist/tui-v2/state/use-session-state.js.map +1 -0
  398. package/dist/tui-v2/state/use-toast.d.ts +3 -0
  399. package/dist/tui-v2/state/use-toast.js +15 -0
  400. package/dist/tui-v2/state/use-toast.js.map +1 -0
  401. package/dist/tui-v2/state/use-transcript-store.d.ts +10 -0
  402. package/dist/tui-v2/state/use-transcript-store.js +12 -0
  403. package/dist/tui-v2/state/use-transcript-store.js.map +1 -0
  404. package/dist/ui/intro-card.js +3 -3
  405. package/dist/ui/intro-card.js.map +1 -1
  406. package/dist/ui/intro-header.d.ts +23 -0
  407. package/dist/ui/intro-header.js +232 -0
  408. package/dist/ui/intro-header.js.map +1 -0
  409. package/dist/ui/markdown.js +0 -1
  410. package/dist/ui/markdown.js.map +1 -1
  411. package/dist/ui/mentions.d.ts +9 -2
  412. package/dist/ui/mentions.js +102 -4
  413. package/dist/ui/mentions.js.map +1 -1
  414. package/dist/ui/output-pane.js +1 -1
  415. package/dist/ui/output-pane.js.map +1 -1
  416. package/dist/ui/plan-pane.d.ts +8 -0
  417. package/dist/ui/plan-pane.js +38 -0
  418. package/dist/ui/plan-pane.js.map +1 -1
  419. package/dist/ui/wordmark.d.ts +5 -0
  420. package/dist/ui/wordmark.js +6 -1
  421. package/dist/ui/wordmark.js.map +1 -1
  422. package/package.json +25 -12
  423. package/dist/tui/App.d.ts +0 -12
  424. package/dist/tui/App.js +0 -2174
  425. package/dist/tui/App.js.map +0 -1
  426. package/dist/tui/components/ConfirmModal.d.ts +0 -9
  427. package/dist/tui/components/ConfirmModal.js +0 -55
  428. package/dist/tui/components/ConfirmModal.js.map +0 -1
  429. package/dist/tui/components/JobsPanel.d.ts +0 -11
  430. package/dist/tui/components/JobsPanel.js +0 -55
  431. package/dist/tui/components/JobsPanel.js.map +0 -1
  432. package/dist/tui/components/Pager.d.ts +0 -14
  433. package/dist/tui/components/Pager.js +0 -56
  434. package/dist/tui/components/Pager.js.map +0 -1
  435. package/dist/tui/components/PickerPanel.d.ts +0 -15
  436. package/dist/tui/components/PickerPanel.js +0 -126
  437. package/dist/tui/components/PickerPanel.js.map +0 -1
  438. package/dist/tui/components/PlanSidebar.d.ts +0 -6
  439. package/dist/tui/components/PlanSidebar.js +0 -99
  440. package/dist/tui/components/PlanSidebar.js.map +0 -1
  441. package/dist/tui/components/SecretInputPanel.d.ts +0 -7
  442. package/dist/tui/components/SecretInputPanel.js +0 -38
  443. package/dist/tui/components/SecretInputPanel.js.map +0 -1
  444. package/dist/tui/confirm.d.ts +0 -16
  445. package/dist/tui/confirm.js.map +0 -1
  446. package/dist/tui/default.d.ts +0 -6
  447. package/dist/tui/default.js +0 -11
  448. package/dist/tui/default.js.map +0 -1
  449. package/dist/tui/hooks/useAgentRunner.d.ts +0 -73
  450. package/dist/tui/hooks/useAgentRunner.js +0 -277
  451. package/dist/tui/hooks/useAgentRunner.js.map +0 -1
  452. package/dist/tui/hooks/useJobs.d.ts +0 -7
  453. package/dist/tui/hooks/useJobs.js +0 -39
  454. package/dist/tui/hooks/useJobs.js.map +0 -1
  455. package/dist/tui/hooks/useSpinner.d.ts +0 -5
  456. package/dist/tui/hooks/useSpinner.js +0 -21
  457. package/dist/tui/hooks/useSpinner.js.map +0 -1
  458. package/dist/tui/hooks/useTerminalSize.d.ts +0 -9
  459. package/dist/tui/hooks/useTerminalSize.js +0 -26
  460. package/dist/tui/hooks/useTerminalSize.js.map +0 -1
  461. package/dist/tui/index.d.ts +0 -10
  462. package/dist/tui/index.js +0 -37
  463. package/dist/tui/index.js.map +0 -1
  464. package/dist/tui/key-intent.d.ts +0 -36
  465. package/dist/tui/key-intent.js +0 -54
  466. package/dist/tui/key-intent.js.map +0 -1
  467. package/dist/tui/mouse.d.ts +0 -8
  468. package/dist/tui/mouse.js +0 -34
  469. package/dist/tui/mouse.js.map +0 -1
  470. package/dist/tui/plan-sidebar-lines.d.ts +0 -2
  471. package/dist/tui/plan-sidebar-lines.js +0 -140
  472. package/dist/tui/plan-sidebar-lines.js.map +0 -1
  473. package/dist/tui/render-lines.d.ts +0 -22
  474. package/dist/tui/render-lines.js +0 -611
  475. package/dist/tui/render-lines.js.map +0 -1
package/dist/tui/App.js DELETED
@@ -1,2174 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text, useApp, useInput, useStdin } from "ink";
3
- import chalk from "chalk";
4
- import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from "react";
5
- import { providerIds } from "../types.js";
6
- import { assertProvider, getProviderInfoText } from "../llm/provider.js";
7
- import { getProvider } from "../llm/router.js";
8
- import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret, setSecret, unsetProviderSecret, } from "../store/keys.js";
9
- import { setActiveSearchProvider } from "../store/config.js";
10
- import { searchProviderIds, } from "../tools/web/types.js";
11
- import { assertSearchProvider, searchProviders, } from "../tools/web/providers/provider.js";
12
- import "../tools/web/providers/duckduckgo.js";
13
- import "../tools/web/providers/brave.js";
14
- import "../tools/web/providers/tavily.js";
15
- import { modelSupportsThinking, modelSupportsVision, } from "../llm/capabilities.js";
16
- import { getConfig, getProviderModel, setDefaultMode, setDefaultProvider, setProviderModel, setThinking, updateConfig, } from "../store/config.js";
17
- import { estimateMessagesTokens } from "../agent/context-manager.js";
18
- import { clearAllHistory, getSession, listSessions, upsertSession, } from "../store/history.js";
19
- import { generateSessionTitle } from "../agent/session-title.js";
20
- import { safeCwd } from "../os/cwd.js";
21
- import { resolve } from "node:path";
22
- import { existsSync } from "node:fs";
23
- import { readFile } from "node:fs/promises";
24
- import { expandMentions, findFileSuggestions, getMentionQuery, loadImageAttachments, } from "../ui/mentions.js";
25
- import { deletePlan, loadPlan, savePlan } from "../store/plan.js";
26
- import { renderPlanDocument } from "../ui/plan-pane.js";
27
- import { visibleWidth } from "../ui/markdown.js";
28
- import { getSlashCommandSuggestions, inferProviderForModel, isKnownSlashCommand, knownModels, slashCommands, } from "../repl.js";
29
- import { initialState, reducer, serializeTranscriptForCompaction, } from "./state.js";
30
- import { renderTranscriptLines } from "./render-lines.js";
31
- import { createTuiConfirmPort } from "./confirm.js";
32
- import { useAgentRunner } from "./hooks/useAgentRunner.js";
33
- import { useJobs } from "./hooks/useJobs.js";
34
- import { useSpinner } from "./hooks/useSpinner.js";
35
- import { useTerminalSize } from "./hooks/useTerminalSize.js";
36
- import { ConfirmModal } from "./components/ConfirmModal.js";
37
- import { Pager } from "./components/Pager.js";
38
- import { JobsPanel } from "./components/JobsPanel.js";
39
- import { PickerPanel } from "./components/PickerPanel.js";
40
- import { SecretInputPanel } from "./components/SecretInputPanel.js";
41
- import { renderPlanSidebarLines } from "./plan-sidebar-lines.js";
42
- import { clearArtifacts, clearAuditLogs } from "../store/logs.js";
43
- import { addScopeTargets, clearScope, loadScope, saveScope, } from "../store/scope.js";
44
- import { formatKeyStatus } from "./format-keys.js";
45
- import { shouldStoreInPromptHistory } from "./input-history.js";
46
- import { DISABLE_MOUSE_REPORTING, ENABLE_MOUSE_REPORTING, isMouseReport, mouseWheelDirection, stripMouseReports, } from "./mouse.js";
47
- import { truncateMiddle, wrapPlainString, relativeTime, shortCwd, } from "./text-format.js";
48
- import { isComposerNewline, newlineHint } from "./key-intent.js";
49
- const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Task 1 (explore) is ALREADY COMPLETE from the planning phase — " +
50
- "do NOT re-list or re-read the directory. Start with the FIRST pending task that still needs implementation work. " +
51
- "For each task: call task.update {taskId, state:'in_progress'} → do the real work → VERIFY it succeeded → " +
52
- "call task.update {taskId, state:'done'}, then move to the NEXT task. " +
53
- "If a tool call FAILS, mark the task 'failed', fix the problem, and retry. Do NOT mark a task done when it failed. " +
54
- "Build the project for real with fs.writeMany (create all files in as few calls as possible). " +
55
- "Do NOT call web.search — you already know everything needed. " +
56
- "Run real commands (installs, servers, verification) — do not claim anything ran without a successful tool call.";
57
- const MAX_FILE_SUGGESTIONS = 6;
58
- function truncateAnsi(line, maxWidth) {
59
- let visibleLen = 0;
60
- let result = "";
61
- let i = 0;
62
- while (i < line.length) {
63
- if (line.startsWith("\x1b[", i)) {
64
- let j = i + 2;
65
- while (j < line.length && !/[a-zA-Z]/.test(line[j])) {
66
- j++;
67
- }
68
- result += line.slice(i, j + 1);
69
- i = j + 1;
70
- }
71
- else {
72
- const codePoint = line.codePointAt(i);
73
- const charLength = codePoint > 0xffff ? 2 : 1;
74
- const char = line.slice(i, i + charLength);
75
- const w = Math.max(1, visibleWidth(char));
76
- if (visibleLen + w > maxWidth) {
77
- let k = i;
78
- while (k < line.length) {
79
- if (line.startsWith("\x1b[", k)) {
80
- let j = k + 2;
81
- while (j < line.length && !/[a-zA-Z]/.test(line[j])) {
82
- j++;
83
- }
84
- result += line.slice(k, j + 1);
85
- k = j + 1;
86
- }
87
- else {
88
- k++;
89
- }
90
- }
91
- break;
92
- }
93
- result += char;
94
- visibleLen += w;
95
- i += charLength;
96
- }
97
- }
98
- return result;
99
- }
100
- /** Pad an ANSI string to a visible width (measuring wide glyphs correctly). */
101
- function padVisible(line, width) {
102
- const w = visibleWidth(line);
103
- return w < width ? line + " ".repeat(width - w) : line;
104
- }
105
- export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false, }) {
106
- const { exit } = useApp();
107
- const { stdin } = useStdin();
108
- const { columns: cols, rows } = useTerminalSize();
109
- const [state, dispatch] = useReducer(reducer, undefined, initialState);
110
- const [mode, setMode] = useState(initialMode);
111
- const [provider, setProvider] = useState(initialProvider);
112
- const [model, setModel] = useState(initialModel);
113
- const [overlay, setOverlay] = useState({ kind: "none" });
114
- const [input, setInput] = useState("");
115
- const [cursor, setCursor] = useState(0);
116
- const [selected, setSelected] = useState(0);
117
- const [secretRequest, setSecretRequest] = useState();
118
- const secretResolver = useRef();
119
- // Provider picker selection is asynchronous when a key is required. Keep
120
- // repeated Enter events (or a fast double-submit) from opening a second
121
- // secret prompt before the first activation has finished persisting.
122
- const providerActivationRef = useRef(undefined);
123
- const exitRef = useRef(undefined);
124
- const [scroll, setScroll] = useState(0); // lines scrolled up from bottom
125
- const [compacting, setCompacting] = useState(false);
126
- const [compactStartedAt, setCompactStartedAt] = useState(undefined);
127
- const [mouseMode, setMouseMode] = useState(true);
128
- const [planPaneVisible, setPlanPaneVisible] = useState(true);
129
- const history = useRef([]);
130
- const historyIdx = useRef(-1);
131
- const historyDraft = useRef("");
132
- const lastCtrlC = useRef(0);
133
- const lastDragTime = useRef(0);
134
- const isCtrlHPressed = useRef(false);
135
- const jobs = useJobs(overlay.kind === "jobs");
136
- const spinner = useSpinner(state.status.running || compacting);
137
- const autosaveTimer = useRef(undefined);
138
- const latestItems = useRef(state.items);
139
- latestItems.current = state.items;
140
- // Tracks model-generated session titles. `titledAtUserCount` records the
141
- // number of user turns the current title was generated from, so we can
142
- // refresh the title as the conversation grows past it without regenerating
143
- // on every single turn. Reset whenever a fresh session starts.
144
- const titleRef = useRef({
145
- titledAtUserCount: 0,
146
- inFlight: false,
147
- requestId: 0,
148
- });
149
- const compactAbortRef = useRef(undefined);
150
- const cancelCompaction = useCallback(() => {
151
- if (compactAbortRef.current) {
152
- compactAbortRef.current.abort();
153
- compactAbortRef.current = undefined;
154
- }
155
- setCompacting(false);
156
- setCompactStartedAt(undefined);
157
- }, []);
158
- useEffect(() => {
159
- if (!process.stdout.isTTY)
160
- return;
161
- process.stdout.write(mouseMode ? ENABLE_MOUSE_REPORTING : DISABLE_MOUSE_REPORTING);
162
- return () => {
163
- process.stdout.write(DISABLE_MOUSE_REPORTING);
164
- };
165
- }, [mouseMode]);
166
- // Confirm port → in-app modal
167
- const confirmController = useMemo(() => createTuiConfirmPort(), []);
168
- const confirmResolver = useRef(undefined);
169
- useEffect(() => {
170
- confirmController.setHandler((req) => new Promise((res) => {
171
- confirmResolver.current = res;
172
- dispatch({
173
- type: "event",
174
- event: { type: "confirm-request", id: "c", ...req },
175
- });
176
- }));
177
- }, [confirmController]);
178
- const ctxRef = useRef({ mode, provider, model });
179
- ctxRef.current = { mode, provider, model };
180
- const requestSecret = useCallback((request) => new Promise((resolveSecret) => {
181
- secretResolver.current = resolveSecret;
182
- setSecretRequest(request);
183
- }), []);
184
- const runner = useAgentRunner({
185
- dispatchEvent: (event) => dispatch({ type: "event", event }),
186
- confirm: confirmController.port,
187
- getContext: useCallback(() => ctxRef.current, []),
188
- requestSecret,
189
- onSwitchToAgent: useCallback(() => {
190
- setMode("agent");
191
- setDefaultMode("agent");
192
- dispatch({ type: "notice", level: "info", text: "mode → agent" });
193
- }, []),
194
- onForceExit: useCallback(() => exitRef.current?.(), []),
195
- });
196
- const exitTui = useCallback(() => {
197
- cancelCompaction();
198
- runner.abort();
199
- if (autosaveTimer.current) {
200
- clearTimeout(autosaveTimer.current);
201
- autosaveTimer.current = undefined;
202
- }
203
- const messages = runner.getMessages();
204
- if (!noHistory && !getConfig().privateMode && messages.length > 0) {
205
- // Shutdown must not wait for a slow history backend.
206
- void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
207
- }
208
- exit();
209
- }, [cancelCompaction, exit, noHistory, runner]);
210
- exitRef.current = exitTui;
211
- // Generate (and later refresh) a concise, model-written title for the
212
- // active session. Runs only when a real exchange exists, throttles by user
213
- // turn count, and guards against overlapping requests and session resets.
214
- const maybeUpdateTitle = useCallback((sessionId) => {
215
- if (noHistory || getConfig().privateMode)
216
- return;
217
- if (titleRef.current.inFlight)
218
- return;
219
- const messages = runner.getMessages();
220
- const userCount = messages.filter((m) => m.role === "user").length;
221
- const hasAssistant = messages.some((m) => m.role === "assistant" && m.content.trim().length > 0);
222
- if (userCount === 0 || !hasAssistant)
223
- return;
224
- const titledAt = titleRef.current.titledAtUserCount;
225
- // Title on the first completed exchange, then refresh once two more
226
- // user turns have accumulated so the name tracks the evolving topic.
227
- const shouldGenerate = titledAt === 0 || userCount - titledAt >= 2;
228
- if (!shouldGenerate)
229
- return;
230
- titleRef.current.inFlight = true;
231
- const requestId = ++titleRef.current.requestId;
232
- const targetCount = userCount;
233
- const ctx = ctxRef.current;
234
- // Keep immutable snapshots. The user may start `/new`, resume another
235
- // session, or exit while the title request is in flight.
236
- const titleMessages = messages.map((message) => ({ ...message }));
237
- const titleTranscript = latestItems.current.map((item) => ({ ...item }));
238
- void generateSessionTitle(messages, {
239
- provider: ctx.provider,
240
- model: ctx.model,
241
- })
242
- .then((title) => {
243
- if (titleRef.current.requestId === requestId) {
244
- titleRef.current.inFlight = false;
245
- }
246
- if (!title)
247
- return;
248
- if (titleRef.current.requestId === requestId) {
249
- titleRef.current.titledAtUserCount = targetCount;
250
- }
251
- // Persist against the original session, not whatever session is
252
- // currently visible in the TUI.
253
- void upsertSession(sessionId, titleMessages, title, titleTranscript).catch(() => undefined);
254
- })
255
- .catch(() => {
256
- if (titleRef.current.requestId === requestId) {
257
- titleRef.current.inFlight = false;
258
- }
259
- });
260
- }, [noHistory, runner]);
261
- useEffect(() => {
262
- if (noHistory || getConfig().privateMode)
263
- return;
264
- if (state.items.length === 0)
265
- return;
266
- if (autosaveTimer.current)
267
- clearTimeout(autosaveTimer.current);
268
- autosaveTimer.current = setTimeout(() => {
269
- const messages = runner.getMessages();
270
- // Only persist sessions that contain an actual conversation. This keeps
271
- // `/new` with no input, or a cleared-then-exited session (notices only,
272
- // no user turns), out of the history list.
273
- if (!messages.some((m) => m.role === "user"))
274
- return;
275
- const sessionId = runner.getSession().sessionId;
276
- void upsertSession(sessionId, messages, undefined, state.items).catch(() => undefined);
277
- maybeUpdateTitle(sessionId);
278
- }, 250);
279
- return () => {
280
- if (autosaveTimer.current)
281
- clearTimeout(autosaveTimer.current);
282
- };
283
- }, [noHistory, runner, state.items, maybeUpdateTitle]);
284
- useEffect(() => () => {
285
- if (autosaveTimer.current)
286
- clearTimeout(autosaveTimer.current);
287
- const messages = runner.getMessages();
288
- if (!noHistory &&
289
- !getConfig().privateMode &&
290
- messages.some((m) => m.role === "user")) {
291
- void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
292
- }
293
- }, [noHistory, runner]);
294
- const startTurn = useCallback(async (display, modelInput, images) => {
295
- setScroll(0);
296
- dispatch({ type: "submit", text: display });
297
- await runner.run(modelInput, images && images.length > 0 ? { images } : undefined);
298
- }, [runner]);
299
- const beginTurn = useCallback((rawText) => {
300
- const vision = modelSupportsVision(provider, model);
301
- const expansion = expandMentions(rawText, safeCwd(), vision);
302
- const images = vision ? loadImageAttachments(rawText, safeCwd()) : [];
303
- const modelInput = expansion.contextBlock
304
- ? `${rawText}\n\n${expansion.contextBlock}`
305
- : rawText;
306
- void startTurn(rawText, modelInput, images);
307
- }, [provider, model, startTurn]);
308
- useEffect(() => {
309
- if (!state.status.running &&
310
- !state.pendingConfirm &&
311
- state.queued.length > 0 &&
312
- !runner.isRunning()) {
313
- const next = state.queued[0];
314
- dispatch({ type: "dequeue" });
315
- beginTurn(next);
316
- }
317
- }, [
318
- state.status.running,
319
- state.pendingConfirm,
320
- state.queued,
321
- runner,
322
- beginTurn,
323
- ]);
324
- const lastToolOutput = useCallback(() => {
325
- for (let i = state.items.length - 1; i >= 0; i -= 1) {
326
- const item = state.items[i];
327
- if (item.kind === "tool" && item.output)
328
- return item;
329
- }
330
- return undefined;
331
- }, [state.items]);
332
- const openToolOutput = useCallback(async (item) => {
333
- let body = item.output;
334
- if (item.artifactPath) {
335
- body = await readFile(item.artifactPath, "utf8").catch(() => item.output);
336
- }
337
- setOverlay({ kind: "pager", title: `${item.name} · full output`, body });
338
- }, []);
339
- const runImplement = useCallback(async () => {
340
- const session = runner.getSession();
341
- const plan = await loadPlan(session.sessionId).catch(() => undefined);
342
- if (!plan) {
343
- dispatch({
344
- type: "notice",
345
- level: "info",
346
- text: "no plan to implement — ask clai to plan a multi-step task first",
347
- });
348
- return;
349
- }
350
- if (plan.tasks.every((t) => t.state === "done")) {
351
- dispatch({
352
- type: "notice",
353
- level: "info",
354
- text: "this plan is already complete ✓",
355
- });
356
- return;
357
- }
358
- plan.status = "approved";
359
- await savePlan(plan).catch(() => undefined);
360
- session.planApproved.value = true;
361
- dispatch({
362
- type: "notice",
363
- level: "info",
364
- text: "✦ plan approved — executing it now",
365
- });
366
- void startTurn("/implement", IMPLEMENT_PROMPT);
367
- }, [runner, startTurn]);
368
- // After a turn ends, if the agent just created (or revised) a plan that is
369
- // still a draft awaiting approval, prompt the user with a yes/no modal to
370
- // implement it now or discard it. /implement and /discard remain available.
371
- const maybePromptPlanApproval = useCallback(async () => {
372
- const session = runner.getSession();
373
- if (session.planApproved.value)
374
- return;
375
- if (confirmResolver.current)
376
- return; // a confirm is already on screen
377
- const plan = await loadPlan(session.sessionId).catch(() => undefined);
378
- if (!plan || plan.status !== "draft" || plan.tasks.length === 0)
379
- return;
380
- const ok = await new Promise((res) => {
381
- confirmResolver.current = res;
382
- dispatch({
383
- type: "event",
384
- event: {
385
- type: "confirm-request",
386
- id: "plan",
387
- kind: "plan",
388
- prompt: `Implement this plan now? "${plan.goal}" — ${plan.tasks.length} task(s). (Y to implement · N to discard)`,
389
- },
390
- });
391
- });
392
- if (ok) {
393
- await runImplement();
394
- }
395
- else {
396
- await deletePlan(session.sessionId).catch(() => undefined);
397
- session.planApproved.value = false;
398
- dispatch({
399
- type: "notice",
400
- level: "info",
401
- text: `plan discarded · ${plan.goal}`,
402
- });
403
- }
404
- }, [runner, runImplement]);
405
- // Detect a turn finishing (running true → false) and offer plan approval.
406
- const prevRunningRef = useRef(false);
407
- useEffect(() => {
408
- const wasRunning = prevRunningRef.current;
409
- prevRunningRef.current = state.status.running;
410
- if (wasRunning && !state.status.running) {
411
- void maybePromptPlanApproval();
412
- }
413
- }, [state.status.running, maybePromptPlanApproval]);
414
- const chooseModel = useCallback(async () => {
415
- const providerImpl = getProvider(provider);
416
- let models = [];
417
- if (providerImpl.listModels) {
418
- try {
419
- const secret = await getProviderSecret(provider);
420
- models = await providerImpl.listModels({
421
- apiKey: secret.value,
422
- baseUrl: provider === "ollama" ? secret.value : undefined,
423
- });
424
- }
425
- catch (error) {
426
- dispatch({
427
- type: "notice",
428
- level: "warn",
429
- text: `could not refresh ${provider} models: ${error instanceof Error ? error.message : String(error)} · showing known models`,
430
- });
431
- }
432
- }
433
- if (models.length === 0)
434
- models = knownModels[provider] ?? [];
435
- if (!models.includes(model))
436
- models.unshift(model);
437
- setOverlay({
438
- kind: "picker",
439
- title: `Models · ${provider}`,
440
- options: models.map((value) => ({
441
- value,
442
- label: value,
443
- active: value === model,
444
- })),
445
- onSelect: (value) => {
446
- setModel(value);
447
- setProviderModel(provider, value);
448
- setOverlay({ kind: "none" });
449
- // If the selected model belongs to a different provider, auto-switch
450
- // so requests are sent to the right endpoint (e.g. minimaxai/minimax-m3
451
- // is an NVIDIA model and must not be sent to the Gemini API).
452
- const inferred = inferProviderForModel(value);
453
- if (inferred && inferred !== provider) {
454
- void activateProvider(assertProvider(inferred));
455
- }
456
- else {
457
- dispatch({ type: "notice", level: "info", text: `model → ${value}` });
458
- }
459
- },
460
- });
461
- }, [model, provider]);
462
- const activateProvider = useCallback(async (next) => {
463
- if (providerActivationRef.current)
464
- return;
465
- providerActivationRef.current = next;
466
- // Unmount the picker before opening the secret prompt. This makes the
467
- // prompt the sole owner of stdin while activation is in progress.
468
- setOverlay({ kind: "none" });
469
- try {
470
- const configured = next === "ollama" ||
471
- Boolean(envValue(next)) ||
472
- Boolean((await getProviderSecret(next)).value);
473
- if (!configured) {
474
- const key = await requestSecret({
475
- title: `${next} API key`,
476
- prompt: `No API key is configured for ${next}. Enter it now to activate this provider.`,
477
- });
478
- if (!key) {
479
- dispatch({
480
- type: "notice",
481
- level: "warn",
482
- text: `provider unchanged · ${next} needs an API key`,
483
- });
484
- return;
485
- }
486
- const trimmedKey = key.trim();
487
- if (!getProvider(next).validateKey(trimmedKey)) {
488
- dispatch({
489
- type: "notice",
490
- level: "warn",
491
- text: `invalid API key format for ${next}`,
492
- });
493
- return;
494
- }
495
- await setProviderSecret(next, trimmedKey);
496
- const saved = await getProviderSecret(next);
497
- if (saved.value !== trimmedKey) {
498
- throw new Error(`could not persist the ${next} API key; provider was not changed`);
499
- }
500
- }
501
- const nextModel = getProviderModel(next);
502
- setDefaultProvider(next);
503
- setProvider(next);
504
- setModel(nextModel);
505
- dispatch({
506
- type: "notice",
507
- level: "info",
508
- text: `provider → ${next} · model → ${nextModel}`,
509
- });
510
- }
511
- catch (error) {
512
- dispatch({
513
- type: "notice",
514
- level: "warn",
515
- text: error instanceof Error ? error.message : String(error),
516
- });
517
- }
518
- finally {
519
- providerActivationRef.current = undefined;
520
- }
521
- }, [requestSecret]);
522
- const chooseProvider = useCallback(() => {
523
- setOverlay({
524
- kind: "picker",
525
- title: "Providers",
526
- searchDescription: false,
527
- options: providerIds.map((value) => ({
528
- value,
529
- label: value,
530
- description: getProviderModel(value),
531
- active: value === provider,
532
- })),
533
- onSelect: (value) => {
534
- const next = assertProvider(value);
535
- void activateProvider(next);
536
- },
537
- });
538
- }, [activateProvider, provider]);
539
- const activateSearchProvider = useCallback(async (next) => {
540
- const adapter = searchProviders[next];
541
- if (adapter?.needsApiKey) {
542
- const current = await getSearchProviderKey(next);
543
- if (!current.value) {
544
- const key = await requestSecret({
545
- title: `${next} search API key`,
546
- prompt: `No API key is configured for ${adapter.displayName}. Enter it now to use this search provider.`,
547
- });
548
- if (!key) {
549
- dispatch({
550
- type: "notice",
551
- level: "warn",
552
- text: `search provider unchanged · ${next} needs an API key`,
553
- });
554
- return;
555
- }
556
- await setSecret("search", next, key);
557
- }
558
- }
559
- setActiveSearchProvider(next);
560
- setOverlay({ kind: "none" });
561
- dispatch({
562
- type: "notice",
563
- level: "info",
564
- text: `search provider → ${next}`,
565
- });
566
- }, [requestSecret]);
567
- const chooseSearchProvider = useCallback(() => {
568
- const active = getConfig().activeSearchProvider;
569
- setOverlay({
570
- kind: "picker",
571
- title: "Search providers",
572
- options: searchProviderIds.map((id) => {
573
- const adapter = searchProviders[id];
574
- return {
575
- value: id,
576
- label: id === active ? `${id} · active` : id,
577
- description: adapter?.needsApiKey
578
- ? `${adapter.displayName} · API key required`
579
- : `${adapter?.displayName ?? id} · keyless`,
580
- };
581
- }),
582
- onSelect: (value) => {
583
- void activateSearchProvider(assertSearchProvider(value));
584
- },
585
- });
586
- }, [activateSearchProvider]);
587
- const setReasoning = useCallback((value) => {
588
- if (value === "off" || value === "none")
589
- setThinking({ enabled: false });
590
- else
591
- setThinking({ enabled: true, effort: value });
592
- dispatch({
593
- type: "notice",
594
- level: "info",
595
- text: value === "off" || value === "none"
596
- ? "thinking → off"
597
- : `thinking → ${value}`,
598
- });
599
- }, []);
600
- const chooseReasoning = useCallback(() => {
601
- const current = getConfig().thinking;
602
- const descriptions = {
603
- off: "disable reasoning",
604
- minimal: "lowest latency",
605
- low: "light reasoning",
606
- medium: "balanced",
607
- high: "deep reasoning",
608
- xhigh: "maximum depth",
609
- };
610
- setOverlay({
611
- kind: "picker",
612
- title: `Reasoning · ${modelSupportsThinking(provider, model) ? "supported" : "model may ignore it"}`,
613
- options: Object.entries(descriptions).map(([value, description]) => ({
614
- value,
615
- label: value,
616
- description,
617
- active: value === (current.enabled ? current.effort : "off"),
618
- })),
619
- onSelect: (value) => {
620
- setReasoning(value);
621
- setOverlay({ kind: "none" });
622
- },
623
- });
624
- }, [model, provider, setReasoning]);
625
- const handleLocalSlash = useCallback((text) => {
626
- const [cmd, ...rest] = text.trim().split(/\s+/);
627
- const arg = rest.join(" ").trim();
628
- const info = (t) => dispatch({ type: "notice", level: "info", text: t });
629
- const warn = (t) => dispatch({ type: "notice", level: "warn", text: t });
630
- switch (cmd) {
631
- case "/ask":
632
- setMode("ask");
633
- setDefaultMode("ask");
634
- info("mode → ask");
635
- return true;
636
- case "/agent":
637
- setMode("agent");
638
- setDefaultMode("agent");
639
- info("mode → agent");
640
- return true;
641
- case "/clear":
642
- cancelCompaction();
643
- runner.reset();
644
- dispatch({ type: "reset" });
645
- info("context cleared");
646
- return true;
647
- case "/new":
648
- cancelCompaction();
649
- runner.abort();
650
- if (autosaveTimer.current) {
651
- clearTimeout(autosaveTimer.current);
652
- autosaveTimer.current = undefined;
653
- }
654
- const messages = runner.getMessages();
655
- if (!noHistory &&
656
- !getConfig().privateMode &&
657
- messages.some((m) => m.role === "user")) {
658
- // Persist in the background; the new session must be interactive
659
- // immediately even if the history backend is slow or locked.
660
- void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
661
- }
662
- runner.reset();
663
- titleRef.current = {
664
- titledAtUserCount: 0,
665
- inFlight: false,
666
- requestId: titleRef.current.requestId + 1,
667
- };
668
- dispatch({ type: "reset" });
669
- info("fresh session started");
670
- return true;
671
- case "/clean":
672
- cancelCompaction();
673
- runner.reset();
674
- titleRef.current = {
675
- titledAtUserCount: 0,
676
- inFlight: false,
677
- requestId: titleRef.current.requestId + 1,
678
- };
679
- dispatch({ type: "reset" });
680
- info("fresh session started");
681
- return true;
682
- case "/think":
683
- case "/thinking":
684
- dispatch({ type: "toggle-thinking" });
685
- return true;
686
- case "/implement":
687
- if (runner.isRunning())
688
- warn("a turn is already running");
689
- else
690
- void runImplement();
691
- return true;
692
- case "/plan":
693
- void (async () => {
694
- const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
695
- if (!plan)
696
- info("no active plan yet");
697
- else
698
- setOverlay({
699
- kind: "pager",
700
- title: "Plan",
701
- body: renderPlanDocument(plan),
702
- });
703
- })();
704
- return true;
705
- case "/jobs":
706
- setOverlay({ kind: "jobs" });
707
- return true;
708
- case "/output": {
709
- const outputs = state.items.filter((item) => item.kind === "tool" && Boolean(item.output));
710
- if (!arg) {
711
- if (outputs.length)
712
- dispatch({ type: "toggle-output" });
713
- else
714
- info("no tool output yet");
715
- return true;
716
- }
717
- if (arg === "list" || arg === "ls") {
718
- info(outputs.length
719
- ? outputs.map((item) => `${item.id} · ${item.name}`).join("\n")
720
- : "no tool output yet");
721
- return true;
722
- }
723
- const selectedOutput = arg && arg !== "last"
724
- ? outputs.find((item) => item.id === arg)
725
- : lastToolOutput();
726
- if (!selectedOutput)
727
- info(arg ? `no tool output: ${arg}` : "no tool output yet");
728
- else
729
- void openToolOutput(selectedOutput);
730
- return true;
731
- }
732
- case "/model":
733
- if (!arg || arg === "list" || arg === "ls") {
734
- void chooseModel();
735
- return true;
736
- }
737
- {
738
- const options = knownModels[provider] ?? [];
739
- const index = Number.parseInt(arg, 10);
740
- const nextModel = Number.isInteger(index) && index >= 1 && index <= options.length
741
- ? options[index - 1]
742
- : arg;
743
- setModel(nextModel);
744
- setProviderModel(provider, nextModel);
745
- // If the model lives under a different provider's list, switch to
746
- // that provider so the request goes to the right endpoint.
747
- const inferred = inferProviderForModel(nextModel);
748
- if (inferred && inferred !== provider) {
749
- void activateProvider(assertProvider(inferred));
750
- }
751
- else {
752
- info(`model → ${nextModel}`);
753
- }
754
- return true;
755
- }
756
- case "/provider":
757
- case "/use":
758
- if (!arg) {
759
- chooseProvider();
760
- return true;
761
- }
762
- try {
763
- const next = assertProvider(arg);
764
- void activateProvider(next);
765
- }
766
- catch {
767
- warn(`unknown provider: ${arg}`);
768
- }
769
- return true;
770
- case "/search":
771
- case "/search-provider":
772
- if (!arg || arg === "list" || arg === "ls") {
773
- chooseSearchProvider();
774
- return true;
775
- }
776
- try {
777
- const next = assertSearchProvider(arg);
778
- void activateSearchProvider(next);
779
- }
780
- catch {
781
- warn(`unknown search provider: ${arg}`);
782
- }
783
- return true;
784
- case "/mouse": {
785
- const normalized = arg.toLowerCase();
786
- if (!normalized) {
787
- info(`mouse=${mouseMode ? "on" : "off"} · on: touchpad scrolls chat; off: native select/copy`);
788
- return true;
789
- }
790
- if (/^(on|true|1|enable)$/i.test(normalized)) {
791
- setMouseMode(true);
792
- info("mouse=on · touchpad scrolls chat; use Option/Shift selection if your terminal requires it");
793
- return true;
794
- }
795
- if (/^(off|false|0|disable)$/i.test(normalized)) {
796
- setMouseMode(false);
797
- info("mouse=off · native select/copy restored; use PageUp/PageDown/Ctrl+U/Ctrl+D or j/k to scroll chat");
798
- return true;
799
- }
800
- warn("usage: /mouse [on|off]");
801
- return true;
802
- }
803
- case "/variants":
804
- case "/reasoning": {
805
- if (!arg) {
806
- chooseReasoning();
807
- return true;
808
- }
809
- const value = arg.toLowerCase();
810
- if (/^(on|enable|true)$/.test(value)) {
811
- setThinking({ enabled: true });
812
- info(`thinking → ${getConfig().thinking.effort}`);
813
- }
814
- else if (["off", "none", "disable", "false"].includes(value)) {
815
- setReasoning("off");
816
- }
817
- else if (["minimal", "low", "medium", "high", "xhigh"].includes(value)) {
818
- setReasoning(value);
819
- }
820
- else
821
- warn("usage: /variants [on|off|minimal|low|medium|high|xhigh]");
822
- return true;
823
- }
824
- case "/cwd": {
825
- if (!arg) {
826
- info(`cwd: ${safeCwd()}`);
827
- return true;
828
- }
829
- const target = resolve(safeCwd(), arg);
830
- if (!existsSync(target)) {
831
- warn(`no such directory: ${target}`);
832
- return true;
833
- }
834
- try {
835
- process.chdir(target);
836
- info(`cwd → ${target}`);
837
- }
838
- catch (e) {
839
- warn(`could not chdir: ${e instanceof Error ? e.message : String(e)}`);
840
- }
841
- return true;
842
- }
843
- case "/allow":
844
- if (!arg || arg === "list" || arg === "ls") {
845
- const list = [...runner.getSession().allow];
846
- info(list.length
847
- ? `allowed: ${list.join(", ")}`
848
- : "no session allowances");
849
- return true;
850
- }
851
- runner.getSession().allow.add(arg);
852
- info(`allowed for session: ${arg}`);
853
- return true;
854
- case "/disallow":
855
- if (arg)
856
- runner.getSession().allow.delete(arg);
857
- info(arg ? `disallowed: ${arg}` : "usage: /disallow <tool>");
858
- return true;
859
- case "/context": {
860
- const msgs = runner.getMessages();
861
- info(`context: ${msgs.length} messages · ~${estimateMessagesTokens(msgs)} tokens`);
862
- return true;
863
- }
864
- case "/compact": {
865
- if (runner.isRunning()) {
866
- warn("wait for the current operation to finish");
867
- return true;
868
- }
869
- cancelCompaction();
870
- const ac = new AbortController();
871
- compactAbortRef.current = ac;
872
- setCompacting(true);
873
- setCompactStartedAt(Date.now());
874
- info("compacting conversation…");
875
- const fullSession = serializeTranscriptForCompaction(state.items);
876
- void runner
877
- .compact(fullSession, 2, ac.signal)
878
- .then((result) => {
879
- if (ac.signal.aborted)
880
- return;
881
- if (!result.summarized || result.after === result.before) {
882
- info("nothing to compact yet — more conversation is needed");
883
- return;
884
- }
885
- const memoMsg = result.messages.find((m) => m.role === "system" &&
886
- m.content.startsWith("Session memory")) ??
887
- result.messages.find((m, i) => i > 0 && m.role === "system");
888
- const summary = memoMsg ? memoMsg.content : "Compacted context";
889
- dispatch({ type: "compacted", summary, keepRecent: 2 });
890
- const freed = Math.max(0, result.beforeTokens - result.afterTokens);
891
- const pct = result.beforeTokens > 0
892
- ? Math.round((freed / result.beforeTokens) * 100)
893
- : 0;
894
- info(`context compacted — earlier turns summarized into a memory · ` +
895
- `freed ~${freed.toLocaleString()} tokens (${pct}% smaller, ` +
896
- `~${result.beforeTokens.toLocaleString()} → ~${result.afterTokens.toLocaleString()} est.)`);
897
- })
898
- .catch((error) => {
899
- if (ac.signal.aborted ||
900
- (error instanceof Error && error.name === "AbortError")) {
901
- info("compaction cancelled");
902
- }
903
- else {
904
- warn(`compaction failed: ${error instanceof Error ? error.message : String(error)}`);
905
- }
906
- })
907
- .finally(() => {
908
- if (compactAbortRef.current === ac) {
909
- compactAbortRef.current = undefined;
910
- setCompacting(false);
911
- setCompactStartedAt(undefined);
912
- }
913
- });
914
- return true;
915
- }
916
- case "/save":
917
- void (async () => {
918
- const msgs = runner.getMessages();
919
- if (msgs.length === 0) {
920
- info("nothing to save yet");
921
- return;
922
- }
923
- // Continuous autosave already persists this conversation under
924
- // its live sessionId — upsert that same row (optionally renaming
925
- // it) instead of minting a separate duplicate entry.
926
- const rec = await upsertSession(runner.getSession().sessionId, msgs, arg || undefined, state.items).catch(() => undefined);
927
- info(rec ? `saved session ${rec.id}` : "save failed");
928
- })();
929
- return true;
930
- case "/history":
931
- void (async () => {
932
- const sessions = await listSessions(50);
933
- const currentMessages = runner.getMessages();
934
- if (sessions.length === 0 && currentMessages.length === 0) {
935
- info("no session history yet — start chatting to create one");
936
- return;
937
- }
938
- setOverlay({
939
- kind: "picker",
940
- title: "Session history",
941
- twoLine: true,
942
- options: [
943
- // Always show where the user currently is, even a brand-new
944
- // /new session with nothing sent yet — otherwise the picker
945
- // can look like it's viewing whichever past session happens
946
- // to sort first, with no indication of "you are here".
947
- {
948
- value: "__current__",
949
- label: "Current session",
950
- description: currentMessages.length
951
- ? `active now · ${currentMessages.length} messages`
952
- : "active now · new, empty session",
953
- active: true,
954
- },
955
- ...sessions.map((session) => {
956
- const date = session.updatedAt ?? session.createdAt;
957
- const when = relativeTime(date);
958
- const stamp = date.slice(0, 16).replace("T", " ");
959
- const count = session.transcript?.length ?? session.messages.length;
960
- const where = shortCwd(session.cwd);
961
- const meta = [
962
- when ? `${when} · ${stamp}` : stamp,
963
- `${count} msgs`,
964
- where,
965
- ]
966
- .filter(Boolean)
967
- .join(" · ");
968
- return {
969
- value: session.id,
970
- label: session.name ?? session.id,
971
- description: meta,
972
- };
973
- }),
974
- ],
975
- onSelect: (id) => {
976
- void (async () => {
977
- if (id === "__current__") {
978
- setOverlay({ kind: "none" });
979
- info("showing current session");
980
- return;
981
- }
982
- const session = await getSession(id);
983
- if (!session) {
984
- warn("session not found");
985
- return;
986
- }
987
- cancelCompaction();
988
- runner.setMessages(session.messages, session.id);
989
- // Keep the resumed session's existing title; only refresh
990
- // once the user adds new turns on top of it.
991
- titleRef.current = {
992
- titledAtUserCount: session.messages.filter((m) => m.role === "user").length,
993
- inFlight: false,
994
- requestId: titleRef.current.requestId + 1,
995
- };
996
- setOverlay({ kind: "none" });
997
- dispatch({
998
- type: "load-history",
999
- messages: session.messages,
1000
- transcript: session.transcript,
1001
- });
1002
- setScroll(0);
1003
- info(`session resumed · ${session.transcript?.length ?? session.messages.length} items`);
1004
- })();
1005
- },
1006
- });
1007
- })();
1008
- return true;
1009
- case "/reset":
1010
- void clearAllHistory().then((result) => info(`history cleared · ${result.detail || "ok"}`));
1011
- return true;
1012
- case "/discard":
1013
- void (async () => {
1014
- const session = runner.getSession();
1015
- const plan = await loadPlan(session.sessionId).catch(() => undefined);
1016
- if (!plan) {
1017
- info("no active plan to discard");
1018
- return;
1019
- }
1020
- await deletePlan(session.sessionId);
1021
- session.planApproved.value = false;
1022
- info(`plan discarded · ${plan.goal}`);
1023
- })();
1024
- return true;
1025
- case "/scope":
1026
- void (async () => {
1027
- const [sub = "show", ...parts] = arg.split(/\s+/).filter(Boolean);
1028
- if (["clear", "reset", "off"].includes(sub)) {
1029
- await clearScope();
1030
- info("engagement scope cleared");
1031
- return;
1032
- }
1033
- if (sub === "show" || sub === "list" || sub === "ls") {
1034
- const scope = await loadScope();
1035
- info(scope
1036
- ? `scope: ${scope.name ?? "unnamed"} · ${scope.authorizedTargets.join(", ")}`
1037
- : "no engagement scope configured");
1038
- return;
1039
- }
1040
- if (sub === "add") {
1041
- const targets = parts
1042
- .join(" ")
1043
- .split(/[\s,]+/)
1044
- .filter(Boolean);
1045
- if (!targets.length) {
1046
- warn("usage: /scope add <target1,target2>");
1047
- return;
1048
- }
1049
- const scope = await addScopeTargets(targets);
1050
- info(`scope updated · ${scope.authorizedTargets.join(", ")}`);
1051
- return;
1052
- }
1053
- if (sub === "new" || sub === "set") {
1054
- const targets = parts
1055
- .join(" ")
1056
- .split(/[\s,]+/)
1057
- .filter(Boolean);
1058
- if (!targets.length) {
1059
- warn("usage: /scope new <target1,target2>");
1060
- return;
1061
- }
1062
- await saveScope({
1063
- authorizedTargets: targets,
1064
- createdAt: new Date().toISOString(),
1065
- });
1066
- info(`scope created · ${targets.join(", ")}`);
1067
- return;
1068
- }
1069
- warn("usage: /scope [show|clear|new <targets>|add <targets>]");
1070
- })().catch((error) => warn(error instanceof Error ? error.message : String(error)));
1071
- return true;
1072
- case "/privacy":
1073
- void (async () => {
1074
- const sub = (arg || "status").toLowerCase();
1075
- if (["on", "enable"].includes(sub)) {
1076
- updateConfig({ privateMode: true });
1077
- info("private mode → on");
1078
- return;
1079
- }
1080
- if (["off", "disable"].includes(sub)) {
1081
- updateConfig({ privateMode: false });
1082
- info("private mode → off");
1083
- return;
1084
- }
1085
- if (sub === "status") {
1086
- info(`private mode: ${getConfig().privateMode ? "on" : "off"}`);
1087
- return;
1088
- }
1089
- if (sub === "clear-history") {
1090
- const result = await clearAllHistory();
1091
- info(`history cleared · ${result.detail || "ok"}`);
1092
- return;
1093
- }
1094
- if (sub === "clear-logs") {
1095
- const result = await clearAuditLogs();
1096
- info(`audit logs cleared · ${result.removed} files`);
1097
- return;
1098
- }
1099
- if (sub === "clear-artifacts") {
1100
- const result = await clearArtifacts();
1101
- info(`artifacts cleared · ${result.removed} files`);
1102
- return;
1103
- }
1104
- if (sub === "clear-all") {
1105
- const [historyResult, logResult, artifactResult] = await Promise.all([
1106
- clearAllHistory(),
1107
- clearAuditLogs(),
1108
- clearArtifacts(),
1109
- ]);
1110
- info(`cleared history (${historyResult.detail || "ok"}), logs (${logResult.removed}), artifacts (${artifactResult.removed})`);
1111
- return;
1112
- }
1113
- warn("usage: /privacy [status|on|off|clear-history|clear-logs|clear-artifacts|clear-all]");
1114
- })();
1115
- return true;
1116
- case "/freeonly": {
1117
- const on = /^(on|true|1|enable)$/i.test(arg), off = /^(off|false|0|disable)$/i.test(arg);
1118
- if (!on && !off) {
1119
- info(`freeOnly=${getConfig().freeOnly}`);
1120
- return true;
1121
- }
1122
- updateConfig({ freeOnly: on });
1123
- info(`freeOnly=${on}`);
1124
- return true;
1125
- }
1126
- case "/fallback": {
1127
- const on = /^(on|true|1|enable)$/i.test(arg), off = /^(off|false|0|disable)$/i.test(arg);
1128
- if (!on && !off) {
1129
- info(`providerFallback=${getConfig().providerFallback}`);
1130
- return true;
1131
- }
1132
- updateConfig({ providerFallback: on });
1133
- info(`providerFallback=${on}`);
1134
- return true;
1135
- }
1136
- case "/info":
1137
- void (async () => {
1138
- const providerVal = arg.trim().toLowerCase();
1139
- let targetProvider = provider;
1140
- if (providerVal) {
1141
- try {
1142
- targetProvider = assertProvider(providerVal);
1143
- }
1144
- catch {
1145
- warn(`unknown provider: ${providerVal}`);
1146
- return;
1147
- }
1148
- }
1149
- const infoText = getProviderInfoText(targetProvider);
1150
- setOverlay({
1151
- kind: "pager",
1152
- title: `${targetProvider} Info`,
1153
- body: infoText,
1154
- });
1155
- })();
1156
- return true;
1157
- case "/keys":
1158
- void (async () => {
1159
- const llm = await listProviderStatuses(provider);
1160
- const activeSearch = getConfig().activeSearchProvider;
1161
- const search = await Promise.all(searchProviderIds.map(async (id) => {
1162
- const secret = await getSearchProviderKey(id);
1163
- const keyless = id === "duckduckgo";
1164
- return {
1165
- provider: id,
1166
- active: id === activeSearch,
1167
- configured: keyless || Boolean(secret.value),
1168
- source: keyless ? "keyless" : secret.source,
1169
- maskedKey: secret.value
1170
- ? maskSecret(secret.value)
1171
- : undefined,
1172
- };
1173
- }));
1174
- setOverlay({
1175
- kind: "pager",
1176
- title: "Credential status",
1177
- body: formatKeyStatus(llm, search),
1178
- });
1179
- })().catch((error) => warn(`could not read keys: ${error instanceof Error ? error.message : String(error)}`));
1180
- return true;
1181
- case "/set": {
1182
- const parts = arg.split(/\s+/).filter(Boolean);
1183
- const providerVal = parts[0];
1184
- const keyVal = parts[1];
1185
- void (async () => {
1186
- if (!providerVal) {
1187
- const llm = await listProviderStatuses(provider);
1188
- const activeSearch = getConfig().activeSearchProvider;
1189
- const search = await Promise.all(searchProviderIds.map(async (id) => {
1190
- const secret = await getSearchProviderKey(id);
1191
- const keyless = id === "duckduckgo";
1192
- return {
1193
- provider: id,
1194
- configured: keyless || Boolean(secret.value),
1195
- maskedKey: secret.value
1196
- ? maskSecret(secret.value)
1197
- : undefined,
1198
- };
1199
- }));
1200
- setOverlay({
1201
- kind: "picker",
1202
- title: "Set API key for provider",
1203
- options: [
1204
- ...llm.map((status) => ({
1205
- value: `llm:${status.provider}`,
1206
- label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
1207
- description: status.model,
1208
- })),
1209
- ...search.map((status) => ({
1210
- value: `search:${status.provider}`,
1211
- label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}`,
1212
- description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
1213
- })),
1214
- ],
1215
- onSelect: (val) => {
1216
- setOverlay({ kind: "none" });
1217
- void (async () => {
1218
- const isSearch = val.startsWith("search:");
1219
- const id = val.split(":")[1];
1220
- if (isSearch) {
1221
- const next = id;
1222
- if (next === "duckduckgo") {
1223
- info("duckduckgo is keyless and requires no setup");
1224
- return;
1225
- }
1226
- const secret = await getSearchProviderKey(next);
1227
- if (secret.value) {
1228
- const reset = await new Promise((resolveConfirm) => {
1229
- confirmResolver.current = resolveConfirm;
1230
- dispatch({
1231
- type: "event",
1232
- event: {
1233
- type: "confirm-request",
1234
- id: "c",
1235
- kind: "reset",
1236
- prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
1237
- },
1238
- });
1239
- });
1240
- if (!reset) {
1241
- info("cancelled");
1242
- return;
1243
- }
1244
- }
1245
- const key = await requestSecret({
1246
- title: `${next} API key`,
1247
- prompt: `Enter API key for ${next}:`,
1248
- });
1249
- if (!key) {
1250
- info("cancelled");
1251
- return;
1252
- }
1253
- await setSecret("search", next, key.trim());
1254
- info(`saved ${next} ${maskSecret(key.trim())}`);
1255
- }
1256
- else {
1257
- const next = id;
1258
- if (next === "ollama") {
1259
- const key = await requestSecret({
1260
- title: "Ollama host URL",
1261
- prompt: `Enter host URL for Ollama:`,
1262
- });
1263
- if (!key) {
1264
- info("cancelled");
1265
- return;
1266
- }
1267
- updateConfig({ ollamaHost: key.trim() });
1268
- info(`saved ollama host → ${key.trim()}`);
1269
- return;
1270
- }
1271
- const secret = await getProviderSecret(next);
1272
- if (secret.value) {
1273
- const reset = await new Promise((resolveConfirm) => {
1274
- confirmResolver.current = resolveConfirm;
1275
- dispatch({
1276
- type: "event",
1277
- event: {
1278
- type: "confirm-request",
1279
- id: "c",
1280
- kind: "reset",
1281
- prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
1282
- },
1283
- });
1284
- });
1285
- if (!reset) {
1286
- info("cancelled");
1287
- return;
1288
- }
1289
- }
1290
- const key = await requestSecret({
1291
- title: `${next} API key`,
1292
- prompt: `Enter API key for ${next}:`,
1293
- });
1294
- if (!key) {
1295
- info("cancelled");
1296
- return;
1297
- }
1298
- if (!getProvider(next).validateKey(key.trim())) {
1299
- warn(`invalid API key format for ${next}`);
1300
- return;
1301
- }
1302
- await setProviderSecret(next, key.trim());
1303
- info(`saved ${next} ${maskSecret(key.trim())}`);
1304
- }
1305
- })();
1306
- },
1307
- });
1308
- }
1309
- else {
1310
- try {
1311
- const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
1312
- if (isSearch) {
1313
- const next = providerVal;
1314
- if (next === "duckduckgo") {
1315
- info("duckduckgo is keyless and requires no setup");
1316
- return;
1317
- }
1318
- let key = keyVal;
1319
- if (!key) {
1320
- const secret = await getSearchProviderKey(next);
1321
- if (secret.value) {
1322
- const reset = await new Promise((resolveConfirm) => {
1323
- confirmResolver.current = resolveConfirm;
1324
- dispatch({
1325
- type: "event",
1326
- event: {
1327
- type: "confirm-request",
1328
- id: "c",
1329
- kind: "reset",
1330
- prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
1331
- },
1332
- });
1333
- });
1334
- if (!reset) {
1335
- info("cancelled");
1336
- return;
1337
- }
1338
- }
1339
- key = await requestSecret({
1340
- title: `${next} API key`,
1341
- prompt: `Enter API key for ${next}:`,
1342
- });
1343
- if (!key) {
1344
- info("cancelled");
1345
- return;
1346
- }
1347
- }
1348
- await setSecret("search", next, key.trim());
1349
- info(`saved ${next} ${maskSecret(key.trim())}`);
1350
- }
1351
- else {
1352
- const next = assertProvider(providerVal);
1353
- if (next === "ollama") {
1354
- let key = keyVal;
1355
- if (!key) {
1356
- key = await requestSecret({
1357
- title: "Ollama host URL",
1358
- prompt: `Enter host URL for Ollama:`,
1359
- });
1360
- if (!key) {
1361
- info("cancelled");
1362
- return;
1363
- }
1364
- }
1365
- updateConfig({ ollamaHost: key.trim() });
1366
- info(`saved ollama host → ${key.trim()}`);
1367
- return;
1368
- }
1369
- let key = keyVal;
1370
- if (!key) {
1371
- const secret = await getProviderSecret(next);
1372
- if (secret.value) {
1373
- const reset = await new Promise((resolveConfirm) => {
1374
- confirmResolver.current = resolveConfirm;
1375
- dispatch({
1376
- type: "event",
1377
- event: {
1378
- type: "confirm-request",
1379
- id: "c",
1380
- kind: "reset",
1381
- prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
1382
- },
1383
- });
1384
- });
1385
- if (!reset) {
1386
- info("cancelled");
1387
- return;
1388
- }
1389
- }
1390
- key = await requestSecret({
1391
- title: `${next} API key`,
1392
- prompt: `Enter API key for ${next}:`,
1393
- });
1394
- if (!key) {
1395
- info("cancelled");
1396
- return;
1397
- }
1398
- }
1399
- if (!getProvider(next).validateKey(key.trim())) {
1400
- warn(`invalid API key format for ${next}`);
1401
- return;
1402
- }
1403
- await setProviderSecret(next, key.trim());
1404
- info(`saved ${next} ${maskSecret(key.trim())}`);
1405
- }
1406
- }
1407
- catch (e) {
1408
- warn(e.message);
1409
- }
1410
- }
1411
- })();
1412
- return true;
1413
- }
1414
- case "/unset": {
1415
- const parts = arg.split(/\s+/).filter(Boolean);
1416
- const providerVal = parts[0];
1417
- void (async () => {
1418
- if (!providerVal) {
1419
- const llm = await listProviderStatuses(provider);
1420
- const activeSearch = getConfig().activeSearchProvider;
1421
- const search = await Promise.all(searchProviderIds.map(async (id) => {
1422
- const secret = await getSearchProviderKey(id);
1423
- const keyless = id === "duckduckgo";
1424
- return {
1425
- provider: id,
1426
- configured: keyless || Boolean(secret.value),
1427
- maskedKey: secret.value
1428
- ? maskSecret(secret.value)
1429
- : undefined,
1430
- };
1431
- }));
1432
- setOverlay({
1433
- kind: "picker",
1434
- title: "Unset API key for provider",
1435
- options: [
1436
- ...llm.map((status) => ({
1437
- value: `llm:${status.provider}`,
1438
- label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
1439
- description: status.model,
1440
- })),
1441
- ...search.map((status) => ({
1442
- value: `search:${status.provider}`,
1443
- label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "keyless") : chalk.red("✗ no key")}`,
1444
- description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
1445
- })),
1446
- ],
1447
- onSelect: (val) => {
1448
- setOverlay({ kind: "none" });
1449
- void (async () => {
1450
- const isSearch = val.startsWith("search:");
1451
- const id = val.split(":")[1];
1452
- if (isSearch) {
1453
- const next = id;
1454
- if (next === "duckduckgo") {
1455
- info("duckduckgo requires no credentials and cannot be unset");
1456
- return;
1457
- }
1458
- const secret = await getSearchProviderKey(next);
1459
- if (!secret.value) {
1460
- warn(`${next} has no key to unset`);
1461
- return;
1462
- }
1463
- const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
1464
- await unsetSearchProviderKey(next);
1465
- info(`unset ${next}`);
1466
- }
1467
- else {
1468
- const next = id;
1469
- if (next === "ollama") {
1470
- info("ollama does not store an API key");
1471
- return;
1472
- }
1473
- const secret = await getProviderSecret(next);
1474
- if (!secret.value) {
1475
- warn(`${next} has no key to unset`);
1476
- return;
1477
- }
1478
- await unsetProviderSecret(next);
1479
- info(`unset ${next}`);
1480
- }
1481
- })();
1482
- },
1483
- });
1484
- }
1485
- else {
1486
- try {
1487
- const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
1488
- if (isSearch) {
1489
- const next = providerVal;
1490
- if (next === "duckduckgo") {
1491
- info("duckduckgo requires no credentials and cannot be unset");
1492
- return;
1493
- }
1494
- const secret = await getSearchProviderKey(next);
1495
- if (!secret.value) {
1496
- warn(`${next} has no key to unset`);
1497
- return;
1498
- }
1499
- const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
1500
- await unsetSearchProviderKey(next);
1501
- info(`unset ${next}`);
1502
- }
1503
- else {
1504
- const next = assertProvider(providerVal);
1505
- if (next === "ollama") {
1506
- info("ollama does not store an API key");
1507
- return;
1508
- }
1509
- const secret = await getProviderSecret(next);
1510
- if (!secret.value) {
1511
- warn(`${next} has no key to unset`);
1512
- return;
1513
- }
1514
- await unsetProviderSecret(next);
1515
- info(`unset ${next}`);
1516
- }
1517
- }
1518
- catch (e) {
1519
- warn(e.message);
1520
- }
1521
- }
1522
- })();
1523
- return true;
1524
- }
1525
- case "/permissions": {
1526
- const current = getConfig().permissions ?? "default";
1527
- if (!arg) {
1528
- setOverlay({
1529
- kind: "picker",
1530
- title: "Permissions mode",
1531
- options: [
1532
- {
1533
- value: "default",
1534
- label: "default",
1535
- description: "normal behavior (prompts for confirmation on mutating actions)",
1536
- active: current === "default",
1537
- },
1538
- {
1539
- value: "allow-all",
1540
- label: "allow-all",
1541
- description: "run everything without confirmation (except passwords)",
1542
- active: current === "allow-all",
1543
- },
1544
- ],
1545
- onSelect: (val) => {
1546
- updateConfig({ permissions: val });
1547
- info(`permissions → ${val}`);
1548
- setOverlay({ kind: "none" });
1549
- },
1550
- });
1551
- return true;
1552
- }
1553
- const value = arg.toLowerCase().trim();
1554
- if (value === "default" || value === "normal") {
1555
- updateConfig({ permissions: "default" });
1556
- info("permissions → default");
1557
- }
1558
- else if (["allow-all", "allowall", "all"].includes(value)) {
1559
- updateConfig({ permissions: "allow-all" });
1560
- info("permissions → allow-all");
1561
- }
1562
- else {
1563
- warn("usage: /permissions [default|allow-all]");
1564
- }
1565
- return true;
1566
- }
1567
- case "/update":
1568
- info(`${cmd} manages updates; use the equivalent \`clai update\` command outside the TUI`);
1569
- return true;
1570
- case "/help":
1571
- setOverlay({
1572
- kind: "picker",
1573
- title: "Command reference",
1574
- options: slashCommands.map((item) => ({
1575
- value: item.command,
1576
- label: `${item.command}${item.usage ? ` ${item.usage}` : ""}`,
1577
- description: item.description,
1578
- })),
1579
- onSelect: (value) => {
1580
- const next = `${value} `;
1581
- setInput(next);
1582
- setCursor(next.length);
1583
- setSelected(0);
1584
- setOverlay({ kind: "none" });
1585
- },
1586
- });
1587
- return true;
1588
- case "/exit":
1589
- case "/quit":
1590
- exitTui();
1591
- return true;
1592
- default:
1593
- return false;
1594
- }
1595
- }, [
1596
- activateProvider,
1597
- activateSearchProvider,
1598
- chooseModel,
1599
- chooseProvider,
1600
- chooseReasoning,
1601
- chooseSearchProvider,
1602
- compacting,
1603
- exitTui,
1604
- mouseMode,
1605
- noHistory,
1606
- provider,
1607
- runner,
1608
- runImplement,
1609
- lastToolOutput,
1610
- openToolOutput,
1611
- setReasoning,
1612
- state.items,
1613
- requestSecret,
1614
- setOverlay,
1615
- ]);
1616
- const submitText = useCallback((text) => {
1617
- const trimmed = text.trim();
1618
- if (!trimmed)
1619
- return;
1620
- historyIdx.current = -1;
1621
- historyDraft.current = "";
1622
- setInput("");
1623
- setCursor(0);
1624
- setSelected(0);
1625
- // A macOS/Linux drag-and-drop commonly starts with an absolute path.
1626
- // Only route a leading slash through the command handler when its first
1627
- // token is an actual clai command; file paths remain normal prompts.
1628
- if (trimmed.startsWith("/") && isKnownSlashCommand(trimmed)) {
1629
- if (!handleLocalSlash(trimmed))
1630
- dispatch({
1631
- type: "notice",
1632
- level: "warn",
1633
- text: `unknown command: ${trimmed}`,
1634
- });
1635
- return;
1636
- }
1637
- if (shouldStoreInPromptHistory(trimmed))
1638
- history.current.push(trimmed);
1639
- if (runner.isRunning())
1640
- dispatch({ type: "queue", text: trimmed });
1641
- else
1642
- beginTurn(trimmed);
1643
- }, [handleLocalSlash, runner, beginTurn]);
1644
- const answerConfirm = useCallback((ok) => {
1645
- confirmResolver.current?.(ok);
1646
- confirmResolver.current = undefined;
1647
- dispatch({ type: "confirm-resolved" });
1648
- }, []);
1649
- const answerSecret = useCallback((value) => {
1650
- secretResolver.current?.(value);
1651
- secretResolver.current = undefined;
1652
- setSecretRequest(undefined);
1653
- }, []);
1654
- // Layout math (keep the composer pinned to the bottom)
1655
- const suggestions = input.startsWith("/")
1656
- ? getSlashCommandSuggestions(input)
1657
- : [];
1658
- const mention = getMentionQuery(input, cursor);
1659
- const fileSuggestions = mention
1660
- ? findFileSuggestions(mention.query, safeCwd(), MAX_FILE_SUGGESTIONS)
1661
- : [];
1662
- const slashMenuOpen = suggestions.length > 0;
1663
- const fileMenuOpen = !slashMenuOpen && Boolean(mention) && fileSuggestions.length > 0;
1664
- const menuOpen = slashMenuOpen || fileMenuOpen;
1665
- const overlayOpen = overlay.kind !== "none";
1666
- const modalActive = Boolean(state.pendingConfirm) || Boolean(secretRequest) || overlayOpen;
1667
- // Leave the terminal's final row unused. Painting through the last cell can
1668
- // trigger an implicit scroll in several terminals, which looks like a full
1669
- // screen flash on every keypress/spinner frame.
1670
- const usableRows = Math.max(8, rows - 1);
1671
- const headerH = 0;
1672
- const inputWidth = Math.max(10, cols - 10);
1673
- let statusH = 0;
1674
- if (secretRequest) {
1675
- const wrappedPromptLines = wrapPlainString(secretRequest.prompt, cols - 6);
1676
- statusH = 5 + wrappedPromptLines.length; // 1 (header) + 1 (password) + 1 (help) + 2 (borders) = 5, plus prompt lines
1677
- }
1678
- else if (state.pendingConfirm) {
1679
- const wrappedPromptLines = wrapPlainString(state.pendingConfirm.prompt, cols - 6);
1680
- statusH = 4 + wrappedPromptLines.length; // 1 (header) + 1 (help) + 2 (borders) = 4, plus prompt lines
1681
- }
1682
- const chromeH = !secretRequest && !state.pendingConfirm ? 1 : 0;
1683
- const gapH = 1;
1684
- const wrappedInputLines = wrapPlainString(input, inputWidth);
1685
- const maxAvailableForComposer = Math.max(3, usableRows - headerH - statusH - chromeH - gapH - 8);
1686
- const maxComposerTextRows = Math.max(1, maxAvailableForComposer - 2);
1687
- const composerTextH = Math.min(wrappedInputLines.length, maxComposerTextRows);
1688
- const composerH = composerTextH + 2;
1689
- const maxMenuRows = Math.max(3, usableRows - headerH - statusH - composerH - chromeH - gapH - 3);
1690
- const menuH = slashMenuOpen
1691
- ? Math.min(suggestions.length, maxMenuRows)
1692
- : fileMenuOpen
1693
- ? fileSuggestions.length
1694
- : 0;
1695
- const viewportH = Math.max(3, usableRows - headerH - statusH - composerH - menuH - chromeH - gapH);
1696
- const slashWindowStart = slashMenuOpen
1697
- ? Math.min(Math.max(0, selected - Math.floor(menuH / 2)), Math.max(0, suggestions.length - menuH))
1698
- : 0;
1699
- const visibleSlashSuggestions = suggestions.slice(slashWindowStart, slashWindowStart + menuH);
1700
- const livePlan = useMemo(() => {
1701
- for (let i = state.items.length - 1; i >= 0; i -= 1) {
1702
- const item = state.items[i];
1703
- if (item?.kind === "plan")
1704
- return item.plan;
1705
- }
1706
- return undefined;
1707
- }, [state.items]);
1708
- // Two columns only when both remain genuinely readable. Below this content-
1709
- // based breakpoint the plan stays available inline and through Ctrl+P.
1710
- const showPlanSidebar = Boolean(planPaneVisible && livePlan && cols >= 100 && overlay.kind === "none");
1711
- const planSidebarWidth = showPlanSidebar
1712
- ? Math.min(48, Math.max(32, Math.floor((cols - 7) * 0.32)))
1713
- : 0;
1714
- const transcriptWidth = showPlanSidebar
1715
- ? cols - 6 - planSidebarWidth - 1
1716
- : cols - 6;
1717
- const transcriptLines = renderTranscriptLines(state, {
1718
- width: transcriptWidth,
1719
- thinkingExpanded: state.thinkingExpanded,
1720
- outputExpanded: state.outputExpanded,
1721
- running: state.status.running,
1722
- version,
1723
- mode,
1724
- provider,
1725
- model,
1726
- permissions: getConfig().permissions ?? "default",
1727
- hidePlanItems: Boolean(livePlan),
1728
- });
1729
- const total = transcriptLines.length;
1730
- const maxOffset = Math.max(0, total - viewportH);
1731
- const offset = Math.min(scroll, maxOffset);
1732
- const end = total - offset;
1733
- const start = Math.max(0, end - viewportH);
1734
- let visible = transcriptLines.slice(start, end);
1735
- if (visible.length < viewportH) {
1736
- visible = [...visible, ...Array(viewportH - visible.length).fill("")];
1737
- }
1738
- // Sidebar rendered to strings (not an Ink flex column) so each viewport row
1739
- // can be composed as ONE Text node — see renderPlanSidebarLines for why.
1740
- const planSidebarLines = showPlanSidebar && livePlan
1741
- ? renderPlanSidebarLines(livePlan, planSidebarWidth, viewportH)
1742
- : [];
1743
- // SGR mouse reporting keeps wheel/trackpad events distinct from cursor-key
1744
- // sequences, so scrolling the transcript never walks prompt history.
1745
- // Mouse wheel scroll stays enabled while a confirm box is showing (so the
1746
- // user can review context before answering); it's disabled for overlays/
1747
- // secret prompts, which own their own content and scrolling.
1748
- useEffect(() => {
1749
- const confirmOnlyModal = Boolean(state.pendingConfirm) && !secretRequest && !overlayOpen;
1750
- if (!stdin || (modalActive && !confirmOnlyModal))
1751
- return;
1752
- const onData = (chunk) => {
1753
- const data = String(chunk);
1754
- if (data === "\x08" || data === "\b") {
1755
- isCtrlHPressed.current = true;
1756
- setPlanPaneVisible((visible) => !visible);
1757
- return;
1758
- }
1759
- const direction = mouseWheelDirection(data);
1760
- if (direction < 0) {
1761
- setScroll((value) => Math.min(maxOffset, value + 3));
1762
- return;
1763
- }
1764
- if (direction > 0) {
1765
- setScroll((value) => Math.max(0, value - 3));
1766
- return;
1767
- }
1768
- // Handle drag to scroll when mouse mode is active
1769
- if (mouseMode) {
1770
- const match = data.match(/\x1b\[<(\d+);(\d+);(\d+)([mM])/);
1771
- if (match) {
1772
- const button = parseInt(match[1], 10);
1773
- const y = parseInt(match[3], 10);
1774
- const action = match[4];
1775
- // Drag event (button 32, pressed/moving action M)
1776
- if (button === 32 && action === "M") {
1777
- const now = Date.now();
1778
- if (now - lastDragTime.current > 100) {
1779
- if (y <= 4) {
1780
- setScroll((value) => Math.min(maxOffset, value + 1));
1781
- lastDragTime.current = now;
1782
- }
1783
- else if (y >= viewportH) {
1784
- setScroll((value) => Math.max(0, value - 1));
1785
- lastDragTime.current = now;
1786
- }
1787
- }
1788
- }
1789
- }
1790
- }
1791
- };
1792
- stdin.prependListener("data", onData);
1793
- return () => {
1794
- stdin.off("data", onData);
1795
- };
1796
- }, [stdin, modalActive, maxOffset, mouseMode, viewportH, state.pendingConfirm, secretRequest, overlayOpen]);
1797
- // Key handling
1798
- const onInputRef = useRef(undefined);
1799
- onInputRef.current = (ch, key) => {
1800
- if (key.backspace && isCtrlHPressed.current) {
1801
- isCtrlHPressed.current = false;
1802
- return;
1803
- }
1804
- // While a confirmation box (sudo/mutating command/plan implement/etc.) is
1805
- // showing, ConfirmModal owns y/n/Esc — but the main transcript behind it
1806
- // should still be scrollable so the user can review context before
1807
- // deciding. Only scroll keys pass through; everything else (including
1808
- // plain text) stays blocked so it can't leak into the confirm prompt.
1809
- // Overlays (pager/picker/jobs) and the secret prompt are excluded here:
1810
- // they render their own content and already handle their own scrolling.
1811
- if (modalActive) {
1812
- if (state.pendingConfirm && !secretRequest && !overlayOpen) {
1813
- if (key.pageUp) {
1814
- setScroll((s) => Math.min(maxOffset, s + viewportH));
1815
- return;
1816
- }
1817
- if (key.pageDown) {
1818
- setScroll((s) => Math.max(0, s - viewportH));
1819
- return;
1820
- }
1821
- if (key.ctrl && ch === "u") {
1822
- setScroll((s) => Math.min(maxOffset, s + Math.floor(viewportH / 2)));
1823
- return;
1824
- }
1825
- if (key.ctrl && ch === "d") {
1826
- setScroll((s) => Math.max(0, s - Math.floor(viewportH / 2)));
1827
- return;
1828
- }
1829
- if (ch === "j" || (key.downArrow && (key.shift || key.ctrl || key.meta))) {
1830
- setScroll((s) => Math.max(0, s - 1));
1831
- return;
1832
- }
1833
- if (ch === "k" || (key.upArrow && (key.shift || key.ctrl || key.meta))) {
1834
- setScroll((s) => Math.min(maxOffset, s + 1));
1835
- return;
1836
- }
1837
- }
1838
- return; // everything else: overlay/modal owns input
1839
- }
1840
- const cleanedChunk = stripMouseReports(ch)
1841
- .replace(/\r\n/g, "\n")
1842
- .replace(/\r/g, "\n");
1843
- if (isMouseReport(ch) && cleanedChunk.length === 0)
1844
- return;
1845
- // Global shortcuts
1846
- if (key.ctrl && ch === "t") {
1847
- dispatch({ type: "toggle-thinking" });
1848
- return;
1849
- }
1850
- if (key.ctrl && ch === "o") {
1851
- const hasOutput = state.items.some((item) => (item.kind === "tool" && Boolean(item.output)) ||
1852
- item.kind === "compacted");
1853
- if (hasOutput)
1854
- dispatch({ type: "toggle-output" });
1855
- else
1856
- dispatch({
1857
- type: "notice",
1858
- level: "info",
1859
- text: "no tool output or compacted context yet",
1860
- });
1861
- return;
1862
- }
1863
- // Ctrl+H is ASCII backspace (0x08). Depending on the terminal/Ink
1864
- // version it arrives either as ctrl+h or as a backspace key event.
1865
- // A normal Backspace is usually DEL (0x7f), so do not consume that.
1866
- const isPlanToggle = (key.ctrl && (ch.toLowerCase() === "h" || ch.toLowerCase() === "b" || key.backspace)) ||
1867
- ch === "\b" ||
1868
- key.sequence === "\x08" ||
1869
- key.sequence === "\b";
1870
- if (isPlanToggle) {
1871
- setPlanPaneVisible((visible) => !visible);
1872
- return;
1873
- }
1874
- if (key.ctrl && ch === "p") {
1875
- void (async () => {
1876
- const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
1877
- if (plan)
1878
- setOverlay({
1879
- kind: "pager",
1880
- title: "Plan",
1881
- body: renderPlanDocument(plan),
1882
- });
1883
- else
1884
- dispatch({
1885
- type: "notice",
1886
- level: "info",
1887
- text: "no active plan yet",
1888
- });
1889
- })();
1890
- return;
1891
- }
1892
- if (key.ctrl && ch === "j") {
1893
- setOverlay({ kind: "jobs" });
1894
- return;
1895
- }
1896
- // Scrolling
1897
- if (key.pageUp) {
1898
- setScroll((s) => Math.min(maxOffset, s + viewportH));
1899
- return;
1900
- }
1901
- if (key.pageDown) {
1902
- setScroll((s) => Math.max(0, s - viewportH));
1903
- return;
1904
- }
1905
- if (key.ctrl && ch === "u") {
1906
- setScroll((s) => Math.min(maxOffset, s + Math.floor(viewportH / 2)));
1907
- return;
1908
- }
1909
- if (key.ctrl && ch === "d") {
1910
- setScroll((s) => Math.max(0, s - Math.floor(viewportH / 2)));
1911
- return;
1912
- }
1913
- if (key.upArrow && (key.shift || key.ctrl || key.meta)) {
1914
- setScroll((s) => Math.min(maxOffset, s + 1));
1915
- return;
1916
- }
1917
- if (key.downArrow && (key.shift || key.ctrl || key.meta)) {
1918
- setScroll((s) => Math.max(0, s - 1));
1919
- return;
1920
- }
1921
- if (!input && maxOffset > 0 && ch === "k") {
1922
- setScroll((s) => Math.min(maxOffset, s + 1));
1923
- return;
1924
- }
1925
- if (!input && maxOffset > 0 && ch === "j") {
1926
- setScroll((s) => Math.max(0, s - 1));
1927
- return;
1928
- }
1929
- if (key.escape) {
1930
- if (compacting) {
1931
- cancelCompaction();
1932
- return;
1933
- }
1934
- if (state.outputExpanded)
1935
- dispatch({ type: "toggle-output" });
1936
- else if (runner.isRunning())
1937
- runner.abort();
1938
- else if (input) {
1939
- setInput("");
1940
- setCursor(0);
1941
- setSelected(0);
1942
- }
1943
- return;
1944
- }
1945
- if (key.ctrl && ch === "c") {
1946
- const now = Date.now();
1947
- if (now - lastCtrlC.current < 1500) {
1948
- exitTui();
1949
- return;
1950
- }
1951
- lastCtrlC.current = now;
1952
- if (runner.isRunning())
1953
- runner.abort();
1954
- return;
1955
- }
1956
- // Slash menu navigation
1957
- if (menuOpen && (key.upArrow || key.downArrow)) {
1958
- setSelected((s) => {
1959
- const n = slashMenuOpen ? suggestions.length : fileSuggestions.length;
1960
- return key.upArrow ? (s - 1 + n) % n : (s + 1) % n;
1961
- });
1962
- return;
1963
- }
1964
- if (menuOpen && key.tab) {
1965
- if (slashMenuOpen) {
1966
- setInput(suggestions[selected].command + " ");
1967
- setCursor(suggestions[selected].command.length + 1);
1968
- }
1969
- else if (mention && fileSuggestions[selected]) {
1970
- const suggestion = fileSuggestions[selected];
1971
- const inserted = `@${suggestion.value}${suggestion.isDir ? "" : " "}`;
1972
- const next = input.slice(0, mention.start) + inserted + input.slice(cursor);
1973
- setInput(next);
1974
- setCursor(mention.start + inserted.length);
1975
- }
1976
- setSelected(0);
1977
- return;
1978
- }
1979
- // Insert a newline instead of submitting. Works on every OS/terminal:
1980
- // Ctrl+J (universal line feed) and Alt/Option+Enter, plus Shift/Meta+Enter
1981
- // where the terminal reports the modifier. Plain Enter still submits below.
1982
- if (isComposerNewline(ch, key)) {
1983
- const next = input.slice(0, cursor) + "\n" + input.slice(cursor);
1984
- setInput(next);
1985
- setCursor(cursor + 1);
1986
- setSelected(0);
1987
- historyIdx.current = -1;
1988
- historyDraft.current = "";
1989
- return;
1990
- }
1991
- if (key.return) {
1992
- if (slashMenuOpen) {
1993
- submitText(suggestions[selected].command);
1994
- return;
1995
- }
1996
- if (fileMenuOpen && mention && fileSuggestions[selected]) {
1997
- const suggestion = fileSuggestions[selected];
1998
- const inserted = `@${suggestion.value}${suggestion.isDir ? "" : " "}`;
1999
- const next = input.slice(0, mention.start) + inserted + input.slice(cursor);
2000
- setInput(next);
2001
- setCursor(mention.start + inserted.length);
2002
- setSelected(0);
2003
- return;
2004
- }
2005
- submitText(input);
2006
- return;
2007
- }
2008
- // History (when menu closed)
2009
- if (!menuOpen && key.upArrow) {
2010
- if (history.current.length === 0)
2011
- return;
2012
- if (historyIdx.current < 0)
2013
- historyDraft.current = input;
2014
- const idx = historyIdx.current < 0
2015
- ? history.current.length - 1
2016
- : Math.max(0, historyIdx.current - 1);
2017
- historyIdx.current = idx;
2018
- const v = history.current[idx] ?? "";
2019
- setInput(v);
2020
- setCursor(v.length);
2021
- return;
2022
- }
2023
- if (!menuOpen && key.downArrow) {
2024
- if (historyIdx.current < 0)
2025
- return;
2026
- const idx = historyIdx.current + 1;
2027
- if (idx >= history.current.length) {
2028
- historyIdx.current = -1;
2029
- const draft = historyDraft.current;
2030
- setInput(draft);
2031
- setCursor(draft.length);
2032
- return;
2033
- }
2034
- historyIdx.current = idx;
2035
- const v = history.current[idx] ?? "";
2036
- setInput(v);
2037
- setCursor(v.length);
2038
- return;
2039
- }
2040
- if (key.leftArrow) {
2041
- setCursor((c) => Math.max(0, c - 1));
2042
- return;
2043
- }
2044
- if (key.rightArrow) {
2045
- setCursor((c) => Math.min(input.length, c + 1));
2046
- return;
2047
- }
2048
- if (key.backspace && (key.ctrl || key.meta)) {
2049
- setInput("");
2050
- setCursor(0);
2051
- setSelected(0);
2052
- historyIdx.current = -1;
2053
- historyDraft.current = "";
2054
- return;
2055
- }
2056
- if (key.backspace || key.delete) {
2057
- if (cursor === 0)
2058
- return;
2059
- setInput(input.slice(0, cursor - 1) + input.slice(cursor));
2060
- setCursor(cursor - 1);
2061
- setSelected(0);
2062
- historyIdx.current = -1;
2063
- historyDraft.current = "";
2064
- return;
2065
- }
2066
- if (key.ctrl || key.meta)
2067
- return;
2068
- if (cleanedChunk) {
2069
- setInput(input.slice(0, cursor) + cleanedChunk + input.slice(cursor));
2070
- setCursor(cursor + cleanedChunk.length);
2071
- setSelected(0);
2072
- historyIdx.current = -1;
2073
- historyDraft.current = "";
2074
- }
2075
- };
2076
- const handleInputStable = useCallback((ch, key) => {
2077
- onInputRef.current?.(ch, key);
2078
- }, []);
2079
- useInput(handleInputStable);
2080
- const closeOverlay = useCallback(() => setOverlay({ kind: "none" }), []);
2081
- // Render
2082
- const timerStart = compacting ? compactStartedAt : state.status.startedAt;
2083
- const elapsed = timerStart
2084
- ? Math.max(0, Math.floor((Date.now() - timerStart) / 1000))
2085
- : 0;
2086
- const before = input.slice(0, cursor);
2087
- const at = input.slice(cursor, cursor + 1) || " ";
2088
- const after = input.slice(cursor + 1);
2089
- const footerInnerWidth = Math.max(20, cols - 6); // outer paddingX(2) + footer paddingX(1)
2090
- const runningBadgeWidth = compacting
2091
- ? " COMPACTING ".length
2092
- : " RUNNING ".length;
2093
- const cancelBadgeWidth = " ESC CANCEL ".length;
2094
- const queuedBadgeWidth = state.queued.length > 0 ? ` ${state.queued.length} QUEUED `.length + 1 : 0;
2095
- const scrollBadgeWidth = (maxOffset > offset ? ` ▲ ${maxOffset - offset} `.length + 1 : 0) +
2096
- (offset > 0 ? ` ▼ ${offset} `.length + 1 : 0);
2097
- const spinnerWidth = 3; // spinner plus following space
2098
- const fixedRunningFooterWidth = runningBadgeWidth +
2099
- 1 +
2100
- spinnerWidth +
2101
- 1 +
2102
- cancelBadgeWidth +
2103
- queuedBadgeWidth +
2104
- scrollBadgeWidth;
2105
- const runningActivityWidth = Math.max(12, footerInnerWidth - fixedRunningFooterWidth);
2106
- const rawRunningActivity = `${compacting ? "compacting conversation" : state.status.activity || "working"}${state.status.step > 0 ? ` (step ${state.status.step})` : ""} · ${elapsed}s`;
2107
- const runningActivity = truncateMiddle(rawRunningActivity.replace(/\s+/g, " ").trim(), runningActivityWidth);
2108
- return (_jsxs(Box, { flexDirection: "column", width: cols, height: usableRows, paddingX: 2, children: [overlay.kind === "pager" ? (_jsx(Pager, { title: overlay.title, body: overlay.body, height: viewportH, onClose: closeOverlay })) : overlay.kind === "jobs" ? (_jsx(JobsPanel, { jobs: jobs, onClose: closeOverlay })) : overlay.kind === "picker" ? (_jsx(PickerPanel, { title: overlay.title, options: overlay.options, searchDescription: overlay.searchDescription, twoLine: overlay.twoLine, height: viewportH, onSelect: overlay.onSelect, onClose: closeOverlay })) : showPlanSidebar && livePlan ? (_jsx(Box, { flexDirection: "column", height: viewportH, width: cols - 6, overflow: "hidden", children: visible.map((line, i) => {
2109
- const cleanLine = line.replace(/\r?\n/g, "");
2110
- const left = padVisible(truncateAnsi(cleanLine, transcriptWidth), transcriptWidth);
2111
- const right = planSidebarLines[i] ?? "";
2112
- return (_jsx(Text, { wrap: "truncate-end", children: `${left} ${right}` }, i));
2113
- }) })) : (_jsx(Box, { flexDirection: "row", height: viewportH, width: cols - 6, overflow: "hidden", children: _jsx(Box, { flexDirection: "column", width: transcriptWidth, height: viewportH, flexShrink: 0, overflow: "hidden", children: visible.map((line, i) => {
2114
- const truncated = truncateAnsi(line, transcriptWidth);
2115
- return (_jsx(Text, { wrap: "wrap", children: truncated === "" ? " " : truncated }, i));
2116
- }) }) })), _jsx(Box, { height: gapH }), slashMenuOpen && !modalActive
2117
- ? visibleSlashSuggestions.map((cmd, i) => {
2118
- const absoluteIndex = slashWindowStart + i;
2119
- return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: absoluteIndex === selected
2120
- ? "#2563EB"
2121
- : absoluteIndex % 2 === 0
2122
- ? "#1E293B"
2123
- : "#0F172A", children: [_jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#67E8F9", bold: true, children: [absoluteIndex === selected ? " ❯ " : " ", cmd.command.padEnd(14)] }), cmd.usage ? (_jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#CBD5E1", children: [cmd.usage, " "] })) : null, _jsxs(Text, { color: "#F8FAFC", children: [" ", cmd.description] }), i === visibleSlashSuggestions.length - 1 &&
2124
- slashWindowStart + menuH < suggestions.length ? (_jsx(Text, { dimColor: true, children: ` · ${suggestions.length - slashWindowStart - menuH} more ↓` })) : null] }, cmd.command));
2125
- })
2126
- : null, fileMenuOpen && !modalActive
2127
- ? fileSuggestions.map((file, i) => (_jsxs(Text, { wrap: "truncate-end", backgroundColor: i === selected ? "#2563EB" : i % 2 === 0 ? "#1E293B" : "#0F172A", children: [_jsxs(Text, { color: i === selected
2128
- ? "#FFFFFF"
2129
- : file.isDir
2130
- ? "#67E8F9"
2131
- : "#F8FAFC", bold: i === selected, children: [i === selected ? "❯ " : " ", file.isDir ? "▸ " : "· ", file.value] }), _jsx(Text, { dimColor: true, children: file.isDir ? " directory" : " attach file" })] }, file.value)))
2132
- : null, secretRequest ? (_jsx(SecretInputPanel, { title: secretRequest.title, prompt: secretRequest.prompt, onSubmit: (value) => answerSecret(value), onCancel: () => answerSecret(undefined) })) : state.pendingConfirm ? (_jsx(ConfirmModal, { confirm: state.pendingConfirm, onAnswer: answerConfirm, overlayOpen: overlayOpen, onViewPlan: state.pendingConfirm.kind === "plan"
2133
- ? () => {
2134
- void (async () => {
2135
- const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
2136
- if (plan)
2137
- setOverlay({
2138
- kind: "pager",
2139
- title: "Plan",
2140
- body: renderPlanDocument(plan),
2141
- });
2142
- })();
2143
- }
2144
- : undefined })) : null, _jsxs(Box, { borderStyle: "round", borderColor: state.pendingConfirm || secretRequest
2145
- ? "yellow"
2146
- : state.status.running
2147
- ? "yellow"
2148
- : "cyan", paddingX: 1, children: [_jsx(Text, { color: state.status.running ? "yellow" : "cyan", bold: true, children: state.pendingConfirm || secretRequest ? "! " : "❯ " }), secretRequest ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 complete the secure input above" })) : state.pendingConfirm ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 answer the confirmation above with Y or N" })) : input.length === 0 ? (_jsx(Text, { dimColor: true, children: state.status.running
2149
- ? "type to queue a message…"
2150
- : `ask anything · / for commands · @file to attach · ${newlineHint()} · esc to cancel` })) : ((() => {
2151
- let cursorLineIdx = 0;
2152
- for (let i = 0; i < wrappedInputLines.length; i++) {
2153
- const line = wrappedInputLines[i];
2154
- if (cursor >= line.startIdx && cursor <= line.endIdx) {
2155
- cursorLineIdx = i;
2156
- if (cursor === line.startIdx) {
2157
- break;
2158
- }
2159
- }
2160
- }
2161
- let startLine = Math.max(0, cursorLineIdx - Math.floor(composerTextH / 2));
2162
- let endLine = Math.min(wrappedInputLines.length - 1, startLine + composerTextH - 1);
2163
- startLine = Math.max(0, endLine - composerTextH + 1);
2164
- const startCharIdx = wrappedInputLines[startLine].startIdx;
2165
- const endCharIdx = wrappedInputLines[endLine].endIdx;
2166
- const slicedInput = input.slice(startCharIdx, endCharIdx);
2167
- const slicedCursor = cursor - startCharIdx;
2168
- const beforeSliced = slicedInput.slice(0, slicedCursor);
2169
- const atSliced = slicedInput.slice(slicedCursor, slicedCursor + 1) || " ";
2170
- const afterSliced = slicedInput.slice(slicedCursor + 1);
2171
- return (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "#F8FAFC", children: beforeSliced }), _jsx(Text, { backgroundColor: "#22D3EE", color: "#020617", bold: true, children: atSliced }), _jsx(Text, { color: "#F8FAFC", children: afterSliced })] }));
2172
- })())] }), !secretRequest && !state.pendingConfirm ? (_jsxs(Box, { paddingX: 1, justifyContent: "center", children: [state.status.running || compacting ? (_jsxs(_Fragment, { children: [_jsxs(Text, { backgroundColor: "#B45309", color: "#FFFFFF", bold: true, children: [" ", compacting ? "COMPACTING" : "RUNNING", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "magenta", children: [spinner, " "] }), _jsx(Box, { width: runningActivityWidth, flexShrink: 1, children: _jsx(Text, { color: "yellow", wrap: "truncate-end", children: runningActivity }) }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "ESC CANCEL", " "] }), state.queued.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${state.queued.length} QUEUED ` })] })) : null] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { backgroundColor: "#166534", color: "#FFFFFF", bold: true, children: [" ", "READY", " "] }), state.queued.length > 0 ? (_jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${state.queued.length} QUEUED ` })) : null, _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "/ COMMANDS", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "CTRL+T THINKING", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "CTRL+O OUTPUT", " "] }), livePlan ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: planPaneVisible ? "#7E22CE" : "#334155", color: "#F8FAFC", children: ` CTRL+H PLAN ${planPaneVisible ? "ON" : "OFF"} ` })] })) : null, _jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: mouseMode ? "#155E75" : "#334155", color: "#F8FAFC", children: ` MOUSE ${mouseMode ? "ON" : "OFF"} ` })] })), maxOffset > offset ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ▲ ${maxOffset - offset} ` })] })) : null, offset > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ▼ ${offset} ` })] })) : null] })) : null] }));
2173
- }
2174
- //# sourceMappingURL=App.js.map