@calliopelabs/cli 0.8.19 → 2.0.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 (402) hide show
  1. package/dist/agents/agent-config-loader.d.ts +60 -0
  2. package/dist/agents/agent-config-loader.d.ts.map +1 -0
  3. package/dist/agents/agent-config-loader.js +402 -0
  4. package/dist/agents/agent-config-loader.js.map +1 -0
  5. package/dist/agents/agent-config-presets.d.ts +10 -0
  6. package/dist/agents/agent-config-presets.d.ts.map +1 -0
  7. package/dist/agents/agent-config-presets.js +940 -0
  8. package/dist/agents/agent-config-presets.js.map +1 -0
  9. package/dist/agents/agent-config-types.d.ts +145 -0
  10. package/dist/agents/agent-config-types.d.ts.map +1 -0
  11. package/dist/agents/agent-config-types.js +12 -0
  12. package/dist/agents/agent-config-types.js.map +1 -0
  13. package/dist/{agterm → agents}/agent-detection.d.ts +1 -1
  14. package/dist/{agterm → agents}/agent-detection.d.ts.map +1 -1
  15. package/dist/{agterm → agents}/agent-detection.js +21 -5
  16. package/dist/agents/agent-detection.js.map +1 -0
  17. package/dist/agents/aggregator.d.ts +19 -0
  18. package/dist/agents/aggregator.d.ts.map +1 -0
  19. package/dist/agents/aggregator.js +141 -0
  20. package/dist/agents/aggregator.js.map +1 -0
  21. package/dist/{agterm → agents}/cli-backend.d.ts +1 -1
  22. package/dist/{agterm → agents}/cli-backend.d.ts.map +1 -1
  23. package/dist/{agterm → agents}/cli-backend.js +90 -12
  24. package/dist/agents/cli-backend.js.map +1 -0
  25. package/dist/agents/council-types.d.ts +113 -0
  26. package/dist/agents/council-types.d.ts.map +1 -0
  27. package/dist/agents/council-types.js +81 -0
  28. package/dist/agents/council-types.js.map +1 -0
  29. package/dist/agents/council.d.ts +107 -0
  30. package/dist/agents/council.d.ts.map +1 -0
  31. package/dist/agents/council.js +586 -0
  32. package/dist/agents/council.js.map +1 -0
  33. package/dist/agents/decomposer.d.ts +33 -0
  34. package/dist/agents/decomposer.d.ts.map +1 -0
  35. package/dist/agents/decomposer.js +138 -0
  36. package/dist/agents/decomposer.js.map +1 -0
  37. package/dist/agents/dynamic-tools.d.ts +52 -0
  38. package/dist/agents/dynamic-tools.d.ts.map +1 -0
  39. package/dist/agents/dynamic-tools.js +395 -0
  40. package/dist/agents/dynamic-tools.js.map +1 -0
  41. package/dist/agents/index.d.ts +29 -0
  42. package/dist/agents/index.d.ts.map +1 -0
  43. package/dist/agents/index.js +29 -0
  44. package/dist/agents/index.js.map +1 -0
  45. package/dist/agents/installer.d.ts +39 -0
  46. package/dist/agents/installer.d.ts.map +1 -0
  47. package/dist/agents/installer.js +205 -0
  48. package/dist/agents/installer.js.map +1 -0
  49. package/dist/{agterm → agents}/orchestrator.d.ts +7 -2
  50. package/dist/agents/orchestrator.d.ts.map +1 -0
  51. package/dist/{agterm → agents}/orchestrator.js +22 -2
  52. package/dist/agents/orchestrator.js.map +1 -0
  53. package/dist/agents/sdk-backend.d.ts +63 -0
  54. package/dist/agents/sdk-backend.d.ts.map +1 -0
  55. package/dist/agents/sdk-backend.js +489 -0
  56. package/dist/agents/sdk-backend.js.map +1 -0
  57. package/dist/agents/swarm-types.d.ts +83 -0
  58. package/dist/agents/swarm-types.d.ts.map +1 -0
  59. package/dist/agents/swarm-types.js +20 -0
  60. package/dist/agents/swarm-types.js.map +1 -0
  61. package/dist/agents/swarm.d.ts +74 -0
  62. package/dist/agents/swarm.d.ts.map +1 -0
  63. package/dist/agents/swarm.js +307 -0
  64. package/dist/agents/swarm.js.map +1 -0
  65. package/dist/{agterm → agents}/tools.d.ts +7 -5
  66. package/dist/agents/tools.d.ts.map +1 -0
  67. package/dist/agents/tools.js +776 -0
  68. package/dist/agents/tools.js.map +1 -0
  69. package/dist/{agterm → agents}/types.d.ts +14 -2
  70. package/dist/agents/types.d.ts.map +1 -0
  71. package/dist/{agterm → agents}/types.js +2 -2
  72. package/dist/agents/types.js.map +1 -0
  73. package/dist/api-server.d.ts +26 -0
  74. package/dist/api-server.d.ts.map +1 -0
  75. package/dist/api-server.js +230 -0
  76. package/dist/api-server.js.map +1 -0
  77. package/dist/auto-checkpoint.d.ts +35 -0
  78. package/dist/auto-checkpoint.d.ts.map +1 -0
  79. package/dist/auto-checkpoint.js +143 -0
  80. package/dist/auto-checkpoint.js.map +1 -0
  81. package/dist/auto-compressor.d.ts +44 -0
  82. package/dist/auto-compressor.d.ts.map +1 -0
  83. package/dist/auto-compressor.js +145 -0
  84. package/dist/auto-compressor.js.map +1 -0
  85. package/dist/background-jobs.d.ts +45 -0
  86. package/dist/background-jobs.d.ts.map +1 -0
  87. package/dist/background-jobs.js +122 -0
  88. package/dist/background-jobs.js.map +1 -0
  89. package/dist/bin.d.ts +6 -2
  90. package/dist/bin.d.ts.map +1 -1
  91. package/dist/bin.js +120 -24
  92. package/dist/bin.js.map +1 -1
  93. package/dist/checkpoint.d.ts +49 -0
  94. package/dist/checkpoint.d.ts.map +1 -0
  95. package/dist/checkpoint.js +219 -0
  96. package/dist/checkpoint.js.map +1 -0
  97. package/dist/circuit-breaker/breaker.d.ts +80 -0
  98. package/dist/circuit-breaker/breaker.d.ts.map +1 -0
  99. package/dist/circuit-breaker/breaker.js +408 -0
  100. package/dist/circuit-breaker/breaker.js.map +1 -0
  101. package/dist/circuit-breaker/defaults.d.ts +8 -0
  102. package/dist/circuit-breaker/defaults.d.ts.map +1 -0
  103. package/dist/circuit-breaker/defaults.js +35 -0
  104. package/dist/circuit-breaker/defaults.js.map +1 -0
  105. package/dist/circuit-breaker/index.d.ts +9 -0
  106. package/dist/circuit-breaker/index.d.ts.map +1 -0
  107. package/dist/circuit-breaker/index.js +8 -0
  108. package/dist/circuit-breaker/index.js.map +1 -0
  109. package/dist/circuit-breaker/types.d.ts +77 -0
  110. package/dist/circuit-breaker/types.d.ts.map +1 -0
  111. package/dist/circuit-breaker/types.js +8 -0
  112. package/dist/circuit-breaker/types.js.map +1 -0
  113. package/dist/circuit-breaker.d.ts +8 -0
  114. package/dist/circuit-breaker.d.ts.map +1 -0
  115. package/dist/circuit-breaker.js +7 -0
  116. package/dist/circuit-breaker.js.map +1 -0
  117. package/dist/cli/agent.d.ts +9 -0
  118. package/dist/cli/agent.d.ts.map +1 -0
  119. package/dist/cli/agent.js +262 -0
  120. package/dist/cli/agent.js.map +1 -0
  121. package/dist/cli/commands.d.ts +12 -0
  122. package/dist/cli/commands.d.ts.map +1 -0
  123. package/dist/{cli.js → cli/commands.js} +285 -422
  124. package/dist/cli/commands.js.map +1 -0
  125. package/dist/cli/index.d.ts +8 -0
  126. package/dist/cli/index.d.ts.map +1 -0
  127. package/dist/cli/index.js +222 -0
  128. package/dist/cli/index.js.map +1 -0
  129. package/dist/cli/types.d.ts +30 -0
  130. package/dist/cli/types.d.ts.map +1 -0
  131. package/dist/cli/types.js +20 -0
  132. package/dist/cli/types.js.map +1 -0
  133. package/dist/companions.d.ts +54 -0
  134. package/dist/companions.d.ts.map +1 -0
  135. package/dist/companions.js +440 -0
  136. package/dist/companions.js.map +1 -0
  137. package/dist/config.d.ts +23 -1
  138. package/dist/config.d.ts.map +1 -1
  139. package/dist/config.js +95 -22
  140. package/dist/config.js.map +1 -1
  141. package/dist/diff.d.ts +27 -0
  142. package/dist/diff.d.ts.map +1 -1
  143. package/dist/diff.js +415 -10
  144. package/dist/diff.js.map +1 -1
  145. package/dist/errors.d.ts.map +1 -1
  146. package/dist/errors.js +20 -11
  147. package/dist/errors.js.map +1 -1
  148. package/dist/git-status.d.ts +23 -0
  149. package/dist/git-status.d.ts.map +1 -0
  150. package/dist/git-status.js +92 -0
  151. package/dist/git-status.js.map +1 -0
  152. package/dist/headless.d.ts +25 -0
  153. package/dist/headless.d.ts.map +1 -0
  154. package/dist/headless.js +182 -0
  155. package/dist/headless.js.map +1 -0
  156. package/dist/hud/api.d.ts +35 -0
  157. package/dist/hud/api.d.ts.map +1 -0
  158. package/dist/hud/api.js +448 -0
  159. package/dist/hud/api.js.map +1 -0
  160. package/dist/hud/palettes.d.ts +9 -0
  161. package/dist/hud/palettes.d.ts.map +1 -0
  162. package/dist/hud/palettes.js +280 -0
  163. package/dist/hud/palettes.js.map +1 -0
  164. package/dist/hud/skins.d.ts +12 -0
  165. package/dist/hud/skins.d.ts.map +1 -0
  166. package/dist/hud/skins.js +365 -0
  167. package/dist/hud/skins.js.map +1 -0
  168. package/dist/hud/theme-packs/api.d.ts +51 -0
  169. package/dist/hud/theme-packs/api.d.ts.map +1 -0
  170. package/dist/hud/theme-packs/api.js +145 -0
  171. package/dist/hud/theme-packs/api.js.map +1 -0
  172. package/dist/hud/theme-packs/index.d.ts +18 -0
  173. package/dist/hud/theme-packs/index.d.ts.map +1 -0
  174. package/dist/hud/theme-packs/index.js +38 -0
  175. package/dist/hud/theme-packs/index.js.map +1 -0
  176. package/dist/hud/theme-packs/types.d.ts +29 -0
  177. package/dist/hud/theme-packs/types.d.ts.map +1 -0
  178. package/dist/hud/theme-packs/types.js +9 -0
  179. package/dist/hud/theme-packs/types.js.map +1 -0
  180. package/dist/hud/types.d.ts +182 -0
  181. package/dist/hud/types.d.ts.map +1 -0
  182. package/dist/hud/types.js +7 -0
  183. package/dist/hud/types.js.map +1 -0
  184. package/dist/idle-eviction.d.ts +34 -0
  185. package/dist/idle-eviction.d.ts.map +1 -0
  186. package/dist/idle-eviction.js +78 -0
  187. package/dist/idle-eviction.js.map +1 -0
  188. package/dist/index.d.ts +9 -3
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +9 -3
  191. package/dist/index.js.map +1 -1
  192. package/dist/iteration-ledger.d.ts +105 -0
  193. package/dist/iteration-ledger.d.ts.map +1 -0
  194. package/dist/iteration-ledger.js +237 -0
  195. package/dist/iteration-ledger.js.map +1 -0
  196. package/dist/markdown.d.ts.map +1 -1
  197. package/dist/markdown.js +1 -27
  198. package/dist/markdown.js.map +1 -1
  199. package/dist/mcp.d.ts +35 -0
  200. package/dist/mcp.d.ts.map +1 -1
  201. package/dist/mcp.js +291 -7
  202. package/dist/mcp.js.map +1 -1
  203. package/dist/memory.d.ts.map +1 -1
  204. package/dist/memory.js +12 -2
  205. package/dist/memory.js.map +1 -1
  206. package/dist/model-detection.d.ts +5 -0
  207. package/dist/model-detection.d.ts.map +1 -1
  208. package/dist/model-detection.js +278 -10
  209. package/dist/model-detection.js.map +1 -1
  210. package/dist/model-router.d.ts.map +1 -1
  211. package/dist/model-router.js +33 -11
  212. package/dist/model-router.js.map +1 -1
  213. package/dist/plugins.d.ts +8 -0
  214. package/dist/plugins.d.ts.map +1 -1
  215. package/dist/plugins.js +97 -6
  216. package/dist/plugins.js.map +1 -1
  217. package/dist/providers/anthropic.d.ts +10 -0
  218. package/dist/providers/anthropic.d.ts.map +1 -0
  219. package/dist/providers/anthropic.js +221 -0
  220. package/dist/providers/anthropic.js.map +1 -0
  221. package/dist/providers/bedrock.d.ts +17 -0
  222. package/dist/providers/bedrock.d.ts.map +1 -0
  223. package/dist/providers/bedrock.js +574 -0
  224. package/dist/providers/bedrock.js.map +1 -0
  225. package/dist/providers/compat.d.ts +13 -0
  226. package/dist/providers/compat.d.ts.map +1 -0
  227. package/dist/providers/compat.js +202 -0
  228. package/dist/providers/compat.js.map +1 -0
  229. package/dist/providers/google.d.ts +10 -0
  230. package/dist/providers/google.d.ts.map +1 -0
  231. package/dist/providers/google.js +203 -0
  232. package/dist/providers/google.js.map +1 -0
  233. package/dist/providers/index.d.ts +23 -0
  234. package/dist/providers/index.d.ts.map +1 -0
  235. package/dist/providers/index.js +145 -0
  236. package/dist/providers/index.js.map +1 -0
  237. package/dist/providers/ollama.d.ts +17 -0
  238. package/dist/providers/ollama.d.ts.map +1 -0
  239. package/dist/providers/ollama.js +289 -0
  240. package/dist/providers/ollama.js.map +1 -0
  241. package/dist/providers/openai.d.ts +121 -0
  242. package/dist/providers/openai.d.ts.map +1 -0
  243. package/dist/providers/openai.js +485 -0
  244. package/dist/providers/openai.js.map +1 -0
  245. package/dist/providers/types.d.ts +63 -0
  246. package/dist/providers/types.d.ts.map +1 -0
  247. package/dist/providers/types.js +164 -0
  248. package/dist/providers/types.js.map +1 -0
  249. package/dist/sandbox-native.d.ts +59 -0
  250. package/dist/sandbox-native.d.ts.map +1 -0
  251. package/dist/sandbox-native.js +292 -0
  252. package/dist/sandbox-native.js.map +1 -0
  253. package/dist/sandbox.d.ts +2 -2
  254. package/dist/sandbox.d.ts.map +1 -1
  255. package/dist/sandbox.js +59 -13
  256. package/dist/sandbox.js.map +1 -1
  257. package/dist/scope.d.ts +3 -1
  258. package/dist/scope.d.ts.map +1 -1
  259. package/dist/scope.js +13 -1
  260. package/dist/scope.js.map +1 -1
  261. package/dist/session-timeout.d.ts +31 -0
  262. package/dist/session-timeout.d.ts.map +1 -0
  263. package/dist/session-timeout.js +100 -0
  264. package/dist/session-timeout.js.map +1 -0
  265. package/dist/setup.d.ts.map +1 -1
  266. package/dist/setup.js +29 -17
  267. package/dist/setup.js.map +1 -1
  268. package/dist/smart-router.d.ts +73 -0
  269. package/dist/smart-router.d.ts.map +1 -0
  270. package/dist/smart-router.js +332 -0
  271. package/dist/smart-router.js.map +1 -0
  272. package/dist/storage.d.ts +19 -0
  273. package/dist/storage.d.ts.map +1 -1
  274. package/dist/storage.js +164 -1
  275. package/dist/storage.js.map +1 -1
  276. package/dist/streaming.d.ts +4 -0
  277. package/dist/streaming.d.ts.map +1 -1
  278. package/dist/streaming.js +12 -0
  279. package/dist/streaming.js.map +1 -1
  280. package/dist/styles.d.ts +32 -0
  281. package/dist/styles.d.ts.map +1 -1
  282. package/dist/styles.js +91 -0
  283. package/dist/styles.js.map +1 -1
  284. package/dist/summarization.d.ts +1 -1
  285. package/dist/summarization.js +4 -4
  286. package/dist/summarization.js.map +1 -1
  287. package/dist/terminal-image.d.ts +115 -0
  288. package/dist/terminal-image.d.ts.map +1 -0
  289. package/dist/terminal-image.js +766 -0
  290. package/dist/terminal-image.js.map +1 -0
  291. package/dist/terminal-recording.d.ts +55 -0
  292. package/dist/terminal-recording.d.ts.map +1 -0
  293. package/dist/terminal-recording.js +182 -0
  294. package/dist/terminal-recording.js.map +1 -0
  295. package/dist/themes.d.ts +19 -35
  296. package/dist/themes.d.ts.map +1 -1
  297. package/dist/themes.js +101 -210
  298. package/dist/themes.js.map +1 -1
  299. package/dist/tmux.d.ts +35 -0
  300. package/dist/tmux.d.ts.map +1 -0
  301. package/dist/tmux.js +106 -0
  302. package/dist/tmux.js.map +1 -0
  303. package/dist/tools.d.ts +3 -3
  304. package/dist/tools.d.ts.map +1 -1
  305. package/dist/tools.js +587 -45
  306. package/dist/tools.js.map +1 -1
  307. package/dist/trust.d.ts +53 -0
  308. package/dist/trust.d.ts.map +1 -0
  309. package/dist/trust.js +154 -0
  310. package/dist/trust.js.map +1 -0
  311. package/dist/types.d.ts +7 -3
  312. package/dist/types.d.ts.map +1 -1
  313. package/dist/types.js +70 -32
  314. package/dist/types.js.map +1 -1
  315. package/dist/ui/agent.d.ts +61 -0
  316. package/dist/ui/agent.d.ts.map +1 -0
  317. package/dist/ui/agent.js +768 -0
  318. package/dist/ui/agent.js.map +1 -0
  319. package/dist/ui/chat-input.d.ts +32 -0
  320. package/dist/ui/chat-input.d.ts.map +1 -0
  321. package/dist/ui/chat-input.js +355 -0
  322. package/dist/ui/chat-input.js.map +1 -0
  323. package/dist/ui/commands.d.ts +92 -0
  324. package/dist/ui/commands.d.ts.map +1 -0
  325. package/dist/ui/commands.js +3006 -0
  326. package/dist/ui/commands.js.map +1 -0
  327. package/dist/ui/completions.d.ts +22 -0
  328. package/dist/ui/completions.d.ts.map +1 -0
  329. package/dist/ui/completions.js +215 -0
  330. package/dist/ui/completions.js.map +1 -0
  331. package/dist/ui/components.d.ts +38 -0
  332. package/dist/ui/components.d.ts.map +1 -0
  333. package/dist/ui/components.js +422 -0
  334. package/dist/ui/components.js.map +1 -0
  335. package/dist/ui/context.d.ts +12 -0
  336. package/dist/ui/context.d.ts.map +1 -0
  337. package/dist/ui/context.js +102 -0
  338. package/dist/ui/context.js.map +1 -0
  339. package/dist/ui/error-boundary.d.ts +33 -0
  340. package/dist/ui/error-boundary.d.ts.map +1 -0
  341. package/dist/ui/error-boundary.js +94 -0
  342. package/dist/ui/error-boundary.js.map +1 -0
  343. package/dist/ui/frame.d.ts +13 -0
  344. package/dist/ui/frame.d.ts.map +1 -0
  345. package/dist/ui/frame.js +89 -0
  346. package/dist/ui/frame.js.map +1 -0
  347. package/dist/ui/index.d.ts +12 -0
  348. package/dist/ui/index.d.ts.map +1 -0
  349. package/dist/ui/index.js +928 -0
  350. package/dist/ui/index.js.map +1 -0
  351. package/dist/ui/messages.d.ts +19 -0
  352. package/dist/ui/messages.d.ts.map +1 -0
  353. package/dist/ui/messages.js +181 -0
  354. package/dist/ui/messages.js.map +1 -0
  355. package/dist/ui/modals.d.ts +52 -0
  356. package/dist/ui/modals.d.ts.map +1 -0
  357. package/dist/ui/modals.js +204 -0
  358. package/dist/ui/modals.js.map +1 -0
  359. package/dist/ui/pack-picker.d.ts +12 -0
  360. package/dist/ui/pack-picker.d.ts.map +1 -0
  361. package/dist/ui/pack-picker.js +101 -0
  362. package/dist/ui/pack-picker.js.map +1 -0
  363. package/dist/ui/status-bar.d.ts +20 -0
  364. package/dist/ui/status-bar.d.ts.map +1 -0
  365. package/dist/ui/status-bar.js +41 -0
  366. package/dist/ui/status-bar.js.map +1 -0
  367. package/dist/ui/theme-picker.d.ts +24 -0
  368. package/dist/ui/theme-picker.d.ts.map +1 -0
  369. package/dist/ui/theme-picker.js +190 -0
  370. package/dist/ui/theme-picker.js.map +1 -0
  371. package/dist/ui/types.d.ts +62 -0
  372. package/dist/ui/types.d.ts.map +1 -0
  373. package/dist/ui/types.js +7 -0
  374. package/dist/ui/types.js.map +1 -0
  375. package/dist/version-check.d.ts.map +1 -1
  376. package/dist/version-check.js +1 -9
  377. package/dist/version-check.js.map +1 -1
  378. package/package.json +7 -2
  379. package/dist/agterm/agent-detection.js.map +0 -1
  380. package/dist/agterm/cli-backend.js.map +0 -1
  381. package/dist/agterm/index.d.ts +0 -12
  382. package/dist/agterm/index.d.ts.map +0 -1
  383. package/dist/agterm/index.js +0 -15
  384. package/dist/agterm/index.js.map +0 -1
  385. package/dist/agterm/orchestrator.d.ts.map +0 -1
  386. package/dist/agterm/orchestrator.js.map +0 -1
  387. package/dist/agterm/tools.d.ts.map +0 -1
  388. package/dist/agterm/tools.js +0 -278
  389. package/dist/agterm/tools.js.map +0 -1
  390. package/dist/agterm/types.d.ts.map +0 -1
  391. package/dist/agterm/types.js.map +0 -1
  392. package/dist/cli.d.ts +0 -14
  393. package/dist/cli.d.ts.map +0 -1
  394. package/dist/cli.js.map +0 -1
  395. package/dist/providers.d.ts +0 -51
  396. package/dist/providers.d.ts.map +0 -1
  397. package/dist/providers.js +0 -1146
  398. package/dist/providers.js.map +0 -1
  399. package/dist/ui-cli.d.ts +0 -17
  400. package/dist/ui-cli.d.ts.map +0 -1
  401. package/dist/ui-cli.js +0 -3728
  402. package/dist/ui-cli.js.map +0 -1
