@calliopelabs/cli 0.8.20 → 2.0.2

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