package/dist/ui-cli.js DELETED
@@ -1,3728 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- /**
3
- * Calliope CLI - Ink UI
4
- *
5
- * Component hierarchy inspired by Claude Code:
6
- * App
7
- * └── TerminalChat (main hub)
8
- * ├── MessageHistory (Static for messages)
9
- * │ └── MessageItem (formatted messages)
10
- * ├── ProcessingIndicator (animated spinner)
11
- * ├── ChatInput (input line)
12
- * └── StatusBar (footer)
13
- */
14
- import React, { useState, useCallback, useRef, useEffect } from 'react';
15
- import { render, Box, Text, useInput, useApp, useStdout, Static } from 'ink';
16
- import * as fs from 'fs';
17
- import * as path from 'path';
18
- import * as config from './config.js';
19
- import { chat, getAvailableProviders, selectProvider, needsSummarization, estimateContextUsage } from './providers.js';
20
- import { executeTool, getTools } from './tools.js';
21
- import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG, RISK_CONFIG, supportsVision, calculateCost } from './types.js';
22
- import { getVersion, getLatestVersion, performUpgrade } from './version-check.js';
23
- import { getAvailableModels, getModelContextLimit, preWarmModelCache } from './model-detection.js';
24
- import { assessToolRisk, detectComplexity } from './risk.js';
25
- import { formatError, classifyError } from './errors.js';
26
- import * as storage from './storage.js';
27
- import { parseFileReferences, processFilesForMessage, formatFileInfo } from './files.js';
28
- import { renderMarkdown } from './markdown.js';
29
- import * as mcp from './mcp.js';
30
- import * as skills from './skills.js';
31
- import * as memory from './memory.js';
32
- import * as hooks from './hooks.js';
33
- import * as modelRouter from './model-router.js';
34
- import * as summarization from './summarization.js';
35
- import { requiresConfirmation } from './risk.js';
36
- import { executeParallel, getParallelizationStats } from './parallel-tools.js';
37
- import { addToScope, removeFromScope, getScopeSummary, getScopeDetails, resetScope } from './scope.js';
38
- import { getAgentStatusReport } from './agterm/index.js';
39
- // Module-level state for agterm mode
40
- let moduleAgtermEnabled = false;
41
- // Debug logging for flow control issues
42
- let debugEnabled = process.env.CALLIOPE_DEBUG === '1';
43
- const debugLog = (label, ...args) => {
44
- if (debugEnabled) {
45
- const timestamp = new Date().toISOString().split('T')[1].slice(0, 12);
46
- console.error(`[${timestamp}] ${label}:`, ...args);
47
- }
48
- };
49
- /**
50
- * Log error to persistent file for debugging
51
- */
52
- function logErrorToFile(error, componentStack) {
53
- try {
54
- const errorLogPath = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.calliope-cli', 'errors.log');
55
- const errorLogDir = path.dirname(errorLogPath);
56
- // Ensure directory exists
57
- if (!fs.existsSync(errorLogDir)) {
58
- fs.mkdirSync(errorLogDir, { recursive: true });
59
- }
60
- const logEntry = {
61
- timestamp: new Date().toISOString(),
62
- error: error?.message || 'Unknown error',
63
- stack: error?.stack || '',
64
- componentStack,
65
- nodeVersion: process.version,
66
- platform: process.platform,
67
- };
68
- const logLine = JSON.stringify(logEntry) + '\n';
69
- // Append to log file (create if doesn't exist)
70
- fs.appendFileSync(errorLogPath, logLine, 'utf-8');
71
- // Rotate log if too large (> 1MB)
72
- const stats = fs.statSync(errorLogPath);
73
- if (stats.size > 1024 * 1024) {
74
- const backupPath = errorLogPath + '.old';
75
- if (fs.existsSync(backupPath)) {
76
- fs.unlinkSync(backupPath);
77
- }
78
- fs.renameSync(errorLogPath, backupPath);
79
- }
80
- }
81
- catch {
82
- // Silently fail if we can't write to log file
83
- }
84
- }
85
- class ErrorBoundary extends React.Component {
86
- constructor(props) {
87
- super(props);
88
- this.state = { hasError: false, error: null, errorInfo: '' };
89
- }
90
- static getDerivedStateFromError(error) {
91
- return { hasError: true, error };
92
- }
93
- componentDidCatch(error, errorInfo) {
94
- // Log error details
95
- const info = errorInfo.componentStack || '';
96
- this.setState({ errorInfo: info });
97
- // Log to console
98
- console.error('Calliope Error:', error);
99
- console.error('Component Stack:', info);
100
- // Log to persistent file for debugging
101
- logErrorToFile(error, info);
102
- }
103
- handleRetry = () => {
104
- this.setState({ hasError: false, error: null, errorInfo: '' });
105
- this.props.onReset?.();
106
- };
107
- render() {
108
- if (this.state.hasError) {
109
- return _jsx(ErrorFallback, { error: this.state.error, errorInfo: this.state.errorInfo, onRetry: this.handleRetry });
110
- }
111
- return this.props.children;
112
- }
113
- }
114
- function ErrorFallback({ error, errorInfo, onRetry }) {
115
- const { exit } = useApp();
116
- useInput((input, key) => {
117
- if (input === 'r' || input === 'R') {
118
- onRetry();
119
- }
120
- else if (input === 'q' || input === 'Q' || key.escape) {
121
- exit();
122
- }
123
- });
124
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", bold: true, children: "\u26A0\uFE0F Calliope encountered an error" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "red", padding: 1, children: [_jsx(Text, { color: "red", children: error?.message || 'Unknown error' }), error?.name && error.name !== 'Error' && (_jsxs(Text, { dimColor: true, children: ["Type: ", error.name] }))] }), errorInfo && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Component trace:" }), _jsx(Text, { dimColor: true, children: errorInfo.split('\n').slice(0, 5).join('\n') })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[R]" }), _jsx(Text, { children: "etry " }), _jsx(Text, { color: "cyan", children: "[Q]" }), _jsx(Text, { children: "uit" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "If this persists, try: calliope --legacy" }) })] }));
125
- }
126
- // ============================================================================
127
- // Constants
128
- // ============================================================================
129
- const BANNER_LINES = [
130
- ' ██████╗ █████╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ███████╗',
131
- '██╔════╝██╔══██╗██║ ██║ ██║██╔═══██╗██╔══██╗██╔════╝',
132
- '██║ ███████║██║ ██║ ██║██║ ██║██████╔╝█████╗ ',
133
- '██║ ██╔══██║██║ ██║ ██║██║ ██║██╔═══╝ ██╔══╝ ',
134
- '╚██████╗██║ ██║███████╗███████╗██║╚██████╔╝██║ ███████╗',
135
- ' ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚══════╝',
136
- ];
137
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
138
- const TOOL_ICONS = {
139
- shell: '⚡',
140
- read_file: '📄',
141
- write_file: '✍️',
142
- list_files: '📁',
143
- think: '💭',
144
- execute_code: '▶️',
145
- web_search: '🔍',
146
- git: '🔀',
147
- mermaid: '📊',
148
- // AGTerm tools
149
- spawn_agent: '🤖',
150
- check_agent: '📋',
151
- list_agents: '📊',
152
- cancel_agent: '🛑',
153
- };
154
- // ============================================================================
155
- // Utility Components
156
- // ============================================================================
157
- function Separator() {
158
- const { stdout } = useStdout();
159
- const width = stdout?.columns || 80;
160
- return _jsx(Text, { dimColor: true, children: '─'.repeat(width) });
161
- }
162
- function ThinkingDisplay({ state }) {
163
- const [frame, setFrame] = useState(0);
164
- useEffect(() => {
165
- const timer = setInterval(() => {
166
- setFrame(f => (f + 1) % SPINNER_FRAMES.length);
167
- }, 80);
168
- return () => clearInterval(timer);
169
- }, []);
170
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", state.status] }), state.iteration != null && state.maxIterations && (_jsxs(Text, { dimColor: true, children: [" (", state.iteration, "/", state.maxIterations, ")"] }))] }), state.detail && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", state.detail] }) })), state.thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { color: "magenta", children: "\uD83D\uDCAD Thinking:" }), state.thinking.split('\n').slice(0, 5).map((line, i) => (_jsxs(Text, { dimColor: true, children: [" ", line.substring(0, 80)] }, i))), state.thinking.split('\n').length > 5 && (_jsx(Text, { dimColor: true, children: " ..." }))] }))] }));
171
- }
172
- // Legacy simple indicator for non-agent operations
173
- function ProcessingIndicator({ label }) {
174
- const [frame, setFrame] = useState(0);
175
- useEffect(() => {
176
- const timer = setInterval(() => {
177
- setFrame(f => (f + 1) % SPINNER_FRAMES.length);
178
- }, 80);
179
- return () => clearInterval(timer);
180
- }, []);
181
- return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
182
- }
183
- // Indicator shown during streaming to show current activity
184
- function StreamingIndicator({ activity }) {
185
- const [frame, setFrame] = useState(0);
186
- const [elapsed, setElapsed] = useState(0);
187
- const pulseFrames = ['·', '•', '●', '•'];
188
- useEffect(() => {
189
- const timer = setInterval(() => {
190
- setFrame(f => (f + 1) % pulseFrames.length);
191
- if (activity) {
192
- setElapsed(Math.floor((Date.now() - activity.startTime) / 1000));
193
- }
194
- }, 200);
195
- return () => clearInterval(timer);
196
- }, [activity]);
197
- if (activity) {
198
- const elapsedStr = elapsed > 0 ? ` (${elapsed}s)` : '';
199
- return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: pulseFrames[frame] }), _jsxs(Text, { children: [" ", activity.action] }), activity.target && _jsxs(Text, { dimColor: true, children: [" ", activity.target] }), _jsx(Text, { dimColor: true, children: elapsedStr })] }) }));
200
- }
201
- return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: pulseFrames[frame] }), _jsx(Text, { dimColor: true, children: " receiving..." })] }));
202
- }
203
- function MessageItem({ msg, collapse }) {
204
- // Determine if this tool should be collapsed
205
- const shouldCollapseThisTool = collapse?.collapseTools &&
206
- collapse.toolDisplayLimit > 0 &&
207
- collapse.toolIndex !== undefined &&
208
- collapse.totalTools !== undefined &&
209
- (collapse.totalTools - collapse.toolIndex) > collapse.toolDisplayLimit;
210
- switch (msg.type) {
211
- case 'user':
212
- return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u203A" }), " ", msg.content] }) }));
213
- case 'assistant': {
214
- // Render markdown with syntax highlighting
215
- const rendered = renderMarkdown(msg.content);
216
- // Collapse consecutive blank lines to single blank line
217
- const lines = rendered.split('\n').reduce((acc, line, i, arr) => {
218
- // Skip if this is a blank line following another blank line
219
- if (line === '' && acc.length > 0 && acc[acc.length - 1] === '') {
220
- return acc;
221
- }
222
- acc.push(line);
223
- return acc;
224
- }, []);
225
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i)))] }));
226
- }
227
- case 'tool': {
228
- const isToolCall = msg.content.startsWith('⚡');
229
- const isThinkTool = msg.content.includes('💭') || msg.content.startsWith('Perfect!') || msg.content.startsWith('Let me');
230
- // Check if this is a think tool that should be collapsed
231
- if (collapse?.collapseThinking && isThinkTool && !isToolCall) {
232
- const preview = msg.content.substring(0, 50).replace(/\n/g, ' ');
233
- return (_jsxs(Text, { dimColor: true, children: ["\u2570\u2500 \uD83D\uDCAD ", _jsxs(Text, { italic: true, children: [preview, "..."] })] }));
234
- }
235
- // Check if this tool should be collapsed (based on toolDisplayLimit)
236
- if (shouldCollapseThisTool || (collapse?.collapseTools && !isToolCall)) {
237
- // Show collapsed single-line version
238
- const firstLine = msg.content.split('\n')[0].substring(0, 60);
239
- return (_jsxs(Text, { dimColor: true, children: ["\u2570\u2500 \u25B8 ", firstLine, msg.content.length > 60 ? '...' : ''] }));
240
- }
241
- if (isToolCall) {
242
- const match = msg.content.match(/^⚡ (\w+): (.*)$/);
243
- if (match) {
244
- const [, toolName, preview] = match;
245
- const icon = TOOL_ICONS[toolName] || '⚙️';
246
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u256D\u2500" }), " ", icon, " ", _jsx(Text, { color: "yellow", children: toolName })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: preview })] })] }));
247
- }
248
- }
249
- // Check for diff output from write_file
250
- const isDiff = msg.content.startsWith('DIFF:');
251
- if (isDiff) {
252
- const lines = msg.content.split('\n');
253
- const header = lines[0];
254
- const isNewFile = header.includes('NEW_FILE:');
255
- const filePath = isNewFile
256
- ? header.replace('DIFF:NEW_FILE:', '')
257
- : header.replace('DIFF:', '');
258
- // Find summary line (starts with ⎿)
259
- const summaryLine = lines.find(l => l.startsWith('⎿'));
260
- const diffStartIdx = summaryLine ? lines.indexOf(summaryLine) + 1 : 1;
261
- const diffLines = lines.slice(diffStartIdx, diffStartIdx + 12);
262
- const hasMore = lines.length > diffStartIdx + 12;
263
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u251C\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: [" ", isNewFile ? '(new file)' : '(modified)'] })] }), summaryLine && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: summaryLine })] })), diffLines.map((line, i) => {
264
- // Check for line number format: " 123 + content" or " 123 - content"
265
- const lineNumMatch = line.match(/^(\s*\d+)\s*([+-])\s{2}(.*)$/);
266
- if (lineNumMatch) {
267
- const [, lineNum, prefix, content] = lineNumMatch;
268
- const color = prefix === '+' ? 'green' : 'red';
269
- return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { dimColor: true, children: [" ", lineNum] }), _jsxs(Text, { color: color, children: [" ", prefix] }), _jsxs(Text, { color: color, children: [" ", content.substring(0, 70)] })] }, i));
270
- }
271
- // Context line with line number: " 123 content"
272
- const contextMatch = line.match(/^(\s*\d+)\s{4}(.*)$/);
273
- if (contextMatch) {
274
- const [, lineNum, content] = contextMatch;
275
- return (_jsx(Text, { children: _jsxs(Text, { dimColor: true, children: ["\u2502 ", lineNum, " ", content.substring(0, 70)] }) }, i));
276
- }
277
- // Fallback for old format or other lines
278
- let color;
279
- if (line.includes(' + ') || line.startsWith('+ '))
280
- color = 'green';
281
- else if (line.includes(' - ') || line.startsWith('- '))
282
- color = 'red';
283
- return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { color: color, children: [" ", line.substring(0, 80)] })] }, i));
284
- }), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: "..." })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", _jsx(Text, { color: "green", children: "\u2713" }), " ", _jsx(Text, { dimColor: true, children: filePath })] })] }));
285
- }
286
- // Regular tool result with enhanced status detection
287
- const allLines = msg.content.split('\n');
288
- const lines = allLines.slice(0, 5);
289
- const totalLines = allLines.length;
290
- const hasMore = totalLines > 5;
291
- // Enhanced status detection
292
- const lowerContent = msg.content.toLowerCase();
293
- const hasError = lowerContent.includes('error') ||
294
- lowerContent.includes('failed') ||
295
- lowerContent.includes('permission denied') ||
296
- lowerContent.includes('not found') ||
297
- lowerContent.includes('exception');
298
- const hasWarning = lowerContent.includes('warning') ||
299
- lowerContent.includes('deprecated') ||
300
- lowerContent.includes('caution');
301
- // Determine status icon and color
302
- let statusIcon = '✓';
303
- let statusColor = 'green';
304
- if (hasError) {
305
- statusIcon = '✗';
306
- statusColor = 'red';
307
- }
308
- else if (hasWarning) {
309
- statusIcon = '⚠';
310
- statusColor = 'yellow';
311
- }
312
- return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: line.substring(0, 100) })] }, i))), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsxs(Text, { dimColor: true, children: ["... (", totalLines - 5, " more lines)"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", _jsx(Text, { color: statusColor, children: statusIcon })] })] }));
313
- }
314
- case 'system':
315
- return _jsx(Text, { color: "yellow", children: msg.content });
316
- case 'error':
317
- return _jsxs(Text, { color: "red", children: ["\u2717 ", msg.content] });
318
- default:
319
- return _jsx(Text, { children: msg.content });
320
- }
321
- }
322
- function MessageHistory({ messages, collapseSettings }) {
323
- // Count tool messages for toolDisplayLimit calculation
324
- const toolMessages = messages.filter(m => m.type === 'tool');
325
- const totalTools = toolMessages.length;
326
- // Track tool index
327
- let toolIndex = 0;
328
- return (_jsx(Static, { items: messages, children: (msg) => {
329
- // For tool messages, pass index for collapse calculation
330
- const msgCollapseSettings = msg.type === 'tool'
331
- ? { ...collapseSettings, toolIndex: toolIndex++, totalTools }
332
- : collapseSettings;
333
- return (_jsx(Box, { children: _jsx(MessageItem, { msg: msg, collapse: msgCollapseSettings }) }, msg.id));
334
- } }));
335
- }
336
- // ============================================================================
337
- // Modal Components
338
- // ============================================================================
339
- function ModelSelector({ models, onSelect, onCancel }) {
340
- const [index, setIndex] = useState(0);
341
- const pageSize = 10;
342
- const start = Math.max(0, Math.min(index - Math.floor(pageSize / 2), models.length - pageSize));
343
- const visible = models.slice(start, start + pageSize);
344
- useInput((input, key) => {
345
- if (key.upArrow)
346
- setIndex(i => Math.max(0, i - 1));
347
- else if (key.downArrow)
348
- setIndex(i => Math.min(models.length - 1, i + 1));
349
- else if (key.return)
350
- onSelect(models[index].id);
351
- else if (key.escape || input === 'q')
352
- onCancel();
353
- });
354
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "yellow", children: "Select model (\u2191/\u2193 navigate, Enter select, Esc cancel):" }), visible.map((model, i) => {
355
- const globalIndex = start + i;
356
- const isSelected = globalIndex === index;
357
- const name = model.name || model.id;
358
- const displayName = name.length > 50 ? name.slice(0, 47) + '...' : name;
359
- return (_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '❯ ' : ' ', displayName] }, model.id));
360
- }), models.length > pageSize && (_jsxs(Text, { dimColor: true, children: [" (", index + 1, "/", models.length, ")"] }))] }));
361
- }
362
- function SessionSelector({ sessions, onSelect, onDelete, onCancel }) {
363
- const [index, setIndex] = useState(0);
364
- const pageSize = 5;
365
- // Keep selection visible - scroll window to follow selection
366
- const start = Math.max(0, Math.min(index - pageSize + 1, sessions.length - pageSize));
367
- const end = Math.min(start + pageSize, sessions.length);
368
- const visible = sessions.slice(start, end);
369
- const hasMore = sessions.length > pageSize;
370
- const hasAbove = start > 0;
371
- const hasBelow = end < sessions.length;
372
- useInput((input, key) => {
373
- if (key.upArrow)
374
- setIndex(i => Math.max(0, i - 1));
375
- else if (key.downArrow)
376
- setIndex(i => Math.min(sessions.length - 1, i + 1));
377
- else if (key.return && sessions.length > 0)
378
- onSelect(sessions[index]);
379
- else if ((key.backspace || key.delete) && sessions.length > 0)
380
- onDelete(sessions[index]);
381
- else if (key.escape || input === 'q')
382
- onCancel();
383
- });
384
- const formatTimeAgo = (dateStr) => {
385
- const diff = Date.now() - new Date(dateStr).getTime();
386
- const hours = Math.floor(diff / (1000 * 60 * 60));
387
- const days = Math.floor(hours / 24);
388
- if (days > 0)
389
- return `${days}d ago`;
390
- if (hours > 0)
391
- return `${hours}h ago`;
392
- const minutes = Math.floor(diff / (1000 * 60));
393
- return `${minutes}m ago`;
394
- };
395
- if (sessions.length === 0) {
396
- return (_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Text, { dimColor: true, children: "No sessions found. Press Esc to close." }) }));
397
- }
398
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "yellow", children: "Sessions (\u2191/\u2193, Enter load, Del delete, Esc cancel):" }), hasMore && hasAbove && _jsx(Text, { dimColor: true, children: " \u2191 more" }), visible.map((session, i) => {
399
- const globalIndex = start + i;
400
- const isSelected = globalIndex === index;
401
- const timeAgo = formatTimeAgo(session.lastAccessedAt);
402
- const name = session.projectName.length > 30 ? session.projectName.slice(0, 27) + '...' : session.projectName;
403
- return (_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '❯ ' : ' ', name, " ", _jsxs(Text, { dimColor: true, children: ["(", timeAgo, ", ", session.messageCount, " msgs)"] })] }, session.id));
404
- }), hasMore && hasBelow && _jsx(Text, { dimColor: true, children: " \u2193 more" }), hasMore && _jsxs(Text, { dimColor: true, children: [" ", index + 1, "/", sessions.length] })] }));
405
- }
406
- function UpgradePrompt({ currentVersion, latestVersion, onConfirm, onCancel }) {
407
- useInput((input, key) => {
408
- if (input === 'y' || input === 'Y')
409
- onConfirm();
410
- else if (input === 'n' || input === 'N' || key.escape)
411
- onCancel();
412
- });
413
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: "yellow", children: ["Update available: v", currentVersion, " \u2192 ", _jsxs(Text, { color: "green", children: ["v", latestVersion] })] }), _jsxs(Text, { children: ["Upgrade now? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
414
- }
415
- function ComplexityWarning({ reason, prompt, onProceed, onPlan, onCancel, }) {
416
- useInput((input, key) => {
417
- if (input === 'p' || input === 'P')
418
- onProceed();
419
- else if (input === 'l' || input === 'L')
420
- onPlan();
421
- else if (key.escape || input === 'c' || input === 'C')
422
- onCancel();
423
- });
424
- // Analyze the prompt for operation preview
425
- const analysis = React.useMemo(() => {
426
- if (!prompt)
427
- return null;
428
- const lower = prompt.toLowerCase();
429
- const cwd = process.cwd();
430
- // Parse file references
431
- const fileRefs = parseFileReferences(prompt, cwd);
432
- // Detect operation types
433
- const operations = [];
434
- if (lower.includes('delete') || lower.includes('remove') || lower.includes('rm ')) {
435
- operations.push('Delete files');
436
- }
437
- if (lower.includes('create') || lower.includes('add') || lower.includes('new ')) {
438
- operations.push('Create files');
439
- }
440
- if (lower.includes('modify') || lower.includes('change') || lower.includes('update') || lower.includes('edit')) {
441
- operations.push('Modify files');
442
- }
443
- if (lower.includes('refactor') || lower.includes('restructure') || lower.includes('reorganize')) {
444
- operations.push('Refactor code');
445
- }
446
- if (lower.includes('install') || lower.includes('npm') || lower.includes('yarn') || lower.includes('pip')) {
447
- operations.push('Install packages');
448
- }
449
- if (lower.includes('git ') || lower.includes('commit') || lower.includes('push') || lower.includes('merge')) {
450
- operations.push('Git operations');
451
- }
452
- if (lower.includes('test') || lower.includes('build') || lower.includes('compile')) {
453
- operations.push('Build/Test');
454
- }
455
- // Estimate risk level based on keywords
456
- let riskLevel = 'medium';
457
- if (lower.includes('delete') || lower.includes('remove') || lower.includes('force') || lower.includes('--hard')) {
458
- riskLevel = 'high';
459
- }
460
- else if (lower.includes('read') || lower.includes('show') || lower.includes('list') || lower.includes('find')) {
461
- riskLevel = 'low';
462
- }
463
- return {
464
- files: fileRefs.files,
465
- operations,
466
- riskLevel,
467
- };
468
- }, [prompt]);
469
- const riskColors = { low: 'green', medium: 'yellow', high: 'red' };
470
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "\uD83D\uDD0D Operation Preview" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: reason }), analysis && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), analysis.operations.length > 0 && (_jsxs(Text, { children: ["Operations: ", _jsx(Text, { color: "cyan", children: analysis.operations.join(', ') })] })), analysis.files.length > 0 && (_jsxs(Text, { children: ["Files referenced: ", _jsx(Text, { color: "cyan", children: analysis.files.length }), analysis.files.length <= 3 && (_jsxs(Text, { dimColor: true, children: [" (", analysis.files.map(f => f.split('/').pop()).join(', '), ")"] }))] })), _jsxs(Text, { children: ["Risk level: ", _jsx(Text, { color: riskColors[analysis.riskLevel], children: analysis.riskLevel.toUpperCase() })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { children: "This operation may affect multiple files or require careful planning." }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "How would you like to proceed?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "[P]" }), _jsx(Text, { children: "roceed directly " }), _jsx(Text, { color: "yellow", children: "[L]" }), _jsx(Text, { children: "et me plan first " }), _jsx(Text, { color: "red", children: "[C]" }), _jsx(Text, { children: "ancel" })] })] }));
471
- }
472
- function SessionResumePrompt({ session, onResume, onNew, }) {
473
- useInput((input, key) => {
474
- if (input === 'r' || input === 'R')
475
- onResume();
476
- else if (input === 'n' || input === 'N' || key.escape)
477
- onNew();
478
- });
479
- const timeAgo = (() => {
480
- const diff = Date.now() - new Date(session.lastAccessedAt).getTime();
481
- const hours = Math.floor(diff / (1000 * 60 * 60));
482
- const days = Math.floor(hours / 24);
483
- if (days > 0)
484
- return `${days} day${days > 1 ? 's' : ''} ago`;
485
- if (hours > 0)
486
- return `${hours} hour${hours > 1 ? 's' : ''} ago`;
487
- const minutes = Math.floor(diff / (1000 * 60));
488
- return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
489
- })();
490
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\uD83D\uDCC2 Previous Session Found" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Project: ", _jsx(Text, { color: "yellow", children: session.projectName })] }), _jsxs(Text, { children: ["Last active: ", _jsx(Text, { dimColor: true, children: timeAgo })] }), _jsxs(Text, { children: ["Messages: ", _jsx(Text, { dimColor: true, children: session.messageCount })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[R]" }), "esume session ", _jsx(Text, { color: "cyan", children: "[N]" }), "ew session"] })] }));
491
- }
492
- function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
493
- useInput((input, key) => {
494
- if (input === 'y' || input === 'Y')
495
- onConfirm();
496
- else if (input === 'n' || input === 'N' || key.escape)
497
- onDeny();
498
- });
499
- const args = toolCall.arguments;
500
- const preview = String(args.command || args.path || args.operation || '...');
501
- const riskColor = riskLevel === 'critical' ? 'red' : 'yellow';
502
- const riskIcon = riskLevel === 'critical' ? '⚠️' : '⚡';
503
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: riskColor, paddingX: 1, children: [_jsxs(Text, { color: riskColor, bold: true, children: [riskIcon, " ", riskLevel.toUpperCase(), " RISK OPERATION"] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Tool: ", _jsx(Text, { color: "cyan", children: toolCall.name })] }), _jsxs(Text, { children: ["Command: ", _jsx(Text, { dimColor: true, children: preview.substring(0, 60) })] }), _jsxs(Text, { children: ["Reason: ", _jsx(Text, { dimColor: true, children: reason })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Execute this operation? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
504
- }
505
- // Keybindings modal component
506
- function KeybindingsModal({ onClose }) {
507
- useInput((input, key) => {
508
- if (key.escape || key.return || input === 'q')
509
- onClose();
510
- });
511
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2328\uFE0F Keyboard Shortcuts" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "General:" }), _jsx(Text, { children: " Enter Submit message" }), _jsx(Text, { children: " Alt/Ctrl+Enter Insert newline (multiline)" }), _jsx(Text, { children: " Shift+Tab Cycle modes (plan/hybrid/work)" }), _jsx(Text, { children: " Esc Cancel operation / show hint" }), _jsx(Text, { children: " Ctrl+C Exit" }), _jsx(Text, { children: " \u2191/\u2193 Navigate input history" }), _jsx(Text, { children: " Tab Auto-complete commands/paths" }), _jsx(Text, { children: " Ctrl+U Clear input line" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "During Processing (queue mode):" }), _jsx(Text, { children: " Enter Queue message for later" }), _jsx(Text, { children: " !message Send directly (interrupt agent)" }), _jsx(Text, { children: " \u2191/\u2193 Edit queued messages" }), _jsx(Text, { children: " Ctrl+D Delete queued message" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "Quick Commands:" }), _jsx(Text, { children: " /keys This help" }), _jsx(Text, { children: " /work Switch to work mode" }), _jsx(Text, { children: " /plan Switch to plan mode" }), _jsx(Text, { children: " /flush Force-process queue" }), _jsx(Text, { children: " /unstick Reset stuck state" }), _jsx(Text, { children: " /debug on/off Toggle debug mode" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press any key to close..." })] }));
512
- }
513
- // ============================================================================
514
- // Slash Commands (for tab completion)
515
- // ============================================================================
516
- const SLASH_COMMANDS = [
517
- '/help', '/h',
518
- '/mode', '/m',
519
- '/provider', '/p',
520
- '/model',
521
- '/models',
522
- '/route',
523
- '/persona',
524
- '/todo',
525
- '/plans',
526
- '/session',
527
- '/sessions',
528
- '/history',
529
- '/context',
530
- '/summarize',
531
- '/clear', '/c',
532
- '/copy',
533
- '/export',
534
- '/edit',
535
- '/undo',
536
- '/redo',
537
- '/confirm',
538
- '/profile',
539
- '/mcp',
540
- '/skills',
541
- '/memory',
542
- '/project',
543
- '/find',
544
- '/branch',
545
- '/theme',
546
- '/hooks',
547
- '/search',
548
- '/status', '/s',
549
- '/config',
550
- '/set',
551
- '/layout',
552
- '/density',
553
- '/collapse',
554
- '/scope',
555
- '/add-dir',
556
- '/remove-dir',
557
- '/agents',
558
- '/upgrade',
559
- '/loop',
560
- '/cancel-loop',
561
- '/exit',
562
- '/keys',
563
- '/?',
564
- '/queue',
565
- '/flush',
566
- '/debug',
567
- '/unstick',
568
- '/work',
569
- '/plan',
570
- '/resume',
571
- ];
572
- // Commands that take a path argument (for file tab completion)
573
- const PATH_COMMANDS = ['/add-dir', '/remove-dir', '/export', '/find'];
574
- /**
575
- * Get file/directory completions for a partial path
576
- */
577
- function getPathCompletions(partial, cwd) {
578
- try {
579
- // fs and path are imported at the top of the file
580
- // Handle empty or relative paths
581
- let searchDir;
582
- let prefix;
583
- if (!partial || partial === '') {
584
- searchDir = cwd;
585
- prefix = '';
586
- }
587
- else if (partial.startsWith('/')) {
588
- // Absolute path
589
- const lastSlash = partial.lastIndexOf('/');
590
- searchDir = partial.substring(0, lastSlash) || '/';
591
- prefix = partial.substring(lastSlash + 1);
592
- }
593
- else if (partial.startsWith('~')) {
594
- // Home directory
595
- const home = process.env.HOME || '/tmp';
596
- const expanded = partial.replace('~', home);
597
- const lastSlash = expanded.lastIndexOf('/');
598
- searchDir = expanded.substring(0, lastSlash) || home;
599
- prefix = expanded.substring(lastSlash + 1);
600
- }
601
- else {
602
- // Relative path
603
- const lastSlash = partial.lastIndexOf('/');
604
- if (lastSlash === -1) {
605
- searchDir = cwd;
606
- prefix = partial;
607
- }
608
- else {
609
- searchDir = path.join(cwd, partial.substring(0, lastSlash));
610
- prefix = partial.substring(lastSlash + 1);
611
- }
612
- }
613
- if (!fs.existsSync(searchDir))
614
- return [];
615
- const entries = fs.readdirSync(searchDir, { withFileTypes: true });
616
- const matches = [];
617
- for (const entry of entries) {
618
- if (entry.name.startsWith('.') && !prefix.startsWith('.'))
619
- continue; // Skip hidden unless typing hidden
620
- if (prefix && !entry.name.toLowerCase().startsWith(prefix.toLowerCase()))
621
- continue;
622
- const fullPath = path.join(searchDir, entry.name);
623
- const displayPath = partial.startsWith('/')
624
- ? fullPath
625
- : partial.startsWith('~')
626
- ? fullPath.replace(process.env.HOME || '', '~')
627
- : path.relative(cwd, fullPath) || entry.name;
628
- matches.push(entry.isDirectory() ? displayPath + '/' : displayPath);
629
- }
630
- return matches.sort().slice(0, 10); // Limit to 10 suggestions
631
- }
632
- catch {
633
- return [];
634
- }
635
- }
636
- // ============================================================================
637
- // Input Components
638
- // ============================================================================
639
- function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled, isProcessing, queuedCount, queuedMessages, editingQueueIndex, onQueueMessage, onEditQueuedMessage, onSetEditingQueueIndex, onDirectSend, cwd, suggestions, onSuggestionsChange, onNavigateHistory,
640
- // Smart suggestion context
641
- currentMode, contextPercentage, recentCommands, hasGitRepo, }) {
642
- const workingDir = cwd || process.cwd();
643
- // Debug logging (set CALLIOPE_DEBUG=1 to enable) - use async to avoid input lag
644
- const debug = process.env.CALLIOPE_DEBUG === '1';
645
- const log = debug
646
- ? (msg) => fs.appendFile('/tmp/calliope-debug.log', `${new Date().toISOString()} [input] ${msg}\n`, () => { })
647
- : () => { };
648
- // CRITICAL FIX: Use refs to track the current value and cursor position
649
- // This prevents stale closure issues when typing rapidly before React re-renders
650
- const valueRef = React.useRef(value);
651
- const cursorRef = React.useRef(value.length); // Cursor position (0 = start, length = end)
652
- const internalChangeRef = React.useRef(false); // Track if change was from typing
653
- // Sync refs when prop changes (from external sources like history navigation)
654
- React.useEffect(() => {
655
- // Only reset cursor if change was external (not from our own typing)
656
- if (!internalChangeRef.current) {
657
- valueRef.current = value;
658
- cursorRef.current = value.length; // Move cursor to end on external change
659
- }
660
- internalChangeRef.current = false;
661
- }, [value]);
662
- // Helper to update value - updates ref IMMEDIATELY, then notifies parent
663
- const updateValue = (newValue, newCursor) => {
664
- valueRef.current = newValue; // Update ref synchronously
665
- cursorRef.current = newCursor ?? newValue.length; // Default cursor to end
666
- internalChangeRef.current = true; // Mark as internal change
667
- onChange(newValue); // Then notify parent (may batch)
668
- };
669
- // Force re-render for cursor position changes (cursor is visual only)
670
- const [, forceRender] = React.useState(0);
671
- const updateCursor = (pos) => {
672
- cursorRef.current = Math.max(0, Math.min(pos, valueRef.current.length));
673
- forceRender(n => n + 1);
674
- };
675
- // Handle ALL keyboard input here - single source of input handling
676
- useInput((input, key) => {
677
- const currentValue = valueRef.current;
678
- log(`key: "${input}" ${JSON.stringify(key)} val="${currentValue}" disabled=${disabled}`);
679
- // ESC to exit (always works)
680
- if (key.escape) {
681
- log('-> escape');
682
- onEscape();
683
- return;
684
- }
685
- // Ctrl+C to exit (always works)
686
- if (key.ctrl && input === 'c') {
687
- onEscape();
688
- return;
689
- }
690
- // When fully disabled (modal), ignore all input
691
- if (disabled) {
692
- return;
693
- }
694
- // When processing, queue messages instead of submitting directly
695
- if (isProcessing) {
696
- // Ensure cursor is valid
697
- let cursor = cursorRef.current;
698
- if (cursor > currentValue.length)
699
- cursor = currentValue.length;
700
- if (cursor < 0)
701
- cursor = 0;
702
- // Left/right arrow for cursor movement
703
- if (key.leftArrow) {
704
- updateCursor(cursor - 1);
705
- return;
706
- }
707
- if (key.rightArrow) {
708
- updateCursor(cursor + 1);
709
- return;
710
- }
711
- // Backspace - support multiple variants including Mac delete key
712
- const isBackspace = key.backspace || key.delete || (key.ctrl && input === 'h') || input === '\x7f' || input === '\b';
713
- if (isBackspace) {
714
- if (cursor > 0) {
715
- const newValue = currentValue.slice(0, cursor - 1) + currentValue.slice(cursor);
716
- updateValue(newValue, cursor - 1);
717
- }
718
- else if (currentValue.length > 0) {
719
- updateValue(currentValue.slice(0, -1), currentValue.length - 1);
720
- }
721
- return;
722
- }
723
- if (key.ctrl && input === 'u') {
724
- updateValue('');
725
- onSetEditingQueueIndex?.(null); // Clear editing state
726
- return;
727
- }
728
- // Ctrl+A to go to start, Ctrl+E to go to end
729
- if (key.ctrl && input === 'a') {
730
- updateCursor(0);
731
- return;
732
- }
733
- if (key.ctrl && input === 'e') {
734
- updateCursor(currentValue.length);
735
- return;
736
- }
737
- // Up/Down arrows to navigate queued messages for editing
738
- if (key.upArrow && queuedMessages && queuedMessages.length > 0) {
739
- if (editingQueueIndex === null || editingQueueIndex === undefined) {
740
- // Start editing the last queued message
741
- const idx = queuedMessages.length - 1;
742
- onSetEditingQueueIndex?.(idx);
743
- updateValue(queuedMessages[idx]);
744
- }
745
- else if (editingQueueIndex > 0) {
746
- // Move to previous message
747
- const idx = editingQueueIndex - 1;
748
- onSetEditingQueueIndex?.(idx);
749
- updateValue(queuedMessages[idx]);
750
- }
751
- return;
752
- }
753
- if (key.downArrow && queuedMessages && editingQueueIndex !== null && editingQueueIndex !== undefined) {
754
- if (editingQueueIndex < queuedMessages.length - 1) {
755
- // Move to next message
756
- const idx = editingQueueIndex + 1;
757
- onSetEditingQueueIndex?.(idx);
758
- updateValue(queuedMessages[idx]);
759
- }
760
- else {
761
- // At the end, clear to new input
762
- onSetEditingQueueIndex?.(null);
763
- updateValue('');
764
- }
765
- return;
766
- }
767
- // Alt+Enter or Ctrl+Enter to insert newline (multiline input)
768
- if (key.return && (key.meta || key.ctrl)) {
769
- updateValue(currentValue + '\n');
770
- return;
771
- }
772
- // Shift+Enter sends directly (interrupts current operation)
773
- // Note: Many terminals don't distinguish Shift+Enter from Enter
774
- // Use ! prefix as reliable alternative: "!message" sends immediately
775
- if (key.return && key.shift && currentValue.trim() && onDirectSend) {
776
- onDirectSend(currentValue.trim());
777
- onSetEditingQueueIndex?.(null);
778
- updateValue('');
779
- return;
780
- }
781
- // ! prefix sends directly: "!fix this now" interrupts and sends
782
- if (key.return && currentValue.trim().startsWith('!') && onDirectSend) {
783
- const msg = currentValue.trim().slice(1).trim(); // Remove ! prefix
784
- if (msg) {
785
- onDirectSend(msg);
786
- onSetEditingQueueIndex?.(null);
787
- updateValue('');
788
- return;
789
- }
790
- }
791
- // Enter queues or updates the message
792
- if (key.return && currentValue.trim()) {
793
- if (editingQueueIndex !== null && editingQueueIndex !== undefined && onEditQueuedMessage) {
794
- // Update existing queued message
795
- onEditQueuedMessage(editingQueueIndex, currentValue.trim());
796
- onSetEditingQueueIndex?.(null);
797
- updateValue('');
798
- }
799
- else if (onQueueMessage) {
800
- // Add new queued message
801
- onQueueMessage(currentValue.trim());
802
- updateValue('');
803
- }
804
- return;
805
- }
806
- // Ctrl+D to delete currently editing queued message
807
- if (key.ctrl && input === 'd' && editingQueueIndex !== null && editingQueueIndex !== undefined && onEditQueuedMessage) {
808
- onEditQueuedMessage(editingQueueIndex, ''); // Empty string signals deletion
809
- onSetEditingQueueIndex?.(null);
810
- updateValue('');
811
- return;
812
- }
813
- // Regular input - insert at cursor position
814
- if (input && !key.ctrl && !key.meta && !key.tab) {
815
- const cursor = cursorRef.current;
816
- const newValue = currentValue.slice(0, cursor) + input + currentValue.slice(cursor);
817
- updateValue(newValue, cursor + input.length);
818
- }
819
- return;
820
- }
821
- // Shift+Tab to cycle mode
822
- if (key.shift && key.tab) {
823
- onCycleMode();
824
- return;
825
- }
826
- // Alt+Enter or Ctrl+Enter to insert newline (multiline input)
827
- if (key.return && (key.meta || key.ctrl)) {
828
- updateValue(currentValue + '\n');
829
- return;
830
- }
831
- // Enter to submit
832
- if (key.return) {
833
- if (currentValue.trim()) {
834
- onSubmit(currentValue);
835
- }
836
- return;
837
- }
838
- // Cursor movement with arrow keys
839
- // Ensure cursor is valid (might be out of sync)
840
- let cursor = cursorRef.current;
841
- if (cursor > currentValue.length)
842
- cursor = currentValue.length;
843
- if (cursor < 0)
844
- cursor = 0;
845
- if (key.leftArrow) {
846
- updateCursor(cursor - 1);
847
- return;
848
- }
849
- if (key.rightArrow) {
850
- updateCursor(cursor + 1);
851
- return;
852
- }
853
- // Backspace deletes character before cursor (or from end if cursor is 0 but there's text)
854
- // Support multiple backspace variants:
855
- // - key.backspace (Ink's detection)
856
- // - key.delete (Mac delete key is often detected as this)
857
- // - Ctrl+H (ASCII backspace control code)
858
- // - \x7f DEL char (Mac delete key raw)
859
- // - \b BS char (traditional backspace)
860
- const isBackspace = key.backspace || key.delete || (key.ctrl && input === 'h') || input === '\x7f' || input === '\b';
861
- if (isBackspace) {
862
- if (cursor > 0) {
863
- const newValue = currentValue.slice(0, cursor - 1) + currentValue.slice(cursor);
864
- updateValue(newValue, cursor - 1);
865
- }
866
- else if (currentValue.length > 0) {
867
- // Fallback: delete from end if cursor is somehow at 0
868
- updateValue(currentValue.slice(0, -1), currentValue.length - 1);
869
- }
870
- return;
871
- }
872
- // Ctrl+U to clear line
873
- if (key.ctrl && input === 'u') {
874
- updateValue('');
875
- return;
876
- }
877
- // Ctrl+A to go to start, Ctrl+E to go to end
878
- if (key.ctrl && input === 'a') {
879
- updateCursor(0);
880
- return;
881
- }
882
- if (key.ctrl && input === 'e') {
883
- updateCursor(currentValue.length);
884
- return;
885
- }
886
- // Tab completion for slash commands and paths
887
- if (key.tab && !key.shift) {
888
- // Check if we're completing a path after a path command
889
- const parts = currentValue.split(/\s+/);
890
- const cmd = parts[0]?.toLowerCase();
891
- if (PATH_COMMANDS.includes(cmd) && parts.length >= 1) {
892
- // Path completion
893
- const pathPart = parts.slice(1).join(' ');
894
- const completions = getPathCompletions(pathPart, workingDir);
895
- if (completions.length === 1) {
896
- updateValue(`${cmd} ${completions[0]}`);
897
- onSuggestionsChange?.([]);
898
- }
899
- else if (completions.length > 1) {
900
- // Find common prefix
901
- let commonPrefix = completions[0];
902
- for (const comp of completions) {
903
- while (!comp.startsWith(commonPrefix)) {
904
- commonPrefix = commonPrefix.slice(0, -1);
905
- }
906
- }
907
- if (commonPrefix.length > pathPart.length) {
908
- updateValue(`${cmd} ${commonPrefix}`);
909
- }
910
- onSuggestionsChange?.(completions);
911
- }
912
- return;
913
- }
914
- // Slash command completion with smart suggestions
915
- if (currentValue.startsWith('/')) {
916
- // Use smart suggestions if context is available
917
- const smartMatches = getSmartCommandSuggestions({
918
- input: currentValue,
919
- hasGitRepo: hasGitRepo ?? false,
920
- contextPercentage: contextPercentage ?? 0,
921
- currentMode: currentMode ?? 'hybrid',
922
- recentCommands: recentCommands ?? [],
923
- isProcessing: isProcessing ?? false,
924
- });
925
- // Fall back to basic matching if smart suggestions didn't find anything
926
- const partial = currentValue.toLowerCase();
927
- const matches = smartMatches.length > 0 ? smartMatches : SLASH_COMMANDS.filter(cmdName => cmdName.startsWith(partial) && cmdName !== partial);
928
- if (matches.length === 1) {
929
- updateValue(matches[0] + ' ');
930
- onSuggestionsChange?.([]);
931
- }
932
- else if (matches.length > 1) {
933
- let commonPrefix = matches[0];
934
- for (const match of matches) {
935
- while (!match.startsWith(commonPrefix)) {
936
- commonPrefix = commonPrefix.slice(0, -1);
937
- }
938
- }
939
- if (commonPrefix.length > currentValue.length) {
940
- updateValue(commonPrefix);
941
- }
942
- onSuggestionsChange?.(matches);
943
- }
944
- return;
945
- }
946
- }
947
- // Up/down arrows for history navigation
948
- if (key.upArrow && onNavigateHistory) {
949
- onNavigateHistory('up');
950
- return;
951
- }
952
- if (key.downArrow && onNavigateHistory) {
953
- onNavigateHistory('down');
954
- return;
955
- }
956
- // Ignore other control keys, meta, and tab
957
- if (key.ctrl || key.meta || key.tab) {
958
- return;
959
- }
960
- // Regular character input - insert at cursor position
961
- if (input) {
962
- const cursorPos = cursorRef.current;
963
- const newValue = currentValue.slice(0, cursorPos) + input + currentValue.slice(cursorPos);
964
- log(`-> char "${input}": "${currentValue}" -> "${newValue}" cursor=${cursorPos}`);
965
- updateValue(newValue, cursorPos + input.length);
966
- }
967
- }, { isActive: !disabled });
968
- // Determine prompt style based on state
969
- const promptColor = disabled ? 'gray' : isProcessing ? 'yellow' : 'cyan';
970
- const isEditing = editingQueueIndex !== null && editingQueueIndex !== undefined;
971
- const promptText = isProcessing
972
- ? (isEditing ? `edit[${editingQueueIndex + 1}]>` : 'queue>')
973
- : 'calliope>';
974
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), suggestions && suggestions.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Tab: " }), _jsx(Text, { color: "cyan", children: suggestions.slice(0, 5).join(' ') }), suggestions.length > 5 && _jsxs(Text, { dimColor: true, children: [" (+", suggestions.length - 5, " more)"] })] })), (queuedCount ?? 0) > 0 && (_jsxs(Box, { children: [_jsxs(Text, { color: "yellow", children: ["\uD83D\uDCE8 ", queuedCount, " queued"] }), _jsx(Text, { dimColor: true, children: " | !msg to send now" })] })), _jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptText, " "] }), _jsx(Text, { children: value.slice(0, cursorRef.current) }), _jsx(Text, { color: promptColor, children: "\u258C" }), _jsx(Text, { children: value.slice(cursorRef.current) })] })] }));
975
- }
976
- // Module-level state for context tracking (persists across renders)
977
- const contextState = {
978
- lastLevel: 'healthy',
979
- warningCounts: { healthy: 0, caution: 0, warning: 0, critical: 0, emergency: 0 },
980
- lastWarningTime: 0,
981
- };
982
- function getContextLevel(percentage) {
983
- if (percentage >= 98)
984
- return 'emergency';
985
- if (percentage >= 95)
986
- return 'critical';
987
- if (percentage >= 85)
988
- return 'warning';
989
- if (percentage >= 70)
990
- return 'caution';
991
- return 'healthy';
992
- }
993
- function getContextLevelIndex(level) {
994
- const order = ['healthy', 'caution', 'warning', 'critical', 'emergency'];
995
- return order.indexOf(level);
996
- }
997
- function shouldShowContextWarning(level) {
998
- if (level === 'healthy')
999
- return false;
1000
- const now = Date.now();
1001
- const timeSinceLastWarning = now - contextState.lastWarningTime;
1002
- const minInterval = level === 'emergency' ? 30000 : 60000; // 30s for emergency, 60s otherwise
1003
- // Always warn on level increase
1004
- if (getContextLevelIndex(level) > getContextLevelIndex(contextState.lastLevel)) {
1005
- return true;
1006
- }
1007
- // Warn again if enough time has passed and we're at critical/emergency
1008
- if ((level === 'critical' || level === 'emergency') && timeSinceLastWarning > minInterval) {
1009
- return true;
1010
- }
1011
- return false;
1012
- }
1013
- function checkAndWarnContextLimit(provider, model, tokens, addMessage) {
1014
- const limit = getModelContextLimit(provider, model);
1015
- const percentage = (tokens / limit) * 100;
1016
- const level = getContextLevel(percentage);
1017
- const used = Math.round(tokens / 1000);
1018
- const limitK = Math.round(limit / 1000);
1019
- if (!shouldShowContextWarning(level))
1020
- return;
1021
- // Update state
1022
- contextState.lastLevel = level;
1023
- contextState.warningCounts[level]++;
1024
- contextState.lastWarningTime = Date.now();
1025
- // Generate warning message based on level
1026
- let message;
1027
- switch (level) {
1028
- case 'emergency':
1029
- message = `\x1b[31m\x1b[1m🚨 EMERGENCY: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
1030
- \x1b[31m Responses WILL be truncated. Take action NOW:\x1b[0m
1031
- \x1b[2m /summarize compact - Auto-compress (recommended)
1032
- /clear - Fresh start
1033
- /branch new "save" - Save and branch\x1b[0m`;
1034
- break;
1035
- case 'critical':
1036
- message = `\x1b[31m🔴 CRITICAL: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
1037
- \x1b[2m Approaching limits. Action recommended:
1038
- /summarize compact | /clear | shorter messages\x1b[0m`;
1039
- break;
1040
- case 'warning':
1041
- message = `\x1b[33m⚠️ WARNING: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
1042
- \x1b[2m Consider: /summarize compact | /clear\x1b[0m`;
1043
- break;
1044
- case 'caution':
1045
- message = `\x1b[36m💡 Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
1046
- \x1b[2m Monitor usage. /context summary for details\x1b[0m`;
1047
- break;
1048
- default:
1049
- return;
1050
- }
1051
- console.log(message + '\n');
1052
- // Also add to UI messages if callback provided (for critical+)
1053
- if (addMessage && (level === 'critical' || level === 'emergency')) {
1054
- const uiMessage = level === 'emergency'
1055
- ? `🚨 EMERGENCY: Context at ${Math.round(percentage)}% - responses will be truncated! Use /summarize compact NOW`
1056
- : `🔴 Context at ${Math.round(percentage)}% - consider /summarize compact`;
1057
- addMessage('system', uiMessage);
1058
- }
1059
- }
1060
- function resetContextWarnings() {
1061
- contextState.lastLevel = 'healthy';
1062
- contextState.warningCounts = { healthy: 0, caution: 0, warning: 0, critical: 0, emergency: 0 };
1063
- contextState.lastWarningTime = 0;
1064
- }
1065
- function getSmartCommandSuggestions(ctx) {
1066
- const { input, hasGitRepo, contextPercentage, currentMode, recentCommands } = ctx;
1067
- if (!input.startsWith('/'))
1068
- return [];
1069
- const suggestions = [];
1070
- const inputLower = input.toLowerCase();
1071
- // All available commands for matching
1072
- const allCommands = [
1073
- '/help', '/clear', '/exit', '/quit',
1074
- '/mode', '/work', '/plan',
1075
- '/provider', '/model', '/models', '/config',
1076
- '/scope', '/add-dir', '/remove-dir', '/find',
1077
- '/summarize', '/context', '/cost', '/session',
1078
- '/debug', '/keys', '/unstick', '/flush',
1079
- '/branch', '/branches', '/switch',
1080
- '/save', '/load', '/sessions',
1081
- '/git', '/run', '/set', '/confirm',
1082
- ];
1083
- // Context-aware prioritization
1084
- const prioritized = [];
1085
- // High context? Suggest compaction commands first
1086
- if (contextPercentage > 70) {
1087
- prioritized.push('/summarize compact', '/clear', '/branch new');
1088
- }
1089
- // Mode-specific suggestions
1090
- if (currentMode === 'plan') {
1091
- prioritized.push('/mode hybrid', '/work');
1092
- }
1093
- else if (currentMode === 'work') {
1094
- prioritized.push('/mode hybrid', '/plan');
1095
- }
1096
- // Git repo? Suggest git commands
1097
- if (hasGitRepo) {
1098
- prioritized.push('/git status', '/git diff', '/git add', '/git commit');
1099
- }
1100
- // Add recent commands (deduplicated)
1101
- for (const cmd of recentCommands.slice(-5)) {
1102
- if (cmd.startsWith('/') && !prioritized.includes(cmd)) {
1103
- prioritized.push(cmd);
1104
- }
1105
- }
1106
- // Filter by what user is typing
1107
- const matchingPrioritized = prioritized.filter(cmd => cmd.toLowerCase().startsWith(inputLower));
1108
- const matchingAll = allCommands.filter(cmd => cmd.toLowerCase().startsWith(inputLower) && !matchingPrioritized.includes(cmd));
1109
- suggestions.push(...matchingPrioritized, ...matchingAll);
1110
- return suggestions.slice(0, 6);
1111
- }
1112
- function StatusBar({ provider, model, stats, mode, contextTokens, }) {
1113
- const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
1114
- const formatCost = (c) => c < 0.01 ? '<$0.01' : `$${c.toFixed(2)}`;
1115
- const displayModel = model.length > 25 ? model.slice(0, 22) + '...' : model;
1116
- const modeConfig = MODE_CONFIG[mode];
1117
- // Context usage indicator - uses model's actual context length from API
1118
- const contextLimit = getModelContextLimit(provider, model);
1119
- const contextPct = Math.min(100, Math.round((contextTokens / contextLimit) * 100));
1120
- const contextColor = contextPct > 80 ? 'red' : contextPct > 50 ? 'yellow' : 'green';
1121
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Text, { dimColor: true, children: [modeConfig.icon, " ", modeConfig.label, ' │ ', provider, ":", displayModel, ' │ ', _jsxs(Text, { color: contextColor, children: [formatTokens(contextTokens), "/", formatTokens(contextLimit)] }), ' │ ', formatTokens(stats.inputTokens + stats.outputTokens), " used", ' │ ', formatCost(stats.cost), ' │ ', _jsx(Text, { dimColor: true, children: "Esc: exit" })] })] }));
1122
- }
1123
- // ============================================================================
1124
- // Main Chat Component
1125
- // ============================================================================
1126
- function TerminalChat() {
1127
- const { exit } = useApp();
1128
- const { stdout } = useStdout();
1129
- const width = stdout?.columns || 80;
1130
- // Core state
1131
- const [input, setInput] = useState('');
1132
- const [suggestions, setSuggestions] = useState([]);
1133
- const [messages, setMessages] = useState([]);
1134
- const [isProcessing, setIsProcessing] = useState(false);
1135
- const [thinkingState, setThinkingState] = useState(null);
1136
- const [streamingResponse, setStreamingResponse] = useState('');
1137
- const [activityState, setActivityState] = useState(null);
1138
- // Input history for up/down arrow navigation
1139
- const [inputHistory, setInputHistory] = useState([]);
1140
- const [historyIndex, setHistoryIndex] = useState(-1);
1141
- const [savedInput, setSavedInput] = useState(''); // Save current input when navigating
1142
- // Smart suggestions context
1143
- const [hasGitRepo] = useState(() => {
1144
- try {
1145
- return fs.existsSync('.git') || fs.existsSync('../.git');
1146
- }
1147
- catch {
1148
- return false;
1149
- }
1150
- });
1151
- const recentCommands = React.useMemo(() => inputHistory.filter(cmd => cmd.startsWith('/')).slice(-10), [inputHistory]);
1152
- // Clear suggestions when input changes significantly
1153
- const handleInputChange = useCallback((newValue) => {
1154
- setInput(newValue);
1155
- // Clear suggestions if user clears input or submits
1156
- if (!newValue || !newValue.startsWith('/')) {
1157
- setSuggestions([]);
1158
- }
1159
- // Reset history navigation when user types
1160
- setHistoryIndex(-1);
1161
- }, []);
1162
- // Navigate input history
1163
- const navigateHistory = useCallback((direction) => {
1164
- if (inputHistory.length === 0)
1165
- return;
1166
- if (direction === 'up') {
1167
- if (historyIndex === -1) {
1168
- // Save current input before navigating
1169
- setSavedInput(input);
1170
- setHistoryIndex(inputHistory.length - 1);
1171
- setInput(inputHistory[inputHistory.length - 1]);
1172
- }
1173
- else if (historyIndex > 0) {
1174
- setHistoryIndex(historyIndex - 1);
1175
- setInput(inputHistory[historyIndex - 1]);
1176
- }
1177
- }
1178
- else {
1179
- if (historyIndex === -1)
1180
- return;
1181
- if (historyIndex < inputHistory.length - 1) {
1182
- setHistoryIndex(historyIndex + 1);
1183
- setInput(inputHistory[historyIndex + 1]);
1184
- }
1185
- else {
1186
- // Return to saved input
1187
- setHistoryIndex(-1);
1188
- setInput(savedInput);
1189
- }
1190
- }
1191
- }, [inputHistory, historyIndex, input, savedInput]);
1192
- // Add to history when submitting
1193
- const addToHistory = useCallback((value) => {
1194
- if (value.trim() && (inputHistory.length === 0 || inputHistory[inputHistory.length - 1] !== value)) {
1195
- setInputHistory(prev => [...prev.slice(-100), value]); // Keep last 100 entries
1196
- }
1197
- setHistoryIndex(-1);
1198
- setSavedInput('');
1199
- }, [inputHistory]);
1200
- // Config state
1201
- // Use lazy initializers to avoid calling config.get() on every render
1202
- const [provider, setProvider] = useState(() => config.get('defaultProvider'));
1203
- const [model, setModel] = useState(() => config.get('defaultModel'));
1204
- const [persona, setPersona] = useState(() => config.get('persona'));
1205
- const [mode, setMode] = useState('hybrid'); // Default to hybrid mode
1206
- const [confirmMode, setConfirmMode] = useState(true); // Require confirmation for risky ops
1207
- const [layout, setLayout] = useState(() => config.get('layout') || 'response-bottom');
1208
- const [density, setDensity] = useState(() => config.get('density') || 'normal');
1209
- const [collapseSettings, setCollapseSettings] = useState(() => ({
1210
- collapseTools: config.get('collapseTools') ?? false,
1211
- collapseThinking: config.get('collapseThinking') ?? false,
1212
- toolDisplayLimit: config.get('toolDisplayLimit') ?? 0,
1213
- }));
1214
- // Modal state
1215
- const [modalMode, setModalMode] = useState('none');
1216
- const [pendingComplexPrompt, setPendingComplexPrompt] = useState(null);
1217
- const [previousSession, setPreviousSession] = useState(null);
1218
- const [pendingToolCall, setPendingToolCall] = useState(null);
1219
- const [availableModels, setAvailableModels] = useState([]);
1220
- const [availableSessions, setAvailableSessions] = useState([]);
1221
- const [latestVersion, setLatestVersion] = useState(null);
1222
- // Stats
1223
- const [stats, setStats] = useState({
1224
- inputTokens: 0,
1225
- outputTokens: 0,
1226
- cost: 0,
1227
- messageCount: 0,
1228
- });
1229
- const [contextTokens, setContextTokens] = useState(0);
1230
- // Message queue for human-in-the-loop feedback during processing
1231
- const [queuedMessages, setQueuedMessages] = useState([]);
1232
- const queuedMessagesRef = useRef([]); // Ref to avoid stale closure in runAgent
1233
- const [queueInput, setQueueInput] = useState('');
1234
- const [editingQueueIndex, setEditingQueueIndex] = useState(null);
1235
- // Keep ref in sync with state
1236
- useEffect(() => {
1237
- queuedMessagesRef.current = queuedMessages;
1238
- }, [queuedMessages]);
1239
- const undoStack = useRef([]);
1240
- const redoStack = useRef([]);
1241
- const MAX_UNDO_HISTORY = 10;
1242
- const [bookmarks, setBookmarks] = useState([]);
1243
- const [templates, setTemplates] = useState([]);
1244
- // Save state before changes (call before modifying messages)
1245
- const saveUndoState = useCallback(() => {
1246
- undoStack.current.push({
1247
- messages: [...messages],
1248
- llmMessages: [...llmMessages.current],
1249
- timestamp: new Date(),
1250
- });
1251
- // Limit stack size
1252
- if (undoStack.current.length > MAX_UNDO_HISTORY) {
1253
- undoStack.current.shift();
1254
- }
1255
- // Clear redo stack on new action
1256
- redoStack.current = [];
1257
- }, [messages]);
1258
- // LLM conversation history
1259
- const llmMessages = useRef([
1260
- { role: 'system', content: getSystemPrompt(persona) }
1261
- ]);
1262
- // Estimate context tokens (rough: ~4 chars per token)
1263
- const estimateContextTokens = useCallback(() => {
1264
- let chars = 0;
1265
- for (const msg of llmMessages.current) {
1266
- if (typeof msg.content === 'string') {
1267
- chars += msg.content.length;
1268
- }
1269
- else if (Array.isArray(msg.content)) {
1270
- for (const block of msg.content) {
1271
- if (block.type === 'text') {
1272
- chars += block.text.length;
1273
- }
1274
- else if (block.type === 'image') {
1275
- chars += 1000; // Images count as ~250 tokens
1276
- }
1277
- }
1278
- }
1279
- }
1280
- return Math.round(chars / 4);
1281
- }, []);
1282
- // Session state
1283
- const sessionRef = useRef(null);
1284
- const [autoRoute, setAutoRoute] = useState(false); // Auto model routing
1285
- const [memoryLoaded, setMemoryLoaded] = useState(false);
1286
- // Ralph Wiggum loop state
1287
- const [loopActive, setLoopActive] = useState(false);
1288
- const [loopPrompt, setLoopPrompt] = useState('');
1289
- const [loopMaxIterations, setLoopMaxIterations] = useState(100);
1290
- const [loopCompletionPromise, setLoopCompletionPromise] = useState();
1291
- const [loopIteration, setLoopIteration] = useState(0);
1292
- const loopCancelledRef = useRef(false);
1293
- // Initialize session and load memory on mount
1294
- useEffect(() => {
1295
- const cwd = process.cwd();
1296
- // Check for existing session with messages
1297
- const existingSessions = storage.listSessions(5);
1298
- const recentSession = existingSessions.find(s => s.projectPath === cwd &&
1299
- s.messageCount > 0 &&
1300
- Date.now() - new Date(s.lastAccessedAt).getTime() < 24 * 60 * 60 * 1000 // Within 24 hours
1301
- );
1302
- if (recentSession && !sessionRef.current) {
1303
- // Offer to resume
1304
- setPreviousSession({
1305
- projectName: recentSession.projectName,
1306
- lastAccessedAt: recentSession.lastAccessedAt,
1307
- messageCount: recentSession.messageCount,
1308
- });
1309
- setModalMode('session-resume');
1310
- }
1311
- const session = storage.getOrCreateSession(cwd);
1312
- sessionRef.current = session;
1313
- // Load memory context into system prompt
1314
- if (!memoryLoaded) {
1315
- const cwd = process.cwd();
1316
- const memoryContext = memory.buildMemoryContext(cwd);
1317
- if (memoryContext.trim()) {
1318
- // Append memory context to system prompt
1319
- const currentSystem = llmMessages.current[0];
1320
- if (currentSystem && currentSystem.role === 'system') {
1321
- const systemContent = typeof currentSystem.content === 'string'
1322
- ? currentSystem.content
1323
- : '';
1324
- llmMessages.current[0] = {
1325
- role: 'system',
1326
- content: systemContent + '\n\n--- Project Context ---\n' + memoryContext,
1327
- };
1328
- }
1329
- }
1330
- setMemoryLoaded(true);
1331
- // Execute session start hooks
1332
- hooks.executeHooks('session-start', {}).catch((err) => {
1333
- debugLog('hooks', 'session-start hook failed:', err instanceof Error ? err.message : err);
1334
- });
1335
- // Load templates from storage
1336
- const savedTemplates = storage.getTemplates();
1337
- if (savedTemplates.length > 0) {
1338
- setTemplates(savedTemplates.map(t => ({
1339
- name: t.name,
1340
- prompt: t.prompt,
1341
- createdAt: new Date(t.createdAt),
1342
- })));
1343
- }
1344
- // Pre-warm model cache in background for faster model switching
1345
- preWarmModelCache().catch((err) => {
1346
- debugLog('cache', 'model cache pre-warm failed:', err instanceof Error ? err.message : err);
1347
- });
1348
- }
1349
- }, [memoryLoaded]);
1350
- // Derived values
1351
- const actualProvider = selectProvider(provider);
1352
- const actualModel = model || DEFAULT_MODELS[actualProvider];
1353
- const isModalActive = modalMode !== 'none';
1354
- // Add message helper
1355
- const addMessage = useCallback((type, content) => {
1356
- setMessages(prev => [...prev, {
1357
- id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
1358
- type,
1359
- content
1360
- }]);
1361
- // Persist user and assistant messages to storage for session history
1362
- if (type === 'user' || type === 'assistant') {
1363
- storage.addChatMessage({ role: type, content });
1364
- }
1365
- }, []);
1366
- // Handler to edit or delete a queued message
1367
- const handleEditQueuedMessage = useCallback((index, newMsg) => {
1368
- if (newMsg === '') {
1369
- // Delete the message
1370
- setQueuedMessages(prev => prev.filter((_, i) => i !== index));
1371
- addMessage('system', `🗑️ Deleted queued message #${index + 1}`);
1372
- }
1373
- else {
1374
- // Update the message
1375
- setQueuedMessages(prev => prev.map((msg, i) => i === index ? newMsg : msg));
1376
- addMessage('system', `✏️ Updated queued message #${index + 1}`);
1377
- }
1378
- }, [addMessage]);
1379
- // Handle slash commands
1380
- const handleCommand = useCallback(async (cmd) => {
1381
- const parts = cmd.split(/\s+/);
1382
- const command = parts[0].toLowerCase();
1383
- switch (command) {
1384
- case '/help':
1385
- case '/h':
1386
- addMessage('system', `Commands:
1387
- /mode [plan|hybrid|work] - Switch modes (Shift+Tab to cycle)
1388
- /provider [name] - Switch AI provider
1389
- /model [name] - Switch model
1390
- /route [on|off|test] - Auto model routing by complexity
1391
- /persona [name] - Switch personality
1392
- /todo [add|done|list] - Manage TODOs
1393
- /plans [list|view] - View plan history
1394
- /session [list|info] - Session management
1395
- /history [search] - Chat history
1396
- /context [load|summary] - Context management
1397
- /summarize [context|compact] - Summarize/compact context
1398
- /clear - Clear conversation
1399
- /copy - Copy last response to clipboard
1400
- /export [file.md] - Export conversation to markdown
1401
- /edit - Edit and resend last message
1402
- /undo - Undo last action (up to 10 steps)
1403
- /redo - Redo undone action
1404
- /confirm [on|off] - Toggle risky op confirmation
1405
- /profile [name|save|del] - Switch/save/delete profiles
1406
- /mcp [add|remove|tools] - Manage MCP servers
1407
- /skills [add|remove] - Manage agent skills
1408
- /memory [init|add|show] - Project memory (CALLIOPE.md)
1409
- /project [init|show|run] - Project config (.calliope)
1410
- /find <pattern> - Fuzzy file search
1411
- /branch [new|switch] - Conversation branches
1412
- /theme [name|list] - Color themes
1413
- /hooks [list|add] - Pre/post tool hooks
1414
- /search <query> - Search conversation
1415
- /status - Show status
1416
- /config - Show config
1417
- /layout [name] - Switch UI layout (classic/split/etc)
1418
- /density [normal|compact] - Set display density
1419
- /collapse [tools|all|off] - Collapse/expand tool output
1420
- /upgrade - Check for updates
1421
- /agents - Show sub-agent status (--agterm mode)
1422
- /scope [details|reset] - Show/manage file access scope
1423
- /add-dir <path> - Add directory to allowed scope
1424
- /remove-dir <path> - Remove directory from scope
1425
- /template [save|use|del] - Manage prompt templates
1426
- /cost - Show cost tracking summary
1427
- /bookmark [name] - Create bookmark at current point
1428
- /bookmark list - List all bookmarks
1429
- /bookmark goto <n> - Jump to bookmark
1430
- /queue [show|clear|flush] - Manage queued messages
1431
- /flush - Force-process queued msgs (unstick)
1432
- /debug [on|off] - Show state / toggle debug logging
1433
- /unstick - Emergency reset of processing state
1434
- /work - Quick switch to work mode
1435
- /plan - Quick switch to plan mode
1436
- /keys or /? - Show keyboard shortcuts
1437
- /resume [n] - Resume previous session (load n messages)
1438
- /exit - Exit
1439
-
1440
- File references: @filename, ./path, /absolute/path
1441
- Modes: 📋 Plan | 🔄 Hybrid | 🔧 Work
1442
- Queue: ↑/↓ edit, Ctrl+D delete, !msg to send directly
1443
- Auto-route: ${autoRoute ? 'ON' : 'OFF'}${moduleAgtermEnabled ? '\nAGTerm: ON (spawn_agent, check_agent tools available)' : ''}`);
1444
- break;
1445
- case '/provider':
1446
- case '/p':
1447
- if (parts[1]) {
1448
- const p = parts[1].toLowerCase();
1449
- setProvider(p);
1450
- addMessage('system', `Provider: ${selectProvider(p)}`);
1451
- }
1452
- else {
1453
- addMessage('system', `Provider: ${actualProvider} | Available: ${getAvailableProviders().join(', ')}`);
1454
- }
1455
- break;
1456
- case '/model':
1457
- case '/m':
1458
- if (parts[1]) {
1459
- setModel(parts[1]);
1460
- addMessage('system', `Model: ${parts[1]}`);
1461
- }
1462
- else {
1463
- addMessage('system', `Discovering models for ${actualProvider}...`);
1464
- try {
1465
- const models = await getAvailableModels(actualProvider);
1466
- if (models.length > 0) {
1467
- setAvailableModels(models);
1468
- setModalMode('model');
1469
- }
1470
- else {
1471
- addMessage('error', 'No models found');
1472
- }
1473
- }
1474
- catch (e) {
1475
- addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
1476
- }
1477
- }
1478
- break;
1479
- case '/models':
1480
- addMessage('system', `Discovering models for ${actualProvider}...`);
1481
- try {
1482
- const models = await getAvailableModels(actualProvider);
1483
- if (models.length > 0) {
1484
- setAvailableModels(models);
1485
- setModalMode('model');
1486
- }
1487
- else {
1488
- addMessage('error', 'No models found');
1489
- }
1490
- }
1491
- catch (e) {
1492
- addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
1493
- }
1494
- break;
1495
- case '/mode':
1496
- if (parts[1] && ['plan', 'hybrid', 'work'].includes(parts[1])) {
1497
- const m = parts[1];
1498
- setMode(m);
1499
- addMessage('system', `Mode: ${MODE_CONFIG[m].icon} ${MODE_CONFIG[m].label} - ${MODE_CONFIG[m].description}`);
1500
- }
1501
- else {
1502
- const currentConfig = MODE_CONFIG[mode];
1503
- addMessage('system', `Mode: ${currentConfig.icon} ${currentConfig.label}\nOptions: plan (📋), hybrid (🔄), work (🔧)\nUse Shift+Tab to cycle`);
1504
- }
1505
- break;
1506
- case '/persona':
1507
- if (parts[1] && ['calliope', 'professional', 'minimal'].includes(parts[1])) {
1508
- const p = parts[1];
1509
- setPersona(p);
1510
- llmMessages.current = [{ role: 'system', content: getSystemPrompt(p) }];
1511
- addMessage('system', `Persona: ${p}`);
1512
- }
1513
- else {
1514
- addMessage('system', `Persona: ${persona} | Options: calliope, professional, minimal`);
1515
- }
1516
- break;
1517
- case '/clear':
1518
- case '/c':
1519
- setMessages([]);
1520
- llmMessages.current = [{ role: 'system', content: getSystemPrompt(persona) }];
1521
- setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
1522
- resetContextWarnings(); // Reset context warning state
1523
- break;
1524
- case '/copy': {
1525
- // Copy last assistant response to clipboard
1526
- const lastAssistant = [...messages].reverse().find(m => m.type === 'assistant');
1527
- if (lastAssistant) {
1528
- try {
1529
- const { execSync } = await import('child_process');
1530
- // Try different clipboard commands based on platform
1531
- const content = lastAssistant.content;
1532
- if (process.platform === 'darwin') {
1533
- execSync('pbcopy', { input: content });
1534
- }
1535
- else if (process.platform === 'win32') {
1536
- execSync('clip', { input: content });
1537
- }
1538
- else {
1539
- // Linux - try xclip, xsel, or wl-copy
1540
- try {
1541
- execSync('xclip -selection clipboard', { input: content });
1542
- }
1543
- catch {
1544
- try {
1545
- execSync('xsel --clipboard --input', { input: content });
1546
- }
1547
- catch {
1548
- execSync('wl-copy', { input: content });
1549
- }
1550
- }
1551
- }
1552
- addMessage('system', '✓ Copied to clipboard');
1553
- }
1554
- catch (e) {
1555
- addMessage('error', `Clipboard not available: ${e instanceof Error ? e.message : String(e)}`);
1556
- }
1557
- }
1558
- else {
1559
- addMessage('system', 'No assistant message to copy');
1560
- }
1561
- break;
1562
- }
1563
- case '/export': {
1564
- // Export conversation to markdown
1565
- const filename = parts[1] || `calliope-export-${Date.now()}.md`;
1566
- const fs = await import('fs');
1567
- const path = await import('path');
1568
- let markdown = `# Calliope Conversation Export\n\n`;
1569
- markdown += `**Date:** ${new Date().toLocaleString()}\n`;
1570
- markdown += `**Provider:** ${actualProvider}\n`;
1571
- markdown += `**Model:** ${actualModel}\n\n---\n\n`;
1572
- for (const msg of messages) {
1573
- if (msg.type === 'user') {
1574
- markdown += `## 👤 User\n\n${msg.content}\n\n`;
1575
- }
1576
- else if (msg.type === 'assistant') {
1577
- markdown += `## 🤖 Assistant\n\n${msg.content}\n\n`;
1578
- }
1579
- else if (msg.type === 'tool') {
1580
- markdown += `> 🔧 Tool: ${msg.content}\n\n`;
1581
- }
1582
- else if (msg.type === 'system') {
1583
- markdown += `> ℹ️ ${msg.content}\n\n`;
1584
- }
1585
- else if (msg.type === 'error') {
1586
- markdown += `> ⚠️ Error: ${msg.content}\n\n`;
1587
- }
1588
- }
1589
- const filepath = path.resolve(process.cwd(), filename);
1590
- fs.writeFileSync(filepath, markdown);
1591
- addMessage('system', `✓ Exported to ${filename}`);
1592
- break;
1593
- }
1594
- case '/edit': {
1595
- // Edit last user message
1596
- const lastUserIdx = [...messages].reverse().findIndex(m => m.type === 'user');
1597
- if (lastUserIdx >= 0) {
1598
- const lastUser = messages[messages.length - 1 - lastUserIdx];
1599
- setInput(lastUser.content);
1600
- addMessage('system', 'Edit the message above and press Enter to resend');
1601
- }
1602
- else {
1603
- addMessage('system', 'No user message to edit');
1604
- }
1605
- break;
1606
- }
1607
- case '/undo': {
1608
- if (undoStack.current.length === 0) {
1609
- addMessage('system', 'Nothing to undo.');
1610
- break;
1611
- }
1612
- // Save current state to redo stack
1613
- redoStack.current.push({
1614
- messages: [...messages],
1615
- llmMessages: [...llmMessages.current],
1616
- timestamp: new Date(),
1617
- });
1618
- // Restore previous state
1619
- const prevState = undoStack.current.pop();
1620
- setMessages(prevState.messages);
1621
- llmMessages.current = prevState.llmMessages;
1622
- setContextTokens(estimateContextTokens());
1623
- addMessage('system', `✓ Undone (${undoStack.current.length} more available)`);
1624
- break;
1625
- }
1626
- case '/redo': {
1627
- if (redoStack.current.length === 0) {
1628
- addMessage('system', 'Nothing to redo.');
1629
- break;
1630
- }
1631
- // Save current state to undo stack
1632
- undoStack.current.push({
1633
- messages: [...messages],
1634
- llmMessages: [...llmMessages.current],
1635
- timestamp: new Date(),
1636
- });
1637
- // Restore redo state
1638
- const redoState = redoStack.current.pop();
1639
- setMessages(redoState.messages);
1640
- llmMessages.current = redoState.llmMessages;
1641
- setContextTokens(estimateContextTokens());
1642
- addMessage('system', `✓ Redone (${redoStack.current.length} more available)`);
1643
- break;
1644
- }
1645
- case '/status':
1646
- case '/s':
1647
- addMessage('system', `${actualProvider}:${actualModel} | ${stats.messageCount} msgs | ${stats.inputTokens + stats.outputTokens} tokens`);
1648
- break;
1649
- case '/config':
1650
- addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}`);
1651
- break;
1652
- case '/agents':
1653
- if (!moduleAgtermEnabled) {
1654
- addMessage('system', 'AGTerm mode not enabled. Start with --agterm flag to unlock multi-agent features.');
1655
- }
1656
- else {
1657
- addMessage('system', getAgentStatusReport());
1658
- }
1659
- break;
1660
- case '/set': {
1661
- // /set <key> <value>
1662
- const key = parts[1];
1663
- const value = parts.slice(2).join(' ');
1664
- if (!key || !value) {
1665
- addMessage('system', `Usage: /set <key> <value>
1666
- Available keys:
1667
- maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
1668
- persona <name> - calliope, professional, minimal
1669
- fancyOutput <bool> - true/false`);
1670
- break;
1671
- }
1672
- try {
1673
- if (key === 'maxIterations') {
1674
- const num = parseInt(value, 10);
1675
- if (isNaN(num) || num < 1 || num > 10000) {
1676
- addMessage('error', 'maxIterations must be 1-10000');
1677
- break;
1678
- }
1679
- config.set('maxIterations', num);
1680
- addMessage('system', `✓ maxIterations set to ${num}`);
1681
- }
1682
- else if (key === 'persona') {
1683
- if (!['calliope', 'professional', 'minimal'].includes(value)) {
1684
- addMessage('error', 'persona must be: calliope, professional, or minimal');
1685
- break;
1686
- }
1687
- config.set('persona', value);
1688
- setPersona(value);
1689
- addMessage('system', `✓ persona set to ${value}`);
1690
- }
1691
- else if (key === 'fancyOutput') {
1692
- const bool = value === 'true';
1693
- config.set('fancyOutput', bool);
1694
- addMessage('system', `✓ fancyOutput set to ${bool}`);
1695
- }
1696
- else {
1697
- addMessage('error', `Unknown config key: ${key}`);
1698
- }
1699
- }
1700
- catch (err) {
1701
- addMessage('error', `Failed to set ${key}: ${err instanceof Error ? err.message : String(err)}`);
1702
- }
1703
- break;
1704
- }
1705
- case '/setup':
1706
- addMessage('system', 'Run `calliope --setup` to reconfigure.');
1707
- break;
1708
- case '/layout': {
1709
- // /layout [classic|response-top|response-bottom|split]
1710
- const layoutArg = parts[1];
1711
- if (!layoutArg) {
1712
- addMessage('system', `Current layout: ${layout}
1713
-
1714
- Available layouts:
1715
- classic - Everything in chronological order
1716
- response-top - Calliope response at top, tools below
1717
- response-bottom - Tools at top, response at bottom (default)
1718
- split - Side by side: tools left, response right
1719
-
1720
- Usage: /layout <name>`);
1721
- break;
1722
- }
1723
- const validLayouts = ['classic', 'response-top', 'response-bottom', 'split'];
1724
- if (!validLayouts.includes(layoutArg)) {
1725
- addMessage('error', `Invalid layout. Choose: ${validLayouts.join(', ')}`);
1726
- break;
1727
- }
1728
- config.set('layout', layoutArg);
1729
- setLayout(layoutArg);
1730
- addMessage('system', `✓ Layout set to: ${layoutArg}`);
1731
- break;
1732
- }
1733
- case '/density': {
1734
- // /density [normal|compact]
1735
- const densityArg = parts[1];
1736
- if (!densityArg) {
1737
- addMessage('system', `Current density: ${density}
1738
-
1739
- Available densities:
1740
- normal - Standard spacing
1741
- compact - Reduced whitespace for more info
1742
-
1743
- Usage: /density <normal|compact>`);
1744
- break;
1745
- }
1746
- const validDensities = ['normal', 'compact'];
1747
- if (!validDensities.includes(densityArg)) {
1748
- addMessage('error', `Invalid density. Choose: normal, compact`);
1749
- break;
1750
- }
1751
- config.set('density', densityArg);
1752
- setDensity(densityArg);
1753
- addMessage('system', `✓ Density set to: ${densityArg}`);
1754
- break;
1755
- }
1756
- case '/collapse': {
1757
- // /collapse [tools|thinking|all|off] [limit N]
1758
- const subCmd = parts[1];
1759
- if (!subCmd) {
1760
- addMessage('system', `Collapse settings:
1761
- collapseTools: ${collapseSettings.collapseTools}
1762
- collapseThinking: ${collapseSettings.collapseThinking}
1763
- toolDisplayLimit: ${collapseSettings.toolDisplayLimit} (0 = all expanded)
1764
-
1765
- Usage:
1766
- /collapse tools - Toggle tool output collapsing
1767
- /collapse thinking - Toggle thinking block collapsing
1768
- /collapse all - Collapse both tools and thinking
1769
- /collapse off - Expand everything
1770
- /collapse limit <N> - Show last N tools expanded (0 = all)`);
1771
- break;
1772
- }
1773
- if (subCmd === 'tools') {
1774
- const newVal = !collapseSettings.collapseTools;
1775
- config.set('collapseTools', newVal);
1776
- setCollapseSettings(prev => ({ ...prev, collapseTools: newVal }));
1777
- addMessage('system', `✓ collapseTools set to ${newVal}`);
1778
- }
1779
- else if (subCmd === 'thinking') {
1780
- const newVal = !collapseSettings.collapseThinking;
1781
- config.set('collapseThinking', newVal);
1782
- setCollapseSettings(prev => ({ ...prev, collapseThinking: newVal }));
1783
- addMessage('system', `✓ collapseThinking set to ${newVal}`);
1784
- }
1785
- else if (subCmd === 'all') {
1786
- config.set('collapseTools', true);
1787
- config.set('collapseThinking', true);
1788
- setCollapseSettings(prev => ({ ...prev, collapseTools: true, collapseThinking: true }));
1789
- addMessage('system', '✓ Collapsing tools and thinking');
1790
- }
1791
- else if (subCmd === 'off') {
1792
- config.set('collapseTools', false);
1793
- config.set('collapseThinking', false);
1794
- setCollapseSettings(prev => ({ ...prev, collapseTools: false, collapseThinking: false }));
1795
- addMessage('system', '✓ Expanding all output');
1796
- }
1797
- else if (subCmd === 'limit') {
1798
- const limit = parseInt(parts[2], 10);
1799
- if (isNaN(limit) || limit < 0 || limit > 100) {
1800
- addMessage('error', 'Limit must be 0-100');
1801
- break;
1802
- }
1803
- config.set('toolDisplayLimit', limit);
1804
- setCollapseSettings(prev => ({ ...prev, toolDisplayLimit: limit }));
1805
- addMessage('system', `✓ toolDisplayLimit set to ${limit}`);
1806
- }
1807
- else {
1808
- addMessage('error', 'Unknown collapse option. Use: tools, thinking, all, off, or limit <N>');
1809
- }
1810
- break;
1811
- }
1812
- case '/loop': {
1813
- // Parse /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
1814
- const loopArgs = parts.slice(1).join(' ');
1815
- const maxIterMatch = loopArgs.match(/--max-iterations\s+(\d+)/);
1816
- const completionMatch = loopArgs.match(/--completion-promise\s+"([^"]+)"/);
1817
- let prompt = loopArgs
1818
- .replace(/--max-iterations\s+\d+/, '')
1819
- .replace(/--completion-promise\s+"[^"]+"/, '')
1820
- .trim();
1821
- // Handle quoted prompt
1822
- const quotedMatch = prompt.match(/^"([^"]+)"$/);
1823
- if (quotedMatch)
1824
- prompt = quotedMatch[1];
1825
- if (!prompt) {
1826
- addMessage('system', `Usage: /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
1827
- Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"`);
1828
- break;
1829
- }
1830
- // Start the loop
1831
- setLoopActive(true);
1832
- setLoopPrompt(prompt);
1833
- setLoopMaxIterations(maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100);
1834
- setLoopCompletionPromise(completionMatch ? completionMatch[1] : undefined);
1835
- setLoopIteration(0);
1836
- loopCancelledRef.current = false;
1837
- addMessage('system', `🔄 Ralph Wiggum Loop Started
1838
- Prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"
1839
- Max iterations: ${maxIterMatch ? maxIterMatch[1] : '100'}
1840
- ${completionMatch ? `Completion promise: "${completionMatch[1]}"` : 'No completion promise (runs until max iterations)'}
1841
- Use /cancel-loop to stop`);
1842
- // Start the loop execution (non-blocking)
1843
- runLoop(prompt, maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100, completionMatch?.[1]);
1844
- break;
1845
- }
1846
- case '/cancel-loop':
1847
- case '/stop':
1848
- if (loopActive) {
1849
- loopCancelledRef.current = true;
1850
- setLoopActive(false);
1851
- addMessage('system', '🛑 Loop cancelled');
1852
- }
1853
- else {
1854
- addMessage('system', 'No active loop to cancel');
1855
- }
1856
- break;
1857
- case '/confirm':
1858
- if (parts[1] === 'on') {
1859
- setConfirmMode(true);
1860
- addMessage('system', '✓ Confirmation mode ON - will ask before risky operations');
1861
- }
1862
- else if (parts[1] === 'off') {
1863
- setConfirmMode(false);
1864
- addMessage('system', '⚠️ Confirmation mode OFF - risky operations will auto-execute');
1865
- }
1866
- else {
1867
- addMessage('system', `Confirm mode: ${confirmMode ? 'ON' : 'OFF'}\nUsage: /confirm [on|off]`);
1868
- }
1869
- break;
1870
- case '/profile': {
1871
- const subCmd = parts[1];
1872
- if (subCmd === 'list' || !subCmd) {
1873
- const profiles = config.listProfiles();
1874
- const active = config.getActiveProfile();
1875
- const list = profiles.map(p => {
1876
- const marker = p.name === active ? '→ ' : ' ';
1877
- const tag = p.builtin ? '(built-in)' : '(custom)';
1878
- return `${marker}${p.name}: ${p.profile.provider}/${p.profile.model || 'default'} ${tag}`;
1879
- }).join('\n');
1880
- addMessage('system', `Profiles:\n${list}\n\nUsage: /profile <name> | /profile save <name>`);
1881
- }
1882
- else if (subCmd === 'save' && parts[2]) {
1883
- const name = parts[2];
1884
- config.saveProfile(name, {
1885
- provider: provider,
1886
- model: model,
1887
- persona: persona,
1888
- confirmMode: confirmMode,
1889
- });
1890
- addMessage('system', `✓ Saved profile: ${name}`);
1891
- }
1892
- else if (subCmd === 'delete' && parts[2]) {
1893
- const name = parts[2];
1894
- if (config.deleteProfile(name)) {
1895
- addMessage('system', `✓ Deleted profile: ${name}`);
1896
- }
1897
- else {
1898
- addMessage('error', `Cannot delete profile: ${name} (built-in or not found)`);
1899
- }
1900
- }
1901
- else {
1902
- // Load profile
1903
- const profile = config.getProfile(subCmd);
1904
- if (profile) {
1905
- setProvider(profile.provider);
1906
- if (profile.model)
1907
- setModel(profile.model);
1908
- setPersona(profile.persona);
1909
- if (profile.confirmMode !== undefined)
1910
- setConfirmMode(profile.confirmMode);
1911
- config.setActiveProfile(subCmd);
1912
- addMessage('system', `✓ Loaded profile: ${subCmd} (${profile.provider}/${profile.model || 'default'})`);
1913
- }
1914
- else {
1915
- addMessage('error', `Profile not found: ${subCmd}\nBuilt-in: fast, smart, cheap, local`);
1916
- }
1917
- }
1918
- break;
1919
- }
1920
- case '/mcp': {
1921
- const subCmd = parts[1];
1922
- if (subCmd === 'list' || !subCmd) {
1923
- const servers = mcp.listServers();
1924
- if (servers.length === 0) {
1925
- addMessage('system', 'No MCP servers registered.\n\nUsage:\n /mcp add <url> - Register MCP server\n /mcp remove <id> - Remove server');
1926
- }
1927
- else {
1928
- const list = servers.map(s => {
1929
- const status = s.status === 'connected' ? '🟢' : s.status === 'error' ? '🔴' : '⚪';
1930
- return `${status} ${s.name} (${s.tools.length} tools)\n ${s.url}`;
1931
- }).join('\n\n');
1932
- addMessage('system', `MCP Servers:\n\n${list}`);
1933
- }
1934
- }
1935
- else if (subCmd === 'add' && parts[2]) {
1936
- const url = parts[2];
1937
- addMessage('system', `Registering MCP server: ${url}...`);
1938
- try {
1939
- const server = await mcp.registerServer(url);
1940
- addMessage('system', `✓ Registered: ${server.name} (${server.tools.length} tools)`);
1941
- }
1942
- catch (e) {
1943
- addMessage('error', `Failed to register: ${e instanceof Error ? e.message : String(e)}`);
1944
- }
1945
- }
1946
- else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
1947
- if (mcp.unregisterServer(parts[2])) {
1948
- addMessage('system', '✓ Server removed');
1949
- }
1950
- else {
1951
- addMessage('error', 'Server not found');
1952
- }
1953
- }
1954
- else if (subCmd === 'refresh') {
1955
- const servers = mcp.listServers();
1956
- let connected = 0;
1957
- for (const s of servers) {
1958
- const updated = await mcp.refreshServer(s.id);
1959
- if (updated?.status === 'connected')
1960
- connected++;
1961
- }
1962
- addMessage('system', `Refreshed ${servers.length} servers (${connected} connected)`);
1963
- }
1964
- else if (subCmd === 'tools') {
1965
- const tools = mcp.getMCPTools();
1966
- if (tools.length === 0) {
1967
- addMessage('system', 'No MCP tools available. Add servers with /mcp add <url>');
1968
- }
1969
- else {
1970
- const list = tools.map(t => `• ${t.name}\n ${t.description}`).join('\n\n');
1971
- addMessage('system', `MCP Tools:\n\n${list}`);
1972
- }
1973
- }
1974
- else {
1975
- addMessage('system', 'Usage: /mcp [list|add <url>|remove <id>|refresh|tools]');
1976
- }
1977
- break;
1978
- }
1979
- case '/skills': {
1980
- const subCmd = parts[1];
1981
- if (subCmd === 'list' || !subCmd) {
1982
- const allSkills = skills.getSkills();
1983
- if (allSkills.length === 0) {
1984
- addMessage('system', 'No skills installed.\n\nUsage:\n /skills add <name> - Install from agentskills.io\n /skills add <github-url> - Install from GitHub\n /skills add <path> - Install from local directory');
1985
- }
1986
- else {
1987
- const list = allSkills.map(s => {
1988
- const src = s.source === 'github' ? '(GitHub)' : s.source === 'registry' ? '(agentskills.io)' : '(local)';
1989
- return `• ${s.metadata.name} ${src}\n ${s.metadata.description.substring(0, 80)}...`;
1990
- }).join('\n\n');
1991
- addMessage('system', `Installed Skills:\n\n${list}`);
1992
- }
1993
- }
1994
- else if (subCmd === 'add' && parts[2]) {
1995
- const source = parts[2];
1996
- addMessage('system', `Installing skill: ${source}...`);
1997
- try {
1998
- let skill;
1999
- if (source.startsWith('http')) {
2000
- skill = await skills.installFromGithub(source);
2001
- }
2002
- else if (fs.existsSync(source)) {
2003
- skill = skills.installLocalSkill(source);
2004
- }
2005
- else {
2006
- skill = await skills.installFromRegistry(source);
2007
- }
2008
- if (skill) {
2009
- addMessage('system', `✓ Installed: ${skill.metadata.name}`);
2010
- }
2011
- else {
2012
- addMessage('error', 'Failed to install skill');
2013
- }
2014
- }
2015
- catch (e) {
2016
- addMessage('error', `Failed: ${e instanceof Error ? e.message : String(e)}`);
2017
- }
2018
- }
2019
- else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
2020
- if (skills.uninstallSkill(parts[2])) {
2021
- addMessage('system', '✓ Skill removed');
2022
- }
2023
- else {
2024
- addMessage('error', 'Skill not found');
2025
- }
2026
- }
2027
- else if (subCmd === 'info' && parts[2]) {
2028
- const skill = skills.getSkill(parts[2]);
2029
- if (skill) {
2030
- let info = `# ${skill.metadata.name}\n\n`;
2031
- info += `${skill.metadata.description}\n\n`;
2032
- if (skill.metadata.compatibility)
2033
- info += `Compatibility: ${skill.metadata.compatibility}\n`;
2034
- if (skill.metadata.license)
2035
- info += `License: ${skill.metadata.license}\n`;
2036
- if (skill.sourceUrl)
2037
- info += `Source: ${skill.sourceUrl}\n`;
2038
- addMessage('system', info);
2039
- }
2040
- else {
2041
- addMessage('error', 'Skill not found');
2042
- }
2043
- }
2044
- else {
2045
- addMessage('system', 'Usage: /skills [list|add <source>|remove <name>|info <name>]');
2046
- }
2047
- break;
2048
- }
2049
- case '/memory': {
2050
- const memory = await import('./memory.js');
2051
- const subCmd = parts[1];
2052
- const cwd = process.cwd();
2053
- if (subCmd === 'init') {
2054
- const memPath = memory.initProjectMemory(cwd);
2055
- addMessage('system', `Created: ${memPath}\nEdit the file to add context and preferences.`);
2056
- }
2057
- else if (subCmd === 'show' || !subCmd) {
2058
- const memPath = memory.findProjectMemory(cwd);
2059
- if (!memPath) {
2060
- addMessage('system', 'No CALLIOPE.md found.\nRun /memory init to create one.');
2061
- }
2062
- else {
2063
- const mem = memory.loadMemory(memPath);
2064
- let info = `Memory: ${memPath}\n\n`;
2065
- if (mem.context.length)
2066
- info += `**Context:**\n${mem.context.map(c => ` - ${c}`).join('\n')}\n\n`;
2067
- if (mem.preferences.length)
2068
- info += `**Preferences:**\n${mem.preferences.map(p => ` - ${p}`).join('\n')}\n\n`;
2069
- if (mem.history.length)
2070
- info += `**History:**\n${mem.history.slice(-5).map(h => ` - ${h}`).join('\n')}\n`;
2071
- addMessage('system', info);
2072
- }
2073
- }
2074
- else if (subCmd === 'add' && parts[2]) {
2075
- const type = parts[2];
2076
- const content = parts.slice(3).join(' ');
2077
- if (!content) {
2078
- addMessage('error', 'Usage: /memory add <type> <content>');
2079
- }
2080
- else {
2081
- let memPath = memory.findProjectMemory(cwd);
2082
- if (!memPath) {
2083
- memPath = memory.initProjectMemory(cwd);
2084
- }
2085
- memory.addMemoryEntry(memPath, {
2086
- type,
2087
- content,
2088
- timestamp: new Date().toISOString().split('T')[0],
2089
- });
2090
- addMessage('system', `Added ${type}: ${content}`);
2091
- }
2092
- }
2093
- else if (subCmd === 'remove' && parts[2]) {
2094
- const type = parts[2];
2095
- const content = parts.slice(3).join(' ');
2096
- const memPath = memory.findProjectMemory(cwd);
2097
- if (memPath && memory.removeMemoryEntry(memPath, type, content)) {
2098
- addMessage('system', `Removed matching ${type}`);
2099
- }
2100
- else {
2101
- addMessage('error', 'Entry not found');
2102
- }
2103
- }
2104
- else if (subCmd === 'global') {
2105
- const globalMem = memory.getGlobalMemory();
2106
- let info = 'Global Memory:\n\n';
2107
- if (globalMem.preferences.length)
2108
- info += `**Preferences:**\n${globalMem.preferences.map(p => ` - ${p}`).join('\n')}\n`;
2109
- if (globalMem.notes.length)
2110
- info += `**Notes:**\n${globalMem.notes.map(n => ` - ${n}`).join('\n')}\n`;
2111
- addMessage('system', info || 'No global memories yet.');
2112
- }
2113
- else {
2114
- addMessage('system', 'Usage: /memory [init|show|add <type> <text>|remove <type> <text>|global]');
2115
- }
2116
- break;
2117
- }
2118
- case '/find': {
2119
- const fuzzy = await import('./fuzzy-search.js');
2120
- const query = parts.slice(1).join(' ');
2121
- if (!query) {
2122
- addMessage('system', 'Usage: /find <pattern>\nFuzzy search for files');
2123
- }
2124
- else {
2125
- const results = fuzzy.searchWithHighlight(process.cwd(), query, { maxResults: 20 });
2126
- if (results.length === 0) {
2127
- addMessage('system', 'No files found');
2128
- }
2129
- else {
2130
- const list = results.map((r, i) => `${i + 1}. ${r.highlighted}`).join('\n');
2131
- addMessage('system', `Found ${results.length} files:\n\n${list}`);
2132
- }
2133
- }
2134
- break;
2135
- }
2136
- case '/branch': {
2137
- const branching = await import('./branching.js');
2138
- const subCmd = parts[1];
2139
- const sessionId = `session_${Date.now()}`; // Would use actual session ID
2140
- if (subCmd === 'list' || !subCmd) {
2141
- const tree = branching.getBranchTree(sessionId);
2142
- addMessage('system', `Branches:\n${tree}`);
2143
- }
2144
- else if (subCmd === 'new' && parts[2]) {
2145
- const branch = branching.createBranch(sessionId, parts[2], llmMessages.current, parts.slice(3).join(' '));
2146
- addMessage('system', `Created branch: ${branch.name}`);
2147
- }
2148
- else if (subCmd === 'switch' && parts[2]) {
2149
- const msgs = branching.switchBranch(sessionId, parts[2], llmMessages.current);
2150
- if (msgs) {
2151
- llmMessages.current = msgs;
2152
- addMessage('system', `Switched to branch: ${parts[2]}`);
2153
- }
2154
- else {
2155
- addMessage('error', 'Branch not found');
2156
- }
2157
- }
2158
- else if (subCmd === 'delete' && parts[2]) {
2159
- if (branching.deleteBranch(sessionId, parts[2])) {
2160
- addMessage('system', 'Branch deleted');
2161
- }
2162
- else {
2163
- addMessage('error', 'Cannot delete branch');
2164
- }
2165
- }
2166
- else {
2167
- addMessage('system', 'Usage: /branch [list|new <name>|switch <name>|delete <name>]');
2168
- }
2169
- break;
2170
- }
2171
- case '/theme': {
2172
- const themes = await import('./themes.js');
2173
- const subCmd = parts[1];
2174
- if (subCmd === 'list' || !subCmd) {
2175
- const list = themes.listThemes();
2176
- const current = themes.getCurrentThemeName();
2177
- const formatted = list.map(t => {
2178
- const marker = t.name === current ? ' *' : '';
2179
- const custom = t.custom ? ' (custom)' : '';
2180
- return ` ${t.name}${marker}${custom} - ${t.description || 'No description'}`;
2181
- }).join('\n');
2182
- addMessage('system', `Available themes:\n${formatted}`);
2183
- }
2184
- else if (themes.setCurrentTheme(subCmd)) {
2185
- themes.clearThemeCache();
2186
- addMessage('system', `Theme set to: ${subCmd}`);
2187
- }
2188
- else {
2189
- addMessage('error', `Theme not found: ${subCmd}`);
2190
- }
2191
- break;
2192
- }
2193
- case '/hooks': {
2194
- const hooks = await import('./hooks.js');
2195
- const subCmd = parts[1];
2196
- if (subCmd === 'list' || !subCmd) {
2197
- addMessage('system', hooks.listHooksFormatted());
2198
- }
2199
- else if (subCmd === 'add' && parts[2]) {
2200
- const event = parts[2];
2201
- const command = parts.slice(3).join(' ');
2202
- if (!command) {
2203
- addMessage('system', 'Usage: /hooks add <event> <command>');
2204
- }
2205
- else {
2206
- hooks.addHook({ event, name: `Hook for ${event}`, command, enabled: true, async: false });
2207
- addMessage('system', 'Hook added');
2208
- }
2209
- }
2210
- else if (subCmd === 'init') {
2211
- hooks.initDefaultHooks();
2212
- addMessage('system', 'Default hooks initialized');
2213
- }
2214
- else {
2215
- addMessage('system', 'Usage: /hooks [list|add <event> <command>|init]');
2216
- }
2217
- break;
2218
- }
2219
- case '/search': {
2220
- const query = parts.slice(1).join(' ');
2221
- if (!query) {
2222
- addMessage('system', 'Usage: /search <query>\nSearch conversation history');
2223
- }
2224
- else {
2225
- const lower = query.toLowerCase();
2226
- const matches = messages.filter(m => m.content.toLowerCase().includes(lower));
2227
- if (matches.length === 0) {
2228
- addMessage('system', 'No matches found');
2229
- }
2230
- else {
2231
- const results = matches.slice(-10).map(m => {
2232
- const preview = m.content.slice(0, 100).replace(/\n/g, ' ');
2233
- return `[${m.type}] ${preview}...`;
2234
- }).join('\n\n');
2235
- addMessage('system', `Found ${matches.length} matches:\n\n${results}`);
2236
- }
2237
- }
2238
- break;
2239
- }
2240
- case '/project': {
2241
- const projectConfig = await import('./project-config.js');
2242
- const subCmd = parts[1];
2243
- const cwd = process.cwd();
2244
- if (subCmd === 'init') {
2245
- const configPath = projectConfig.createProjectConfig(cwd);
2246
- addMessage('system', `Created project config: ${configPath}\nEdit the file to customize settings.`);
2247
- }
2248
- else if (subCmd === 'show' || !subCmd) {
2249
- const configPath = projectConfig.findProjectConfig(cwd);
2250
- if (!configPath) {
2251
- addMessage('system', 'No project config found.\nRun /project init to create one.');
2252
- }
2253
- else {
2254
- const cfg = projectConfig.loadProjectConfig(configPath);
2255
- if (cfg) {
2256
- let info = `Config: ${configPath}\n\n`;
2257
- if (cfg.project)
2258
- info += `Project: ${cfg.project}\n`;
2259
- if (cfg.provider)
2260
- info += `Provider: ${cfg.provider}\n`;
2261
- if (cfg.model)
2262
- info += `Model: ${cfg.model}\n`;
2263
- if (cfg.tech?.length)
2264
- info += `Tech: ${cfg.tech.join(', ')}\n`;
2265
- if (cfg.conventions?.length)
2266
- info += `\nConventions:\n${cfg.conventions.map(c => ` - ${c}`).join('\n')}\n`;
2267
- if (cfg.commands)
2268
- info += `\nCommands: ${Object.keys(cfg.commands).join(', ')}\n`;
2269
- addMessage('system', info);
2270
- }
2271
- else {
2272
- addMessage('error', 'Failed to parse config');
2273
- }
2274
- }
2275
- }
2276
- else if (subCmd === 'run' && parts[2]) {
2277
- const configPath = projectConfig.findProjectConfig(cwd);
2278
- const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
2279
- const cmdName = parts[2];
2280
- if (cfg?.commands?.[cmdName]) {
2281
- addMessage('system', `Running: ${cfg.commands[cmdName]}`);
2282
- // Queue the command to run
2283
- const { spawn } = await import('child_process');
2284
- const proc = spawn('sh', ['-c', cfg.commands[cmdName]], { cwd, stdio: 'pipe' });
2285
- let output = '';
2286
- proc.stdout?.on('data', (d) => output += d.toString());
2287
- proc.stderr?.on('data', (d) => output += d.toString());
2288
- proc.on('close', (code) => {
2289
- addMessage('system', `Exit ${code}\n${output}`);
2290
- });
2291
- }
2292
- else {
2293
- addMessage('error', `Command not found: ${cmdName}`);
2294
- }
2295
- }
2296
- else {
2297
- addMessage('system', 'Usage: /project [init|show|run <cmd>]');
2298
- }
2299
- break;
2300
- }
2301
- case '/route':
2302
- case '/autoroute': {
2303
- if (parts[1] === 'on') {
2304
- setAutoRoute(true);
2305
- addMessage('system', '✓ Auto-routing ON - model selected based on task complexity');
2306
- }
2307
- else if (parts[1] === 'off') {
2308
- setAutoRoute(false);
2309
- addMessage('system', '✓ Auto-routing OFF - using fixed model');
2310
- }
2311
- else if (parts[1] === 'test' && parts[2]) {
2312
- const testMsg = parts.slice(2).join(' ');
2313
- const decision = modelRouter.routeRequest(testMsg, actualProvider);
2314
- addMessage('system', `Route test: ${decision.tier} tier (${decision.complexity})\nModel: ${decision.model.model}\nReason: ${decision.reason}\nConfidence: ${Math.round(decision.confidence * 100)}%`);
2315
- }
2316
- else {
2317
- const tiers = modelRouter.getAllTiers(actualProvider);
2318
- addMessage('system', `Auto-route: ${autoRoute ? 'ON' : 'OFF'}\n\nModel tiers for ${actualProvider}:\n fast: ${tiers.fast.model}\n balanced: ${tiers.balanced.model}\n smart: ${tiers.smart.model}\n\nUsage: /route [on|off|test <message>]`);
2319
- }
2320
- break;
2321
- }
2322
- case '/summarize': {
2323
- const subCmd = parts[1];
2324
- if (subCmd === 'context' || !subCmd) {
2325
- const msgCount = llmMessages.current.length;
2326
- if (msgCount < 5) {
2327
- addMessage('system', 'Not enough messages to summarize.');
2328
- }
2329
- else {
2330
- const summary = summarization.extractKeyInfo(llmMessages.current);
2331
- let info = 'Context Summary:\n\n';
2332
- if (summary.topics.length)
2333
- info += `**Topics:** ${summary.topics.join(', ')}\n`;
2334
- if (summary.decisions.length)
2335
- info += `**Decisions:**\n${summary.decisions.map(d => ` - ${d}`).join('\n')}\n`;
2336
- if (summary.actions.length)
2337
- info += `**Actions:**\n${summary.actions.map(a => ` - ${a}`).join('\n')}\n`;
2338
- if (summary.codeChanges.length)
2339
- info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map(c => ` - ${c}`).join('\n')}\n`;
2340
- addMessage('system', info || 'No key information extracted.');
2341
- }
2342
- }
2343
- else if (subCmd === 'compact') {
2344
- // Summarize and compact the conversation
2345
- const result = summarization.summarizeConversation(llmMessages.current, { maxTokens: 50000 });
2346
- if (result.summarizedCount > 0) {
2347
- llmMessages.current = result.messages;
2348
- setContextTokens(estimateContextTokens());
2349
- addMessage('system', `✓ Compacted ${result.summarizedCount} messages (${result.originalTokens} → ${result.reducedTokens} tokens)`);
2350
- }
2351
- else {
2352
- addMessage('system', 'Context already within limits, no compaction needed.');
2353
- }
2354
- }
2355
- else {
2356
- addMessage('system', 'Usage: /summarize [context|compact]');
2357
- }
2358
- break;
2359
- }
2360
- case '/upgrade':
2361
- addMessage('system', 'Checking for updates...');
2362
- try {
2363
- const current = getVersion();
2364
- const latest = await getLatestVersion();
2365
- if (!latest) {
2366
- addMessage('error', 'Could not check for updates');
2367
- break;
2368
- }
2369
- const [cMaj, cMin, cPat] = current.split('.').map(Number);
2370
- const [lMaj, lMin, lPat] = latest.split('.').map(Number);
2371
- const hasUpdate = lMaj > cMaj || (lMaj === cMaj && lMin > cMin) || (lMaj === cMaj && lMin === cMin && lPat > cPat);
2372
- if (hasUpdate) {
2373
- setLatestVersion(latest);
2374
- setModalMode('upgrade');
2375
- }
2376
- else {
2377
- addMessage('system', `You're on the latest version (v${current})`);
2378
- }
2379
- }
2380
- catch (e) {
2381
- addMessage('error', `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`);
2382
- }
2383
- break;
2384
- case '/session':
2385
- case '/sessions':
2386
- if (parts[1] === 'list' || !parts[1]) {
2387
- const sessions = storage.listSessions(20);
2388
- if (sessions.length === 0) {
2389
- addMessage('system', 'No previous sessions found.');
2390
- }
2391
- else {
2392
- setAvailableSessions(sessions);
2393
- setModalMode('sessions');
2394
- }
2395
- }
2396
- else if (parts[1] === 'info') {
2397
- const session = sessionRef.current;
2398
- if (session) {
2399
- addMessage('system', `Session: ${session.projectName}\nCreated: ${new Date(session.createdAt).toLocaleString()}\nMessages: ${session.messageCount}`);
2400
- }
2401
- else {
2402
- addMessage('system', 'No active session.');
2403
- }
2404
- }
2405
- else {
2406
- addMessage('system', 'Usage: /session [list|info] or just /sessions');
2407
- }
2408
- break;
2409
- case '/todo': {
2410
- const subCommand = parts[1];
2411
- if (subCommand === 'add' && parts.length > 2) {
2412
- const content = parts.slice(2).join(' ');
2413
- const isGlobal = content.includes('--global');
2414
- const isHigh = content.includes('--priority') && content.includes('high');
2415
- const cleanContent = content.replace(/--global|--priority\s*\w+/g, '').trim();
2416
- const todo = storage.addTodo(cleanContent, {
2417
- global: isGlobal,
2418
- priority: isHigh ? 'high' : 'normal',
2419
- });
2420
- addMessage('system', `✓ TODO added (#${todo.id.slice(-4)}${isGlobal ? ', global' : ''})`);
2421
- }
2422
- else if (subCommand === 'done' && parts[2]) {
2423
- const id = parts[2];
2424
- const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
2425
- const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
2426
- if (todo) {
2427
- storage.updateTodo(todo.id, { status: 'completed' });
2428
- addMessage('system', `✓ TODO #${id} marked done`);
2429
- }
2430
- else {
2431
- addMessage('error', `TODO #${id} not found`);
2432
- }
2433
- }
2434
- else if (subCommand === 'list' || !subCommand) {
2435
- const sessionTodos = storage.getSessionTodos();
2436
- const globalTodos = storage.getGlobalTodos();
2437
- const pending = [...sessionTodos, ...globalTodos].filter(t => t.status !== 'completed');
2438
- const completed = [...sessionTodos, ...globalTodos].filter(t => t.status === 'completed').slice(-3);
2439
- if (pending.length === 0 && completed.length === 0) {
2440
- addMessage('system', 'No TODOs. Use /todo add <task> to create one.');
2441
- }
2442
- else {
2443
- let output = '📋 TODOs:\n';
2444
- if (pending.length > 0) {
2445
- output += pending.map(t => ` ${t.priority === 'high' ? '!' : '□'} #${t.id.slice(-4)} ${t.content}`).join('\n');
2446
- }
2447
- if (completed.length > 0) {
2448
- output += '\n\nCompleted:\n' + completed.map(t => ` ✓ #${t.id.slice(-4)} ${t.content}`).join('\n');
2449
- }
2450
- addMessage('system', output);
2451
- }
2452
- }
2453
- else if (subCommand === 'work' && parts[2]) {
2454
- const id = parts[2];
2455
- const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
2456
- const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
2457
- if (todo) {
2458
- storage.setActiveTodo(todo.id);
2459
- storage.updateTodo(todo.id, { status: 'in_progress' });
2460
- addMessage('system', `✓ Working on: ${todo.content}\n\nTip: I'll help you complete this task. Describe what you need.`);
2461
- }
2462
- else {
2463
- addMessage('error', `TODO #${id} not found`);
2464
- }
2465
- }
2466
- else if (subCommand === 'clear') {
2467
- storage.setActiveTodo(null);
2468
- addMessage('system', '✓ Active TODO cleared');
2469
- }
2470
- else {
2471
- addMessage('system', 'Usage: /todo [add <task>|done <id>|work <id>|clear|list]');
2472
- }
2473
- break;
2474
- }
2475
- case '/plans': {
2476
- const subCommand = parts[1];
2477
- if (subCommand === 'list' || !subCommand) {
2478
- const plans = storage.getPlans();
2479
- if (plans.length === 0) {
2480
- addMessage('system', 'No plans yet. Plans are created in hybrid mode.');
2481
- }
2482
- else {
2483
- const list = plans.slice(0, 5).map(p => `${p.status === 'completed' ? '✓' : '○'} ${p.id.slice(-4)}: ${p.title}`).join('\n');
2484
- addMessage('system', `📋 Plans:\n${list}`);
2485
- }
2486
- }
2487
- else if (subCommand === 'view' && parts[2]) {
2488
- const plans = storage.getPlans();
2489
- const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
2490
- if (plan) {
2491
- const phases = plan.phases.map(ph => ` ${ph.status === 'completed' ? '✓' : '○'} ${ph.name} (${ph.risk} risk)`).join('\n');
2492
- addMessage('system', `Plan: ${plan.title}\nStatus: ${plan.status}\n\nPhases:\n${phases}`);
2493
- }
2494
- else {
2495
- addMessage('error', `Plan #${parts[2]} not found`);
2496
- }
2497
- }
2498
- else if (subCommand === 'rerun' && parts[2]) {
2499
- const plans = storage.getPlans();
2500
- const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
2501
- if (plan) {
2502
- // Reset plan status and activate
2503
- plan.status = 'in_progress';
2504
- plan.phases.forEach(ph => ph.status = 'pending');
2505
- storage.savePlan(plan);
2506
- storage.setActivePlan(plan);
2507
- // Generate prompt for re-execution
2508
- const phaseList = plan.phases.map(ph => `- ${ph.name}`).join('\n');
2509
- const prompt = `Please help me execute this plan:\n\n**${plan.title}**\n\nPhases:\n${phaseList}\n\nStart with the first phase.`;
2510
- setInput(prompt);
2511
- addMessage('system', `✓ Plan loaded: ${plan.title}\nPress Enter to start execution.`);
2512
- }
2513
- else {
2514
- addMessage('error', `Plan #${parts[2]} not found`);
2515
- }
2516
- }
2517
- else {
2518
- addMessage('system', 'Usage: /plans [list|view <id>|rerun <id>]');
2519
- }
2520
- break;
2521
- }
2522
- case '/history': {
2523
- const subCommand = parts[1];
2524
- if (subCommand === 'search' && parts[2]) {
2525
- const query = parts.slice(2).join(' ');
2526
- const results = storage.searchChatHistory(query);
2527
- if (results.length === 0) {
2528
- addMessage('system', `No matches for "${query}"`);
2529
- }
2530
- else {
2531
- const list = results.slice(-5).map(m => `${new Date(m.timestamp).toLocaleTimeString()}: ${m.content.substring(0, 60)}...`).join('\n');
2532
- addMessage('system', `🔍 Found ${results.length} matches:\n${list}`);
2533
- }
2534
- }
2535
- else if (subCommand === 'clear') {
2536
- addMessage('system', 'History is preserved per session. Start a new session for fresh history.');
2537
- }
2538
- else {
2539
- const history = storage.getChatHistory(5);
2540
- if (history.length === 0) {
2541
- addMessage('system', 'No chat history yet.');
2542
- }
2543
- else {
2544
- const list = history.map(m => `${m.role}: ${m.content.substring(0, 50)}...`).join('\n');
2545
- addMessage('system', `Recent history:\n${list}\n\nUse /history search <query> to search.`);
2546
- }
2547
- }
2548
- break;
2549
- }
2550
- case '/context': {
2551
- const subCommand = parts[1];
2552
- if (subCommand === 'load') {
2553
- const limit = parseInt(parts[2]) || 20;
2554
- const history = storage.getChatHistory(limit);
2555
- if (history.length > 0) {
2556
- // Load history into LLM context
2557
- for (const msg of history) {
2558
- if (msg.role === 'user' || msg.role === 'assistant') {
2559
- llmMessages.current.push({
2560
- role: msg.role,
2561
- content: msg.content,
2562
- });
2563
- }
2564
- }
2565
- addMessage('system', `✓ Loaded ${history.length} messages into context`);
2566
- }
2567
- else {
2568
- addMessage('system', 'No history to load.');
2569
- }
2570
- }
2571
- else if (subCommand === 'summary' || !subCommand) {
2572
- // Enhanced context summary with model limits
2573
- const msgCount = llmMessages.current.length;
2574
- const estTokens = estimateContextTokens();
2575
- const modelLimit = getModelContextLimit(actualProvider, actualModel);
2576
- const percentage = Math.round((estTokens / modelLimit) * 100);
2577
- const formatK = (n) => n >= 1000 ? `${Math.round(n / 1000)}K` : String(n);
2578
- let status = '🟢 Healthy';
2579
- if (percentage > 90)
2580
- status = '🔴 Critical';
2581
- else if (percentage > 80)
2582
- status = '🟡 Warning';
2583
- else if (percentage > 60)
2584
- status = '🟠 Caution';
2585
- addMessage('system', `**Context Status: ${status}**
2586
-
2587
- **Usage:** ${formatK(estTokens)} / ${formatK(modelLimit)} tokens (${percentage}%)
2588
- **Messages:** ${msgCount}
2589
- **Provider:** ${actualProvider}
2590
- **Model:** ${actualModel}
2591
-
2592
- **Commands:**
2593
- /summarize compact - Auto-compress context
2594
- /context load [n] - Load n messages from history
2595
- /clear - Start fresh`);
2596
- }
2597
- else {
2598
- addMessage('system', 'Usage: /context [load [n]|summary]\n\nShow context status or load history.');
2599
- }
2600
- break;
2601
- }
2602
- case '/scope':
2603
- case '/dirs': {
2604
- const subCmd = parts[1];
2605
- if (subCmd === 'details' || subCmd === 'full') {
2606
- addMessage('system', getScopeDetails());
2607
- }
2608
- else if (subCmd === 'reset') {
2609
- resetScope(process.cwd());
2610
- addMessage('system', '✓ Scope reset to current directory only');
2611
- }
2612
- else {
2613
- addMessage('system', getScopeSummary());
2614
- }
2615
- break;
2616
- }
2617
- case '/add-dir': {
2618
- const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
2619
- if (!dirPath) {
2620
- addMessage('system', 'Usage: /add-dir <path>\n\nAdd a directory to the allowed scope.\nThe agent can only access files within scope.');
2621
- }
2622
- else {
2623
- const result = addToScope(dirPath);
2624
- if (result.success) {
2625
- addMessage('system', `✓ ${result.message}`);
2626
- }
2627
- else {
2628
- addMessage('error', result.message);
2629
- }
2630
- }
2631
- break;
2632
- }
2633
- case '/remove-dir': {
2634
- const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
2635
- if (!dirPath) {
2636
- addMessage('system', 'Usage: /remove-dir <path>\n\nRemove a directory from the allowed scope.');
2637
- }
2638
- else {
2639
- const result = removeFromScope(dirPath);
2640
- if (result.success) {
2641
- addMessage('system', `✓ ${result.message}`);
2642
- }
2643
- else {
2644
- addMessage('error', result.message);
2645
- }
2646
- }
2647
- break;
2648
- }
2649
- case '/template':
2650
- case '/t': {
2651
- const subCmd = parts[1];
2652
- if (subCmd === 'list' || !subCmd) {
2653
- if (templates.length === 0) {
2654
- addMessage('system', 'No templates saved.\n\nUsage:\n /template save <name> <prompt>\n /template use <name>\n /template delete <name>');
2655
- }
2656
- else {
2657
- const list = templates.map((t, i) => ` ${i + 1}. ${t.name}: "${t.prompt.substring(0, 50)}${t.prompt.length > 50 ? '...' : ''}"`).join('\n');
2658
- addMessage('system', `Templates:\n${list}`);
2659
- }
2660
- }
2661
- else if (subCmd === 'save' && parts[2]) {
2662
- const name = parts[2];
2663
- const prompt = parts.slice(3).join(' ').replace(/^["']|["']$/g, '');
2664
- if (!prompt) {
2665
- addMessage('error', 'Usage: /template save <name> "<prompt>"');
2666
- }
2667
- else {
2668
- storage.saveTemplate(name, prompt);
2669
- setTemplates(prev => {
2670
- const filtered = prev.filter(t => t.name !== name);
2671
- return [...filtered, { name, prompt, createdAt: new Date() }];
2672
- });
2673
- addMessage('system', `✓ Template saved: ${name}`);
2674
- }
2675
- }
2676
- else if (subCmd === 'use' && parts[2]) {
2677
- const name = parts[2];
2678
- const template = templates.find(t => t.name === name);
2679
- if (template) {
2680
- setInput(template.prompt);
2681
- addMessage('system', `✓ Template loaded: ${name} (press Enter to send)`);
2682
- }
2683
- else {
2684
- addMessage('error', `Template not found: ${name}`);
2685
- }
2686
- }
2687
- else if (subCmd === 'delete' && parts[2]) {
2688
- const name = parts[2];
2689
- const found = templates.find(t => t.name === name);
2690
- if (found) {
2691
- storage.deleteTemplate(name);
2692
- setTemplates(prev => prev.filter(t => t.name !== name));
2693
- addMessage('system', `✓ Template deleted: ${name}`);
2694
- }
2695
- else {
2696
- addMessage('error', `Template not found: ${name}`);
2697
- }
2698
- }
2699
- else {
2700
- addMessage('system', 'Usage: /template [list|save <name> <prompt>|use <name>|delete <name>]');
2701
- }
2702
- break;
2703
- }
2704
- case '/cost':
2705
- case '/costs': {
2706
- const subCmd = parts[1];
2707
- if (subCmd === 'reset') {
2708
- storage.resetCosts();
2709
- addMessage('system', '✓ Cost tracking reset');
2710
- }
2711
- else {
2712
- addMessage('system', storage.getCostSummary());
2713
- }
2714
- break;
2715
- }
2716
- case '/bookmark':
2717
- case '/bm': {
2718
- const subCmd = parts[1];
2719
- if (!subCmd || subCmd === 'list') {
2720
- // List bookmarks
2721
- if (bookmarks.length === 0) {
2722
- addMessage('system', 'No bookmarks. Use /bookmark "name" to create one.');
2723
- }
2724
- else {
2725
- const list = bookmarks.map((b, i) => ` ${i + 1}. 🔖 ${b.name} (message #${b.messageIndex})`).join('\n');
2726
- addMessage('system', `Bookmarks:\n${list}\n\nUse /bookmark goto <number> to jump.`);
2727
- }
2728
- }
2729
- else if (subCmd === 'goto' && parts[2]) {
2730
- const idx = parseInt(parts[2]) - 1;
2731
- if (idx >= 0 && idx < bookmarks.length) {
2732
- const bm = bookmarks[idx];
2733
- // Save current state for undo
2734
- saveUndoState();
2735
- // Restore to bookmark point
2736
- setMessages(messages.slice(0, bm.messageIndex + 1));
2737
- llmMessages.current = llmMessages.current.slice(0, bm.llmMessageIndex + 1);
2738
- setContextTokens(estimateContextTokens());
2739
- addMessage('system', `✓ Jumped to bookmark: ${bm.name}`);
2740
- }
2741
- else {
2742
- addMessage('error', `Invalid bookmark number. Use /bookmark list to see available.`);
2743
- }
2744
- }
2745
- else if (subCmd === 'delete' && parts[2]) {
2746
- const idx = parseInt(parts[2]) - 1;
2747
- if (idx >= 0 && idx < bookmarks.length) {
2748
- const removed = bookmarks[idx];
2749
- setBookmarks(prev => prev.filter((_, i) => i !== idx));
2750
- addMessage('system', `✓ Deleted bookmark: ${removed.name}`);
2751
- }
2752
- else {
2753
- addMessage('error', 'Invalid bookmark number.');
2754
- }
2755
- }
2756
- else {
2757
- // Create bookmark with given name
2758
- const name = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
2759
- const bm = {
2760
- id: `bm_${Date.now()}`,
2761
- name,
2762
- messageIndex: messages.length - 1,
2763
- llmMessageIndex: llmMessages.current.length - 1,
2764
- timestamp: new Date(),
2765
- };
2766
- setBookmarks(prev => [...prev, bm]);
2767
- addMessage('system', `🔖 Bookmark created: "${name}"`);
2768
- }
2769
- break;
2770
- }
2771
- case '/queue':
2772
- case '/q': {
2773
- // /q is now queue, use /exit to quit
2774
- if (command === '/q' && !parts[1]) {
2775
- // Just /q with no args shows queue
2776
- if (queuedMessages.length === 0) {
2777
- addMessage('system', 'No messages queued. Type while agent is processing to queue feedback.');
2778
- }
2779
- else {
2780
- const list = queuedMessages.map((m, i) => ` ${i + 1}. ${m.substring(0, 60)}${m.length > 60 ? '...' : ''}`).join('\n');
2781
- addMessage('system', `📨 Queued messages (${queuedMessages.length}):\n${list}\n\nUse /queue clear to remove all.`);
2782
- }
2783
- break;
2784
- }
2785
- const subCmd = parts[1];
2786
- if (subCmd === 'clear') {
2787
- const count = queuedMessages.length;
2788
- setQueuedMessages([]);
2789
- addMessage('system', `✓ Cleared ${count} queued message${count !== 1 ? 's' : ''}`);
2790
- }
2791
- else if (subCmd === 'show' || !subCmd) {
2792
- if (queuedMessages.length === 0) {
2793
- addMessage('system', 'No messages queued.');
2794
- }
2795
- else {
2796
- const list = queuedMessages.map((m, i) => ` ${i + 1}. ${m}`).join('\n');
2797
- addMessage('system', `📨 Queued messages:\n${list}`);
2798
- }
2799
- }
2800
- else if (subCmd === 'flush') {
2801
- // Force-process queued messages even if stuck
2802
- if (queuedMessages.length === 0) {
2803
- addMessage('system', 'No messages to flush.');
2804
- }
2805
- else {
2806
- const queued = [...queuedMessages];
2807
- setQueuedMessages([]);
2808
- setIsProcessing(false); // Force reset processing state
2809
- setThinkingState(null);
2810
- setStreamingResponse('');
2811
- addMessage('system', `🔄 Flushing ${queued.length} queued message(s)...`);
2812
- const followUp = queued.length === 1
2813
- ? queued[0]
2814
- : `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
2815
- setTimeout(() => {
2816
- setIsProcessing(true);
2817
- runAgent(followUp).finally(() => {
2818
- setIsProcessing(false);
2819
- setThinkingState(null);
2820
- setStreamingResponse('');
2821
- });
2822
- }, 50);
2823
- }
2824
- }
2825
- else {
2826
- addMessage('system', 'Usage: /queue [show|clear|flush]\n\nTip: Type while agent is processing to queue follow-up messages.');
2827
- }
2828
- break;
2829
- }
2830
- case '/flush': {
2831
- // Shortcut for /queue flush - force-process queued messages
2832
- if (queuedMessages.length === 0) {
2833
- addMessage('system', 'No messages to flush. Use /debug to see current state.');
2834
- }
2835
- else {
2836
- const queued = [...queuedMessages];
2837
- setQueuedMessages([]);
2838
- setIsProcessing(false); // Force reset processing state
2839
- setThinkingState(null);
2840
- setStreamingResponse('');
2841
- addMessage('system', `🔄 Flushing ${queued.length} queued message(s)...`);
2842
- const followUp = queued.length === 1
2843
- ? queued[0]
2844
- : `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
2845
- setTimeout(() => {
2846
- setIsProcessing(true);
2847
- runAgent(followUp).finally(() => {
2848
- setIsProcessing(false);
2849
- setThinkingState(null);
2850
- setStreamingResponse('');
2851
- });
2852
- }, 50);
2853
- }
2854
- break;
2855
- }
2856
- case '/debug': {
2857
- const subCmd = parts[1];
2858
- if (subCmd === 'on') {
2859
- debugEnabled = true;
2860
- addMessage('system', '🔍 Debug logging ON (output to stderr). Use /debug off to disable.');
2861
- }
2862
- else if (subCmd === 'off') {
2863
- debugEnabled = false;
2864
- addMessage('system', '🔍 Debug logging OFF');
2865
- }
2866
- else {
2867
- // Show internal state for debugging stuck issues
2868
- const debugInfo = [
2869
- `isProcessing: ${isProcessing}`,
2870
- `queuedMessages: ${queuedMessages.length}`,
2871
- `modalMode: ${modalMode}`,
2872
- `confirmMode: ${confirmMode}`,
2873
- `loopActive: ${loopActive}`,
2874
- `thinkingState: ${thinkingState ? JSON.stringify(thinkingState) : 'null'}`,
2875
- `streamingResponse length: ${streamingResponse.length}`,
2876
- `llmMessages count: ${llmMessages.current.length}`,
2877
- `mode: ${mode}`,
2878
- `debugEnabled: ${debugEnabled}`,
2879
- ];
2880
- addMessage('system', `🔍 Debug State:\n${debugInfo.join('\n')}\n\nUse /debug on|off to toggle logging.`);
2881
- }
2882
- break;
2883
- }
2884
- case '/unstick': {
2885
- // Emergency reset of processing state
2886
- setIsProcessing(false);
2887
- setThinkingState(null);
2888
- setStreamingResponse('');
2889
- setLoopActive(false);
2890
- setModalMode('none');
2891
- setPendingComplexPrompt(null);
2892
- // Also reset to hybrid mode if stuck in plan mode
2893
- if (mode === 'plan') {
2894
- setMode('hybrid');
2895
- addMessage('system', '🔧 Reset processing state + switched from plan to hybrid mode.');
2896
- }
2897
- else {
2898
- addMessage('system', '🔧 Reset processing state. You can now submit new messages.');
2899
- }
2900
- break;
2901
- }
2902
- case '/keys':
2903
- case '/?': {
2904
- // Show keybindings modal
2905
- setModalMode('keys');
2906
- break;
2907
- }
2908
- case '/work': {
2909
- // Quick shortcut to enter work mode
2910
- setMode('work');
2911
- addMessage('system', `Mode: ${MODE_CONFIG['work'].icon} ${MODE_CONFIG['work'].label} - ${MODE_CONFIG['work'].description}`);
2912
- break;
2913
- }
2914
- case '/plan': {
2915
- // Quick shortcut to enter plan mode
2916
- setMode('plan');
2917
- addMessage('system', `Mode: ${MODE_CONFIG['plan'].icon} ${MODE_CONFIG['plan'].label} - ${MODE_CONFIG['plan'].description}`);
2918
- break;
2919
- }
2920
- case '/resume': {
2921
- // Resume previous session manually
2922
- const history = storage.getChatHistory(parseInt(parts[1]) || 20);
2923
- if (history.length === 0) {
2924
- addMessage('system', 'No previous messages to resume.');
2925
- }
2926
- else {
2927
- for (const msg of history) {
2928
- if (msg.role === 'user' || msg.role === 'assistant') {
2929
- llmMessages.current.push({
2930
- role: msg.role,
2931
- content: msg.content,
2932
- });
2933
- }
2934
- }
2935
- addMessage('system', `✓ Loaded ${history.length} messages from previous session`);
2936
- setContextTokens(estimateContextTokens());
2937
- }
2938
- break;
2939
- }
2940
- case '/exit':
2941
- case '/quit':
2942
- exit();
2943
- break;
2944
- default:
2945
- addMessage('error', `Unknown command: ${command}. Type /help for help.`);
2946
- }
2947
- }, [actualProvider, actualModel, persona, stats, addMessage, exit]);
2948
- // Validate and repair message history to ensure tool_use always has tool_result
2949
- const validateAndRepairMessages = useCallback(() => {
2950
- const messages = llmMessages.current;
2951
- let repaired = false;
2952
- for (let i = 0; i < messages.length; i++) {
2953
- const msg = messages[i];
2954
- if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
2955
- // Check that each tool_use has a corresponding tool_result
2956
- for (const toolCall of msg.toolCalls) {
2957
- const hasResult = messages.slice(i + 1).some(m => m.role === 'tool' && m.toolCallId === toolCall.id);
2958
- if (!hasResult) {
2959
- // Add a placeholder tool_result for the missing tool call
2960
- debugLog('repair', 'Adding missing tool_result for', toolCall.id);
2961
- // Find the right position to insert (right after this assistant message or after existing tool results)
2962
- let insertPos = i + 1;
2963
- while (insertPos < messages.length && messages[insertPos].role === 'tool') {
2964
- insertPos++;
2965
- }
2966
- messages.splice(insertPos, 0, {
2967
- role: 'tool',
2968
- content: '[Error: Tool execution was interrupted. Please retry.]',
2969
- toolCallId: toolCall.id,
2970
- });
2971
- repaired = true;
2972
- }
2973
- }
2974
- }
2975
- }
2976
- if (repaired) {
2977
- addMessage('system', '🔧 Repaired corrupted message history (missing tool results).');
2978
- }
2979
- return repaired;
2980
- }, [addMessage]);
2981
- // Run agent with user prompt
2982
- const runAgent = useCallback(async (content) => {
2983
- debugLog('runAgent', 'ENTER', typeof content === 'string' ? content.substring(0, 50) : '[complex]');
2984
- // Validate message history before adding new content
2985
- validateAndRepairMessages();
2986
- llmMessages.current.push({ role: 'user', content });
2987
- setStats(s => ({ ...s, messageCount: s.messageCount + 1 }));
2988
- setStreamingResponse('');
2989
- // Auto-route to appropriate model based on task complexity
2990
- let effectiveModel = model;
2991
- if (autoRoute && typeof content === 'string') {
2992
- const routeDecision = modelRouter.routeRequest(content, provider, {
2993
- messageCount: stats.messageCount,
2994
- hasCode: content.includes('```') || /\.(ts|js|py|go|rs|java)/.test(content),
2995
- });
2996
- effectiveModel = routeDecision.model.model;
2997
- if (effectiveModel !== model) {
2998
- addMessage('system', `[Auto-route: ${routeDecision.tier} tier - ${routeDecision.reason}]`);
2999
- }
3000
- }
3001
- const maxIterations = config.get('maxIterations');
3002
- let completedNaturally = false;
3003
- // Check context limit and warn if approaching capacity
3004
- // Uses model's actual context length from API when available
3005
- let currentContextTokens = estimateContextTokens();
3006
- const modelLimit = getModelContextLimit(actualProvider, effectiveModel || actualModel);
3007
- let contextPercentage = (currentContextTokens / modelLimit) * 100;
3008
- // Auto-compact if we're over 95% capacity to prevent API errors
3009
- if (contextPercentage > 95) {
3010
- addMessage('system', `🔄 Context at ${Math.round(contextPercentage)}% - auto-compacting to prevent errors...`);
3011
- const result = summarization.summarizeConversation(llmMessages.current, {
3012
- maxTokens: Math.floor(modelLimit * 0.7), // Target 70% of limit after compaction
3013
- preserveRecent: 15,
3014
- });
3015
- if (result.summarizedCount > 0) {
3016
- llmMessages.current = result.messages;
3017
- currentContextTokens = estimateContextTokens();
3018
- contextPercentage = (currentContextTokens / modelLimit) * 100;
3019
- setContextTokens(currentContextTokens);
3020
- addMessage('system', `✓ Compacted ${result.summarizedCount} messages. Now at ${Math.round(contextPercentage)}% (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K)`);
3021
- }
3022
- else {
3023
- // If compaction didn't help enough, warn user
3024
- if (contextPercentage > 98) {
3025
- addMessage('error', `🚨 Context at ${Math.round(contextPercentage)}% - cannot proceed safely. Please use /clear or reduce message size.`);
3026
- setIsProcessing(false);
3027
- return;
3028
- }
3029
- }
3030
- }
3031
- else if (contextPercentage > 85) {
3032
- addMessage('system', `⚠️ Context at ${Math.round(contextPercentage)}% capacity (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K tokens)
3033
- Consider: /summarize compact | /clear | shorter messages`);
3034
- }
3035
- for (let i = 0; i < maxIterations; i++) {
3036
- // Safety check at start of each iteration - context may have grown from tool results
3037
- if (i > 0) {
3038
- const iterContextTokens = estimateContextTokens();
3039
- const iterContextPercentage = (iterContextTokens / modelLimit) * 100;
3040
- if (iterContextPercentage > 95) {
3041
- addMessage('system', `🔄 Context grew to ${Math.round(iterContextPercentage)}% - auto-compacting...`);
3042
- const result = summarization.summarizeConversation(llmMessages.current, {
3043
- maxTokens: Math.floor(modelLimit * 0.7),
3044
- preserveRecent: 15,
3045
- });
3046
- if (result.summarizedCount > 0) {
3047
- llmMessages.current = result.messages;
3048
- setContextTokens(estimateContextTokens());
3049
- addMessage('system', `✓ Compacted ${result.summarizedCount} messages during iteration ${i + 1}`);
3050
- }
3051
- }
3052
- }
3053
- try {
3054
- // Update thinking state for LLM call
3055
- setThinkingState({
3056
- status: i === 0 ? 'Analyzing request...' : 'Processing response...',
3057
- detail: `Iteration ${i + 1}/${maxIterations}`,
3058
- iteration: i + 1,
3059
- maxIterations,
3060
- });
3061
- setActivityState({
3062
- action: i === 0 ? 'Analyzing request' : 'Processing',
3063
- target: `iteration ${i + 1}`,
3064
- startTime: Date.now(),
3065
- });
3066
- // Streaming callback for final response
3067
- const onToken = (token) => {
3068
- setThinkingState(null); // Clear thinking when streaming starts
3069
- setStreamingResponse(prev => prev + token);
3070
- };
3071
- // Retry callback for error recovery
3072
- const onRetry = (attempt, error, delayMs) => {
3073
- setThinkingState({
3074
- status: `Retrying... (attempt ${attempt + 1})`,
3075
- detail: `${error.message.substring(0, 40)}... Waiting ${Math.round(delayMs / 1000)}s`,
3076
- iteration: i + 1,
3077
- maxIterations,
3078
- });
3079
- };
3080
- debugLog('chat', 'WAITING for LLM response', `iteration=${i + 1}`);
3081
- // Validate message history to prevent orphaned tool_result errors
3082
- let validatedMessages = summarization.validateMessageHistory(llmMessages.current);
3083
- if (validatedMessages.length !== llmMessages.current.length) {
3084
- debugLog('chat', 'CLEANED orphaned tool results', `removed=${llmMessages.current.length - validatedMessages.length}`);
3085
- llmMessages.current = validatedMessages;
3086
- }
3087
- // Pre-request summarization check - summarize BEFORE sending if context is too large
3088
- const tools = getTools(moduleAgtermEnabled);
3089
- const contextCheck = estimateContextUsage(provider, effectiveModel || DEFAULT_MODELS[provider], validatedMessages, tools);
3090
- debugLog('chat', 'CONTEXT CHECK', `estimated=${contextCheck.estimated}, limit=${contextCheck.limit}, percent=${contextCheck.percent}%`);
3091
- if (contextCheck.needsSummarization) {
3092
- debugLog('chat', 'PRE-REQUEST SUMMARIZING', `estimated=${contextCheck.estimated} >= 80% of ${contextCheck.limit}`);
3093
- const result = summarization.summarizeConversation(validatedMessages, { maxTokens: Math.floor(contextCheck.limit * 0.6) });
3094
- if (result.summarizedCount > 0) {
3095
- llmMessages.current = result.messages;
3096
- validatedMessages = result.messages;
3097
- debugLog('chat', 'PRE-SUMMARIZED', `removed=${result.summarizedCount} messages, reduced from ${result.originalTokens} to ${result.reducedTokens}`);
3098
- }
3099
- }
3100
- const response = await chat(provider, validatedMessages, tools, effectiveModel, onToken, onRetry);
3101
- debugLog('chat', 'GOT response', `toolCalls=${response.toolCalls?.length ?? 0}`);
3102
- // Update token stats and cost
3103
- if (response.usage) {
3104
- const usageCost = calculateCost(model || DEFAULT_MODELS[provider], response.usage.inputTokens, response.usage.outputTokens);
3105
- setStats(s => ({
3106
- ...s,
3107
- inputTokens: s.inputTokens + response.usage.inputTokens,
3108
- outputTokens: s.outputTokens + response.usage.outputTokens,
3109
- cost: s.cost + usageCost,
3110
- }));
3111
- // Persist cost to storage
3112
- storage.recordCost(usageCost, actualProvider, sessionRef.current?.id);
3113
- // Auto-summarize if context is getting too full (85% threshold)
3114
- if (needsSummarization(provider, model || DEFAULT_MODELS[provider], response.usage.inputTokens)) {
3115
- debugLog('chat', 'AUTO-SUMMARIZING', `inputTokens=${response.usage.inputTokens}`);
3116
- const result = summarization.summarizeConversation(llmMessages.current, { maxTokens: 100000 });
3117
- if (result.summarizedCount > 0) {
3118
- llmMessages.current = result.messages;
3119
- debugLog('chat', 'SUMMARIZED', `removed=${result.summarizedCount} messages`);
3120
- }
3121
- }
3122
- }
3123
- // Handle tool calls with parallel execution support
3124
- if (response.toolCalls?.length) {
3125
- llmMessages.current.push({
3126
- role: 'assistant',
3127
- content: response.content,
3128
- toolCalls: response.toolCalls,
3129
- });
3130
- const preChecks = [];
3131
- const executableTools = [];
3132
- for (const toolCall of response.toolCalls) {
3133
- const args = toolCall.arguments;
3134
- const toolPreview = String(args.command || args.path || '...');
3135
- const risk = assessToolRisk(toolCall);
3136
- const riskConfig = RISK_CONFIG[risk.level];
3137
- const riskDisplay = risk.level !== 'none' ? ` [${riskConfig.bar}]` : '';
3138
- const preCheck = {
3139
- toolCall,
3140
- args,
3141
- preview: toolPreview,
3142
- risk,
3143
- riskDisplay,
3144
- blocked: false,
3145
- };
3146
- // Check blocking conditions
3147
- if (mode === 'plan' && toolCall.name !== 'think') {
3148
- preCheck.blocked = true;
3149
- preCheck.blockReason = 'plan mode';
3150
- preCheck.blockContent = '[Plan mode: Tool not executed. Describe what this would do.]';
3151
- addMessage('tool', `📋 ${toolCall.name}: ${toolPreview}${riskDisplay} (plan mode - not executed)`);
3152
- }
3153
- else if (confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
3154
- preCheck.blocked = true;
3155
- preCheck.blockReason = 'confirmation required';
3156
- preCheck.blockContent = `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`;
3157
- const riskIcon = risk.level === 'critical' ? '🛑' : '⚠️';
3158
- addMessage('tool', `${riskIcon} ${toolCall.name}: ${toolPreview}${riskDisplay}\n → Requires confirmation (use /confirm off to disable)`);
3159
- }
3160
- else {
3161
- // Check pre-tool hooks
3162
- const preHookResult = await hooks.checkHooksAllow('pre-tool', {
3163
- tool: toolCall.name,
3164
- toolArgs: args,
3165
- });
3166
- if (!preHookResult.allowed) {
3167
- preCheck.blocked = true;
3168
- preCheck.blockReason = 'blocked by hook';
3169
- preCheck.blockContent = `[Blocked by hook: ${preHookResult.reason}]`;
3170
- addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
3171
- addMessage('tool', `🛑 Blocked by hook: ${preHookResult.reason}`);
3172
- }
3173
- else {
3174
- // Tool can be executed
3175
- executableTools.push(toolCall);
3176
- addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
3177
- }
3178
- }
3179
- preChecks.push(preCheck);
3180
- // Add blocked tool results to LLM messages
3181
- if (preCheck.blocked) {
3182
- llmMessages.current.push({
3183
- role: 'tool',
3184
- content: preCheck.blockContent,
3185
- toolCallId: toolCall.id,
3186
- });
3187
- }
3188
- }
3189
- // ============================================================
3190
- // Phase 2: Execute tools (parallel when beneficial)
3191
- // ============================================================
3192
- if (executableTools.length > 0) {
3193
- const parallelStats = getParallelizationStats(executableTools);
3194
- const useParallel = parallelStats.maxParallel > 1 && executableTools.length > 1;
3195
- if (useParallel) {
3196
- // Show parallelization info
3197
- setThinkingState({
3198
- status: `Executing ${executableTools.length} tools in parallel...`,
3199
- detail: `${parallelStats.stages} stages, up to ${parallelStats.maxParallel}x speedup`,
3200
- iteration: i + 1,
3201
- maxIterations,
3202
- });
3203
- setActivityState({
3204
- action: `Executing ${executableTools.length} tools`,
3205
- target: 'in parallel',
3206
- startTime: Date.now(),
3207
- });
3208
- // Execute in parallel using dependency-aware staging
3209
- debugLog('tools', 'PARALLEL exec start', `count=${executableTools.length}`);
3210
- const results = await executeParallel(executableTools, async (call) => {
3211
- const result = await executeTool(call, process.cwd());
3212
- return result.result;
3213
- }, (completed, total, current) => {
3214
- const args = current.arguments;
3215
- const target = args.path || args.command?.substring(0, 30) || current.name;
3216
- setActivityState({
3217
- action: `Running ${current.name}`,
3218
- target: target,
3219
- startTime: Date.now(),
3220
- });
3221
- setThinkingState({
3222
- status: `Executing tools... (${completed + 1}/${total})`,
3223
- detail: current.name,
3224
- iteration: i + 1,
3225
- maxIterations,
3226
- });
3227
- });
3228
- debugLog('tools', 'PARALLEL exec done', `results=${results.length}`);
3229
- // Process results sequentially for UI and LLM messages
3230
- for (const result of results) {
3231
- const toolCall = result.toolCall;
3232
- const args = toolCall.arguments;
3233
- // Execute post-tool hooks
3234
- hooks.executeHooks('post-tool', {
3235
- tool: toolCall.name,
3236
- toolArgs: args,
3237
- toolResult: result.result,
3238
- }).catch((err) => {
3239
- debugLog('hooks', `post-tool hook failed for ${toolCall.name}:`, err instanceof Error ? err.message : err);
3240
- });
3241
- // Display result
3242
- if (toolCall.name === 'think') {
3243
- const thought = String(args.thought || '');
3244
- addMessage('tool', thought);
3245
- }
3246
- else if (result.error) {
3247
- addMessage('tool', `Error: ${result.error}`);
3248
- }
3249
- else {
3250
- const preview = result.result.split('\n').slice(0, 3).join('\n');
3251
- addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
3252
- }
3253
- llmMessages.current.push({
3254
- role: 'tool',
3255
- content: result.error ? `Error: ${result.error}` : result.result,
3256
- toolCallId: toolCall.id,
3257
- });
3258
- }
3259
- }
3260
- else {
3261
- // Sequential execution (single tool or dependencies prevent parallelization)
3262
- debugLog('tools', 'SEQUENTIAL exec start', `count=${executableTools.length}`);
3263
- for (const toolCall of executableTools) {
3264
- const args = toolCall.arguments;
3265
- const toolPreview = String(args.command || args.path || args.content?.toString().substring(0, 30) || '...');
3266
- // Set activity state for streaming indicator
3267
- const actionMap = {
3268
- read_file: 'Reading',
3269
- write_file: 'Writing',
3270
- edit_file: 'Editing',
3271
- bash: 'Running',
3272
- search: 'Searching',
3273
- glob: 'Finding',
3274
- think: 'Thinking',
3275
- };
3276
- const action = actionMap[toolCall.name] || `Executing ${toolCall.name}`;
3277
- const target = toolCall.name === 'bash'
3278
- ? args.command?.substring(0, 40) + (args.command?.length > 40 ? '...' : '')
3279
- : toolCall.name === 'think'
3280
- ? undefined
3281
- : args.path || args.pattern;
3282
- setActivityState({ action, target, startTime: Date.now() });
3283
- // Special handling for think tool UI
3284
- if (toolCall.name === 'think') {
3285
- const thought = String(args.thought || '');
3286
- setThinkingState({
3287
- status: 'Reasoning...',
3288
- detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
3289
- thinking: thought,
3290
- iteration: i + 1,
3291
- maxIterations,
3292
- });
3293
- }
3294
- else {
3295
- setThinkingState({
3296
- status: `Executing ${toolCall.name}...`,
3297
- detail: toolPreview.substring(0, 60),
3298
- thinking: undefined,
3299
- iteration: i + 1,
3300
- maxIterations,
3301
- });
3302
- }
3303
- debugLog('tools', 'EXEC', toolCall.name, toolPreview.substring(0, 30));
3304
- const result = await executeTool(toolCall, process.cwd());
3305
- debugLog('tools', 'DONE', toolCall.name);
3306
- // Execute post-tool hooks
3307
- hooks.executeHooks('post-tool', {
3308
- tool: toolCall.name,
3309
- toolArgs: args,
3310
- toolResult: result.result,
3311
- }).catch((err) => {
3312
- debugLog('hooks', `post-tool hook failed for ${toolCall.name}:`, err instanceof Error ? err.message : err);
3313
- });
3314
- // Display result
3315
- if (toolCall.name === 'think') {
3316
- const thought = String(args.thought || '');
3317
- addMessage('tool', thought);
3318
- }
3319
- else {
3320
- const preview = result.result.split('\n').slice(0, 3).join('\n');
3321
- addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
3322
- }
3323
- llmMessages.current.push({
3324
- role: 'tool',
3325
- content: result.result,
3326
- toolCallId: toolCall.id,
3327
- });
3328
- }
3329
- }
3330
- }
3331
- continue;
3332
- }
3333
- // Final response - move streaming content to message history
3334
- setThinkingState(null);
3335
- llmMessages.current.push({ role: 'assistant', content: response.content });
3336
- addMessage('assistant', response.content);
3337
- setStreamingResponse('');
3338
- setContextTokens(estimateContextTokens());
3339
- checkAndWarnContextLimit(actualProvider, actualModel, estimateContextTokens(), addMessage);
3340
- // Auto-continue if response was truncated due to length
3341
- if (response.finishReason === 'length') {
3342
- addMessage('system', '(auto-continuing...)');
3343
- llmMessages.current.push({ role: 'user', content: 'Please continue where you left off.' });
3344
- continue; // Loop again to get continuation
3345
- }
3346
- completedNaturally = true;
3347
- break;
3348
- }
3349
- catch (error) {
3350
- setThinkingState(null);
3351
- setActivityState(null);
3352
- setStreamingResponse('');
3353
- // Format error with provider context for better suggestions
3354
- const errorMsg = formatError(error, { provider: actualProvider });
3355
- addMessage('error', errorMsg);
3356
- // Classify error to provide additional recovery suggestions
3357
- const classified = classifyError(error);
3358
- const availableProviders = getAvailableProviders();
3359
- const otherProviders = availableProviders.filter(p => p !== actualProvider);
3360
- // Suggest alternatives based on error type
3361
- if (classified.category === 'rate_limit' || classified.category === 'server') {
3362
- if (otherProviders.length > 0) {
3363
- addMessage('system', `💡 Try switching providers: /provider ${otherProviders[0]} or /models to see alternatives`);
3364
- }
3365
- }
3366
- else if (classified.category === 'timeout' || classified.category === 'network') {
3367
- addMessage('system', `💡 Network issue detected. Check connection and try again, or use /provider to switch.`);
3368
- }
3369
- else if (classified.category === 'auth') {
3370
- addMessage('system', `💡 Run 'calliope --setup' to reconfigure API keys.`);
3371
- }
3372
- completedNaturally = true; // Error counts as "done" - don't show iteration warning
3373
- // On error, clear queued messages to prevent infinite retry loop
3374
- if (queuedMessages.length > 0) {
3375
- addMessage('system', `⚠️ Cleared ${queuedMessages.length} queued message(s) due to error. Use /clear to reset conversation.`);
3376
- setQueuedMessages([]);
3377
- }
3378
- return; // Exit early on error - don't process queued messages
3379
- }
3380
- }
3381
- // Only show warning if we actually hit the iteration limit (not errors or natural completion)
3382
- if (!completedNaturally) {
3383
- addMessage('system', `⚠️ Reached ${maxIterations} iterations limit. Task may be incomplete. Adjust with /set maxIterations <number>.`);
3384
- }
3385
- // Update context tokens after agent run
3386
- setContextTokens(estimateContextTokens());
3387
- // Process any queued messages (human-in-the-loop feedback)
3388
- // CRITICAL: Use ref to get current value, not stale closure
3389
- const currentQueued = queuedMessagesRef.current;
3390
- debugLog('runAgent', 'EXIT loop', `queued=${currentQueued.length}`);
3391
- if (currentQueued.length > 0) {
3392
- const queued = [...currentQueued];
3393
- setQueuedMessages([]); // Clear the queue
3394
- queuedMessagesRef.current = []; // Also clear ref immediately
3395
- // Combine queued messages into a single follow-up
3396
- const followUp = queued.length === 1
3397
- ? queued[0]
3398
- : `[Multiple follow-up messages from user:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
3399
- addMessage('system', `📨 Processing ${queued.length} queued message${queued.length > 1 ? 's' : ''}...`);
3400
- // Recursively run agent with follow-up
3401
- // Use setTimeout to avoid stack overflow and allow UI to update
3402
- // Note: handleSubmit's finally will set isProcessing=false, so we need to re-enable it
3403
- debugLog('runAgent', 'SCHEDULING recursive call for queued messages');
3404
- setTimeout(() => {
3405
- debugLog('runAgent', 'RECURSIVE call starting');
3406
- setIsProcessing(true);
3407
- runAgent(followUp).finally(() => {
3408
- setIsProcessing(false);
3409
- setThinkingState(null);
3410
- setActivityState(null);
3411
- setStreamingResponse('');
3412
- setEditingQueueIndex(null);
3413
- });
3414
- }, 100);
3415
- }
3416
- debugLog('runAgent', 'RETURN');
3417
- }, [provider, model, addMessage, mode, estimateContextTokens]); // Note: queuedMessages accessed via ref
3418
- // Ralph Wiggum loop - runs prompt repeatedly until completion promise or max iterations
3419
- const runLoop = useCallback(async (prompt, maxIter, completionPromise) => {
3420
- setIsProcessing(true);
3421
- for (let i = 0; i < maxIter; i++) {
3422
- // Check if cancelled
3423
- if (loopCancelledRef.current) {
3424
- addMessage('system', '🛑 Loop cancelled by user');
3425
- break;
3426
- }
3427
- setLoopIteration(i + 1);
3428
- addMessage('system', `🔄 Loop iteration ${i + 1}/${maxIter}`);
3429
- // Add the loop prompt as user message
3430
- llmMessages.current.push({ role: 'user', content: prompt });
3431
- try {
3432
- // Run the agent
3433
- await runAgent(prompt);
3434
- // Check for completion promise in the last assistant message
3435
- if (completionPromise) {
3436
- const lastMessage = llmMessages.current[llmMessages.current.length - 1];
3437
- if (lastMessage?.role === 'assistant') {
3438
- const content = typeof lastMessage.content === 'string'
3439
- ? lastMessage.content
3440
- : JSON.stringify(lastMessage.content);
3441
- if (content.includes(completionPromise)) {
3442
- addMessage('system', `🎉 Completion promise "${completionPromise}" detected! Loop finished.`);
3443
- break;
3444
- }
3445
- }
3446
- }
3447
- // Check cancelled again after agent run
3448
- if (loopCancelledRef.current) {
3449
- addMessage('system', '🛑 Loop cancelled by user');
3450
- break;
3451
- }
3452
- // Small delay between iterations
3453
- await new Promise(r => setTimeout(r, 500));
3454
- }
3455
- catch (error) {
3456
- addMessage('error', `Loop error: ${error instanceof Error ? error.message : String(error)}`);
3457
- break;
3458
- }
3459
- }
3460
- // If we completed all iterations without hitting completion promise
3461
- if (!loopCancelledRef.current && !completionPromise) {
3462
- addMessage('system', `✅ Loop completed ${maxIter} iterations`);
3463
- }
3464
- setLoopActive(false);
3465
- setIsProcessing(false);
3466
- }, [runAgent, addMessage]);
3467
- // Handle input submission
3468
- const handleSubmit = useCallback(async (value) => {
3469
- const trimmed = value.trim();
3470
- if (!trimmed || isProcessing)
3471
- return;
3472
- // Add to history for up/down arrow navigation
3473
- addToHistory(trimmed);
3474
- setInput('');
3475
- if (trimmed.startsWith('/')) {
3476
- await handleCommand(trimmed);
3477
- return;
3478
- }
3479
- // In hybrid mode, check for complex operations
3480
- if (mode === 'hybrid') {
3481
- const complexity = detectComplexity(trimmed);
3482
- if (complexity.isComplex) {
3483
- setPendingComplexPrompt({ prompt: trimmed, complexity });
3484
- setModalMode('complexity-warning');
3485
- return;
3486
- }
3487
- }
3488
- // Save state for undo before modifying conversation
3489
- saveUndoState();
3490
- // Parse file references from input
3491
- const { text: cleanText, files } = parseFileReferences(trimmed, process.cwd());
3492
- // Show user message (with file info if any)
3493
- if (files.length > 0) {
3494
- const fileInfo = formatFileInfo(files);
3495
- addMessage('user', `${cleanText}\n📎 ${fileInfo}`);
3496
- }
3497
- else {
3498
- addMessage('user', trimmed);
3499
- }
3500
- setIsProcessing(true);
3501
- try {
3502
- // Build message content (with file/image support)
3503
- let messageContent;
3504
- if (files.length > 0) {
3505
- const visionSupported = supportsVision(provider, model);
3506
- const { content, warnings } = processFilesForMessage(cleanText || trimmed, files, visionSupported);
3507
- // Show any warnings about files
3508
- for (const warning of warnings) {
3509
- addMessage('system', warning);
3510
- }
3511
- messageContent = content;
3512
- }
3513
- else {
3514
- messageContent = trimmed;
3515
- }
3516
- await runAgent(messageContent);
3517
- }
3518
- finally {
3519
- setIsProcessing(false);
3520
- setThinkingState(null);
3521
- setStreamingResponse('');
3522
- }
3523
- }, [isProcessing, handleCommand, runAgent, addMessage, provider, model, saveUndoState, addToHistory]);
3524
- // Modal handlers
3525
- const handleModelSelect = useCallback((selectedModel) => {
3526
- setModel(selectedModel);
3527
- addMessage('system', `Model: ${selectedModel}`);
3528
- setModalMode('none');
3529
- setAvailableModels([]);
3530
- }, [addMessage]);
3531
- const handleModalCancel = useCallback(() => {
3532
- setModalMode('none');
3533
- setAvailableModels([]);
3534
- setLatestVersion(null);
3535
- }, []);
3536
- const handleUpgradeConfirm = useCallback(async () => {
3537
- setModalMode('none');
3538
- addMessage('system', 'Upgrading...');
3539
- try {
3540
- const success = await performUpgrade();
3541
- if (success) {
3542
- addMessage('system', 'Upgrade complete! Restarting...');
3543
- const { spawn } = await import('child_process');
3544
- const child = spawn(process.argv[0], process.argv.slice(1), {
3545
- stdio: 'inherit',
3546
- detached: true,
3547
- });
3548
- child.unref();
3549
- process.exit(0);
3550
- }
3551
- else {
3552
- addMessage('error', 'Upgrade failed. Try: npm install -g @calliopelabs/cli@latest');
3553
- }
3554
- }
3555
- catch (e) {
3556
- addMessage('error', `Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
3557
- }
3558
- setLatestVersion(null);
3559
- }, [addMessage]);
3560
- // Cycle through modes
3561
- const cycleMode = useCallback(() => {
3562
- setMode(current => {
3563
- const modes = ['plan', 'hybrid', 'work'];
3564
- const idx = modes.indexOf(current);
3565
- const next = modes[(idx + 1) % modes.length];
3566
- return next;
3567
- });
3568
- }, []);
3569
- // Handle Escape key - cancel operation if processing, otherwise show hint
3570
- const handleEscape = useCallback(() => {
3571
- if (isProcessing) {
3572
- // Cancel current operation
3573
- setIsProcessing(false);
3574
- setThinkingState(null);
3575
- setStreamingResponse('');
3576
- setLoopActive(false);
3577
- setEditingQueueIndex(null);
3578
- addMessage('system', '⏹ Operation cancelled. Use /exit to quit.');
3579
- }
3580
- else if (modalMode !== 'none') {
3581
- // Close any open modal
3582
- setModalMode('none');
3583
- setPendingComplexPrompt(null);
3584
- }
3585
- else {
3586
- // Not processing - show hint instead of exiting
3587
- addMessage('system', '💡 Use /exit to quit, or Ctrl+C.');
3588
- }
3589
- }, [isProcessing, modalMode, addMessage]);
3590
- // Handle direct send (Shift+Enter) - interrupts current operation and sends immediately
3591
- const handleDirectSend = useCallback((msg) => {
3592
- // Stop current processing
3593
- setIsProcessing(false);
3594
- setThinkingState(null);
3595
- setStreamingResponse('');
3596
- setEditingQueueIndex(null);
3597
- // Show what happened
3598
- addMessage('system', '⚡ Direct send - interrupting current operation');
3599
- addMessage('user', msg);
3600
- // Start new agent run with this message
3601
- setIsProcessing(true);
3602
- runAgent(msg).finally(() => {
3603
- setIsProcessing(false);
3604
- setThinkingState(null);
3605
- setStreamingResponse('');
3606
- setEditingQueueIndex(null);
3607
- });
3608
- }, [addMessage, runAgent]);
3609
- // Streaming response component (reused across layouts)
3610
- const StreamingResponseBox = streamingResponse ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), streamingResponse.split('\n').map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i))), _jsx(Text, { color: "cyan", children: "\u258C" })] })) : null;
3611
- // Thinking/Processing indicator component
3612
- const ProcessingBox = (_jsxs(_Fragment, { children: [isProcessing && thinkingState && !streamingResponse && _jsx(ThinkingDisplay, { state: thinkingState }), isProcessing && !thinkingState && !streamingResponse && _jsx(ProcessingIndicator, { label: "Waiting for response..." }), isProcessing && streamingResponse && _jsx(StreamingIndicator, { activity: activityState ?? undefined })] }));
3613
- // Render based on layout
3614
- return (_jsxs(Box, { flexDirection: "column", width: width, children: [layout === 'split' && (_jsxs(Box, { flexDirection: "row", width: width, children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsx(Text, { color: "yellow", dimColor: true, children: "\u2500 Tools \u2500" }), _jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox] }), _jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderLeft: true, borderColor: "gray", children: [_jsx(Text, { color: "cyan", dimColor: true, children: "\u2500 Response \u2500" }), StreamingResponseBox] })] })), layout === 'classic' && (_jsxs(_Fragment, { children: [_jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox, StreamingResponseBox] })), layout === 'response-top' && (_jsxs(_Fragment, { children: [StreamingResponseBox, _jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox] })), layout === 'response-bottom' && (_jsxs(_Fragment, { children: [_jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox, StreamingResponseBox] })), debugEnabled && (_jsx(Box, { marginY: 0, children: _jsxs(Text, { dimColor: true, children: ["[dbg] proc=", isProcessing ? 'Y' : 'N', " think=", thinkingState ? 'Y' : 'N', " stream=", streamingResponse.length, " mode=", mode, " queue=", queuedMessages.length, " activity=", activityState?.action || 'none'] }) })), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'sessions' && (_jsx(SessionSelector, { sessions: availableSessions, onSelect: (session) => {
3615
- // Load history from selected session
3616
- addMessage('system', `Loading session: ${session.projectName}...`);
3617
- // Note: We can't easily switch sessions, but we can show the path
3618
- addMessage('system', `Session path: ${session.projectPath}\nTo load this session, run calliope from that directory.`);
3619
- setModalMode('none');
3620
- }, onDelete: (session) => {
3621
- if (storage.deleteSession(session.id)) {
3622
- addMessage('system', `🗑️ Deleted session: ${session.projectName}`);
3623
- // Refresh the list
3624
- setAvailableSessions(prev => prev.filter(s => s.id !== session.id));
3625
- }
3626
- else {
3627
- addMessage('error', `Failed to delete session: ${session.projectName}`);
3628
- }
3629
- }, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), modalMode === 'session-resume' && previousSession && (_jsx(SessionResumePrompt, { session: previousSession, onResume: () => {
3630
- // Load chat history into context
3631
- const history = storage.getChatHistory(20);
3632
- if (history.length > 0) {
3633
- for (const msg of history) {
3634
- if (msg.role === 'user' || msg.role === 'assistant') {
3635
- llmMessages.current.push({
3636
- role: msg.role,
3637
- content: msg.content,
3638
- });
3639
- }
3640
- }
3641
- addMessage('system', `✓ Resumed session with ${history.length} messages loaded`);
3642
- setContextTokens(estimateContextTokens());
3643
- }
3644
- setModalMode('none');
3645
- setPreviousSession(null);
3646
- }, onNew: () => {
3647
- addMessage('system', '✓ Starting fresh session');
3648
- setModalMode('none');
3649
- setPreviousSession(null);
3650
- } })), modalMode === 'complexity-warning' && pendingComplexPrompt && (_jsx(ComplexityWarning, { reason: pendingComplexPrompt.complexity.reason || 'Complex operation detected', prompt: typeof pendingComplexPrompt.prompt === 'string' ? pendingComplexPrompt.prompt : undefined, onProceed: async () => {
3651
- setModalMode('none');
3652
- const prompt = pendingComplexPrompt.prompt;
3653
- setPendingComplexPrompt(null);
3654
- // Proceed with execution
3655
- saveUndoState();
3656
- addMessage('user', typeof prompt === 'string' ? prompt : JSON.stringify(prompt));
3657
- setIsProcessing(true);
3658
- try {
3659
- await runAgent(prompt);
3660
- }
3661
- finally {
3662
- setIsProcessing(false);
3663
- }
3664
- }, onPlan: () => {
3665
- setModalMode('none');
3666
- const prompt = pendingComplexPrompt.prompt;
3667
- setPendingComplexPrompt(null);
3668
- // Switch to plan mode and proceed
3669
- setMode('plan');
3670
- addMessage('system', '📋 Switched to Plan mode - I\'ll describe what I would do without executing.');
3671
- saveUndoState();
3672
- addMessage('user', typeof prompt === 'string' ? prompt : JSON.stringify(prompt));
3673
- setIsProcessing(true);
3674
- runAgent(prompt).finally(() => setIsProcessing(false));
3675
- }, onCancel: () => {
3676
- setModalMode('none');
3677
- setPendingComplexPrompt(null);
3678
- addMessage('system', 'Operation cancelled.');
3679
- } })), modalMode === 'keys' && (_jsx(KeybindingsModal, { onClose: () => setModalMode('none') })), _jsx(ChatInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onEscape: handleEscape, onCycleMode: cycleMode, disabled: isModalActive, isProcessing: isProcessing, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, editingQueueIndex: editingQueueIndex, onQueueMessage: (msg) => {
3680
- setQueuedMessages(prev => [...prev, msg]);
3681
- addMessage('system', `📨 Queued: "${msg.substring(0, 50)}${msg.length > 50 ? '...' : ''}"`);
3682
- }, onEditQueuedMessage: handleEditQueuedMessage, onSetEditingQueueIndex: setEditingQueueIndex, onDirectSend: handleDirectSend, cwd: process.cwd(), suggestions: suggestions, onSuggestionsChange: setSuggestions, onNavigateHistory: navigateHistory,
3683
- // Smart suggestions context
3684
- currentMode: mode, contextPercentage: Math.round((contextTokens / getModelContextLimit(actualProvider, actualModel)) * 100), recentCommands: recentCommands, hasGitRepo: hasGitRepo }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
3685
- }
3686
- // ============================================================================
3687
- // App Wrapper & Entry Point
3688
- // ============================================================================
3689
- function App() {
3690
- const [resetKey, setResetKey] = React.useState(0);
3691
- const handleReset = React.useCallback(() => {
3692
- setResetKey(k => k + 1);
3693
- }, []);
3694
- return (_jsx(ErrorBoundary, { onReset: handleReset, children: _jsx(TerminalChat, {}, resetKey) }));
3695
- }
3696
- // Print banner before Ink takes over (stays fixed at top)
3697
- function printBanner() {
3698
- const provider = selectProvider(config.get('defaultProvider'));
3699
- const model = config.get('defaultModel') || DEFAULT_MODELS[provider];
3700
- const cyan = '\x1b[36m';
3701
- const cyanBright = '\x1b[96m';
3702
- const dim = '\x1b[2m';
3703
- const reset = '\x1b[0m';
3704
- console.log();
3705
- console.log(`${cyanBright}${BANNER_LINES[0]}${reset}`);
3706
- console.log(`${cyanBright}${BANNER_LINES[1]}${reset}`);
3707
- console.log(`${cyan}${BANNER_LINES[2]}${reset}`);
3708
- console.log(`${cyan}${BANNER_LINES[3]}${reset}`);
3709
- console.log(`${cyanBright}${BANNER_LINES[4]}${reset}`);
3710
- console.log(`${cyan}${BANNER_LINES[5]}${reset}`);
3711
- console.log();
3712
- console.log(`${dim} The Muse of Digital Eloquence${reset}`);
3713
- console.log();
3714
- console.log(`${dim} v${getVersion()} | ${provider}:${model}${reset}`);
3715
- console.log(`${dim} /help for commands | ESC to exit${reset}`);
3716
- console.log();
3717
- }
3718
- export async function startInkCLI(options = {}) {
3719
- // Set module-level agterm state
3720
- moduleAgtermEnabled = options.agtermEnabled ?? false;
3721
- // Print banner BEFORE Ink starts - it stays fixed at the top
3722
- printBanner();
3723
- const { waitUntilExit } = render(_jsx(App, {}), {
3724
- patchConsole: true, // Prevent console.log during session from mixing with Ink
3725
- });
3726
- await waitUntilExit();
3727
- }
3728
- //# sourceMappingURL=ui-cli.js.map