@prestyj/cli 4.3.209 → 4.3.237

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 (889) hide show
  1. package/README.md +17 -9
  2. package/dist/cli/auth.d.ts +4 -0
  3. package/dist/cli/auth.d.ts.map +1 -0
  4. package/dist/cli/auth.js +344 -0
  5. package/dist/cli/auth.js.map +1 -0
  6. package/dist/cli/command-routing.d.ts +28 -0
  7. package/dist/cli/command-routing.d.ts.map +1 -0
  8. package/dist/cli/command-routing.js +53 -0
  9. package/dist/cli/command-routing.js.map +1 -0
  10. package/dist/cli/command-routing.test.d.ts +2 -0
  11. package/dist/cli/command-routing.test.d.ts.map +1 -0
  12. package/dist/cli/command-routing.test.js +60 -0
  13. package/dist/cli/command-routing.test.js.map +1 -0
  14. package/dist/cli/mcp.d.ts +2 -0
  15. package/dist/cli/mcp.d.ts.map +1 -0
  16. package/dist/cli/mcp.js +309 -0
  17. package/dist/cli/mcp.js.map +1 -0
  18. package/dist/cli/pixel.d.ts +27 -0
  19. package/dist/cli/pixel.d.ts.map +1 -0
  20. package/dist/cli/pixel.js +103 -0
  21. package/dist/cli/pixel.js.map +1 -0
  22. package/dist/cli/shared.d.ts +13 -0
  23. package/dist/cli/shared.d.ts.map +1 -0
  24. package/dist/cli/shared.js +82 -0
  25. package/dist/cli/shared.js.map +1 -0
  26. package/dist/cli.d.ts +4 -1
  27. package/dist/cli.d.ts.map +1 -1
  28. package/dist/cli.js +174 -724
  29. package/dist/cli.js.map +1 -1
  30. package/dist/config.d.ts +4 -1
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +14 -1
  33. package/dist/config.js.map +1 -1
  34. package/dist/config.test.d.ts +2 -0
  35. package/dist/config.test.d.ts.map +1 -0
  36. package/dist/config.test.js +29 -0
  37. package/dist/config.test.js.map +1 -0
  38. package/dist/core/agent-session-compaction.test.d.ts +2 -0
  39. package/dist/core/agent-session-compaction.test.d.ts.map +1 -0
  40. package/dist/core/agent-session-compaction.test.js +114 -0
  41. package/dist/core/agent-session-compaction.test.js.map +1 -0
  42. package/dist/core/agent-session.d.ts +0 -23
  43. package/dist/core/agent-session.d.ts.map +1 -1
  44. package/dist/core/agent-session.js +6 -101
  45. package/dist/core/agent-session.js.map +1 -1
  46. package/dist/core/checkpoint-store.d.ts +93 -0
  47. package/dist/core/checkpoint-store.d.ts.map +1 -0
  48. package/dist/core/checkpoint-store.js +188 -0
  49. package/dist/core/checkpoint-store.js.map +1 -0
  50. package/dist/core/checkpoint-store.test.d.ts +2 -0
  51. package/dist/core/checkpoint-store.test.d.ts.map +1 -0
  52. package/dist/core/checkpoint-store.test.js +111 -0
  53. package/dist/core/checkpoint-store.test.js.map +1 -0
  54. package/dist/core/compaction/compactor.d.ts +4 -0
  55. package/dist/core/compaction/compactor.d.ts.map +1 -1
  56. package/dist/core/compaction/compactor.js +81 -28
  57. package/dist/core/compaction/compactor.js.map +1 -1
  58. package/dist/core/compaction/compactor.test.js +33 -9
  59. package/dist/core/compaction/compactor.test.js.map +1 -1
  60. package/dist/core/compaction/token-estimator.test.js +1 -1
  61. package/dist/core/continue-replay-inventory.test.d.ts +2 -0
  62. package/dist/core/continue-replay-inventory.test.d.ts.map +1 -0
  63. package/dist/core/continue-replay-inventory.test.js +42 -0
  64. package/dist/core/continue-replay-inventory.test.js.map +1 -0
  65. package/dist/core/goal-controller.d.ts +4 -0
  66. package/dist/core/goal-controller.d.ts.map +1 -1
  67. package/dist/core/goal-controller.js +223 -32
  68. package/dist/core/goal-controller.js.map +1 -1
  69. package/dist/core/goal-controller.test.js +387 -22
  70. package/dist/core/goal-controller.test.js.map +1 -1
  71. package/dist/core/goal-lifecycle-smoke.test.js +49 -8
  72. package/dist/core/goal-lifecycle-smoke.test.js.map +1 -1
  73. package/dist/core/goal-prerequisites.d.ts +23 -0
  74. package/dist/core/goal-prerequisites.d.ts.map +1 -0
  75. package/dist/core/goal-prerequisites.js +114 -0
  76. package/dist/core/goal-prerequisites.js.map +1 -0
  77. package/dist/core/goal-prerequisites.test.d.ts +2 -0
  78. package/dist/core/goal-prerequisites.test.d.ts.map +1 -0
  79. package/dist/core/goal-prerequisites.test.js +118 -0
  80. package/dist/core/goal-prerequisites.test.js.map +1 -0
  81. package/dist/core/goal-store.d.ts +12 -1
  82. package/dist/core/goal-store.d.ts.map +1 -1
  83. package/dist/core/goal-store.js +146 -40
  84. package/dist/core/goal-store.js.map +1 -1
  85. package/dist/core/goal-store.test.js +65 -8
  86. package/dist/core/goal-store.test.js.map +1 -1
  87. package/dist/core/goal-verifier.d.ts.map +1 -1
  88. package/dist/core/goal-verifier.js +4 -1
  89. package/dist/core/goal-verifier.js.map +1 -1
  90. package/dist/core/goal-verifier.test.js +43 -0
  91. package/dist/core/goal-verifier.test.js.map +1 -1
  92. package/dist/core/goal-worker.d.ts +2 -0
  93. package/dist/core/goal-worker.d.ts.map +1 -1
  94. package/dist/core/goal-worker.js +31 -7
  95. package/dist/core/goal-worker.js.map +1 -1
  96. package/dist/core/goal-worker.test.js +44 -0
  97. package/dist/core/goal-worker.test.js.map +1 -1
  98. package/dist/core/ideal-review.d.ts +20 -0
  99. package/dist/core/ideal-review.d.ts.map +1 -0
  100. package/dist/core/ideal-review.js +55 -0
  101. package/dist/core/ideal-review.js.map +1 -0
  102. package/dist/core/ideal-review.test.d.ts +2 -0
  103. package/dist/core/ideal-review.test.d.ts.map +1 -0
  104. package/dist/core/ideal-review.test.js +59 -0
  105. package/dist/core/ideal-review.test.js.map +1 -0
  106. package/dist/core/index.d.ts +1 -0
  107. package/dist/core/index.d.ts.map +1 -1
  108. package/dist/core/index.js +1 -0
  109. package/dist/core/index.js.map +1 -1
  110. package/dist/core/loop-breaker.d.ts +46 -0
  111. package/dist/core/loop-breaker.d.ts.map +1 -0
  112. package/dist/core/loop-breaker.js +79 -0
  113. package/dist/core/loop-breaker.js.map +1 -0
  114. package/dist/core/loop-breaker.test.d.ts +2 -0
  115. package/dist/core/loop-breaker.test.d.ts.map +1 -0
  116. package/dist/core/loop-breaker.test.js +110 -0
  117. package/dist/core/loop-breaker.test.js.map +1 -0
  118. package/dist/core/mcp/client.d.ts +20 -0
  119. package/dist/core/mcp/client.d.ts.map +1 -1
  120. package/dist/core/mcp/client.js +55 -11
  121. package/dist/core/mcp/client.js.map +1 -1
  122. package/dist/core/mcp/defaults.d.ts +7 -0
  123. package/dist/core/mcp/defaults.d.ts.map +1 -1
  124. package/dist/core/mcp/defaults.js +14 -0
  125. package/dist/core/mcp/defaults.js.map +1 -1
  126. package/dist/core/mcp/index.d.ts +6 -1
  127. package/dist/core/mcp/index.d.ts.map +1 -1
  128. package/dist/core/mcp/index.js +3 -1
  129. package/dist/core/mcp/index.js.map +1 -1
  130. package/dist/core/mcp/parse-add-command.d.ts +25 -0
  131. package/dist/core/mcp/parse-add-command.d.ts.map +1 -0
  132. package/dist/core/mcp/parse-add-command.js +220 -0
  133. package/dist/core/mcp/parse-add-command.js.map +1 -0
  134. package/dist/core/mcp/parse-add-command.test.d.ts +2 -0
  135. package/dist/core/mcp/parse-add-command.test.d.ts.map +1 -0
  136. package/dist/core/mcp/parse-add-command.test.js +75 -0
  137. package/dist/core/mcp/parse-add-command.test.js.map +1 -0
  138. package/dist/core/mcp/store.d.ts +80 -0
  139. package/dist/core/mcp/store.d.ts.map +1 -0
  140. package/dist/core/mcp/store.js +157 -0
  141. package/dist/core/mcp/store.js.map +1 -0
  142. package/dist/core/mcp/store.test.d.ts +2 -0
  143. package/dist/core/mcp/store.test.d.ts.map +1 -0
  144. package/dist/core/mcp/store.test.js +101 -0
  145. package/dist/core/mcp/store.test.js.map +1 -0
  146. package/dist/core/model-registry.d.ts +2 -2
  147. package/dist/core/model-registry.js +4 -4
  148. package/dist/core/model-registry.js.map +1 -1
  149. package/dist/core/model-registry.test.js +1 -1
  150. package/dist/core/model-registry.test.js.map +1 -1
  151. package/dist/core/prompt-commands.d.ts.map +1 -1
  152. package/dist/core/prompt-commands.js +64 -894
  153. package/dist/core/prompt-commands.js.map +1 -1
  154. package/dist/core/prompt-commands.test.js +49 -41
  155. package/dist/core/prompt-commands.test.js.map +1 -1
  156. package/dist/core/regrounding.d.ts +23 -0
  157. package/dist/core/regrounding.d.ts.map +1 -0
  158. package/dist/core/regrounding.js +21 -0
  159. package/dist/core/regrounding.js.map +1 -0
  160. package/dist/core/regrounding.test.d.ts +2 -0
  161. package/dist/core/regrounding.test.d.ts.map +1 -0
  162. package/dist/core/regrounding.test.js +38 -0
  163. package/dist/core/regrounding.test.js.map +1 -0
  164. package/dist/core/runtime-mode.d.ts +23 -0
  165. package/dist/core/runtime-mode.d.ts.map +1 -0
  166. package/dist/core/runtime-mode.js +30 -0
  167. package/dist/core/runtime-mode.js.map +1 -0
  168. package/dist/core/session-manager.d.ts +4 -0
  169. package/dist/core/session-manager.d.ts.map +1 -1
  170. package/dist/core/session-manager.js +20 -0
  171. package/dist/core/session-manager.js.map +1 -1
  172. package/dist/core/session-restore-display.test.d.ts +2 -0
  173. package/dist/core/session-restore-display.test.d.ts.map +1 -0
  174. package/dist/core/session-restore-display.test.js +78 -0
  175. package/dist/core/session-restore-display.test.js.map +1 -0
  176. package/dist/core/settings-manager.d.ts +4 -2
  177. package/dist/core/settings-manager.d.ts.map +1 -1
  178. package/dist/core/settings-manager.js +3 -6
  179. package/dist/core/settings-manager.js.map +1 -1
  180. package/dist/core/slash-commands.d.ts +0 -2
  181. package/dist/core/slash-commands.d.ts.map +1 -1
  182. package/dist/core/slash-commands.js +10 -15
  183. package/dist/core/slash-commands.js.map +1 -1
  184. package/dist/core/style-packs/index.js +1 -1
  185. package/dist/core/style-packs/index.js.map +1 -1
  186. package/dist/core/task-store.d.ts +26 -0
  187. package/dist/core/task-store.d.ts.map +1 -0
  188. package/dist/core/task-store.js +147 -0
  189. package/dist/core/task-store.js.map +1 -0
  190. package/dist/core/tasks-store.d.ts +24 -0
  191. package/dist/core/tasks-store.d.ts.map +1 -0
  192. package/dist/core/tasks-store.js +81 -0
  193. package/dist/core/tasks-store.js.map +1 -0
  194. package/dist/core/thinking-level.d.ts +5 -0
  195. package/dist/core/thinking-level.d.ts.map +1 -0
  196. package/dist/core/thinking-level.js +59 -0
  197. package/dist/core/thinking-level.js.map +1 -0
  198. package/dist/core/thinking-level.test.d.ts +2 -0
  199. package/dist/core/thinking-level.test.d.ts.map +1 -0
  200. package/dist/core/thinking-level.test.js +38 -0
  201. package/dist/core/thinking-level.test.js.map +1 -0
  202. package/dist/core/verify-commands.js +4 -4
  203. package/dist/core/verify-commands.js.map +1 -1
  204. package/dist/index.d.ts +1 -1
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +1 -1
  207. package/dist/index.js.map +1 -1
  208. package/dist/interactive.d.ts.map +1 -1
  209. package/dist/interactive.js +3 -1
  210. package/dist/interactive.js.map +1 -1
  211. package/dist/system-prompt.d.ts +5 -1
  212. package/dist/system-prompt.d.ts.map +1 -1
  213. package/dist/system-prompt.js +76 -48
  214. package/dist/system-prompt.js.map +1 -1
  215. package/dist/system-prompt.test.js +98 -46
  216. package/dist/system-prompt.test.js.map +1 -1
  217. package/dist/tools/bash.d.ts +6 -1
  218. package/dist/tools/bash.d.ts.map +1 -1
  219. package/dist/tools/bash.js +21 -3
  220. package/dist/tools/bash.js.map +1 -1
  221. package/dist/tools/checkpoint-hook.test.d.ts +2 -0
  222. package/dist/tools/checkpoint-hook.test.d.ts.map +1 -0
  223. package/dist/tools/checkpoint-hook.test.js +65 -0
  224. package/dist/tools/checkpoint-hook.test.js.map +1 -0
  225. package/dist/tools/edit-diff.d.ts +17 -4
  226. package/dist/tools/edit-diff.d.ts.map +1 -1
  227. package/dist/tools/edit-diff.js +78 -16
  228. package/dist/tools/edit-diff.js.map +1 -1
  229. package/dist/tools/edit-diff.test.js +31 -1
  230. package/dist/tools/edit-diff.test.js.map +1 -1
  231. package/dist/tools/edit-robustness.test.d.ts +2 -0
  232. package/dist/tools/edit-robustness.test.d.ts.map +1 -0
  233. package/dist/tools/edit-robustness.test.js +415 -0
  234. package/dist/tools/edit-robustness.test.js.map +1 -0
  235. package/dist/tools/edit.d.ts +8 -2
  236. package/dist/tools/edit.d.ts.map +1 -1
  237. package/dist/tools/edit.js +48 -20
  238. package/dist/tools/edit.js.map +1 -1
  239. package/dist/tools/edit.test.js +23 -22
  240. package/dist/tools/edit.test.js.map +1 -1
  241. package/dist/tools/enter-plan.d.ts +1 -1
  242. package/dist/tools/enter-plan.d.ts.map +1 -1
  243. package/dist/tools/enter-plan.js +8 -11
  244. package/dist/tools/enter-plan.js.map +1 -1
  245. package/dist/tools/exit-plan.d.ts.map +1 -1
  246. package/dist/tools/exit-plan.js +8 -9
  247. package/dist/tools/exit-plan.js.map +1 -1
  248. package/dist/tools/goal-mode.test.d.ts +2 -0
  249. package/dist/tools/goal-mode.test.d.ts.map +1 -0
  250. package/dist/tools/goal-mode.test.js +121 -0
  251. package/dist/tools/goal-mode.test.js.map +1 -0
  252. package/dist/tools/goals.d.ts +14 -2
  253. package/dist/tools/goals.d.ts.map +1 -1
  254. package/dist/tools/goals.js +257 -26
  255. package/dist/tools/goals.js.map +1 -1
  256. package/dist/tools/goals.test.js +365 -1
  257. package/dist/tools/goals.test.js.map +1 -1
  258. package/dist/tools/html-extract.d.ts +58 -0
  259. package/dist/tools/html-extract.d.ts.map +1 -0
  260. package/dist/tools/html-extract.js +130 -0
  261. package/dist/tools/html-extract.js.map +1 -0
  262. package/dist/tools/html-extract.test.d.ts +2 -0
  263. package/dist/tools/html-extract.test.d.ts.map +1 -0
  264. package/dist/tools/html-extract.test.js +60 -0
  265. package/dist/tools/html-extract.test.js.map +1 -0
  266. package/dist/tools/index.d.ts +16 -4
  267. package/dist/tools/index.d.ts.map +1 -1
  268. package/dist/tools/index.js +11 -7
  269. package/dist/tools/index.js.map +1 -1
  270. package/dist/tools/pdf-extract.d.ts +18 -0
  271. package/dist/tools/pdf-extract.d.ts.map +1 -0
  272. package/dist/tools/pdf-extract.js +43 -0
  273. package/dist/tools/pdf-extract.js.map +1 -0
  274. package/dist/tools/pdf-extract.test.d.ts +2 -0
  275. package/dist/tools/pdf-extract.test.d.ts.map +1 -0
  276. package/dist/tools/pdf-extract.test.js +15 -0
  277. package/dist/tools/pdf-extract.test.js.map +1 -0
  278. package/dist/tools/plan-mode.test.js +82 -221
  279. package/dist/tools/plan-mode.test.js.map +1 -1
  280. package/dist/tools/prompt-hints.d.ts.map +1 -1
  281. package/dist/tools/prompt-hints.js +6 -9
  282. package/dist/tools/prompt-hints.js.map +1 -1
  283. package/dist/tools/read-only-bash.d.ts +13 -0
  284. package/dist/tools/read-only-bash.d.ts.map +1 -0
  285. package/dist/tools/read-only-bash.js +155 -0
  286. package/dist/tools/read-only-bash.js.map +1 -0
  287. package/dist/tools/read-only-bash.test.d.ts +2 -0
  288. package/dist/tools/read-only-bash.test.d.ts.map +1 -0
  289. package/dist/tools/read-only-bash.test.js +47 -0
  290. package/dist/tools/read-only-bash.test.js.map +1 -0
  291. package/dist/tools/read.d.ts.map +1 -1
  292. package/dist/tools/read.js +14 -1
  293. package/dist/tools/read.js.map +1 -1
  294. package/dist/tools/read.test.js +4 -0
  295. package/dist/tools/read.test.js.map +1 -1
  296. package/dist/tools/screenshot.d.ts +25 -0
  297. package/dist/tools/screenshot.d.ts.map +1 -0
  298. package/dist/tools/screenshot.js +165 -0
  299. package/dist/tools/screenshot.js.map +1 -0
  300. package/dist/tools/screenshot.test.d.ts +2 -0
  301. package/dist/tools/screenshot.test.d.ts.map +1 -0
  302. package/dist/tools/screenshot.test.js +145 -0
  303. package/dist/tools/screenshot.test.js.map +1 -0
  304. package/dist/tools/subagent.d.ts +7 -2
  305. package/dist/tools/subagent.d.ts.map +1 -1
  306. package/dist/tools/subagent.js +32 -11
  307. package/dist/tools/subagent.js.map +1 -1
  308. package/dist/tools/tasks.d.ts.map +1 -1
  309. package/dist/tools/tasks.js +22 -58
  310. package/dist/tools/tasks.js.map +1 -1
  311. package/dist/tools/web-fetch.d.ts +16 -1
  312. package/dist/tools/web-fetch.d.ts.map +1 -1
  313. package/dist/tools/web-fetch.js +357 -45
  314. package/dist/tools/web-fetch.js.map +1 -1
  315. package/dist/tools/web-fetch.test.js +263 -2
  316. package/dist/tools/web-fetch.test.js.map +1 -1
  317. package/dist/tools/web-search.d.ts +14 -0
  318. package/dist/tools/web-search.d.ts.map +1 -1
  319. package/dist/tools/web-search.js +321 -35
  320. package/dist/tools/web-search.js.map +1 -1
  321. package/dist/tools/web-search.test.js +144 -1
  322. package/dist/tools/web-search.test.js.map +1 -1
  323. package/dist/tools/write.d.ts +8 -2
  324. package/dist/tools/write.d.ts.map +1 -1
  325. package/dist/tools/write.js +36 -9
  326. package/dist/tools/write.js.map +1 -1
  327. package/dist/tools/write.test.js +0 -16
  328. package/dist/tools/write.test.js.map +1 -1
  329. package/dist/ui/App.d.ts +52 -272
  330. package/dist/ui/App.d.ts.map +1 -1
  331. package/dist/ui/App.js +1606 -2199
  332. package/dist/ui/App.js.map +1 -1
  333. package/dist/ui/activity-phrases.d.ts.map +1 -1
  334. package/dist/ui/activity-phrases.js +0 -2
  335. package/dist/ui/activity-phrases.js.map +1 -1
  336. package/dist/ui/app-items.d.ts +276 -0
  337. package/dist/ui/app-items.d.ts.map +1 -0
  338. package/dist/ui/app-items.js +52 -0
  339. package/dist/ui/app-items.js.map +1 -0
  340. package/dist/ui/app-items.test.d.ts +2 -0
  341. package/dist/ui/app-items.test.d.ts.map +1 -0
  342. package/dist/ui/app-items.test.js +38 -0
  343. package/dist/ui/app-items.test.js.map +1 -0
  344. package/dist/ui/app-state-persistence.test.js +45 -160
  345. package/dist/ui/app-state-persistence.test.js.map +1 -1
  346. package/dist/ui/assistant-stream-flush-parity.test.d.ts +2 -0
  347. package/dist/ui/assistant-stream-flush-parity.test.d.ts.map +1 -0
  348. package/dist/ui/assistant-stream-flush-parity.test.js +85 -0
  349. package/dist/ui/assistant-stream-flush-parity.test.js.map +1 -0
  350. package/dist/ui/chat-layout-pinning.test.d.ts +2 -0
  351. package/dist/ui/chat-layout-pinning.test.d.ts.map +1 -0
  352. package/dist/ui/chat-layout-pinning.test.js +525 -0
  353. package/dist/ui/chat-layout-pinning.test.js.map +1 -0
  354. package/dist/ui/components/ActivityIndicator.d.ts +1 -2
  355. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  356. package/dist/ui/components/ActivityIndicator.js +63 -94
  357. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  358. package/dist/ui/components/AnimationContext.d.ts.map +1 -1
  359. package/dist/ui/components/AnimationContext.js +3 -5
  360. package/dist/ui/components/AnimationContext.js.map +1 -1
  361. package/dist/ui/components/AssistantMessage.d.ts +6 -2
  362. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  363. package/dist/ui/components/AssistantMessage.js +9 -4
  364. package/dist/ui/components/AssistantMessage.js.map +1 -1
  365. package/dist/ui/components/AssistantMessage.test.d.ts +2 -0
  366. package/dist/ui/components/AssistantMessage.test.d.ts.map +1 -0
  367. package/dist/ui/components/AssistantMessage.test.js +387 -0
  368. package/dist/ui/components/AssistantMessage.test.js.map +1 -0
  369. package/dist/ui/components/BackgroundTasksBar.d.ts +1 -3
  370. package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
  371. package/dist/ui/components/BackgroundTasksBar.js +8 -10
  372. package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
  373. package/dist/ui/components/Banner.d.ts +1 -2
  374. package/dist/ui/components/Banner.d.ts.map +1 -1
  375. package/dist/ui/components/Banner.js +9 -4
  376. package/dist/ui/components/Banner.js.map +1 -1
  377. package/dist/ui/components/ChatFooterPane.d.ts +25 -0
  378. package/dist/ui/components/ChatFooterPane.d.ts.map +1 -0
  379. package/dist/ui/components/ChatFooterPane.js +15 -0
  380. package/dist/ui/components/ChatFooterPane.js.map +1 -0
  381. package/dist/ui/components/ChatInputStack.d.ts +36 -0
  382. package/dist/ui/components/ChatInputStack.d.ts.map +1 -0
  383. package/dist/ui/components/ChatInputStack.js +10 -0
  384. package/dist/ui/components/ChatInputStack.js.map +1 -0
  385. package/dist/ui/components/ChatLayout.d.ts +37 -0
  386. package/dist/ui/components/ChatLayout.d.ts.map +1 -0
  387. package/dist/ui/components/ChatLayout.js +21 -0
  388. package/dist/ui/components/ChatLayout.js.map +1 -0
  389. package/dist/ui/components/ChatLivePane.d.ts +18 -0
  390. package/dist/ui/components/ChatLivePane.d.ts.map +1 -0
  391. package/dist/ui/components/ChatLivePane.js +22 -0
  392. package/dist/ui/components/ChatLivePane.js.map +1 -0
  393. package/dist/ui/components/ChatScreen.d.ts +136 -0
  394. package/dist/ui/components/ChatScreen.d.ts.map +1 -0
  395. package/dist/ui/components/ChatScreen.js +16 -0
  396. package/dist/ui/components/ChatScreen.js.map +1 -0
  397. package/dist/ui/components/ChatStatusRow.d.ts +34 -0
  398. package/dist/ui/components/ChatStatusRow.d.ts.map +1 -0
  399. package/dist/ui/components/ChatStatusRow.js +11 -0
  400. package/dist/ui/components/ChatStatusRow.js.map +1 -0
  401. package/dist/ui/components/CompactionNotice.d.ts +4 -2
  402. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  403. package/dist/ui/components/CompactionNotice.js +4 -4
  404. package/dist/ui/components/CompactionNotice.js.map +1 -1
  405. package/dist/ui/components/Footer.d.ts +27 -6
  406. package/dist/ui/components/Footer.d.ts.map +1 -1
  407. package/dist/ui/components/Footer.js +75 -30
  408. package/dist/ui/components/Footer.js.map +1 -1
  409. package/dist/ui/components/FooterStatusRow.d.ts +20 -0
  410. package/dist/ui/components/FooterStatusRow.d.ts.map +1 -0
  411. package/dist/ui/components/FooterStatusRow.js +10 -0
  412. package/dist/ui/components/FooterStatusRow.js.map +1 -0
  413. package/dist/ui/components/FullScreenOverlayRouter.d.ts +19 -0
  414. package/dist/ui/components/FullScreenOverlayRouter.d.ts.map +1 -0
  415. package/dist/ui/components/FullScreenOverlayRouter.js +18 -0
  416. package/dist/ui/components/FullScreenOverlayRouter.js.map +1 -0
  417. package/dist/ui/components/GoalOverlay.d.ts +28 -20
  418. package/dist/ui/components/GoalOverlay.d.ts.map +1 -1
  419. package/dist/ui/components/GoalOverlay.js +283 -253
  420. package/dist/ui/components/GoalOverlay.js.map +1 -1
  421. package/dist/ui/components/GoalStatusBar.d.ts.map +1 -1
  422. package/dist/ui/components/GoalStatusBar.js +2 -1
  423. package/dist/ui/components/GoalStatusBar.js.map +1 -1
  424. package/dist/ui/components/IdealHookMessage.d.ts +14 -0
  425. package/dist/ui/components/IdealHookMessage.d.ts.map +1 -0
  426. package/dist/ui/components/IdealHookMessage.js +26 -0
  427. package/dist/ui/components/IdealHookMessage.js.map +1 -0
  428. package/dist/ui/components/InputArea.d.ts +31 -6
  429. package/dist/ui/components/InputArea.d.ts.map +1 -1
  430. package/dist/ui/components/InputArea.js +208 -50
  431. package/dist/ui/components/InputArea.js.map +1 -1
  432. package/dist/ui/components/InputArea.test.js +11 -1
  433. package/dist/ui/components/InputArea.test.js.map +1 -1
  434. package/dist/ui/components/LiveToolPanel.d.ts +30 -0
  435. package/dist/ui/components/LiveToolPanel.d.ts.map +1 -0
  436. package/dist/ui/components/LiveToolPanel.js +66 -0
  437. package/dist/ui/components/LiveToolPanel.js.map +1 -0
  438. package/dist/ui/components/Markdown.d.ts +11 -11
  439. package/dist/ui/components/Markdown.d.ts.map +1 -1
  440. package/dist/ui/components/Markdown.js +28 -198
  441. package/dist/ui/components/Markdown.js.map +1 -1
  442. package/dist/ui/components/ModelSelector.d.ts.map +1 -1
  443. package/dist/ui/components/ModelSelector.js +18 -18
  444. package/dist/ui/components/ModelSelector.js.map +1 -1
  445. package/dist/ui/components/PlanModeLogo.d.ts +3 -0
  446. package/dist/ui/components/PlanModeLogo.d.ts.map +1 -0
  447. package/dist/ui/components/PlanModeLogo.js +49 -0
  448. package/dist/ui/components/PlanModeLogo.js.map +1 -0
  449. package/dist/ui/components/PlanOverlay.d.ts +7 -0
  450. package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
  451. package/dist/ui/components/PlanOverlay.js +17 -3
  452. package/dist/ui/components/PlanOverlay.js.map +1 -1
  453. package/dist/ui/components/PlanOverlay.test.d.ts +2 -0
  454. package/dist/ui/components/PlanOverlay.test.d.ts.map +1 -0
  455. package/dist/ui/components/PlanOverlay.test.js +24 -0
  456. package/dist/ui/components/PlanOverlay.test.js.map +1 -0
  457. package/dist/ui/components/QueueIndicator.d.ts +9 -0
  458. package/dist/ui/components/QueueIndicator.d.ts.map +1 -0
  459. package/dist/ui/components/QueueIndicator.js +9 -0
  460. package/dist/ui/components/QueueIndicator.js.map +1 -0
  461. package/dist/ui/components/RewindOverlay.d.ts +18 -0
  462. package/dist/ui/components/RewindOverlay.d.ts.map +1 -0
  463. package/dist/ui/components/RewindOverlay.js +52 -0
  464. package/dist/ui/components/RewindOverlay.js.map +1 -0
  465. package/dist/ui/components/SelectList.d.ts.map +1 -1
  466. package/dist/ui/components/SelectList.js +5 -0
  467. package/dist/ui/components/SelectList.js.map +1 -1
  468. package/dist/ui/components/ServerToolExecution.d.ts +2 -0
  469. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  470. package/dist/ui/components/ServerToolExecution.js +7 -23
  471. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  472. package/dist/ui/components/SessionSummary.d.ts +5 -0
  473. package/dist/ui/components/SessionSummary.d.ts.map +1 -0
  474. package/dist/ui/components/SessionSummary.js +32 -0
  475. package/dist/ui/components/SessionSummary.js.map +1 -0
  476. package/dist/ui/components/SlashCommandMenu.d.ts +4 -3
  477. package/dist/ui/components/SlashCommandMenu.d.ts.map +1 -1
  478. package/dist/ui/components/SlashCommandMenu.js +38 -26
  479. package/dist/ui/components/SlashCommandMenu.js.map +1 -1
  480. package/dist/ui/components/SlashStyledSelectList.d.ts +21 -0
  481. package/dist/ui/components/SlashStyledSelectList.d.ts.map +1 -0
  482. package/dist/ui/components/SlashStyledSelectList.js +71 -0
  483. package/dist/ui/components/SlashStyledSelectList.js.map +1 -0
  484. package/dist/ui/components/StreamingArea.d.ts +11 -2
  485. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  486. package/dist/ui/components/StreamingArea.js +20 -23
  487. package/dist/ui/components/StreamingArea.js.map +1 -1
  488. package/dist/ui/components/StreamingArea.test.d.ts +2 -0
  489. package/dist/ui/components/StreamingArea.test.d.ts.map +1 -0
  490. package/dist/ui/components/StreamingArea.test.js +18 -0
  491. package/dist/ui/components/StreamingArea.test.js.map +1 -0
  492. package/dist/ui/components/SubAgentPanel.d.ts +2 -1
  493. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  494. package/dist/ui/components/SubAgentPanel.js +4 -3
  495. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  496. package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
  497. package/dist/ui/components/TaskOverlay.js +53 -82
  498. package/dist/ui/components/TaskOverlay.js.map +1 -1
  499. package/dist/ui/components/TaskPickerMenu.d.ts +9 -0
  500. package/dist/ui/components/TaskPickerMenu.d.ts.map +1 -0
  501. package/dist/ui/components/TaskPickerMenu.js +33 -0
  502. package/dist/ui/components/TaskPickerMenu.js.map +1 -0
  503. package/dist/ui/components/ThemeSelector.js +2 -2
  504. package/dist/ui/components/ThemeSelector.js.map +1 -1
  505. package/dist/ui/components/ToolExecution.d.ts +2 -0
  506. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  507. package/dist/ui/components/ToolExecution.js +18 -52
  508. package/dist/ui/components/ToolExecution.js.map +1 -1
  509. package/dist/ui/components/ToolGroupExecution.d.ts +6 -2
  510. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  511. package/dist/ui/components/ToolGroupExecution.js +15 -147
  512. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  513. package/dist/ui/components/ToolUseLoader.d.ts +3 -1
  514. package/dist/ui/components/ToolUseLoader.d.ts.map +1 -1
  515. package/dist/ui/components/ToolUseLoader.js +2 -2
  516. package/dist/ui/components/ToolUseLoader.js.map +1 -1
  517. package/dist/ui/components/TranscriptViewport.d.ts +34 -0
  518. package/dist/ui/components/TranscriptViewport.d.ts.map +1 -0
  519. package/dist/ui/components/TranscriptViewport.js +45 -0
  520. package/dist/ui/components/TranscriptViewport.js.map +1 -0
  521. package/dist/ui/components/UserMessage.d.ts.map +1 -1
  522. package/dist/ui/components/UserMessage.js +15 -10
  523. package/dist/ui/components/UserMessage.js.map +1 -1
  524. package/dist/ui/components/UserMessage.test.d.ts +2 -0
  525. package/dist/ui/components/UserMessage.test.d.ts.map +1 -0
  526. package/dist/ui/components/UserMessage.test.js +39 -0
  527. package/dist/ui/components/UserMessage.test.js.map +1 -0
  528. package/dist/ui/components/index.d.ts +5 -0
  529. package/dist/ui/components/index.d.ts.map +1 -1
  530. package/dist/ui/components/index.js +5 -0
  531. package/dist/ui/components/index.js.map +1 -1
  532. package/dist/ui/duration-format.d.ts +2 -0
  533. package/dist/ui/duration-format.d.ts.map +1 -0
  534. package/dist/ui/duration-format.js +9 -0
  535. package/dist/ui/duration-format.js.map +1 -0
  536. package/dist/ui/duration-summary.d.ts +2 -0
  537. package/dist/ui/duration-summary.d.ts.map +1 -0
  538. package/dist/ui/duration-summary.js +66 -0
  539. package/dist/ui/duration-summary.js.map +1 -0
  540. package/dist/ui/error-item.d.ts +8 -0
  541. package/dist/ui/error-item.d.ts.map +1 -0
  542. package/dist/ui/error-item.js +32 -0
  543. package/dist/ui/error-item.js.map +1 -0
  544. package/dist/ui/footer-jump-regression.test.d.ts +2 -0
  545. package/dist/ui/footer-jump-regression.test.d.ts.map +1 -0
  546. package/dist/ui/footer-jump-regression.test.js +177 -0
  547. package/dist/ui/footer-jump-regression.test.js.map +1 -0
  548. package/dist/ui/footer-status-layout.test.js +22 -7
  549. package/dist/ui/footer-status-layout.test.js.map +1 -1
  550. package/dist/ui/goal-events.d.ts.map +1 -1
  551. package/dist/ui/goal-events.js +14 -6
  552. package/dist/ui/goal-events.js.map +1 -1
  553. package/dist/ui/goal-events.test.js +40 -2
  554. package/dist/ui/goal-events.test.js.map +1 -1
  555. package/dist/ui/goal-lifecycle-orchestration.test.d.ts +2 -0
  556. package/dist/ui/goal-lifecycle-orchestration.test.d.ts.map +1 -0
  557. package/dist/ui/goal-lifecycle-orchestration.test.js +512 -0
  558. package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -0
  559. package/dist/ui/goal-overlay.test.js +122 -44
  560. package/dist/ui/goal-overlay.test.js.map +1 -1
  561. package/dist/ui/goal-status-bar.test.js +29 -0
  562. package/dist/ui/goal-status-bar.test.js.map +1 -1
  563. package/dist/ui/goal-summary.d.ts +14 -0
  564. package/dist/ui/goal-summary.d.ts.map +1 -0
  565. package/dist/ui/goal-summary.js +194 -0
  566. package/dist/ui/goal-summary.js.map +1 -0
  567. package/dist/ui/hooks/useAgentLoop.d.ts +25 -4
  568. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  569. package/dist/ui/hooks/useAgentLoop.js +197 -22
  570. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  571. package/dist/ui/hooks/useAgentLoop.test.d.ts +2 -0
  572. package/dist/ui/hooks/useAgentLoop.test.d.ts.map +1 -0
  573. package/dist/ui/hooks/useAgentLoop.test.js +8 -0
  574. package/dist/ui/hooks/useAgentLoop.test.js.map +1 -0
  575. package/dist/ui/hooks/useChatLayoutMeasurements.d.ts +45 -0
  576. package/dist/ui/hooks/useChatLayoutMeasurements.d.ts.map +1 -0
  577. package/dist/ui/hooks/useChatLayoutMeasurements.js +97 -0
  578. package/dist/ui/hooks/useChatLayoutMeasurements.js.map +1 -0
  579. package/dist/ui/hooks/useContextCompaction.d.ts +41 -0
  580. package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -0
  581. package/dist/ui/hooks/useContextCompaction.js +149 -0
  582. package/dist/ui/hooks/useContextCompaction.js.map +1 -0
  583. package/dist/ui/hooks/useModeState.d.ts +56 -0
  584. package/dist/ui/hooks/useModeState.d.ts.map +1 -0
  585. package/dist/ui/hooks/useModeState.js +65 -0
  586. package/dist/ui/hooks/useModeState.js.map +1 -0
  587. package/dist/ui/hooks/usePixelFixFlow.d.ts +57 -0
  588. package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -0
  589. package/dist/ui/hooks/usePixelFixFlow.js +102 -0
  590. package/dist/ui/hooks/usePixelFixFlow.js.map +1 -0
  591. package/dist/ui/hooks/useSessionPersistence.d.ts +34 -0
  592. package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -0
  593. package/dist/ui/hooks/useSessionPersistence.js +67 -0
  594. package/dist/ui/hooks/useSessionPersistence.js.map +1 -0
  595. package/dist/ui/hooks/useTaskPickerController.d.ts +19 -0
  596. package/dist/ui/hooks/useTaskPickerController.d.ts.map +1 -0
  597. package/dist/ui/hooks/useTaskPickerController.js +41 -0
  598. package/dist/ui/hooks/useTaskPickerController.js.map +1 -0
  599. package/dist/ui/hooks/useTerminalSize.d.ts +13 -10
  600. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  601. package/dist/ui/hooks/useTerminalSize.js +21 -16
  602. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  603. package/dist/ui/hooks/useTranscriptHistory.d.ts +48 -0
  604. package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -0
  605. package/dist/ui/hooks/useTranscriptHistory.js +113 -0
  606. package/dist/ui/hooks/useTranscriptHistory.js.map +1 -0
  607. package/dist/ui/hooks/useTranscriptHistory.test.d.ts +2 -0
  608. package/dist/ui/hooks/useTranscriptHistory.test.d.ts.map +1 -0
  609. package/dist/ui/hooks/useTranscriptHistory.test.js +61 -0
  610. package/dist/ui/hooks/useTranscriptHistory.test.js.map +1 -0
  611. package/dist/ui/hooks/useTranscriptScroll.d.ts +30 -0
  612. package/dist/ui/hooks/useTranscriptScroll.d.ts.map +1 -0
  613. package/dist/ui/hooks/useTranscriptScroll.js +44 -0
  614. package/dist/ui/hooks/useTranscriptScroll.js.map +1 -0
  615. package/dist/ui/hooks/useTranscriptScroll.test.d.ts +2 -0
  616. package/dist/ui/hooks/useTranscriptScroll.test.d.ts.map +1 -0
  617. package/dist/ui/hooks/useTranscriptScroll.test.js +119 -0
  618. package/dist/ui/hooks/useTranscriptScroll.test.js.map +1 -0
  619. package/dist/ui/item-helpers.d.ts +26 -0
  620. package/dist/ui/item-helpers.d.ts.map +1 -0
  621. package/dist/ui/item-helpers.js +114 -0
  622. package/dist/ui/item-helpers.js.map +1 -0
  623. package/dist/ui/layout-decisions.d.ts +81 -0
  624. package/dist/ui/layout-decisions.d.ts.map +1 -0
  625. package/dist/ui/layout-decisions.js +89 -0
  626. package/dist/ui/layout-decisions.js.map +1 -0
  627. package/dist/ui/live-area-clamp.test.d.ts +2 -0
  628. package/dist/ui/live-area-clamp.test.d.ts.map +1 -0
  629. package/dist/ui/live-area-clamp.test.js +88 -0
  630. package/dist/ui/live-area-clamp.test.js.map +1 -0
  631. package/dist/ui/live-area-height.d.ts +35 -0
  632. package/dist/ui/live-area-height.d.ts.map +1 -0
  633. package/dist/ui/live-area-height.js +80 -0
  634. package/dist/ui/live-area-height.js.map +1 -0
  635. package/dist/ui/live-area-height.test.d.ts +2 -0
  636. package/dist/ui/live-area-height.test.d.ts.map +1 -0
  637. package/dist/ui/live-area-height.test.js +67 -0
  638. package/dist/ui/live-area-height.test.js.map +1 -0
  639. package/dist/ui/live-frame-height.test.d.ts +2 -0
  640. package/dist/ui/live-frame-height.test.d.ts.map +1 -0
  641. package/dist/ui/live-frame-height.test.js +116 -0
  642. package/dist/ui/live-frame-height.test.js.map +1 -0
  643. package/dist/ui/live-item-flush.d.ts +2 -2
  644. package/dist/ui/live-item-flush.d.ts.map +1 -1
  645. package/dist/ui/live-item-flush.js +8 -4
  646. package/dist/ui/live-item-flush.js.map +1 -1
  647. package/dist/ui/login.js +1 -1
  648. package/dist/ui/long-prompt-regression-harness.test.d.ts +2 -0
  649. package/dist/ui/long-prompt-regression-harness.test.d.ts.map +1 -0
  650. package/dist/ui/long-prompt-regression-harness.test.js +195 -0
  651. package/dist/ui/long-prompt-regression-harness.test.js.map +1 -0
  652. package/dist/ui/mcp.d.ts +62 -0
  653. package/dist/ui/mcp.d.ts.map +1 -0
  654. package/dist/ui/mcp.js +306 -0
  655. package/dist/ui/mcp.js.map +1 -0
  656. package/dist/ui/plan-overlay.test.js +7 -29
  657. package/dist/ui/plan-overlay.test.js.map +1 -1
  658. package/dist/ui/prompt-routing.d.ts +11 -0
  659. package/dist/ui/prompt-routing.d.ts.map +1 -0
  660. package/dist/ui/prompt-routing.js +61 -0
  661. package/dist/ui/prompt-routing.js.map +1 -0
  662. package/dist/ui/queued-message.test.d.ts +2 -0
  663. package/dist/ui/queued-message.test.d.ts.map +1 -0
  664. package/dist/ui/queued-message.test.js +162 -0
  665. package/dist/ui/queued-message.test.js.map +1 -0
  666. package/dist/ui/render.d.ts +38 -34
  667. package/dist/ui/render.d.ts.map +1 -1
  668. package/dist/ui/render.js +128 -41
  669. package/dist/ui/render.js.map +1 -1
  670. package/dist/ui/render.test.d.ts +2 -0
  671. package/dist/ui/render.test.d.ts.map +1 -0
  672. package/dist/ui/render.test.js +16 -0
  673. package/dist/ui/render.test.js.map +1 -0
  674. package/dist/ui/scroll-stabilization.test.js +1 -7
  675. package/dist/ui/scroll-stabilization.test.js.map +1 -1
  676. package/dist/ui/session-summary.d.ts +63 -0
  677. package/dist/ui/session-summary.d.ts.map +1 -0
  678. package/dist/ui/session-summary.js +81 -0
  679. package/dist/ui/session-summary.js.map +1 -0
  680. package/dist/ui/slash-command-images.test.js +12 -11
  681. package/dist/ui/slash-command-images.test.js.map +1 -1
  682. package/dist/ui/stores/transcript-scroll-store.d.ts +27 -0
  683. package/dist/ui/stores/transcript-scroll-store.d.ts.map +1 -0
  684. package/dist/ui/stores/transcript-scroll-store.js +73 -0
  685. package/dist/ui/stores/transcript-scroll-store.js.map +1 -0
  686. package/dist/ui/stores/transcript-scroll-store.test.d.ts +2 -0
  687. package/dist/ui/stores/transcript-scroll-store.test.d.ts.map +1 -0
  688. package/dist/ui/stores/transcript-scroll-store.test.js +73 -0
  689. package/dist/ui/stores/transcript-scroll-store.test.js.map +1 -0
  690. package/dist/ui/streaming-flush-bounce.test.d.ts +2 -0
  691. package/dist/ui/streaming-flush-bounce.test.d.ts.map +1 -0
  692. package/dist/ui/streaming-flush-bounce.test.js +156 -0
  693. package/dist/ui/streaming-flush-bounce.test.js.map +1 -0
  694. package/dist/ui/submit-prompt-command.d.ts +49 -0
  695. package/dist/ui/submit-prompt-command.d.ts.map +1 -0
  696. package/dist/ui/submit-prompt-command.js +107 -0
  697. package/dist/ui/submit-prompt-command.js.map +1 -0
  698. package/dist/ui/submit-slash-commands.d.ts +12 -0
  699. package/dist/ui/submit-slash-commands.d.ts.map +1 -0
  700. package/dist/ui/submit-slash-commands.js +32 -0
  701. package/dist/ui/submit-slash-commands.js.map +1 -0
  702. package/dist/ui/terminal-history-format.d.ts +43 -0
  703. package/dist/ui/terminal-history-format.d.ts.map +1 -0
  704. package/dist/ui/terminal-history-format.js +133 -0
  705. package/dist/ui/terminal-history-format.js.map +1 -0
  706. package/dist/ui/terminal-history-retry-dedup.test.d.ts +2 -0
  707. package/dist/ui/terminal-history-retry-dedup.test.d.ts.map +1 -0
  708. package/dist/ui/terminal-history-retry-dedup.test.js +61 -0
  709. package/dist/ui/terminal-history-retry-dedup.test.js.map +1 -0
  710. package/dist/ui/terminal-history-spacing.d.ts +2 -0
  711. package/dist/ui/terminal-history-spacing.d.ts.map +1 -0
  712. package/dist/ui/terminal-history-spacing.js +2 -0
  713. package/dist/ui/terminal-history-spacing.js.map +1 -0
  714. package/dist/ui/terminal-history-status-renderers.d.ts +15 -0
  715. package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -0
  716. package/dist/ui/terminal-history-status-renderers.js +99 -0
  717. package/dist/ui/terminal-history-status-renderers.js.map +1 -0
  718. package/dist/ui/terminal-history.d.ts +26 -0
  719. package/dist/ui/terminal-history.d.ts.map +1 -0
  720. package/dist/ui/terminal-history.js +916 -0
  721. package/dist/ui/terminal-history.js.map +1 -0
  722. package/dist/ui/terminal-history.test.d.ts +2 -0
  723. package/dist/ui/terminal-history.test.d.ts.map +1 -0
  724. package/dist/ui/terminal-history.test.js +550 -0
  725. package/dist/ui/terminal-history.test.js.map +1 -0
  726. package/dist/ui/testing/screen-recorder.d.ts +29 -0
  727. package/dist/ui/testing/screen-recorder.d.ts.map +1 -0
  728. package/dist/ui/testing/screen-recorder.js +179 -0
  729. package/dist/ui/testing/screen-recorder.js.map +1 -0
  730. package/dist/ui/thinking-level-cycle.test.d.ts +2 -0
  731. package/dist/ui/thinking-level-cycle.test.d.ts.map +1 -0
  732. package/dist/ui/thinking-level-cycle.test.js +45 -0
  733. package/dist/ui/thinking-level-cycle.test.js.map +1 -0
  734. package/dist/ui/thinking-level.d.ts +2 -0
  735. package/dist/ui/thinking-level.d.ts.map +1 -0
  736. package/dist/ui/thinking-level.js +2 -0
  737. package/dist/ui/thinking-level.js.map +1 -0
  738. package/dist/ui/tool-group-summary.d.ts +24 -0
  739. package/dist/ui/tool-group-summary.d.ts.map +1 -0
  740. package/dist/ui/tool-group-summary.js +233 -0
  741. package/dist/ui/tool-group-summary.js.map +1 -0
  742. package/dist/ui/tool-group-summary.test.d.ts +2 -0
  743. package/dist/ui/tool-group-summary.test.d.ts.map +1 -0
  744. package/dist/ui/tool-group-summary.test.js +79 -0
  745. package/dist/ui/tool-group-summary.test.js.map +1 -0
  746. package/dist/ui/tool-line-summary.d.ts +29 -0
  747. package/dist/ui/tool-line-summary.d.ts.map +1 -0
  748. package/dist/ui/tool-line-summary.js +153 -0
  749. package/dist/ui/tool-line-summary.js.map +1 -0
  750. package/dist/ui/transcript/MiscRows.d.ts +26 -0
  751. package/dist/ui/transcript/MiscRows.d.ts.map +1 -0
  752. package/dist/ui/transcript/MiscRows.js +68 -0
  753. package/dist/ui/transcript/MiscRows.js.map +1 -0
  754. package/dist/ui/transcript/StatusRow.d.ts +14 -0
  755. package/dist/ui/transcript/StatusRow.d.ts.map +1 -0
  756. package/dist/ui/transcript/StatusRow.js +14 -0
  757. package/dist/ui/transcript/StatusRow.js.map +1 -0
  758. package/dist/ui/transcript/ToolRows.d.ts +20 -0
  759. package/dist/ui/transcript/ToolRows.d.ts.map +1 -0
  760. package/dist/ui/transcript/ToolRows.js +25 -0
  761. package/dist/ui/transcript/ToolRows.js.map +1 -0
  762. package/dist/ui/transcript/TranscriptItemFrame.d.ts +8 -0
  763. package/dist/ui/transcript/TranscriptItemFrame.d.ts.map +1 -0
  764. package/dist/ui/transcript/TranscriptItemFrame.js +9 -0
  765. package/dist/ui/transcript/TranscriptItemFrame.js.map +1 -0
  766. package/dist/ui/transcript/TranscriptRenderer.d.ts +22 -0
  767. package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -0
  768. package/dist/ui/transcript/TranscriptRenderer.js +111 -0
  769. package/dist/ui/transcript/TranscriptRenderer.js.map +1 -0
  770. package/dist/ui/transcript/presentation.d.ts +61 -0
  771. package/dist/ui/transcript/presentation.d.ts.map +1 -0
  772. package/dist/ui/transcript/presentation.js +86 -0
  773. package/dist/ui/transcript/presentation.js.map +1 -0
  774. package/dist/ui/transcript/spacing.d.ts +56 -0
  775. package/dist/ui/transcript/spacing.d.ts.map +1 -0
  776. package/dist/ui/transcript/spacing.js +129 -0
  777. package/dist/ui/transcript/spacing.js.map +1 -0
  778. package/dist/ui/transcript/spacing.test.d.ts +2 -0
  779. package/dist/ui/transcript/spacing.test.d.ts.map +1 -0
  780. package/dist/ui/transcript/spacing.test.js +193 -0
  781. package/dist/ui/transcript/spacing.test.js.map +1 -0
  782. package/dist/ui/transcript/tool-presentation.d.ts +12 -0
  783. package/dist/ui/transcript/tool-presentation.d.ts.map +1 -0
  784. package/dist/ui/transcript/tool-presentation.js +55 -0
  785. package/dist/ui/transcript/tool-presentation.js.map +1 -0
  786. package/dist/ui/transcript/transcript-lines.d.ts +32 -0
  787. package/dist/ui/transcript/transcript-lines.d.ts.map +1 -0
  788. package/dist/ui/transcript/transcript-lines.js +86 -0
  789. package/dist/ui/transcript/transcript-lines.js.map +1 -0
  790. package/dist/ui/transcript/transcript-lines.test.d.ts +2 -0
  791. package/dist/ui/transcript/transcript-lines.test.d.ts.map +1 -0
  792. package/dist/ui/transcript/transcript-lines.test.js +76 -0
  793. package/dist/ui/transcript/transcript-lines.test.js.map +1 -0
  794. package/dist/ui/transcript-viewport-pinning.test.d.ts +2 -0
  795. package/dist/ui/transcript-viewport-pinning.test.d.ts.map +1 -0
  796. package/dist/ui/transcript-viewport-pinning.test.js +148 -0
  797. package/dist/ui/transcript-viewport-pinning.test.js.map +1 -0
  798. package/dist/ui/tui-history-parity.test.d.ts +2 -0
  799. package/dist/ui/tui-history-parity.test.d.ts.map +1 -0
  800. package/dist/ui/tui-history-parity.test.js +381 -0
  801. package/dist/ui/tui-history-parity.test.js.map +1 -0
  802. package/dist/ui/tui-simulation.test.d.ts +2 -0
  803. package/dist/ui/tui-simulation.test.d.ts.map +1 -0
  804. package/dist/ui/tui-simulation.test.js +139 -0
  805. package/dist/ui/tui-simulation.test.js.map +1 -0
  806. package/dist/ui/tui-terminal-recorder.test.d.ts +2 -0
  807. package/dist/ui/tui-terminal-recorder.test.d.ts.map +1 -0
  808. package/dist/ui/tui-terminal-recorder.test.js +134 -0
  809. package/dist/ui/tui-terminal-recorder.test.js.map +1 -0
  810. package/dist/ui/utils/assistant-stream-split.d.ts +23 -0
  811. package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -0
  812. package/dist/ui/utils/assistant-stream-split.js +41 -0
  813. package/dist/ui/utils/assistant-stream-split.js.map +1 -0
  814. package/dist/ui/utils/assistant-stream-split.test.d.ts +2 -0
  815. package/dist/ui/utils/assistant-stream-split.test.d.ts.map +1 -0
  816. package/dist/ui/utils/assistant-stream-split.test.js +40 -0
  817. package/dist/ui/utils/assistant-stream-split.test.js.map +1 -0
  818. package/dist/ui/utils/latex-to-unicode.d.ts +22 -0
  819. package/dist/ui/utils/latex-to-unicode.d.ts.map +1 -0
  820. package/dist/ui/utils/latex-to-unicode.js +538 -0
  821. package/dist/ui/utils/latex-to-unicode.js.map +1 -0
  822. package/dist/ui/utils/markdown-renderer.d.ts +20 -0
  823. package/dist/ui/utils/markdown-renderer.d.ts.map +1 -0
  824. package/dist/ui/utils/markdown-renderer.js +327 -0
  825. package/dist/ui/utils/markdown-renderer.js.map +1 -0
  826. package/dist/ui/utils/markdown-table.d.ts +9 -0
  827. package/dist/ui/utils/markdown-table.d.ts.map +1 -0
  828. package/dist/ui/utils/markdown-table.js +95 -0
  829. package/dist/ui/utils/markdown-table.js.map +1 -0
  830. package/dist/ui/utils/terminal-graphics.d.ts +16 -0
  831. package/dist/ui/utils/terminal-graphics.d.ts.map +1 -0
  832. package/dist/ui/utils/terminal-graphics.js +68 -0
  833. package/dist/ui/utils/terminal-graphics.js.map +1 -0
  834. package/dist/ui/utils/terminal-graphics.test.d.ts +2 -0
  835. package/dist/ui/utils/terminal-graphics.test.d.ts.map +1 -0
  836. package/dist/ui/utils/terminal-graphics.test.js +61 -0
  837. package/dist/ui/utils/terminal-graphics.test.js.map +1 -0
  838. package/dist/ui/utils/terminal-input.d.ts +3 -0
  839. package/dist/ui/utils/terminal-input.d.ts.map +1 -0
  840. package/dist/ui/utils/terminal-input.js +28 -0
  841. package/dist/ui/utils/terminal-input.js.map +1 -0
  842. package/dist/ui/utils/terminal-input.test.d.ts +2 -0
  843. package/dist/ui/utils/terminal-input.test.d.ts.map +1 -0
  844. package/dist/ui/utils/terminal-input.test.js +15 -0
  845. package/dist/ui/utils/terminal-input.test.js.map +1 -0
  846. package/dist/ui/utils/text-utils.d.ts +8 -0
  847. package/dist/ui/utils/text-utils.d.ts.map +1 -0
  848. package/dist/ui/utils/text-utils.js +16 -0
  849. package/dist/ui/utils/text-utils.js.map +1 -0
  850. package/dist/ui/utils/token-to-ansi.js +19 -9
  851. package/dist/ui/utils/token-to-ansi.js.map +1 -1
  852. package/dist/ui/utils/user-message-display.d.ts +7 -0
  853. package/dist/ui/utils/user-message-display.d.ts.map +1 -0
  854. package/dist/ui/utils/user-message-display.js +26 -0
  855. package/dist/ui/utils/user-message-display.js.map +1 -0
  856. package/dist/utils/format.js +7 -9
  857. package/dist/utils/format.js.map +1 -1
  858. package/dist/utils/image.d.ts +9 -0
  859. package/dist/utils/image.d.ts.map +1 -1
  860. package/dist/utils/image.js +25 -0
  861. package/dist/utils/image.js.map +1 -1
  862. package/dist/utils/plan-steps.d.ts.map +1 -1
  863. package/dist/utils/plan-steps.js +5 -1
  864. package/dist/utils/plan-steps.js.map +1 -1
  865. package/dist/utils/plan-steps.test.d.ts +2 -0
  866. package/dist/utils/plan-steps.test.d.ts.map +1 -0
  867. package/dist/utils/plan-steps.test.js +16 -0
  868. package/dist/utils/plan-steps.test.js.map +1 -0
  869. package/package.json +50 -10
  870. package/dist/core/repomap-context.d.ts +0 -11
  871. package/dist/core/repomap-context.d.ts.map +0 -1
  872. package/dist/core/repomap-context.js +0 -68
  873. package/dist/core/repomap-context.js.map +0 -1
  874. package/dist/core/repomap-context.test.d.ts +0 -2
  875. package/dist/core/repomap-context.test.d.ts.map +0 -1
  876. package/dist/core/repomap-context.test.js +0 -47
  877. package/dist/core/repomap-context.test.js.map +0 -1
  878. package/dist/core/repomap.d.ts +0 -74
  879. package/dist/core/repomap.d.ts.map +0 -1
  880. package/dist/core/repomap.js +0 -906
  881. package/dist/core/repomap.js.map +0 -1
  882. package/dist/core/repomap.test.d.ts +0 -2
  883. package/dist/core/repomap.test.d.ts.map +0 -1
  884. package/dist/core/repomap.test.js +0 -494
  885. package/dist/core/repomap.test.js.map +0 -1
  886. package/dist/ui/components/EyesOverlay.d.ts +0 -10
  887. package/dist/ui/components/EyesOverlay.d.ts.map +0 -1
  888. package/dist/ui/components/EyesOverlay.js +0 -220
  889. package/dist/ui/components/EyesOverlay.js.map +0 -1
package/dist/ui/App.js CHANGED
@@ -1,254 +1,149 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
3
- import { Box, Text, Static } from "ink";
3
+ import { Box, useStdout } from "ink";
4
4
  import { useTerminalSize } from "./hooks/useTerminalSize.js";
5
+ import { useChatLayoutMeasurements } from "./hooks/useChatLayoutMeasurements.js";
6
+ import { useTaskPickerController } from "./hooks/useTaskPickerController.js";
7
+ import { useModeState } from "./hooks/useModeState.js";
8
+ import { useSessionPersistence } from "./hooks/useSessionPersistence.js";
9
+ import { useContextCompaction } from "./hooks/useContextCompaction.js";
10
+ import { usePixelFixFlow } from "./hooks/usePixelFixFlow.js";
5
11
  import { useDoublePress } from "./hooks/useDoublePress.js";
6
12
  import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
7
- import { createHash } from "node:crypto";
8
- import { readFileSync, writeFileSync } from "node:fs";
9
- import { homedir } from "node:os";
10
- import { join } from "node:path";
11
13
  import { playNotificationSound } from "../utils/sound.js";
12
- import { formatError, } from "@prestyj/ai";
13
- import { extractImagePaths } from "../utils/image.js";
14
+ import {} from "@prestyj/ai";
15
+ import { downscaleForPreview, extractImagePaths } from "../utils/image.js";
14
16
  import { useAgentLoop } from "./hooks/useAgentLoop.js";
15
- import { isEyesActive, journalCount } from "@prestyj/eyes";
16
- import { UserMessage } from "./components/UserMessage.js";
17
- import { AssistantMessage } from "./components/AssistantMessage.js";
18
- import { ToolExecution } from "./components/ToolExecution.js";
19
- import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
20
- import { ServerToolExecution } from "./components/ServerToolExecution.js";
21
- import { SubAgentPanel } from "./components/SubAgentPanel.js";
22
- import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
17
+ import { useTranscriptHistory } from "./hooks/useTranscriptHistory.js";
23
18
  import { createWebSearchTool } from "../tools/web-search.js";
24
- import { StreamingArea } from "./components/StreamingArea.js";
25
- import { ActivityIndicator } from "./components/ActivityIndicator.js";
26
- import { InputArea } from "./components/InputArea.js";
27
- import { Footer } from "./components/Footer.js";
28
- import { GoalStatusBar, reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
29
- import { Banner } from "./components/Banner.js";
30
- import { PlanOverlay } from "./components/PlanOverlay.js";
31
- import { ModelSelector } from "./components/ModelSelector.js";
32
- import { TaskOverlay } from "./components/TaskOverlay.js";
33
- import { GoalOverlay } from "./components/GoalOverlay.js";
34
- import { PixelOverlay } from "./components/PixelOverlay.js";
35
- import { SkillsOverlay } from "./components/SkillsOverlay.js";
36
- import { EyesOverlay } from "./components/EyesOverlay.js";
37
- import { ThemeSelector } from "./components/ThemeSelector.js";
38
- import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
19
+ import { ChatScreen } from "./components/ChatScreen.js";
20
+ import { LIVE_TOOL_PANEL_ROWS } from "./components/LiveToolPanel.js";
21
+ import { FullScreenOverlayRouter } from "./components/FullScreenOverlayRouter.js";
22
+ import { SessionSummaryDisplay } from "./components/SessionSummary.js";
39
23
  import { useTheme, useSetTheme } from "./theme/theme.js";
40
24
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
41
25
  import { getGitBranch } from "../utils/git.js";
42
- import { getModel, getContextWindow, getMaxThinkingLevel } from "../core/model-registry.js";
26
+ import { getModel } from "../core/model-registry.js";
43
27
  import { SessionManager } from "../core/session-manager.js";
44
- import { appendMessagesToSession as appendSessionMessages, createCompactedSessionCheckpoint, } from "../core/session-compaction.js";
45
28
  import { log } from "../core/logger.js";
46
29
  import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
47
30
  import { generateSessionTitle } from "../utils/session-title.js";
48
31
  import { SettingsManager } from "../core/settings-manager.js";
49
- import { shouldCompact, compact, getCompactionReserveTokens, } from "../core/compaction/compactor.js";
50
- import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
51
32
  import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
52
33
  import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
53
34
  import { loadCustomCommands } from "../core/custom-commands.js";
54
- import { buildSystemPrompt } from "../system-prompt.js";
55
- import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
35
+ import { detectLanguages } from "../core/language-detector.js";
56
36
  import { detectVerifyCommands } from "../core/verify-commands.js";
57
- import { FOCUSED_REPO_MAP_MAX_CHARS, FIRST_TURN_REPO_MAP_MAX_CHARS, buildRepoMap, createRepoMapCache, } from "../core/repomap.js";
58
- import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMessages, } from "../core/repomap-context.js";
37
+ import { RewindOverlay } from "./components/RewindOverlay.js";
38
+ import { reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
59
39
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
60
40
  import { getMCPServers } from "../core/mcp/index.js";
61
41
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
62
- import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
63
- import { canCompleteGoalRun, decideGoalNextAction, shouldCreateVerifierFixTask, } from "../core/goal-controller.js";
42
+ import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
43
+ import { getNextPendingTask, markTaskInProgress } from "../core/tasks-store.js";
44
+ import { buildUserContentWithAttachments } from "./prompt-routing.js";
45
+ import { submitPromptCommand } from "./submit-prompt-command.js";
46
+ import { handleUiSlashCommand } from "./submit-slash-commands.js";
47
+ import { buildIdealReviewMessage, evaluateIdealReview } from "../core/ideal-review.js";
48
+ import { buildLoopBreakMessage, evaluateLoopBreak } from "../core/loop-breaker.js";
49
+ import { buildRegroundingMessage } from "../core/regrounding.js";
50
+ import { getNextThinkingLevel, isThinkingLevelSupported } from "./thinking-level.js";
51
+ import { getDoneFlushDecision, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
52
+ import { isTranscriptSpacingItem } from "./transcript/spacing.js";
53
+ import { buildTranscriptLines } from "./transcript/transcript-lines.js";
54
+ import { useTranscriptScroll } from "./hooks/useTranscriptScroll.js";
55
+ import { scrollTranscriptByLines } from "./stores/transcript-scroll-store.js";
56
+ import { renderTranscriptItem } from "./transcript/TranscriptRenderer.js";
57
+ import { formatDuration } from "./duration-format.js";
58
+ import { pickDurationVerb } from "./duration-summary.js";
59
+ import { toErrorItem } from "./error-item.js";
60
+ import { appendGoalDecision, appendGoalEvidence, createGoalTask, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
61
+ import { canCompleteGoalRun, decideGoalNextAction, } from "../core/goal-controller.js";
62
+ import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
64
63
  import { runGoalVerifierCommand } from "../core/goal-verifier.js";
65
64
  import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
66
- import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
67
- /** Where ezcoder bugs should be reported. Surfaced in the guidance line. */
68
- const GGCODER_BUG_REPORT_URL = "github.com/Gahroot/ezcoder/issues";
69
- /**
70
- * Build an ErrorItem from any thrown value. Centralises headline / message /
71
- * guidance extraction so every error answers the same question for the user:
72
- * "Should I retry, or is this a ezcoder bug to report?"
73
- */
74
- function toErrorItem(err, id, contextPrefix) {
75
- const f = formatError(err);
76
- const headline = contextPrefix ? `${contextPrefix} — ${f.headline}` : f.headline;
77
- // For ezcoder bugs, swap the generic "see /help" guidance for an actual URL
78
- // so users have a clear place to send the report.
79
- const guidance = f.source === "ezcoder"
80
- ? `This looks like a ezcoder bug — please send it to the dev at ${GGCODER_BUG_REPORT_URL}.`
81
- : f.guidance;
82
- // Mirror every user-visible error into ~/.ezcoder/debug.log so reports can be
83
- // diagnosed even after the terminal scrollback is gone.
84
- log("ERROR", "ui-error", headline, {
85
- source: f.source,
86
- message: f.message,
87
- ...(f.provider ? { provider: f.provider } : {}),
88
- ...(f.statusCode != null ? { statusCode: String(f.statusCode) } : {}),
89
- ...(f.requestId ? { requestId: f.requestId } : {}),
90
- ...(err instanceof Error && err.stack ? { stack: err.stack } : {}),
91
- });
92
- return {
93
- kind: "error",
94
- headline,
95
- message: f.message,
96
- guidance,
97
- id,
98
- };
99
- }
100
- export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS, customCommands = []) {
101
- const trimmed = input.trim();
102
- if (!trimmed.startsWith("/"))
103
- return null;
104
- const parts = trimmed.slice(1).split(" ");
105
- const cmdName = parts[0];
106
- const cmdArgs = parts.slice(1).join(" ").trim();
107
- const builtinCmd = promptCommands.find((c) => c.name === cmdName || c.aliases.includes(cmdName));
108
- const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
109
- const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
110
- if (!promptText)
111
- return null;
112
- return {
113
- cmdName,
114
- cmdArgs,
115
- promptText,
116
- fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
117
- };
118
- }
119
- export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
120
- if (inputImages.length === 0)
121
- return text;
122
- const parts = [];
123
- if (text) {
124
- parts.push({ type: "text", text });
125
- }
126
- for (const img of inputImages) {
127
- if (img.kind === "text") {
128
- parts.push({
129
- type: "text",
130
- text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
131
- });
132
- }
133
- else if (modelSupportsImages) {
134
- parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
135
- }
136
- else {
137
- // GLM models: save image to temp file and instruct model to use vision MCP tool
138
- const ext = img.mediaType.split("/")[1] ?? "png";
139
- const tmpPath = `/tmp/ezcoder-img-${Date.now()}.${ext}`;
140
- try {
141
- writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
142
- parts.push({
143
- type: "text",
144
- text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
145
- });
146
- }
147
- catch {
148
- parts.push({
149
- type: "text",
150
- text: `[User attached an image but it could not be saved for analysis]`,
151
- });
152
- }
153
- }
154
- }
155
- // If only text parts remain after stripping images, simplify to plain string
156
- return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
157
- }
158
- /** Tools that get aggregated into a single compact group when concurrent. */
159
- const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
65
+ import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, } from "./goal-events.js";
66
+ import { buildGoalFinalSummarySections, buildGoalSummaryRows, goalPassedDetail, } from "./goal-summary.js";
67
+ import { addLinesChanged, buildSessionSummary, createSessionStats, recordServerToolCall, recordToolEnd, recordTurnEnd, } from "./session-summary.js";
68
+ import { compactHistory, getNextGeneratedItemId, isActiveItem, isSameAssistantText, normalizeAssistantText, partitionCompleted, pinStreamingTextBeforeToolBoundary, removeItemsWithIds, uniqueItemsById, } from "./item-helpers.js";
69
+ import { IDEAL_HOOK_NOTICE_TEXT, LOOP_BREAK_NOTICE_TEXT, REGROUNDING_NOTICE_TEXT, lastVisibleTranscriptItem, } from "./app-items.js";
70
+ export { buildUserContentWithAttachments, routePromptCommandInput } from "./prompt-routing.js";
71
+ export { getNextThinkingLevel } from "./thinking-level.js";
72
+ export { getChatControlsLayoutDecision, getDoneFlushDecision, getScrollStabilizationDecision, getStaticHistoryKey, hasParagraphBreakLiveUserMessage, isTallLiveUserMessage, shouldHideHistoryForOverlayView, shouldHideStaticItemsForOverlayView, shouldStabilizeOverlayPaneRerender, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceAssistantAfterToolBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
73
+ export { getNextGeneratedItemId, isActiveItem, partitionCompleted, pinStreamingTextBeforeToolBoundary, } from "./item-helpers.js";
74
+ /** Tools that get aggregated into a single compact group when possible. */
75
+ const AGGREGATABLE_TOOLS = new Set([
76
+ "read",
77
+ "grep",
78
+ "find",
79
+ "ls",
80
+ "mcp__kencode-search__searchCode",
81
+ "mcp__kencode-search__referenceSources",
82
+ "mcp__kencode-search__discoverRepos",
83
+ ]);
160
84
  const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
161
- /**
162
- * Cap memory by replacing old items with tiny tombstones. Ink's <Static>
163
- * tracks rendered items by array length — the array must never shrink, but
164
- * we can swap out heavy objects for lightweight `{ kind: "tombstone", id }`
165
- * entries so GC can reclaim the original data.
166
- */
167
- const MAX_LIVE_HISTORY = 200;
168
- function compactHistory(items) {
169
- if (items.length <= MAX_LIVE_HISTORY)
170
- return items;
171
- const cutoff = items.length - MAX_LIVE_HISTORY;
172
- const compacted = new Array(items.length);
173
- for (let i = 0; i < cutoff; i++) {
174
- const it = items[i];
175
- compacted[i] = it.kind === "tombstone" ? it : { kind: "tombstone", id: it.id };
176
- }
177
- for (let i = cutoff; i < items.length; i++) {
178
- compacted[i] = items[i];
179
- }
180
- return compacted;
181
- }
182
85
  function summarizeGoalCompletion(summary) {
183
- const lines = summary
184
- .split("\n")
185
- .map((line) => line.trim())
186
- .filter((line) => line.length > 0 && line !== "[agent_done]");
187
- const statusLine = lines.find((line) => /^Status:/i.test(line));
188
- const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
189
- const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
190
- return statusLine ?? changedLine ?? verificationLine ?? lines[0];
86
+ const trimmed = summary.trim();
87
+ if (!trimmed)
88
+ return undefined;
89
+ return trimmed.length > 400 ? `${trimmed.slice(0, 397)}...` : trimmed;
191
90
  }
192
91
  function formatGoalWorkerFinishedTitle(taskTitle, status) {
193
- return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
92
+ return status === "done" ? `Worker finished: ${taskTitle}` : `Worker failed: ${taskTitle}`;
194
93
  }
195
- function countGoalTasksByStatus(tasks, status) {
196
- return tasks.filter((task) => task.status === status).length;
94
+ function goalCompletionReason(check) {
95
+ return check.reason;
96
+ }
97
+ function getGoalContinuationChoiceKey({ runId, decision, }) {
98
+ switch (decision.kind) {
99
+ case "create_task":
100
+ return `${runId}:create_task:${decision.title}`;
101
+ case "start_worker":
102
+ case "pause":
103
+ return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
104
+ case "run_verifier":
105
+ return `${runId}:run_verifier:${decision.command}`;
106
+ case "terminal":
107
+ return `${runId}:terminal:${decision.status}`;
108
+ case "blocked":
109
+ case "wait":
110
+ case "complete":
111
+ return `${runId}:${decision.kind}:${decision.reason}`;
112
+ }
197
113
  }
198
- function firstText(values) {
199
- return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
114
+ function goalTerminalRunIdFromItem(item) {
115
+ if (item.kind !== "goal_progress" || item.phase !== "terminal")
116
+ return undefined;
117
+ if (!item.id.startsWith("goal-terminal-"))
118
+ return undefined;
119
+ return item.id.slice("goal-terminal-".length);
200
120
  }
201
- function truncateGoalSummary(value, maxLength = 90) {
202
- const normalized = value.replace(/\s+/g, " ").trim();
203
- if (normalized.length <= maxLength)
204
- return normalized;
205
- return `${normalized.slice(0, maxLength - 1)}…`;
121
+ function goalProgressMatchesDraft(item, draft) {
122
+ return (item.title === draft.title &&
123
+ item.detail === draft.detail &&
124
+ item.status === draft.status &&
125
+ JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
126
+ JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
206
127
  }
207
- export function buildGoalSummaryRows(run) {
208
- const rows = [];
209
- const doneTasks = countGoalTasksByStatus(run.tasks, "done");
210
- const failedTasks = countGoalTasksByStatus(run.tasks, "failed");
211
- const blockedTasks = countGoalTasksByStatus(run.tasks, "blocked");
212
- const taskSuffix = [
213
- failedTasks > 0 ? `${failedTasks} failed` : undefined,
214
- blockedTasks > 0 ? `${blockedTasks} blocked` : undefined,
215
- ].filter((item) => item !== undefined);
216
- rows.push({
217
- label: "Tasks",
218
- value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
219
- ...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
220
- });
221
- const verifierResult = run.verifier?.lastResult;
222
- const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
223
- rows.push({
224
- label: "Verifier",
225
- value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
226
- ...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
227
- });
228
- const latestEvidence = run.evidence.at(-1);
229
- rows.push({
230
- label: "Evidence",
231
- value: `${run.evidence.length} recorded`,
232
- ...(latestEvidence
233
- ? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
234
- : {}),
128
+ export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
129
+ const runIds = new Set(runs.map((run) => run.id));
130
+ const terminalByRun = new Map(runs
131
+ .map((run) => [run.id, formatGoalTerminalProgress(run)])
132
+ .filter((entry) => entry[1] !== null));
133
+ if (runIds.size === 0)
134
+ return items;
135
+ let changed = false;
136
+ const reconciled = items.map((item, index) => {
137
+ const runId = goalTerminalRunIdFromItem(item);
138
+ if (!runId || !runIds.has(runId))
139
+ return item;
140
+ const draft = terminalByRun.get(runId);
141
+ if (item.kind === "goal_progress" && draft && goalProgressMatchesDraft(item, draft))
142
+ return item;
143
+ changed = true;
144
+ return { kind: "tombstone", id: `tombstone-${item.id}-${index}` };
235
145
  });
236
- if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
237
- rows.push({
238
- label: run.status === "paused" ? "Paused on" : "Blocked on",
239
- value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
240
- ? formatGoalBlockingPrerequisites(run)
241
- : (run.blockers[0] ?? "manual review"), 110),
242
- });
243
- }
244
- else if (run.successCriteria.length > 0) {
245
- rows.push({
246
- label: "Criteria",
247
- value: `${run.successCriteria.length} checked`,
248
- detail: truncateGoalSummary(run.successCriteria[0] ?? "", 80),
249
- });
250
- }
251
- return rows.slice(0, 4);
146
+ return changed ? reconciled : items;
252
147
  }
253
148
  export function formatGoalTerminalProgress(run) {
254
149
  switch (run.status) {
@@ -257,8 +152,9 @@ export function formatGoalTerminalProgress(run) {
257
152
  kind: "goal_progress",
258
153
  phase: "terminal",
259
154
  title: `Goal passed: ${run.title}`,
260
- detail: "Verifier evidence is recorded; auto-continuation stopped.",
155
+ detail: goalPassedDetail(run),
261
156
  summaryRows: buildGoalSummaryRows(run),
157
+ summarySections: buildGoalFinalSummarySections(run),
262
158
  status: run.status,
263
159
  };
264
160
  case "failed":
@@ -297,221 +193,49 @@ export function formatGoalTerminalProgress(run) {
297
193
  return null;
298
194
  }
299
195
  }
300
- export function shouldHideHistoryForOverlayView(_isOverlayView, _isAgentRunning) {
301
- // Ink Static is append-only. Passing [] for overlay panes rewrites the Static
302
- // accumulator and can destroy scrollback when the pane closes. Keep history
303
- // mounted and let overlays render below it.
304
- return false;
305
- }
306
- export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
307
- return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
308
- }
309
- export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender, }) {
310
- return shouldHideHistoryForOverlay && !stabilizeOverlayPaneRerender;
311
- }
312
- export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
313
- const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
314
- return {
315
- preserveStatic: shouldStabilize && hasNewOutput,
316
- autoFollow: !shouldStabilize,
317
- };
318
- }
319
- export function isTallLiveUserMessage(text, rows) {
320
- return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
321
- }
322
- export function getStaticHistoryKey({ resizeKey }) {
323
- return `${resizeKey}`;
324
- }
325
- // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
326
- /** Check whether an item is still active (running spinner, pending result). */
327
- function isActiveItem(item) {
328
- switch (item.kind) {
329
- case "tool_start":
330
- case "server_tool_start":
331
- case "compacting":
332
- return true;
333
- case "tool_group":
334
- return item.tools.some((t) => t.status === "running");
335
- case "subagent_group":
336
- return item.agents.some((a) => a.status === "running");
337
- default:
338
- return false;
339
- }
340
- }
341
- /**
342
- * Partition live items into completed (flushable to Static) and still-active.
343
- * Completed items precede active ones — we flush the longest contiguous prefix
344
- * of completed items to keep ordering stable.
345
- */
346
- function partitionCompleted(items) {
347
- // Find the first active item — everything before it is safe to flush
348
- const firstActiveIdx = items.findIndex(isActiveItem);
349
- if (firstActiveIdx === -1) {
350
- // All items are completed
351
- return { flushed: items, remaining: [] };
352
- }
353
- if (firstActiveIdx === 0) {
354
- return { flushed: [], remaining: items };
355
- }
356
- return {
357
- flushed: items.slice(0, firstActiveIdx),
358
- remaining: items.slice(firstActiveIdx),
359
- };
360
- }
361
- // ── Duration summary ─────────────────────────────────────
362
- function formatDuration(ms) {
363
- const totalSec = Math.round(ms / 1000);
364
- if (totalSec < 60)
365
- return `${totalSec}s`;
366
- const min = Math.floor(totalSec / 60);
367
- const sec = totalSec % 60;
368
- return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
369
- }
370
- function pickDurationVerb(toolsUsed) {
371
- const has = (name) => toolsUsed.includes(name);
372
- const hasAny = (...names) => names.some(has);
373
- const writing = has("edit") || has("write");
374
- const reading = has("read") || has("grep") || has("find") || has("ls");
375
- // Multi-tool combos (most specific first)
376
- if (has("subagent") && writing)
377
- return "Orchestrated changes for";
378
- if (has("subagent"))
379
- return "Delegated work for";
380
- if (has("web-fetch") && writing)
381
- return "Researched & coded for";
382
- if (has("web-fetch") && reading)
383
- return "Researched for";
384
- if (has("web-fetch"))
385
- return "Fetched the web for";
386
- if (has("bash") && writing)
387
- return "Built & ran for";
388
- if (has("edit") && has("write"))
389
- return "Crafted code for";
390
- if (has("edit") && has("bash"))
391
- return "Refactored & tested for";
392
- if (has("edit") && reading)
393
- return "Refactored for";
394
- if (has("edit"))
395
- return "Refactored for";
396
- if (has("write") && has("bash"))
397
- return "Wrote & ran for";
398
- if (has("write") && reading)
399
- return "Wrote code for";
400
- if (has("write"))
401
- return "Wrote code for";
402
- if (has("bash") && has("grep"))
403
- return "Hacked away for";
404
- if (has("bash") && reading)
405
- return "Ran & investigated for";
406
- if (has("bash"))
407
- return "Executed commands for";
408
- if (hasAny("tasks", "task-output", "task-stop"))
409
- return "Managed tasks for";
410
- if (has("grep") && has("read"))
411
- return "Investigated for";
412
- if (has("grep") && has("find"))
413
- return "Scoured the codebase for";
414
- if (has("grep"))
415
- return "Searched for";
416
- if (has("read") && has("find"))
417
- return "Explored for";
418
- if (has("read"))
419
- return "Studied the code for";
420
- if (has("find") || has("ls"))
421
- return "Browsed files for";
422
- // No tools used — pure text response
423
- const phrases = [
424
- "Pondered for",
425
- "Thought for",
426
- "Reasoned for",
427
- "Mulled it over for",
428
- "Noodled on it for",
429
- "Brewed up a response in",
430
- "Cooked up an answer in",
431
- "Worked out a reply in",
432
- "Channeled wisdom for",
433
- "Conjured a response in",
434
- ];
435
- return phrases[Math.floor(Math.random() * phrases.length)];
436
- }
437
- // ── Animated thinking border ────────────────────────────────
438
- const THINKING_BORDER_COLORS = ["#60a5fa", "#818cf8", "#a78bfa", "#818cf8", "#60a5fa"];
439
- // ── Task count helper ───────────────────────────────────────
440
- function getTaskCount(cwd) {
441
- try {
442
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
443
- const data = readFileSync(join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json"), "utf-8");
444
- const tasks = JSON.parse(data);
445
- return tasks.filter((t) => t.status !== "done").length;
446
- }
447
- catch {
448
- return 0;
449
- }
450
- }
451
- function getNextPendingTask(cwd) {
452
- try {
453
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
454
- const data = readFileSync(join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json"), "utf-8");
455
- const tasks = JSON.parse(data);
456
- const pending = tasks.find((t) => t.status === "pending");
457
- if (!pending)
458
- return null;
459
- return {
460
- id: pending.id,
461
- title: pending.title,
462
- prompt: pending.prompt || pending.text || pending.title,
463
- };
464
- }
465
- catch {
466
- return null;
467
- }
468
- }
469
- function markTaskInProgress(cwd, taskId) {
470
- try {
471
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
472
- const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
473
- const data = readFileSync(filePath, "utf-8");
474
- const tasks = JSON.parse(data);
475
- const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "in-progress" } : t));
476
- writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
477
- }
478
- catch {
479
- // ignore
480
- }
196
+ export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
197
+ if (wasGoalSetupTurn)
198
+ return "off";
199
+ if (currentMode === "planner" || currentMode === "setup")
200
+ return currentMode;
201
+ if (queuedSyntheticEvents > 0)
202
+ return "coordinator";
203
+ if (activeContinuationFlights > 0)
204
+ return "coordinator";
205
+ if (currentMode === "coordinator" && runningGoalIds > 0)
206
+ return "coordinator";
207
+ return "off";
481
208
  }
209
+ // ── App Component ──────────────────────────────────────────
482
210
  /**
483
- * Mark a task as done in the on-disk tasks.json. Used by run-all auto-advance
484
- * so the in-flight task is recorded as completed even when the agent forgets
485
- * to call the `tasks` tool. Errors are swallowed — if the file is missing or
486
- * the id is stale, the next auto-advance step still proceeds correctly.
211
+ * Extract inline image previews carried on a tool result's `details` payload.
212
+ * Image-producing tools (e.g. screenshot) attach `{ imagePreviews: [...] }` so
213
+ * the terminal-history printer can render the captured pixels inline.
487
214
  */
488
- function markTaskDone(cwd, taskId) {
489
- try {
490
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
491
- const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
492
- const data = readFileSync(filePath, "utf-8");
493
- const tasks = JSON.parse(data);
494
- const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "done" } : t));
495
- writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
496
- }
497
- catch {
498
- // ignore
499
- }
215
+ function extractToolImagePreviews(details) {
216
+ if (!details || typeof details !== "object")
217
+ return undefined;
218
+ const previews = details.imagePreviews;
219
+ if (!Array.isArray(previews))
220
+ return undefined;
221
+ const valid = previews.filter((p) => typeof p === "object" &&
222
+ p !== null &&
223
+ typeof p.base64 === "string" &&
224
+ typeof p.mediaType === "string");
225
+ return valid.length > 0 ? valid : undefined;
500
226
  }
501
- // ── App Component ──────────────────────────────────────────
502
227
  export function App(props) {
503
228
  const theme = useTheme();
504
229
  const switchTheme = useSetTheme();
505
- const { columns, resizeKey } = useTerminalSize();
230
+ const { write: writeStdout } = useStdout();
231
+ const { columns, rows } = useTerminalSize();
506
232
  // Hoisted before terminal title hook so it can reference them
507
233
  const [lastUserMessage, setLastUserMessage] = useState("");
234
+ // Bumped on every prompt submit; the fullscreen transcript scroll controller
235
+ // watches this to snap back to the bottom so the newest output is visible.
236
+ const [scrollResetToken, setScrollResetToken] = useState(0);
508
237
  const [exitPending, setExitPending] = useState(false);
509
- // Initialize from planModeRef (lives outside React in cli.ts) so plan
510
- // mode survives /clear's unmount/remount, matching the prior behavior
511
- // where /clear didn't toggle plan mode off.
512
- const [planMode, setPlanMode] = useState(props.planModeRef?.current ?? false);
513
- const planModeLocalRef = useRef(false);
514
- planModeLocalRef.current = planMode;
238
+ const [quittingSummary, setQuittingSummary] = useState(null);
515
239
  // Terminal title — updated later after agentLoop is created
516
240
  // (hoisted here so the hook is always called in the same order)
517
241
  const [titleRunning, setTitleRunning] = useState(false);
@@ -521,14 +245,11 @@ export function App(props) {
521
245
  isRunning: titleRunning,
522
246
  sessionTitle,
523
247
  });
524
- // Items scrolled into Static (history). For restored sessions, seed the
525
- // initial array directly matches how every other Ink chat agent passes
526
- // messages to <Static> (cat-code, harness, p90-cli, openai-chatgpt, lms,
527
- // gatsby). Ink's Static (build/components/Static.js) starts with index=0
528
- // so slice(0) returns the full array regardless of length.
248
+ // Completed transcript rows are kept as durable session data but are no longer
249
+ // rendered through Ink history. They are serialized once into real terminal
250
+ // scrollback via terminalHistoryPrinter, while Ink owns only live rows and
251
+ // controls. This avoids Static/log-update replay drift on resize/remount.
529
252
  const [history, setHistory] = useState(() => {
530
- // sessionStore wins (lives across remount). Falls back to initialHistory
531
- // (loaded from a session file at startup), then a fresh banner-only list.
532
253
  const stored = props.sessionStore?.history;
533
254
  if (stored && stored.length > 0)
534
255
  return stored;
@@ -538,57 +259,68 @@ export function App(props) {
538
259
  return [{ kind: "banner", id: "banner" }];
539
260
  });
540
261
  // Items from the current/last turn — rendered in the live area so they stay visible.
541
- // Seed from sessionStore so Goal progress/completion rows and other live output
542
- // survive pane/overlay/resize remounts before they are flushed to <Static>.
543
- const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
262
+ // Seed from sessionStore so live output
263
+ // survives pane/overlay/resize remounts before it is finalized.
264
+ const [liveItems, setLiveItems] = useState(() => {
265
+ const restoredLiveItems = uniqueItemsById(props.sessionStore?.liveItems ?? []);
266
+ const restoredHistoryIds = new Set(history.map((item) => item.id));
267
+ return removeItemsWithIds(restoredLiveItems, restoredHistoryIds);
268
+ });
269
+ // Rolling feed of recent tool actions for the pinned LiveToolPanel. Kept
270
+ // separate from `liveItems` (the scrollback record) so tool calls mutate in
271
+ // place above the activity bar instead of spamming the transcript.
272
+ const [liveToolFeed, setLiveToolFeed] = useState([]);
544
273
  // overlay seeded from sessionStore (lives across remount). Falls back to
545
274
  // props.initialOverlay (CLI launched with one), then null.
546
275
  const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
547
- const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
548
- const [goalCount, setGoalCount] = useState(0);
549
- const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
550
- const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
551
276
  const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
552
- // Seed from sessionStore so "Run All" chaining survives the resetUI()
553
- // remount that startTask() triggers between tasks.
277
+ // Signal that pushes text into the InputArea composer (e.g. restoring queued
278
+ // messages after an interrupt). Bumping `nonce` triggers the injection even
279
+ // when the text is identical to a prior restore.
280
+ const [composerInject, setComposerInject] = useState(null);
281
+ const agentRunningRef = useRef(false);
554
282
  const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
555
283
  const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
556
- // Task id of the run-all task currently driving the agent. Tracked so the
557
- // run-all `onDone` auto-advance can mark the just-finished task as done
558
- // even when the agent forgets to call the tasks tool. Mirrored through
559
- // sessionStore so the value survives the resetUI() remount between tasks.
560
- const currentTaskIdRef = useRef(props.sessionStore?.currentTaskId ?? null);
561
284
  const startTaskRef = useRef(() => { });
562
- const agentRunningRef = useRef(false);
563
- const runningGoalIdsRef = useRef(new Set());
564
- const activeVerifierRunIdsRef = useRef(new Set());
565
- const startGoalRunRef = useRef(() => { });
566
285
  const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
567
286
  const currentPixelFixRef = useRef(null);
568
287
  const startPixelFixRef = useRef(() => { });
569
288
  const cwdRef = useRef(props.cwd);
570
289
  const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
290
+ // /rewind overlay: holds the checkpoint list while the picker is open.
291
+ const [rewindCheckpoints, setRewindCheckpoints] = useState(null);
292
+ // Monotonic user-turn counter keying per-turn checkpoints.
293
+ const rewindTurnRef = useRef(0);
294
+ const taskPicker = useTaskPickerController({
295
+ displayedCwd,
296
+ onStartTask: (title, prompt, taskId) => startTaskRef.current(title, prompt, taskId),
297
+ onRunAllTasksChange: setRunAllTasks,
298
+ });
571
299
  const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
572
300
  // Suppress "done" status when a plan overlay is about to open
573
301
  const planOverlayPendingRef = useRef(false);
574
302
  const [gitBranch, setGitBranch] = useState(null);
575
303
  const [currentModel, setCurrentModel] = useState(props.model);
304
+ const currentModelRef = useRef(props.model);
576
305
  const [currentProvider, setCurrentProvider] = useState(props.provider);
306
+ const currentProviderRef = useRef(props.provider);
577
307
  const [currentTools, setCurrentTools] = useState(props.tools);
578
308
  const currentToolsRef = useRef(props.tools);
579
- const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
309
+ const [thinkingLevel, setThinkingLevel] = useState(props.thinking);
310
+ const [renderMarkdown, setRenderMarkdown] = useState(true);
580
311
  const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
581
- const repoMapInjectionEnabledRef = useRef(true);
582
- const repoMapDirtyRef = useRef(true);
583
- const repoMapMarkdownRef = useRef("");
584
- const repoMapSnapshotRef = useRef(undefined);
585
- const repoMapChangedCountRef = useRef(0);
586
- const repoMapCacheRef = useRef(createRepoMapCache());
587
312
  const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
313
+ const [goalAutoExpand, setGoalAutoExpand] = useState(props.sessionStore?.goalAutoExpand ?? false);
314
+ const goalAutoExpandRef = useRef(props.sessionStore?.goalAutoExpand ?? false);
315
+ const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
316
+ const runningGoalIdsRef = useRef(new Set());
317
+ const activeVerifierRunIdsRef = useRef(new Set());
318
+ const goalContinuationFlightsRef = useRef(new Set());
319
+ const goalContinuationRecentChoicesRef = useRef(new Map());
320
+ const startGoalRunRef = useRef(() => { });
588
321
  const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
589
322
  const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
590
323
  const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
591
- const planModeStateRef = useRef(planMode);
592
324
  // Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
593
325
  // times we've nudged the agent to continue the same step. Reset whenever a
594
326
  // new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
@@ -596,32 +328,25 @@ export function App(props) {
596
328
  const followUpNudgesRef = useRef({ step: 0, count: 0 });
597
329
  // Seed the per-item ID counter so it doesn't collide with IDs already in
598
330
  // sessionStore.history (which survives remount). Without this, a remount
599
- // (resize, overlay toggle, etc.) starts the counter at 0 and new items
600
- // generate ids "0", "1", "2"… that collide with the same ids from the
601
- // previous mount, triggering React's duplicate-key warning and causing
602
- // duplicate/omitted renders.
603
- const nextIdRef = useRef((() => {
604
- const items = [
605
- ...(props.sessionStore?.history ?? props.initialHistory ?? []),
606
- ...(props.sessionStore?.liveItems ?? []),
607
- ];
608
- let max = -1;
609
- for (const item of items) {
610
- const n = Number(item.id);
611
- if (Number.isFinite(n) && n > max)
612
- max = n;
613
- }
614
- return max + 1;
615
- })());
331
+ // (resize, overlay toggle, task pane open, etc.) starts the counter at 0
332
+ // and new items generate ids "ui-0", "ui-1", "ui-2"… that collide with
333
+ // the same ids from the previous mount, triggering React's duplicate-key
334
+ // warning and causing duplicate/omitted renders.
335
+ const nextIdRef = useRef(getNextGeneratedItemId([
336
+ ...(props.sessionStore?.history ?? props.initialHistory ?? []),
337
+ ...(props.sessionStore?.liveItems ?? []),
338
+ ]));
616
339
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
617
340
  const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
618
341
  const persistedIndexRef = useRef(messagesRef.current.length);
342
+ const sessionStatsRef = useRef(props.sessionStore?.sessionStats ??
343
+ createSessionStats({ sessionId: props.sessionStore?.sessionId ?? props.sessionId }));
344
+ const [idealReviewEnabled, setIdealReviewEnabled] = useState(props.sessionStore?.idealReviewEnabled ?? props.idealReviewEnabled ?? true);
345
+ const idealReviewEnabledRef = useRef(idealReviewEnabled);
619
346
  /** Last actual API-reported input token count (from turn_end). */
620
347
  const lastActualTokensRef = useRef(0);
621
348
  /** Timestamp (ms) when lastActualTokensRef was last updated by turn_end. */
622
349
  const lastActualTokensTimestampRef = useRef(0);
623
- /** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
624
- const lastCompactionTimeRef = useRef(0);
625
350
  /**
626
351
  * Languages whose style packs are currently injected into the system prompt.
627
352
  * Grown by `maybeInjectLanguagePacks` after `write`/`bash` tool results when
@@ -643,6 +368,29 @@ export function App(props) {
643
368
  */
644
369
  const triggerAutoSetupRef = useRef(async () => { });
645
370
  const getId = () => `ui-${nextIdRef.current++}`;
371
+ useEffect(() => {
372
+ idealReviewEnabledRef.current = idealReviewEnabled;
373
+ if (props.sessionStore)
374
+ props.sessionStore.idealReviewEnabled = idealReviewEnabled;
375
+ }, [idealReviewEnabled, props.sessionStore]);
376
+ const sessionStore = props.sessionStore;
377
+ const { planMode, goalMode, rebuildSystemPrompt, replaceSystemPrompt, setPlanModeAndPrompt, setGoalModeAndPrompt, } = useModeState({
378
+ initialPlanMode: props.sessionStore?.planMode ?? props.planModeRef?.current ?? false,
379
+ initialGoalMode: props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off",
380
+ skills: props.skills,
381
+ planModeRef: props.planModeRef,
382
+ goalModeRef: props.goalModeRef,
383
+ sessionStore: props.sessionStore,
384
+ cwdRef,
385
+ currentToolsRef,
386
+ providerRef: currentProviderRef,
387
+ approvedPlanPathRef,
388
+ injectedLanguagesRef,
389
+ messagesRef,
390
+ });
391
+ const appendGoalAgentTransition = useCallback((text) => {
392
+ setLiveItems((prev) => [...prev, { kind: "goal_agent_transition", text, id: getId() }]);
393
+ }, []);
646
394
  const appendGoalProgress = useCallback((item) => {
647
395
  setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
648
396
  }, []);
@@ -663,47 +411,64 @@ export function App(props) {
663
411
  return next;
664
412
  });
665
413
  }, [props.sessionStore]);
666
- // Two-phase flush: items waiting to be moved to Static history after the
667
- // live area has been cleared and Ink has committed the smaller output.
668
- const pendingFlushRef = useRef([]);
669
- const [flushGeneration, setFlushGeneration] = useState(0);
670
- /** Queue items for two-phase flush and signal the drain effect. */
671
- const queueFlush = useCallback((items) => {
672
- if (items.length === 0)
673
- return;
674
- pendingFlushRef.current = [...pendingFlushRef.current, ...items];
675
- if (props.sessionStore) {
676
- const queuedIds = new Set(items.map((item) => item.id));
677
- props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
678
- }
679
- setFlushGeneration((g) => g + 1);
680
- }, [props.sessionStore]);
414
+ const { pendingHistoryFlushRef, streamedAssistantFlushRef, queueFlush, finalizeSubmittedUserItem, clearPendingHistory, } = useTranscriptHistory({
415
+ // In fullscreen alt-screen mode the transcript renders inside Ink (the
416
+ // viewport), so we must NOT write completed rows to native scrollback —
417
+ // doing so would scroll the live frame. Dropping the printer keeps history
418
+ // accumulating in React state (still flushed + persisted) without any
419
+ // stdout writes. The legacy scrollback path keeps the printer.
420
+ terminalHistoryPrinter: props.fullscreen ? undefined : props.terminalHistoryPrinter,
421
+ terminalHistoryContext: {
422
+ theme,
423
+ columns,
424
+ version: props.version,
425
+ model: currentModel,
426
+ provider: currentProvider,
427
+ cwd: displayedCwd,
428
+ },
429
+ writeStdout,
430
+ sessionPathRef,
431
+ sessionManagerRef,
432
+ sessionStore,
433
+ history,
434
+ setHistory,
435
+ setLiveItems,
436
+ });
681
437
  // Mirror runtime state choices (model/provider/thinking) into renderApp's
682
438
  // closure so unmount/remount preserves them.
683
439
  const onRuntimeStateChange = props.onRuntimeStateChange;
684
440
  useEffect(() => {
441
+ currentModelRef.current = currentModel;
685
442
  onRuntimeStateChange?.({ model: currentModel });
686
443
  }, [currentModel, onRuntimeStateChange]);
687
444
  useEffect(() => {
688
445
  onRuntimeStateChange?.({ provider: currentProvider });
689
446
  }, [currentProvider, onRuntimeStateChange]);
447
+ useEffect(() => {
448
+ if (thinkingLevel && !isThinkingLevelSupported(currentProvider, currentModel, thinkingLevel)) {
449
+ setThinkingLevel(getNextThinkingLevel(currentProvider, currentModel, undefined));
450
+ }
451
+ }, [currentProvider, currentModel, thinkingLevel]);
690
452
  useEffect(() => {
691
453
  onRuntimeStateChange?.({
692
- thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
454
+ thinking: thinkingLevel,
693
455
  });
694
- }, [thinkingEnabled, currentModel, onRuntimeStateChange]);
456
+ }, [thinkingLevel, onRuntimeStateChange]);
695
457
  // Mirror session state into renderApp's closure so resetUI() can re-seed
696
458
  // the conversation on remount. Each panel that previously did a bare ANSI
697
- // screen clear (overlay open/close, plan accept/reject, /clear, startTask)
459
+ // screen clear (overlay open/close, plan accept/reject, /clear)
698
460
  // now goes through resetUI; without these mirrors, the chat would vanish.
699
- const sessionStore = props.sessionStore;
461
+ const historyRef = useRef(history);
700
462
  useEffect(() => {
463
+ historyRef.current = history;
701
464
  if (sessionStore)
702
465
  sessionStore.history = history;
703
466
  }, [history, sessionStore]);
704
467
  useEffect(() => {
705
- if (sessionStore)
706
- sessionStore.liveItems = liveItems;
468
+ if (!sessionStore)
469
+ return;
470
+ const historyIds = new Set(historyRef.current.map((item) => item.id));
471
+ sessionStore.liveItems = removeItemsWithIds(uniqueItemsById(liveItems), historyIds);
707
472
  }, [liveItems, sessionStore]);
708
473
  useEffect(() => {
709
474
  if (sessionStore)
@@ -721,10 +486,27 @@ export function App(props) {
721
486
  if (sessionStore)
722
487
  sessionStore.overlay = overlay;
723
488
  }, [overlay, sessionStore]);
489
+ useEffect(() => {
490
+ goalAutoExpandRef.current = goalAutoExpand;
491
+ if (sessionStore)
492
+ sessionStore.goalAutoExpand = goalAutoExpand;
493
+ }, [goalAutoExpand, sessionStore]);
724
494
  useEffect(() => {
725
495
  if (sessionStore)
726
496
  sessionStore.goalStatusEntries = goalStatusEntries;
727
497
  }, [goalStatusEntries, sessionStore]);
498
+ useEffect(() => {
499
+ if (sessionStore)
500
+ sessionStore.planMode = planMode;
501
+ }, [planMode, sessionStore]);
502
+ useEffect(() => {
503
+ if (sessionStore)
504
+ sessionStore.goalMode = goalMode;
505
+ }, [goalMode, sessionStore]);
506
+ useEffect(() => {
507
+ if (sessionStore)
508
+ sessionStore.sessionStats = sessionStatsRef.current;
509
+ }, [sessionStore]);
728
510
  // pendingAction is consumed via a useEffect AFTER agentLoop is created
729
511
  // — see below where useAgentLoop is set up.
730
512
  const pendingActionConsumedRef = useRef(false);
@@ -742,14 +524,13 @@ export function App(props) {
742
524
  }, [displayedCwd]);
743
525
  useEffect(() => {
744
526
  let cancelled = false;
745
- const refreshGoalCount = () => {
527
+ const refreshGoalState = () => {
746
528
  void reconcileActiveGoalRuns(props.cwd, {
747
529
  isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
748
530
  }).then(({ runs }) => {
749
- const counts = summarizeGoalCountsFromRuns(runs);
750
531
  if (cancelled)
751
532
  return;
752
- setGoalCount(counts.active);
533
+ setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
753
534
  setGoalStatusEntries((prev) => {
754
535
  const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
755
536
  isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
@@ -763,13 +544,13 @@ export function App(props) {
763
544
  });
764
545
  });
765
546
  };
766
- refreshGoalCount();
767
- const interval = setInterval(refreshGoalCount, 1000);
547
+ refreshGoalState();
548
+ const interval = setInterval(refreshGoalState, 1_000);
768
549
  return () => {
769
550
  cancelled = true;
770
551
  clearInterval(interval);
771
552
  };
772
- }, [props.cwd]);
553
+ }, [props.cwd, props.sessionStore]);
773
554
  // Periodic update check during long sessions
774
555
  useEffect(() => {
775
556
  startPeriodicUpdateCheck(props.version, (msg) => {
@@ -789,27 +570,29 @@ export function App(props) {
789
570
  useEffect(() => {
790
571
  currentToolsRef.current = currentTools;
791
572
  }, [currentTools]);
792
- // ── Plan mode wiring ─────────────────────────────────────
793
- // Sync planModeRef with React state
794
573
  useEffect(() => {
795
- planModeStateRef.current = planMode;
796
- if (props.planModeRef) {
797
- props.planModeRef.current = planMode;
798
- }
799
- }, [planMode, props.planModeRef]);
800
- const rebuildSystemPrompt = useCallback(async (options) => {
801
- const approvedPlanPath = options?.clearApprovedPlan
802
- ? undefined
803
- : (options?.approvedPlanPath ?? approvedPlanPathRef.current);
804
- return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills, options?.planMode ?? planModeStateRef.current, approvedPlanPath, (options?.tools ?? currentToolsRef.current).map((tool) => tool.name), options?.activeLanguages ?? injectedLanguagesRef.current);
805
- }, [props.skills]);
806
- const replaceSystemPrompt = useCallback(async (options) => {
807
- const newPrompt = await rebuildSystemPrompt(options);
808
- if (messagesRef.current[0]?.role === "system") {
809
- messagesRef.current[0] = { role: "system", content: newPrompt };
810
- }
811
- return newPrompt;
812
- }, [rebuildSystemPrompt]);
574
+ if (!props.connectInitialMcpTools)
575
+ return;
576
+ let cancelled = false;
577
+ void props
578
+ .connectInitialMcpTools()
579
+ .then((mcpTools) => {
580
+ if (cancelled || mcpTools.length === 0)
581
+ return;
582
+ setCurrentTools((prev) => {
583
+ const next = [...prev.filter((tool) => !tool.name.startsWith("mcp__")), ...mcpTools];
584
+ currentToolsRef.current = next;
585
+ void replaceSystemPrompt({ tools: next });
586
+ return next;
587
+ });
588
+ })
589
+ .catch((err) => {
590
+ log("WARN", "mcp", `MCP initialization failed: ${err instanceof Error ? err.message : String(err)}`);
591
+ });
592
+ return () => {
593
+ cancelled = true;
594
+ };
595
+ }, [props.connectInitialMcpTools, replaceSystemPrompt]);
813
596
  /**
814
597
  * Unified "apply detection result" pipeline. Called from three sites:
815
598
  * 1. Initial mount (existing project at startup).
@@ -894,89 +677,17 @@ export function App(props) {
894
677
  useEffect(() => {
895
678
  void applyLanguageDetectionRef.current("initial");
896
679
  }, []);
897
- // Rebuild system prompt when plan mode changes
898
- useEffect(() => {
899
- void replaceSystemPrompt({ planMode });
900
- }, [planMode, replaceSystemPrompt]);
901
- // Wire onEnterPlan callback ref
902
- useEffect(() => {
903
- if (props.onEnterPlanRef) {
904
- props.onEnterPlanRef.current = (reason) => {
905
- setPlanMode(true);
906
- const msg = reason ? `Plan Mode Activated — ${reason}` : "Plan Mode Activated";
907
- setLiveItems((prev) => [
908
- ...prev,
909
- { kind: "plan_transition", text: msg, active: true, id: getId() },
910
- ]);
911
- };
912
- }
913
- }, [props.onEnterPlanRef]);
914
- // Wire onExitPlan callback ref
915
- useEffect(() => {
916
- if (props.onExitPlanRef) {
917
- props.onExitPlanRef.current = async (planPath) => {
918
- // Deactivate plan mode, store approved plan path, open pane
919
- planModeStateRef.current = false;
920
- setPlanMode(false);
921
- approvedPlanPathRef.current = planPath;
922
- await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
923
- // Use setTimeout to open pane after the current tool execution completes,
924
- // so the turn can finish and the UI transitions cleanly
925
- // Flag that the plan overlay is about to open — suppresses the
926
- // premature "done" status that fires when the agent loop finishes
927
- planOverlayPendingRef.current = true;
928
- setTimeout(() => {
929
- setPlanAutoExpand(true);
930
- setOverlay("plan");
931
- // Don't clear planOverlayPendingRef here — keep it true until
932
- // the user actually approves/rejects the plan. Clearing it on a
933
- // timer causes a race where agent_done fires after the 300ms
934
- // timeout but before the user interacts, triggering a premature
935
- // completion sound.
936
- }, 300);
937
- return ("Plan submitted. Exiting plan mode.\n" +
938
- "The plan pane is opening for user review.\n" +
939
- "Plan saved at: " +
940
- planPath);
941
- };
942
- }
943
- }, [props.onExitPlanRef, replaceSystemPrompt]);
944
- const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
945
- const sm = sessionManagerRef.current;
946
- if (!sm)
947
- return;
948
- await appendSessionMessages(sm, sessionPath, messages, startIndex);
949
- }, []);
950
- const persistCompactedSession = useCallback(async (compactedMessages) => {
951
- const sm = sessionManagerRef.current;
952
- if (!sm)
953
- return;
954
- const session = await createCompactedSessionCheckpoint(sm, {
955
- cwd: cwdRef.current,
956
- provider: currentProvider,
957
- model: currentModel,
958
- messages: compactedMessages,
959
- });
960
- sessionPathRef.current = session.path;
961
- persistedIndexRef.current = compactedMessages.length;
962
- if (sessionStore) {
963
- sessionStore.sessionPath = session.path;
964
- sessionStore.messages = [...compactedMessages];
965
- }
966
- log("INFO", "compaction", "Persisted compacted session checkpoint", { path: session.path });
967
- }, [currentModel, currentProvider, sessionStore]);
968
- const persistNewMessages = useCallback(async () => {
969
- const sp = sessionPathRef.current;
970
- if (!sp)
971
- return;
972
- const allMsgs = messagesRef.current;
973
- await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
974
- persistedIndexRef.current = allMsgs.length;
975
- if (sessionStore) {
976
- sessionStore.messages = [...allMsgs];
977
- sessionStore.sessionPath = sp;
978
- }
979
- }, [appendMessagesToSession, sessionStore]);
680
+ const { persistCompactedSession, persistNewMessages } = useSessionPersistence({
681
+ sessionManagerRef,
682
+ sessionPathRef,
683
+ sessionStatsRef,
684
+ persistedIndexRef,
685
+ messagesRef,
686
+ cwdRef,
687
+ currentProvider,
688
+ currentModel,
689
+ sessionStore,
690
+ });
980
691
  /**
981
692
  * Run the language detector against the current cwd. If the detected set is a
982
693
  * strict superset of what's already injected, rebuild the system prompt with
@@ -987,7 +698,7 @@ export function App(props) {
987
698
  * Other tool kinds skip detection entirely to avoid wasted filesystem stats.
988
699
  *
989
700
  * No restart required: the system prompt is mutated in place, same mechanism
990
- * already used for plan mode + pixel-fix chdir.
701
+ * used for pixel-fix chdir.
991
702
  *
992
703
  * Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
993
704
  * empty to keep agent-loop options stable) can call the freshest version.
@@ -1011,189 +722,25 @@ export function App(props) {
1011
722
  });
1012
723
  }
1013
724
  }, [props.settingsFile]);
1014
- const compactionAbortRef = useRef(null);
1015
- const compactConversation = useCallback(async (messages, signal) => {
1016
- const contextWindow = getContextWindow(currentModel, contextWindowOptions);
1017
- const tokensBefore = estimateConversationTokens(messages);
1018
- const spinId = getId();
1019
- log("INFO", "compaction", `Running compaction`, {
1020
- messages: String(messages.length),
1021
- estimatedTokens: String(tokensBefore),
1022
- contextWindow: String(contextWindow),
1023
- });
1024
- // Show animated spinner
1025
- setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
1026
- const ownedAbort = signal ? null : new AbortController();
1027
- const compactionSignal = signal ?? ownedAbort?.signal;
1028
- if (ownedAbort)
1029
- compactionAbortRef.current = ownedAbort;
1030
- try {
1031
- // Resolve fresh credentials for compaction too
1032
- let compactApiKey = activeApiKey;
1033
- let compactAccountId = activeAccountId;
1034
- let compactProjectId = activeProjectId;
1035
- let compactBaseUrl = activeBaseUrl;
1036
- if (props.authStorage) {
1037
- const creds = await props.authStorage.resolveCredentials(currentProvider);
1038
- compactApiKey = creds.accessToken;
1039
- compactAccountId = creds.accountId;
1040
- compactProjectId = creds.projectId;
1041
- compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
1042
- }
1043
- const result = await compact(messages, {
1044
- provider: currentProvider,
1045
- model: currentModel,
1046
- apiKey: compactApiKey,
1047
- accountId: compactAccountId,
1048
- projectId: compactProjectId,
1049
- baseUrl: compactBaseUrl,
1050
- contextWindow,
1051
- signal: compactionSignal,
1052
- approvedPlanPath: approvedPlanPathRef.current,
1053
- });
1054
- if (result.result.compacted) {
1055
- // Replace spinner with completed notice
1056
- setLiveItems((prev) => prev.map((item) => item.id === spinId
1057
- ? {
1058
- kind: "compacted",
1059
- originalCount: result.result.originalCount,
1060
- newCount: result.result.newCount,
1061
- tokensBefore: result.result.tokensBeforeEstimate,
1062
- tokensAfter: result.result.tokensAfterEstimate,
1063
- id: spinId,
1064
- }
1065
- : item));
1066
- }
1067
- else {
1068
- // Nothing was actually compacted — remove spinner silently
1069
- log("INFO", "compaction", `Compaction skipped: ${result.result.reason ?? "unknown"}`);
1070
- setLiveItems((prev) => prev.filter((item) => item.id !== spinId));
1071
- }
1072
- return result.messages;
1073
- }
1074
- catch (err) {
1075
- const msg = err instanceof Error ? err.message : String(err);
1076
- const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
1077
- log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
1078
- setLiveItems((prev) => isAbort
1079
- ? prev.filter((item) => item.id !== spinId)
1080
- : prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
1081
- return messages; // Return unchanged on failure/abort
1082
- }
1083
- finally {
1084
- if (ownedAbort && compactionAbortRef.current === ownedAbort)
1085
- compactionAbortRef.current = null;
1086
- }
1087
- }, [
725
+ const { compactionAbortRef, compactConversation, transformContext } = useContextCompaction({
1088
726
  currentModel,
1089
727
  currentProvider,
728
+ maxTokens: props.maxTokens,
729
+ authStorage: props.authStorage,
730
+ contextWindowOptions,
1090
731
  activeApiKey,
1091
732
  activeAccountId,
1092
733
  activeProjectId,
1093
734
  activeBaseUrl,
1094
- contextWindowOptions,
1095
- props.authStorage,
1096
- ]);
1097
- const getRepoMapSignalCount = useCallback(() => {
1098
- return ((props.repoMapChangedFilesRef?.current.size ?? 0) +
1099
- (props.repoMapReadFilesRef?.current.size ?? 0));
1100
- }, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef]);
1101
- const getRepoMapBudget = useCallback(() => {
1102
- const userTurns = messagesRef.current.filter((message) => message.role === "user").length;
1103
- const readCount = props.repoMapReadFilesRef?.current.size ?? 0;
1104
- if (userTurns <= 1 && readCount === 0)
1105
- return FIRST_TURN_REPO_MAP_MAX_CHARS;
1106
- if (readCount > 0)
1107
- return FOCUSED_REPO_MAP_MAX_CHARS;
1108
- return FOCUSED_REPO_MAP_MAX_CHARS + 1000;
1109
- }, [props.repoMapReadFilesRef]);
1110
- const refreshRepoMap = useCallback(async (latestUserPrompt) => {
1111
- const rendered = await buildRepoMap({
1112
- cwd: cwdRef.current,
1113
- maxChars: getRepoMapBudget(),
1114
- changedFiles: [...(props.repoMapChangedFilesRef?.current ?? new Set())],
1115
- readFiles: [...(props.repoMapReadFilesRef?.current ?? new Set())],
1116
- focusTerms: latestUserPrompt ? [latestUserPrompt] : [],
1117
- cache: repoMapCacheRef.current,
1118
- });
1119
- repoMapMarkdownRef.current = rendered.markdown;
1120
- repoMapSnapshotRef.current = rendered.snapshot;
1121
- repoMapChangedCountRef.current = getRepoMapSignalCount();
1122
- repoMapDirtyRef.current = false;
1123
- return rendered.markdown;
1124
- }, [
1125
- getRepoMapBudget,
1126
- getRepoMapSignalCount,
1127
- props.repoMapChangedFilesRef,
1128
- props.repoMapReadFilesRef,
1129
- ]);
1130
- const stripRepoMapMessages = useCallback((messages) => {
1131
- return stripRepoMapContextMessages(messages);
1132
- }, []);
1133
- const injectRepoMapContext = useCallback(async (messages) => {
1134
- if (!repoMapInjectionEnabledRef.current)
1135
- return stripRepoMapMessages(messages);
1136
- const stripped = stripRepoMapMessages(messages);
1137
- const latestUserPrompt = getLatestUserText(stripped);
1138
- const signalCount = getRepoMapSignalCount();
1139
- if (signalCount !== repoMapChangedCountRef.current)
1140
- repoMapDirtyRef.current = true;
1141
- if (repoMapDirtyRef.current || !repoMapMarkdownRef.current) {
1142
- await refreshRepoMap(latestUserPrompt);
1143
- }
1144
- if (!repoMapMarkdownRef.current)
1145
- return stripped;
1146
- return injectRepoMapContextMessages(stripped, repoMapMarkdownRef.current);
1147
- }, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef, refreshRepoMap, stripRepoMapMessages]);
1148
- /**
1149
- * transformContext callback for the agent loop.
1150
- * Called before each LLM call and on context overflow.
1151
- * Compacts persistent chat only, then injects the dynamic repo map transiently.
1152
- */
1153
- const transformContext = useCallback(async (messages, options) => {
1154
- const stripped = stripRepoMapMessages(messages);
1155
- const settings = settingsRef.current;
1156
- const autoCompact = settings?.get("autoCompact") ?? true;
1157
- const threshold = settings?.get("compactThreshold") ?? 0.8;
1158
- // Force-compact on context overflow regardless of settings
1159
- if (options?.force) {
1160
- const result = await compactConversation(stripped);
1161
- if (result !== stripped) {
1162
- messagesRef.current = result;
1163
- await persistCompactedSession(result);
1164
- }
1165
- lastCompactionTimeRef.current = Date.now();
1166
- return injectRepoMapContext(result);
1167
- }
1168
- if (!autoCompact)
1169
- return injectRepoMapContext(stripped);
1170
- // Time-based cooldown: skip if compaction ran within the last 30 seconds
1171
- if (Date.now() - lastCompactionTimeRef.current < 30_000) {
1172
- log("INFO", "compaction", `Skipping compaction — cooldown active`);
1173
- return injectRepoMapContext(stripped);
1174
- }
1175
- const contextWindow = getContextWindow(currentModel, contextWindowOptions);
1176
- const reserveTokens = getCompactionReserveTokens(props.maxTokens);
1177
- const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
1178
- const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
1179
- if (shouldCompact(stripped, contextWindow, threshold, actualTokens, reserveTokens)) {
1180
- const result = await compactConversation(stripped);
1181
- if (result !== stripped) {
1182
- messagesRef.current = result;
1183
- await persistCompactedSession(result);
1184
- }
1185
- lastCompactionTimeRef.current = Date.now();
1186
- return injectRepoMapContext(result);
1187
- }
1188
- return injectRepoMapContext(stripped);
1189
- }, [
1190
- currentModel,
1191
- compactConversation,
1192
- contextWindowOptions,
1193
- injectRepoMapContext,
735
+ setLiveItems,
736
+ getId,
737
+ approvedPlanPathRef,
738
+ settingsRef,
739
+ messagesRef,
740
+ lastActualTokensRef,
741
+ lastActualTokensTimestampRef,
1194
742
  persistCompactedSession,
1195
- stripRepoMapMessages,
1196
- ]);
743
+ });
1197
744
  // ── Background task bar state (external store) ──────────
1198
745
  const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
1199
746
  useTaskBarPolling(props.processManager);
@@ -1225,16 +772,56 @@ export function App(props) {
1225
772
  tools: currentTools,
1226
773
  webSearch: props.webSearch,
1227
774
  maxTokens: props.maxTokens,
1228
- thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
775
+ thinking: thinkingLevel,
1229
776
  apiKey: activeApiKey,
1230
777
  baseUrl: activeBaseUrl,
1231
778
  accountId: activeAccountId,
1232
779
  projectId: activeProjectId,
1233
780
  resolveCredentials,
1234
781
  transformContext,
782
+ getIdealReviewMessage: (stats) => {
783
+ if (!idealReviewEnabledRef.current)
784
+ return null;
785
+ const decision = evaluateIdealReview(stats);
786
+ if (!decision.shouldReview)
787
+ return null;
788
+ log("INFO", "ideal", "Injecting ideal review before final response", {
789
+ score: String(decision.score),
790
+ reasons: decision.reasons.join(", "),
791
+ });
792
+ setLiveItems((prev) => [
793
+ ...prev,
794
+ { kind: "ideal_hook", text: IDEAL_HOOK_NOTICE_TEXT, tone: "review", id: getId() },
795
+ ]);
796
+ return buildIdealReviewMessage(decision.reasons);
797
+ },
798
+ getLoopBreakMessage: (stats) => {
799
+ if (!idealReviewEnabledRef.current)
800
+ return null;
801
+ const decision = evaluateLoopBreak(stats);
802
+ if (!decision.shouldBreak)
803
+ return null;
804
+ log("INFO", "loop-break", "Injecting loop-break nudge", {
805
+ reasons: decision.reasons.join(", "),
806
+ });
807
+ setLiveItems((prev) => [
808
+ ...prev,
809
+ { kind: "ideal_hook", text: LOOP_BREAK_NOTICE_TEXT, tone: "warning", id: getId() },
810
+ ]);
811
+ return buildLoopBreakMessage(decision.reasons);
812
+ },
813
+ getRegroundingMessage: (originalRequest) => {
814
+ if (!idealReviewEnabledRef.current)
815
+ return null;
816
+ log("INFO", "reground", "Injecting re-grounding after compaction", {});
817
+ setLiveItems((prev) => [
818
+ ...prev,
819
+ { kind: "ideal_hook", text: REGROUNDING_NOTICE_TEXT, tone: "info", id: getId() },
820
+ ]);
821
+ return buildRegroundingMessage(originalRequest);
822
+ },
1235
823
  }, {
1236
824
  onComplete: useCallback(() => {
1237
- messagesRef.current = stripRepoMapMessages(messagesRef.current);
1238
825
  persistNewMessages();
1239
826
  // Auto-clear plan progress and approved plan when all steps are completed
1240
827
  const steps = planStepsRef.current;
@@ -1287,8 +874,6 @@ export function App(props) {
1287
874
  }
1288
875
  }, [
1289
876
  persistNewMessages,
1290
- stripRepoMapMessages,
1291
- planMode,
1292
877
  props.cwd,
1293
878
  props.skills,
1294
879
  currentProvider,
@@ -1298,6 +883,8 @@ export function App(props) {
1298
883
  resolveCredentials,
1299
884
  ]),
1300
885
  onTurnText: useCallback((text, thinking, thinkingMs) => {
886
+ const hadStreamedAssistantFlush = streamedAssistantFlushRef.current.flushedChars > 0;
887
+ const unflushedAssistantText = text.slice(streamedAssistantFlushRef.current.flushedChars);
1301
888
  // Track [DONE:n] markers for plan step progress
1302
889
  if (planStepsRef.current.length > 0) {
1303
890
  const completed = findCompletedMarkers(text);
@@ -1312,15 +899,9 @@ export function App(props) {
1312
899
  followUpNudgesRef.current = { step: 0, count: 0 };
1313
900
  }
1314
901
  }
1315
- // Flush all completed items from the previous turn to Static history.
1316
- // This keeps liveItems bounded per-turn, preventing Ink's live area from
1317
- // growing unbounded, which makes Ink's live-area re-renders expensive.
1318
- //
1319
- // Items are queued in pendingFlushRef (not sent to setHistory directly)
1320
- // so the Static write happens in a SEPARATE render cycle from the
1321
- // live-area change — avoiding both Ink cursor-math clipping and the
1322
- // brief duplicate that occurred when setHistory was nested inside the
1323
- // setLiveItems updater.
902
+ // Flush completed rows from the previous turn to finalized terminal
903
+ // history. Ink keeps only the active turn, preventing live-area growth
904
+ // and avoiding Static/log-update replay during resize/remount churn.
1324
905
  setLiveItems((prev) => {
1325
906
  const flushed = flushOnTurnText(prev);
1326
907
  if (flushed.length > 0) {
@@ -1329,10 +910,7 @@ export function App(props) {
1329
910
  // Split text on [DONE:N] markers so each marker renders inline as
1330
911
  // a styled "✓ Step N: <description>" item at the position the
1331
912
  // agent emitted it, instead of vanishing into stripped whitespace.
1332
- // Falls back to a single assistant item containing the
1333
- // marker-stripped text when there are no markers (keeps the
1334
- // common case zero-cost).
1335
- const segments = segmentDisplayText(text, planStepsRef.current);
913
+ const segments = segmentDisplayText(unflushedAssistantText, planStepsRef.current);
1336
914
  const items = [];
1337
915
  let thinkingAttached = false;
1338
916
  for (const seg of segments) {
@@ -1345,7 +923,7 @@ export function App(props) {
1345
923
  // contains multiple text chunks split by markers.
1346
924
  thinking: thinkingAttached ? undefined : thinking,
1347
925
  thinkingMs: thinkingAttached ? undefined : thinkingMs,
1348
- planMode: planModeLocalRef.current,
926
+ continuation: hadStreamedAssistantFlush,
1349
927
  id: getId(),
1350
928
  });
1351
929
  thinkingAttached = true;
@@ -1360,36 +938,54 @@ export function App(props) {
1360
938
  }
1361
939
  }
1362
940
  // No segments at all (text was empty/whitespace, no markers).
1363
- // Still emit an assistant item so a thinking block renders if
1364
- // there was thinking content for this turn.
941
+ // Still persist an assistant item so a thinking block renders in
942
+ // terminal history if there was thinking content for this turn.
1365
943
  if (items.length === 0) {
1366
944
  items.push({
1367
945
  kind: "assistant",
1368
946
  text: "",
1369
947
  thinking,
1370
948
  thinkingMs,
1371
- planMode: planModeLocalRef.current,
1372
949
  id: getId(),
1373
950
  });
1374
951
  }
1375
- return items;
952
+ const assistantItems = prev.filter((item) => item.kind === "assistant");
953
+ const newAssistantText = normalizeAssistantText(unflushedAssistantText);
954
+ const duplicatePinnedText = newAssistantText.length > 0 &&
955
+ [...assistantItems, ...pendingHistoryFlushRef.current, ...historyRef.current].some((item) => isSameAssistantText(item, newAssistantText));
956
+ const nextItems = duplicatePinnedText
957
+ ? items.filter((item) => !isSameAssistantText(item, newAssistantText))
958
+ : items;
959
+ const flushablePrev = prev.filter((item) => item.kind !== "assistant");
960
+ if (flushablePrev.length > 0)
961
+ queueFlush(flushablePrev);
962
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
963
+ return [...assistantItems, ...nextItems];
1376
964
  });
1377
- }, []),
1378
- onToolStart: useCallback((toolCallId, name, args) => {
965
+ }, [queueFlush]),
966
+ onToolStart: useCallback((toolCallId, name, args, stream) => {
1379
967
  log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
1380
968
  const startedAt = Date.now();
1381
969
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1382
- // Flush completed items (assistant text, finished tools) to Static
1383
- // before adding tool UI. Keeping both in the live area makes it tall
1384
- // and causes Ink's cursor math to clip the top.
1385
- setLiveItems((prev) => {
1386
- const { flushed, remaining } = partitionCompleted(prev);
970
+ // Feed the pinned LiveToolPanel. Keep a small tail (panel shows the
971
+ // last few rows) so memory stays bounded across long sessions.
972
+ setLiveToolFeed((prev) => [...prev, { id: toolCallId, name, args, status: "running" }].slice(-(LIVE_TOOL_PANEL_ROWS * 2)));
973
+ const appendToolStart = (prev) => {
974
+ const visible = pinStreamingTextBeforeToolBoundary({
975
+ items: prev,
976
+ visibleStreamingText: stream.text,
977
+ thinking: stream.thinking,
978
+ thinkingMs: stream.thinkingMs,
979
+ makeId: getId,
980
+ });
981
+ const { flushed, remaining } = partitionCompleted(visible);
1387
982
  if (flushed.length > 0) {
1388
983
  queueFlush(flushed);
1389
984
  }
1390
985
  return remaining;
1391
- });
986
+ };
1392
987
  if (name === "subagent") {
988
+ setLiveItems(appendToolStart);
1393
989
  // Create or update the sub-agent group item
1394
990
  const newAgent = {
1395
991
  toolCallId,
@@ -1414,25 +1010,32 @@ export function App(props) {
1414
1010
  });
1415
1011
  }
1416
1012
  else if (AGGREGATABLE_TOOLS.has(name)) {
1417
- // Group concurrent read-only tools into a single compact item
1418
1013
  setLiveItems((prev) => {
1419
- // Find an active tool group (has at least one running tool)
1420
- const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1421
- item.tools.some((t) => t.status === "running"));
1422
- if (groupIdx !== -1) {
1423
- const group = prev[groupIdx];
1424
- const next = [...prev];
1425
- next[groupIdx] = {
1426
- ...group,
1427
- tools: [
1428
- ...group.tools,
1429
- { toolCallId, name, args, status: "running", animateUntil },
1430
- ],
1431
- };
1432
- return next;
1014
+ const reusableGroupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1015
+ item.tools.every((tool) => tool.name === name && !tool.isError));
1016
+ const prior = reusableGroupIdx === -1 ? [] : prev.slice(0, reusableGroupIdx);
1017
+ if (reusableGroupIdx !== -1 && prior.every((item) => !isActiveItem(item))) {
1018
+ const flushablePrior = prior.filter((item) => item.kind !== "assistant");
1019
+ if (flushablePrior.length > 0)
1020
+ queueFlush(flushablePrior);
1021
+ const pinnedPrior = prior.filter((item) => item.kind === "assistant");
1022
+ const candidates = prev.slice(reusableGroupIdx);
1023
+ const group = candidates[0];
1024
+ return [
1025
+ ...pinnedPrior,
1026
+ {
1027
+ ...group,
1028
+ tools: [
1029
+ ...group.tools,
1030
+ { toolCallId, name, args, status: "running", animateUntil },
1031
+ ],
1032
+ },
1033
+ ...candidates.slice(1),
1034
+ ];
1433
1035
  }
1036
+ const remaining = appendToolStart(prev);
1434
1037
  return [
1435
- ...prev,
1038
+ ...remaining,
1436
1039
  {
1437
1040
  kind: "tool_group",
1438
1041
  tools: [{ toolCallId, name, args, status: "running", animateUntil }],
@@ -1443,11 +1046,11 @@ export function App(props) {
1443
1046
  }
1444
1047
  else {
1445
1048
  setLiveItems((prev) => [
1446
- ...prev,
1049
+ ...appendToolStart(prev),
1447
1050
  { kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
1448
1051
  ]);
1449
1052
  }
1450
- }, []),
1053
+ }, [queueFlush]),
1451
1054
  onToolUpdate: useCallback((toolCallId, update) => {
1452
1055
  const u = update;
1453
1056
  // Bash progress streaming — append output to tool_start item
@@ -1489,6 +1092,17 @@ export function App(props) {
1489
1092
  });
1490
1093
  }, []),
1491
1094
  onToolEnd: useCallback((toolCallId, name, result, isError, durationMs, details) => {
1095
+ recordToolEnd(sessionStatsRef.current, name, isError, durationMs);
1096
+ setLiveToolFeed((prev) => prev.map((entry) => entry.id === toolCallId
1097
+ ? { ...entry, status: "done", isError, result, details }
1098
+ : entry));
1099
+ if (name === "edit" && !isError) {
1100
+ const diff = details?.diff ?? result;
1101
+ addLinesChanged(sessionStatsRef.current, {
1102
+ added: (diff.match(/^\+[^+]/gm) ?? []).length,
1103
+ removed: (diff.match(/^-[^-]/gm) ?? []).length,
1104
+ });
1105
+ }
1492
1106
  // Language-pack detection — gated on `write`/`bash` inside the
1493
1107
  // helper; cheap to call unconditionally. Fire-and-forget; the next
1494
1108
  // LLM turn picks up the swapped system prompt automatically.
@@ -1520,7 +1134,7 @@ export function App(props) {
1520
1134
  };
1521
1135
  const next = [...prev];
1522
1136
  next[groupIdx] = { ...group, agents: updatedAgents };
1523
- // Flush completed items to Static to keep the live area small
1137
+ // Flush completed items to finalized history to keep the live area small
1524
1138
  const { flushed, remaining } = partitionCompleted(next);
1525
1139
  if (flushed.length > 0) {
1526
1140
  queueFlush(flushed);
@@ -1530,6 +1144,19 @@ export function App(props) {
1530
1144
  }
1531
1145
  else {
1532
1146
  setLiveItems((prev) => {
1147
+ if (name === "enter_plan") {
1148
+ const updated = prev
1149
+ .filter((item) => !(item.kind === "tool_start" && item.toolCallId === toolCallId))
1150
+ .map((item) => item.kind === "plan_transition" && item.active
1151
+ ? { ...item, active: false }
1152
+ : item);
1153
+ const { flushed, remaining } = partitionCompleted(updated);
1154
+ if (flushed.length > 0) {
1155
+ queueFlush(flushed);
1156
+ return remaining;
1157
+ }
1158
+ return updated;
1159
+ }
1533
1160
  // Check if this tool is in a tool_group
1534
1161
  const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1535
1162
  item.tools.some((t) => t.toolCallId === toolCallId));
@@ -1557,6 +1184,7 @@ export function App(props) {
1557
1184
  isError,
1558
1185
  durationMs,
1559
1186
  details,
1187
+ imagePreviews: extractToolImagePreviews(details),
1560
1188
  id: startItem.id,
1561
1189
  };
1562
1190
  updated = [...prev];
@@ -1574,12 +1202,13 @@ export function App(props) {
1574
1202
  isError,
1575
1203
  durationMs,
1576
1204
  details,
1205
+ imagePreviews: extractToolImagePreviews(details),
1577
1206
  id: getId(),
1578
1207
  },
1579
1208
  ];
1580
1209
  }
1581
1210
  }
1582
- // Flush completed items to Static to keep the live area small
1211
+ // Flush completed items to finalized history to keep the live area small
1583
1212
  const { flushed, remaining } = partitionCompleted(updated);
1584
1213
  if (flushed.length > 0) {
1585
1214
  queueFlush(flushed);
@@ -1595,14 +1224,22 @@ export function App(props) {
1595
1224
  });
1596
1225
  }
1597
1226
  }, []),
1598
- onServerToolCall: useCallback((id, name, input) => {
1227
+ onServerToolCall: useCallback((id, name, input, stream) => {
1228
+ recordServerToolCall(sessionStatsRef.current);
1599
1229
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
1600
1230
  const startedAt = Date.now();
1601
1231
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1602
- // Flush completed items (including assistant text) to Static before
1603
- // adding server tool UI — same rationale as onToolStart.
1232
+ // Flush completed items (including assistant text) before adding server
1233
+ // tool UI — same rationale as onToolStart.
1604
1234
  setLiveItems((prev) => {
1605
- const { flushed, remaining } = partitionCompleted(prev);
1235
+ const visible = pinStreamingTextBeforeToolBoundary({
1236
+ items: prev,
1237
+ visibleStreamingText: stream.text,
1238
+ thinking: stream.thinking,
1239
+ thinkingMs: stream.thinkingMs,
1240
+ makeId: getId,
1241
+ });
1242
+ const { flushed, remaining } = partitionCompleted(visible);
1606
1243
  if (flushed.length > 0) {
1607
1244
  queueFlush(flushed);
1608
1245
  }
@@ -1619,7 +1256,7 @@ export function App(props) {
1619
1256
  },
1620
1257
  ];
1621
1258
  });
1622
- }, []),
1259
+ }, [queueFlush]),
1623
1260
  onServerToolResult: useCallback((toolUseId, resultType, data) => {
1624
1261
  log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
1625
1262
  setLiveItems((prev) => {
@@ -1653,15 +1290,16 @@ export function App(props) {
1653
1290
  },
1654
1291
  ];
1655
1292
  }
1656
- // Flush completed items to Static
1293
+ // Flush completed items to finalized history
1657
1294
  const { flushed, remaining } = partitionCompleted(updated);
1658
1295
  if (flushed.length > 0) {
1659
1296
  queueFlush(flushed);
1660
1297
  }
1661
1298
  return remaining;
1662
1299
  });
1663
- }, []),
1300
+ }, [queueFlush]),
1664
1301
  onTurnEnd: useCallback((turn, stopReason, usage) => {
1302
+ recordTurnEnd(sessionStatsRef.current, usage);
1665
1303
  log("INFO", "turn", `Turn ${turn} ended`, {
1666
1304
  stopReason,
1667
1305
  inputTokens: String(usage.inputTokens),
@@ -1676,8 +1314,8 @@ export function App(props) {
1676
1314
  lastActualTokensRef.current =
1677
1315
  currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
1678
1316
  lastActualTokensTimestampRef.current = Date.now();
1679
- // For tool-only turns (no text), flush completed items to Static so
1680
- // liveItems doesn't grow unbounded across consecutive tool-only turns.
1317
+ // For tool-only turns (no text), flush completed items to finalized
1318
+ // history so liveItems doesn't grow unbounded across consecutive turns.
1681
1319
  setLiveItems((prev) => {
1682
1320
  const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
1683
1321
  if (flushed.length > 0) {
@@ -1685,46 +1323,41 @@ export function App(props) {
1685
1323
  }
1686
1324
  return remaining;
1687
1325
  });
1688
- }, []),
1689
- onDone: useCallback((durationMs, toolsUsed, errored) => {
1326
+ }, [queueFlush]),
1327
+ onDone: useCallback((durationMs, toolsUsed) => {
1690
1328
  log("INFO", "agent", `Agent done`, {
1691
1329
  duration: `${durationMs}ms`,
1692
1330
  toolsUsed: toolsUsed.join(",") || "none",
1693
1331
  });
1694
- // Don't show "done" status when plan overlay is about to open —
1695
- // the agent loop finished but we're waiting for user plan review
1696
- if (planOverlayPendingRef.current)
1697
- return;
1698
- setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1699
- playNotificationSound();
1700
- // Two-phase flush to avoid Ink text clipping.
1701
- // Phase 1 (here): clear the live area so Ink commits a render with
1702
- // the smaller output and updates its internal line counter.
1703
- // Phase 2 (useEffect below): push items to Static history in a
1704
- // separate render cycle so the Static write never coincides with
1705
- // a live-area height change in the same frame.
1706
- setLiveItems((prev) => {
1707
- if (prev.length > 0)
1708
- queueFlush(prev);
1709
- return [];
1332
+ const doneDecision = getDoneFlushDecision({
1333
+ planOverlayPending: planOverlayPendingRef.current,
1710
1334
  });
1711
- // Run-all: auto-start next pending task after a short delay
1712
- // (allow the two-phase flush to complete first)
1335
+ // Don't show "done" status when the plan review pane is about to open —
1336
+ // the agent loop finished but we're waiting for user approval/review.
1337
+ // Still flush live transcript rows before the pane remounts; otherwise
1338
+ // setup output remains in ephemeral liveItems and appears to vanish.
1339
+ if (doneDecision.showDoneStatus) {
1340
+ setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1341
+ playNotificationSound();
1342
+ }
1343
+ // Keep the final assistant response mounted in the live frame after a
1344
+ // normal chat turn finishes. Moving a large final response to terminal
1345
+ // history at this moment writes many scrollback rows while the footer is
1346
+ // still mounted, which visibly pushes the input/footer upward. The final
1347
+ // response is flushed on the next submit before the new prompt is shown.
1348
+ // Non-chat overlay transitions still flush so setup/plan output
1349
+ // does not vanish during remounts.
1350
+ if (doneDecision.flushLiveItems && !doneDecision.showDoneStatus) {
1351
+ setLiveItems((prev) => {
1352
+ if (prev.length > 0)
1353
+ queueFlush(prev);
1354
+ return prev;
1355
+ });
1356
+ }
1357
+ // Run-all: auto-start next pending task after a short delay.
1713
1358
  if (runAllTasksRef.current) {
1714
1359
  setTimeout(() => {
1715
1360
  const cwd = cwdRef.current;
1716
- // Clean turn: mark the in-flight task done so the row flips to ✓.
1717
- // Error path: leave it at `in-progress` so the failed task is
1718
- // visibly distinct from completed ones. The chain still advances
1719
- // either way — matching how errors were handled before this fix.
1720
- const finishedId = currentTaskIdRef.current;
1721
- if (finishedId) {
1722
- if (!errored)
1723
- markTaskDone(cwd, finishedId);
1724
- currentTaskIdRef.current = null;
1725
- if (props.sessionStore)
1726
- props.sessionStore.currentTaskId = null;
1727
- }
1728
1361
  const next = getNextPendingTask(cwd);
1729
1362
  if (next) {
1730
1363
  markTaskInProgress(cwd, next.id);
@@ -1736,11 +1369,6 @@ export function App(props) {
1736
1369
  }
1737
1370
  }, 500);
1738
1371
  }
1739
- // Goal loop: after the orchestrator handles a worker/verifier event,
1740
- // continue the same Goal automatically until it reaches a terminal state.
1741
- for (const runId of [...runningGoalIdsRef.current]) {
1742
- setTimeout(() => continueGoalRun(runId), 500);
1743
- }
1744
1372
  // Pixel fix: observe branch + commits, patch status, optionally pick
1745
1373
  // up the next open error if run-all is active.
1746
1374
  const pendingFix = currentPixelFixRef.current;
@@ -1782,12 +1410,6 @@ export function App(props) {
1782
1410
  setRunAllTasks(false);
1783
1411
  setRunAllPixel(false);
1784
1412
  currentPixelFixRef.current = null;
1785
- // Clear the run-all task id so a subsequent run-all does not
1786
- // mistakenly mark this aborted task as done. The on-disk row stays
1787
- // at `in-progress`, which is the correct truth for an aborted task.
1788
- currentTaskIdRef.current = null;
1789
- if (props.sessionStore)
1790
- props.sessionStore.currentTaskId = null;
1791
1413
  setDoneStatus(null);
1792
1414
  setLiveItems((prev) => {
1793
1415
  const next = prev.map((item) => {
@@ -1833,42 +1455,16 @@ export function App(props) {
1833
1455
  }, []),
1834
1456
  onQueuedStart: useCallback((content) => {
1835
1457
  // When a queued message starts processing, show it as a UserItem
1836
- // and flush prior items to history. Synthetic system events are hidden
1837
- // from the transcript but still routed through the main agent context.
1458
+ // and flush prior items to history.
1838
1459
  const displayText = typeof content === "string"
1839
1460
  ? content
1840
1461
  : content
1841
1462
  .filter((c) => c.type === "text")
1842
1463
  .map((c) => c.text)
1843
1464
  .join("\n");
1844
- if (isGoalSyntheticEvent(displayText)) {
1845
- const eventInfo = parseGoalSyntheticEvent(displayText);
1846
- setLiveItems((prev) => {
1847
- if (prev.length > 0)
1848
- queueFlush(prev);
1849
- return [];
1850
- });
1851
- setDoneStatus(null);
1852
- appendGoalProgress({
1853
- kind: "goal_progress",
1854
- phase: "orchestrator_reviewing",
1855
- title: "Orchestrator reviewing Goal update",
1856
- detail: eventInfo?.kind === "worker"
1857
- ? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
1858
- : `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
1859
- workerId: eventInfo?.worker,
1860
- status: eventInfo?.status,
1861
- });
1862
- return;
1863
- }
1864
1465
  const imageCount = typeof content === "string"
1865
1466
  ? undefined
1866
1467
  : content.filter((c) => c.type === "image").length || undefined;
1867
- setLiveItems((prev) => {
1868
- if (prev.length > 0)
1869
- queueFlush(prev);
1870
- return [];
1871
- });
1872
1468
  const userItem = {
1873
1469
  kind: "user",
1874
1470
  text: displayText,
@@ -1877,8 +1473,8 @@ export function App(props) {
1877
1473
  };
1878
1474
  setLastUserMessage(displayText);
1879
1475
  setDoneStatus(null);
1880
- setLiveItems([userItem]);
1881
- }, []),
1476
+ finalizeSubmittedUserItem(userItem);
1477
+ }, [finalizeSubmittedUserItem]),
1882
1478
  // Inject a "continue with the next step" follow-up when the agent
1883
1479
  // would otherwise stop mid-plan. The prompt-only instruction wasn't
1884
1480
  // enough — some models (notably Opus) treat each [DONE:n] as a
@@ -1910,11 +1506,282 @@ export function App(props) {
1910
1506
  ];
1911
1507
  }, []),
1912
1508
  });
1913
- // First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
1914
- // scope so the ref closure can dispatch to it. Called from the initial
1915
- // language-detection path when `isFirstTimeSetup(cwd)` is true. Pushes a
1916
- // notice item explaining what's happening, then runs the audit prompt.
1917
- triggerAutoSetupRef.current = async () => {
1509
+ const continueGoalRun = useCallback((run) => {
1510
+ const cwd = cwdRef.current;
1511
+ const decision = decideGoalNextAction(run);
1512
+ const choiceKey = getGoalContinuationChoiceKey({ runId: run.id, decision });
1513
+ const now = Date.now();
1514
+ for (const [key, timestamp] of goalContinuationRecentChoicesRef.current.entries()) {
1515
+ if (now - timestamp > 5_000)
1516
+ goalContinuationRecentChoicesRef.current.delete(key);
1517
+ }
1518
+ if (goalContinuationFlightsRef.current.has(choiceKey))
1519
+ return;
1520
+ const lastChosenAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
1521
+ if (lastChosenAt && now - lastChosenAt < 1_500)
1522
+ return;
1523
+ goalContinuationFlightsRef.current.add(choiceKey);
1524
+ goalContinuationRecentChoicesRef.current.set(choiceKey, now);
1525
+ const releaseChoice = () => {
1526
+ setTimeout(() => goalContinuationFlightsRef.current.delete(choiceKey), 250);
1527
+ };
1528
+ void (async () => {
1529
+ try {
1530
+ if (decision.kind === "create_task") {
1531
+ const nextTask = createGoalTask({ title: decision.title, prompt: decision.prompt });
1532
+ const updated = await upsertGoalRun(cwd, {
1533
+ ...run,
1534
+ status: "ready",
1535
+ tasks: [...run.tasks, nextTask],
1536
+ });
1537
+ await appendGoalDecision(cwd, run.id, decision);
1538
+ appendGoalProgress({
1539
+ kind: "goal_progress",
1540
+ phase: "orchestrator_working",
1541
+ title: `Goal ${goalNumberForRun(run.id)} task queued`,
1542
+ detail: decision.reason,
1543
+ status: updated.status,
1544
+ });
1545
+ continueGoalRun(updated);
1546
+ return;
1547
+ }
1548
+ if (decision.kind === "start_worker") {
1549
+ const updatedRun = await updateGoalTask(cwd, run.id, decision.task.id, {
1550
+ status: "running",
1551
+ attempts: decision.attempts,
1552
+ lastSummary: `Worker attempt ${decision.attempts} running.`,
1553
+ });
1554
+ const taskForPrompt = updatedRun?.tasks.find((task) => task.id === decision.task.id) ?? {
1555
+ ...decision.task,
1556
+ attempts: decision.attempts,
1557
+ };
1558
+ const worker = await startGoalWorker({
1559
+ cwd,
1560
+ goalRunId: run.id,
1561
+ goalTaskId: taskForPrompt.id,
1562
+ taskTitle: taskForPrompt.title,
1563
+ provider: currentProviderRef.current,
1564
+ model: currentModelRef.current,
1565
+ prompt: taskForPrompt.prompt,
1566
+ });
1567
+ upsertGoalStatusEntry({
1568
+ runId: run.id,
1569
+ label: run.title,
1570
+ phase: "worker",
1571
+ detail: `Worker: ${decision.task.title}`,
1572
+ workerId: worker.id,
1573
+ startedAt: Date.now(),
1574
+ });
1575
+ appendGoalProgress({
1576
+ kind: "goal_progress",
1577
+ phase: "worker_started",
1578
+ title: `Goal ${goalNumberForRun(run.id)} worker started`,
1579
+ detail: decision.task.title,
1580
+ workerId: worker.id,
1581
+ status: "running",
1582
+ });
1583
+ return;
1584
+ }
1585
+ if (decision.kind === "run_verifier") {
1586
+ if (activeVerifierRunIdsRef.current.has(run.id))
1587
+ return;
1588
+ activeVerifierRunIdsRef.current.add(run.id);
1589
+ upsertGoalStatusEntry({
1590
+ runId: run.id,
1591
+ label: run.title,
1592
+ phase: "verifier",
1593
+ detail: "Verifier running",
1594
+ startedAt: Date.now(),
1595
+ });
1596
+ appendGoalProgress({
1597
+ kind: "goal_progress",
1598
+ phase: "verifier_started",
1599
+ title: `Goal ${goalNumberForRun(run.id)} verifier started`,
1600
+ detail: decision.command,
1601
+ status: "running",
1602
+ });
1603
+ try {
1604
+ const completed = await runGoalVerifierCommand({
1605
+ cwd,
1606
+ runId: run.id,
1607
+ command: decision.command,
1608
+ });
1609
+ const verification = completed.verification;
1610
+ const verificationStatus = verification.status === "pass" ? "pass" : "fail";
1611
+ const latestBeforeVerifier = (await loadGoalRuns(cwd)).find((candidate) => candidate.id === run.id) ?? run;
1612
+ const latest = await upsertGoalRun(cwd, {
1613
+ ...latestBeforeVerifier,
1614
+ status: verificationStatus === "pass" ? "passed" : "ready",
1615
+ verifier: {
1616
+ ...(latestBeforeVerifier.verifier ?? { description: "Goal verifier" }),
1617
+ command: verification.command ?? decision.command,
1618
+ lastResult: verification,
1619
+ },
1620
+ });
1621
+ await appendGoalEvidence(cwd, run.id, {
1622
+ kind: "log",
1623
+ label: "Verifier result",
1624
+ content: formatGoalVerifierCompletionEvent(latest, verificationStatus, verification.command ?? decision.command, verification.exitCode ?? (verificationStatus === "pass" ? 0 : 1), verification.summary),
1625
+ ...(verification.outputPath ? { path: verification.outputPath } : {}),
1626
+ });
1627
+ appendGoalProgress({
1628
+ kind: "goal_progress",
1629
+ phase: "verifier_finished",
1630
+ title: `Goal ${goalNumberForRun(run.id)} verifier ${verificationStatus}`,
1631
+ detail: verification.summary,
1632
+ status: verificationStatus,
1633
+ });
1634
+ upsertGoalStatusEntry({
1635
+ runId: run.id,
1636
+ label: latest.title,
1637
+ phase: verificationStatus === "pass" ? "reviewing" : "failed",
1638
+ detail: verificationStatus === "pass" ? "Verifier passed" : "Verifier failed",
1639
+ startedAt: Date.now(),
1640
+ });
1641
+ continueGoalRun(latest);
1642
+ }
1643
+ catch (error) {
1644
+ appendGoalProgress({
1645
+ kind: "goal_progress",
1646
+ phase: "verifier_finished",
1647
+ title: `Goal ${goalNumberForRun(run.id)} verifier failed`,
1648
+ detail: error instanceof Error ? error.message : String(error),
1649
+ status: "failed",
1650
+ });
1651
+ }
1652
+ finally {
1653
+ activeVerifierRunIdsRef.current.delete(run.id);
1654
+ }
1655
+ return;
1656
+ }
1657
+ if (decision.kind === "complete") {
1658
+ const completion = canCompleteGoalRun(run);
1659
+ const updated = await upsertGoalRun(cwd, { ...run, status: "passed" });
1660
+ const summary = completion.ok
1661
+ ? "Verifier evidence proves the Goal success criteria."
1662
+ : goalCompletionReason(completion);
1663
+ await appendGoalDecision(cwd, run.id, {
1664
+ kind: "summary",
1665
+ content: summary,
1666
+ });
1667
+ appendGoalProgress({
1668
+ kind: "goal_progress",
1669
+ phase: "terminal",
1670
+ title: `Goal passed: ${updated.title}`,
1671
+ detail: summary,
1672
+ status: updated.status,
1673
+ });
1674
+ clearGoalStatusEntry(run.id);
1675
+ return;
1676
+ }
1677
+ if (decision.kind === "pause") {
1678
+ const updated = await upsertGoalRun(cwd, {
1679
+ ...run,
1680
+ status: "paused",
1681
+ blockers: [...run.blockers, decision.reason],
1682
+ });
1683
+ await appendGoalDecision(cwd, run.id, decision);
1684
+ upsertGoalStatusEntry({
1685
+ runId: updated.id,
1686
+ label: updated.title,
1687
+ phase: "failed",
1688
+ detail: decision.reason,
1689
+ startedAt: Date.now(),
1690
+ });
1691
+ appendGoalProgress({
1692
+ kind: "goal_progress",
1693
+ phase: "terminal",
1694
+ title: `Goal paused: ${updated.title}`,
1695
+ detail: decision.reason,
1696
+ summaryRows: buildGoalSummaryRows(updated),
1697
+ status: "paused",
1698
+ });
1699
+ return;
1700
+ }
1701
+ if (decision.kind === "terminal") {
1702
+ const updated = await upsertGoalRun(cwd, { ...run, status: decision.status });
1703
+ if (decision.status === "passed")
1704
+ clearGoalStatusEntry(run.id);
1705
+ appendGoalProgress({
1706
+ kind: "goal_progress",
1707
+ phase: "terminal",
1708
+ title: `Goal ${decision.status}: ${updated.title}`,
1709
+ detail: decision.reason,
1710
+ summaryRows: buildGoalSummaryRows(updated),
1711
+ status: decision.status,
1712
+ });
1713
+ return;
1714
+ }
1715
+ if (decision.kind === "blocked") {
1716
+ const updated = await upsertGoalRun(cwd, {
1717
+ ...run,
1718
+ status: "blocked",
1719
+ blockers: [...run.blockers, decision.reason],
1720
+ });
1721
+ upsertGoalStatusEntry({
1722
+ runId: updated.id,
1723
+ label: updated.title,
1724
+ phase: "failed",
1725
+ detail: decision.reason,
1726
+ startedAt: Date.now(),
1727
+ });
1728
+ appendGoalProgress({
1729
+ kind: "goal_progress",
1730
+ phase: "terminal",
1731
+ title: `Goal ${goalNumberForRun(run.id)} blocked`,
1732
+ detail: decision.reason,
1733
+ status: "blocked",
1734
+ });
1735
+ }
1736
+ }
1737
+ finally {
1738
+ releaseChoice();
1739
+ }
1740
+ })();
1741
+ }, [appendGoalProgress, clearGoalStatusEntry, goalNumberForRun, upsertGoalStatusEntry]);
1742
+ const handleGoalWorkerCompletion = useCallback((completion) => {
1743
+ const cwd = cwdRef.current;
1744
+ runningGoalIdsRef.current.delete(completion.worker.goalRunId);
1745
+ void (async () => {
1746
+ const run = (await loadGoalRuns(cwd)).find((candidate) => candidate.id === completion.worker.goalRunId);
1747
+ const taskTitle = run?.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
1748
+ completion.worker.goalTaskId;
1749
+ await appendGoalEvidence(cwd, completion.worker.goalRunId, {
1750
+ kind: "log",
1751
+ label: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
1752
+ content: run
1753
+ ? formatGoalWorkerCompletionEvent(run, taskTitle, completion)
1754
+ : completion.summary,
1755
+ path: completion.worker.logFile,
1756
+ });
1757
+ appendGoalProgress({
1758
+ kind: "goal_progress",
1759
+ phase: "worker_finished",
1760
+ status: completion.status === "done" ? "done" : "failed",
1761
+ title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
1762
+ detail: summarizeGoalCompletion(completion.summary),
1763
+ workerId: completion.worker.id,
1764
+ });
1765
+ if (!run) {
1766
+ clearGoalStatusEntry(completion.worker.goalRunId);
1767
+ return;
1768
+ }
1769
+ upsertGoalStatusEntry({
1770
+ runId: run.id,
1771
+ label: run.title,
1772
+ phase: completion.status === "done" ? "orchestrating" : "failed",
1773
+ detail: completion.status === "done" ? "Worker finished" : "Worker failed",
1774
+ startedAt: Date.now(),
1775
+ });
1776
+ continueGoalRun(run);
1777
+ })();
1778
+ }, [appendGoalProgress, clearGoalStatusEntry, continueGoalRun, upsertGoalStatusEntry]);
1779
+ useEffect(() => subscribeGoalWorkerCompletions(handleGoalWorkerCompletion), [handleGoalWorkerCompletion]);
1780
+ // First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
1781
+ // scope so the ref closure can dispatch to it. Called from the initial
1782
+ // language-detection path when `isFirstTimeSetup(cwd)` is true. Pushes a
1783
+ // notice item explaining what's happening, then runs the audit prompt.
1784
+ triggerAutoSetupRef.current = async () => {
1918
1785
  const setupCmd = getPromptCommand("setup");
1919
1786
  if (!setupCmd) {
1920
1787
  log("WARN", "setup", "Auto-setup skipped — /setup command not found in registry.");
@@ -1948,27 +1815,6 @@ export function App(props) {
1948
1815
  ]);
1949
1816
  }
1950
1817
  };
1951
- // Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
1952
- // and Ink renders the smaller live area (updating its internal line
1953
- // counter), this effect pushes the stashed items into Static history.
1954
- // Because the Static write happens in a SEPARATE render cycle from the
1955
- // live-area shrink, Ink's log-update never needs to erase the old tall
1956
- // live area AND write Static content in the same frame — avoiding the
1957
- // cursor-math mismatch that caused text clipping.
1958
- useEffect(() => {
1959
- if (pendingFlushRef.current.length > 0) {
1960
- const items = pendingFlushRef.current;
1961
- pendingFlushRef.current = [];
1962
- setHistory((h) => {
1963
- const next = compactHistory([...h, ...trimFlushedItems(items)]);
1964
- if (sessionStore)
1965
- sessionStore.history = next;
1966
- return next;
1967
- });
1968
- if (sessionStore)
1969
- sessionStore.liveItems = liveItems;
1970
- }
1971
- }, [flushGeneration]);
1972
1818
  // Sync terminal title with agent loop state
1973
1819
  useEffect(() => {
1974
1820
  setTitleRunning(agentLoop.isRunning);
@@ -1996,10 +1842,28 @@ export function App(props) {
1996
1842
  return () => clearTimeout(timer);
1997
1843
  }
1998
1844
  }, [agentLoop.isRunning, sessionStore, props.resetUI]);
1999
- // Consume sessionStore.pendingAction once on mount. Set by resetUI options
2000
- // for paths that remount AND immediately drive the agent (plan accept,
2001
- // plan reject, startTask, pixel fix). The action survives the unmount
2002
- // because it lives in renderApp's closure (sessionStore), not React state.
1845
+ const showSessionSummaryAndExit = useCallback(() => {
1846
+ const summary = buildSessionSummary({
1847
+ stats: sessionStatsRef.current,
1848
+ provider: currentProvider,
1849
+ model: currentModel,
1850
+ cwd: displayedCwd,
1851
+ footer: sessionStatsRef.current.sessionId
1852
+ ? `To resume this session: ezcoder --resume ${sessionStatsRef.current.sessionId}`
1853
+ : undefined,
1854
+ });
1855
+ setDoneStatus(null);
1856
+ setExitPending(false);
1857
+ setOverlay(null);
1858
+ setLiveItems([]);
1859
+ setQuittingSummary(summary);
1860
+ writeStdout("\x1b[2J\x1b[3J\x1b[H");
1861
+ setTimeout(() => process.exit(0), 150);
1862
+ }, [currentModel, currentProvider, displayedCwd, writeStdout]);
1863
+ // Consume pending post-remount work once on mount. Set by resetUI options
1864
+ // for paths that remount AND immediately drive work (plan accept/reject,
1865
+ // pixel fix, plan accept/reject). The work survives the unmount because
1866
+ // it lives in renderApp's closure (sessionStore), not React state.
2003
1867
  useEffect(() => {
2004
1868
  if (pendingActionConsumedRef.current)
2005
1869
  return;
@@ -2007,8 +1871,10 @@ export function App(props) {
2007
1871
  if (!action)
2008
1872
  return;
2009
1873
  pendingActionConsumedRef.current = true;
2010
- if (sessionStore)
1874
+ if (sessionStore) {
2011
1875
  sessionStore.pendingAction = undefined;
1876
+ }
1877
+ setDoneStatus(null);
2012
1878
  if (action.planEvent) {
2013
1879
  const ev = action.planEvent;
2014
1880
  setLiveItems((prev) => [
@@ -2022,7 +1888,6 @@ export function App(props) {
2022
1888
  { kind: "info", text: action.infoText, id: getId() },
2023
1889
  ]);
2024
1890
  }
2025
- setDoneStatus(null);
2026
1891
  void agentLoop.run(action.prompt).catch((err) => {
2027
1892
  const errMsg = err instanceof Error ? err.message : String(err);
2028
1893
  log("ERROR", "error", errMsg);
@@ -2030,14 +1895,6 @@ export function App(props) {
2030
1895
  });
2031
1896
  // Intentional one-shot: run once on mount, never re-fire on re-render.
2032
1897
  }, []);
2033
- // Refresh eyes badge count when the agent settles (end of a turn) — a turn
2034
- // may have logged new rough/wish/blocked signals. Also covers the case where
2035
- // /eyes was run for the first time (manifest now exists).
2036
- useEffect(() => {
2037
- if (!agentLoop.isRunning) {
2038
- setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
2039
- }
2040
- }, [agentLoop.isRunning, props.cwd]);
2041
1898
  const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
2042
1899
  const trimmed = input.trim();
2043
1900
  if (trimmed.startsWith("/")) {
@@ -2052,241 +1909,141 @@ export function App(props) {
2052
1909
  // has not grown.
2053
1910
  await applyLanguageDetectionRef.current("input");
2054
1911
  }
2055
- // Handle /model directly open inline selector
2056
- if (trimmed === "/model" || trimmed === "/m" || trimmed === "/models") {
2057
- setOverlay("model");
2058
- return;
2059
- }
2060
- // Handle /compact — compact conversation
2061
- if (trimmed === "/compact" || trimmed === "/c") {
2062
- const ac = new AbortController();
2063
- compactionAbortRef.current = ac;
2064
- const compacted = await compactConversation(messagesRef.current, ac.signal);
2065
- if (!ac.signal.aborted && compacted !== messagesRef.current) {
2066
- messagesRef.current = compacted;
2067
- await persistCompactedSession(compacted);
2068
- }
2069
- if (compactionAbortRef.current === ac)
2070
- compactionAbortRef.current = null;
2071
- return;
2072
- }
2073
- // Handle /quit — exit the agent
2074
- if (trimmed === "/quit" || trimmed === "/q" || trimmed === "/exit") {
2075
- process.exit(0);
2076
- }
2077
- // Handle /clear — tear down the entire Ink instance and rebuild fresh.
2078
- // Avoid direct ANSI terminal clears here; they can erase scrollback.
2079
- // Runtime state (model, provider, thinking) survives via renderApp's
2080
- // closure-held `runtimeState`, mirrored from React state via the
2081
- // useEffects above.
2082
- if (trimmed === "/clear") {
2083
- if (props.resetUI) {
2084
- void (async () => {
2085
- const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
2086
- props.resetUI?.({
2087
- wipeSession: true,
2088
- messages: [{ role: "system", content: newPrompt }],
2089
- });
2090
- })();
2091
- return;
2092
- }
2093
- // Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
2094
- // React state in place without touching terminal scrollback.
2095
- pendingFlushRef.current = [];
2096
- setHistory([{ kind: "banner", id: "banner" }]);
2097
- setLiveItems([]);
2098
- setDoneStatus(null);
2099
- approvedPlanPathRef.current = undefined;
2100
- planStepsRef.current = [];
2101
- setPlanSteps([]);
2102
- void (async () => {
2103
- const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
2104
- messagesRef.current = [{ role: "system", content: newPrompt }];
2105
- persistedIndexRef.current = messagesRef.current.length;
2106
- })();
2107
- agentLoop.reset();
2108
- setSessionTitle(undefined);
2109
- sessionTitleGeneratedRef.current = false;
2110
- setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
2111
- return;
2112
- }
2113
- // Handle /theme — open theme selector overlay
2114
- if (trimmed === "/theme" || trimmed === "/t") {
2115
- setOverlay("theme");
2116
- return;
2117
- }
2118
- // Open the Eyes pane — read-only review of installed probes + open signals.
2119
- // Gated by the ezcoder-eyes manifest: in projects without /eyes set up,
2120
- // there's nothing useful to show.
2121
- if (trimmed === "/eyes-view" || trimmed === "/ev") {
2122
- if (!isEyesActive(props.cwd)) {
2123
- setLiveItems((prev) => [
2124
- ...prev,
2125
- {
2126
- kind: "info",
2127
- text: "Eyes not set up in this project. Run /setup-eyes to get started.",
2128
- id: getId(),
2129
- },
2130
- ]);
2131
- return;
1912
+ if (trimmed === "/ideal-on" || trimmed === "/ideal-off") {
1913
+ const next = trimmed === "/ideal-on";
1914
+ setIdealReviewEnabled(next);
1915
+ if (props.settingsFile) {
1916
+ const sm = new SettingsManager(props.settingsFile);
1917
+ await sm.load();
1918
+ await sm.set("idealReviewEnabled", next);
2132
1919
  }
2133
- setOverlay("eyes");
2134
- return;
2135
- }
2136
- // Handle /plan — toggle plan mode
2137
- if (trimmed === "/plan" || trimmed === "/plan on") {
2138
- setPlanMode(true);
2139
- setLiveItems((prev) => [
2140
- ...prev,
2141
- { kind: "plan_transition", text: "Plan Mode Activated", active: true, id: getId() },
2142
- ]);
2143
- return;
2144
- }
2145
- if (trimmed === "/plan off") {
2146
- setPlanMode(false);
2147
1920
  setLiveItems((prev) => [
2148
1921
  ...prev,
2149
1922
  {
2150
- kind: "plan_transition",
2151
- text: "Plan Mode Deactivated",
2152
- active: false,
1923
+ kind: "info",
1924
+ text: next
1925
+ ? "Ideal review enabled. Use /ideal-off to disable it."
1926
+ : "Ideal review disabled. Use /ideal-on to enable it.",
2153
1927
  id: getId(),
2154
1928
  },
2155
1929
  ]);
2156
1930
  return;
2157
1931
  }
2158
- // Handle /clearplandismiss the approved plan
2159
- if (trimmed === "/clearplan") {
2160
- approvedPlanPathRef.current = undefined;
2161
- planStepsRef.current = [];
2162
- setPlanSteps([]);
2163
- // Rebuild system prompt without the plan
2164
- void replaceSystemPrompt({ clearApprovedPlan: true });
2165
- setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
2166
- return;
2167
- }
2168
- // Handle /map — show, refresh, or toggle dynamic repo map injection
2169
- if (trimmed === "/map" ||
2170
- trimmed === "/map refresh" ||
2171
- trimmed === "/map on" ||
2172
- trimmed === "/map off") {
2173
- const action = trimmed.slice("/map".length).trim();
2174
- if (action === "on") {
2175
- repoMapInjectionEnabledRef.current = true;
2176
- repoMapDirtyRef.current = true;
1932
+ // /rewindopen the checkpoint picker (needs React state + the store).
1933
+ if (trimmed === "/rewind") {
1934
+ const store = props.checkpointStore;
1935
+ if (!store) {
2177
1936
  setLiveItems((prev) => [
2178
1937
  ...prev,
2179
- { kind: "info", text: "Dynamic repo map injection is on.", id: getId() },
1938
+ { kind: "info", text: "Checkpoints are not available in this session.", id: getId() },
2180
1939
  ]);
2181
1940
  return;
2182
1941
  }
2183
- if (action === "off") {
2184
- repoMapInjectionEnabledRef.current = false;
2185
- messagesRef.current = stripRepoMapMessages(messagesRef.current);
1942
+ const cps = await store.listCheckpoints();
1943
+ if (cps.length === 0) {
2186
1944
  setLiveItems((prev) => [
2187
1945
  ...prev,
2188
1946
  {
2189
1947
  kind: "info",
2190
- text: "Dynamic repo map injection is off for this session.",
1948
+ text: "No checkpoints yet \u2014 edit a file through ezcoder first.",
2191
1949
  id: getId(),
2192
1950
  },
2193
1951
  ]);
2194
1952
  return;
2195
1953
  }
2196
- if (action === "refresh")
2197
- repoMapDirtyRef.current = true;
2198
- const markdown = await refreshRepoMap(getLatestUserText(messagesRef.current));
2199
- setLiveItems((prev) => [
2200
- ...prev,
2201
- {
2202
- kind: "info",
2203
- text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
2204
- id: getId(),
2205
- },
2206
- ]);
1954
+ setRewindCheckpoints(cps);
2207
1955
  return;
2208
1956
  }
2209
- // Handle /goals — open goal pane
2210
- if (trimmed === "/goals") {
2211
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2212
- props.sessionStore.overlay = "goal";
2213
- props.sessionStore.planAutoExpand = false;
2214
- props.resetUI();
2215
- }
2216
- else {
2217
- if (props.sessionStore) {
2218
- props.sessionStore.overlay = "goal";
2219
- props.sessionStore.planAutoExpand = false;
2220
- if (agentLoop.isRunning)
2221
- props.sessionStore.pendingResetUI = true;
1957
+ if (await handleUiSlashCommand(trimmed, {
1958
+ openModelSelector: () => setOverlay("model"),
1959
+ compactConversation: async () => {
1960
+ const ac = new AbortController();
1961
+ compactionAbortRef.current = ac;
1962
+ const compacted = await compactConversation(messagesRef.current, ac.signal);
1963
+ if (!ac.signal.aborted && compacted !== messagesRef.current) {
1964
+ messagesRef.current = compacted;
1965
+ await persistCompactedSession(compacted);
2222
1966
  }
2223
- setPlanAutoExpand(false);
2224
- setOverlay("goal");
2225
- }
2226
- return;
2227
- }
2228
- // Handle /plans — open plan pane
2229
- if (trimmed === "/plans") {
2230
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2231
- props.sessionStore.overlay = "plan";
2232
- props.sessionStore.planAutoExpand = false;
2233
- props.resetUI();
2234
- }
2235
- else {
2236
- if (props.sessionStore) {
2237
- props.sessionStore.overlay = "plan";
2238
- props.sessionStore.planAutoExpand = false;
2239
- if (agentLoop.isRunning)
2240
- props.sessionStore.pendingResetUI = true;
1967
+ if (compactionAbortRef.current === ac)
1968
+ compactionAbortRef.current = null;
1969
+ },
1970
+ quit: showSessionSummaryAndExit,
1971
+ clearSession: () => {
1972
+ if (props.resetUI) {
1973
+ void (async () => {
1974
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1975
+ props.resetUI?.({
1976
+ wipeSession: true,
1977
+ messages: [{ role: "system", content: newPrompt }],
1978
+ });
1979
+ })();
1980
+ return;
2241
1981
  }
2242
- setPlanAutoExpand(false);
2243
- setOverlay("plan");
2244
- }
1982
+ clearPendingHistory();
1983
+ setHistory([{ kind: "banner", id: "banner" }]);
1984
+ setLiveItems([]);
1985
+ setDoneStatus(null);
1986
+ approvedPlanPathRef.current = undefined;
1987
+ planStepsRef.current = [];
1988
+ setPlanSteps([]);
1989
+ void (async () => {
1990
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1991
+ messagesRef.current = [{ role: "system", content: newPrompt }];
1992
+ persistedIndexRef.current = messagesRef.current.length;
1993
+ })();
1994
+ agentLoop.reset();
1995
+ setSessionTitle(undefined);
1996
+ sessionTitleGeneratedRef.current = false;
1997
+ setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
1998
+ },
1999
+ openThemeSelector: () => setOverlay("theme"),
2000
+ toggleMarkdown: () => {
2001
+ setRenderMarkdown((prev) => {
2002
+ const next = !prev;
2003
+ setLiveItems([
2004
+ {
2005
+ kind: "info",
2006
+ text: next ? "Rendered markdown mode." : "Raw markdown mode.",
2007
+ id: getId(),
2008
+ },
2009
+ ]);
2010
+ return next;
2011
+ });
2012
+ },
2013
+ clearApprovedPlan: () => {
2014
+ approvedPlanPathRef.current = undefined;
2015
+ planStepsRef.current = [];
2016
+ setPlanSteps([]);
2017
+ void replaceSystemPrompt({ clearApprovedPlan: true });
2018
+ setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
2019
+ },
2020
+ })) {
2245
2021
  return;
2246
2022
  }
2247
- // Handle prompt-template commands (built-in + custom from .ezcoder/commands/)
2248
- const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
2249
- if (promptCommandRoute) {
2250
- const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
2251
- log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
2252
- // Move live items into history before starting
2253
- setLiveItems((prev) => {
2254
- if (prev.length > 0) {
2255
- pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
2023
+ if (await submitPromptCommand({
2024
+ trimmed,
2025
+ inputImages,
2026
+ currentModel,
2027
+ customCommands,
2028
+ setLastUserMessage,
2029
+ setDoneStatus,
2030
+ finalizeSubmittedUserItem,
2031
+ runAgent: (content) => agentLoop.run(content),
2032
+ setLiveItems,
2033
+ getId,
2034
+ reloadCustomCommands,
2035
+ messagesRef,
2036
+ setGoalModeAndPrompt,
2037
+ onGoalStage: appendGoalAgentTransition,
2038
+ onGoalSetupComplete: () => {
2039
+ setGoalAutoExpand(true);
2040
+ if (props.sessionStore) {
2041
+ props.sessionStore.overlay = "goal";
2042
+ props.sessionStore.goalAutoExpand = true;
2256
2043
  }
2257
- return [];
2258
- });
2259
- const hasImages = inputImages.length > 0;
2260
- const modelInfo = getModel(currentModel);
2261
- const modelSupportsImages = modelInfo?.supportsImages ?? true;
2262
- const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
2263
- // Show the typed command as the user message
2264
- const userItem = {
2265
- kind: "user",
2266
- text: trimmed,
2267
- imageCount: hasImages ? inputImages.length : undefined,
2268
- id: getId(),
2269
- };
2270
- setLastUserMessage(trimmed);
2271
- setDoneStatus(null);
2272
- setLiveItems([userItem]);
2273
- // Send the full prompt to the agent, with user args appended if provided
2274
- try {
2275
- await agentLoop.run(userContent);
2276
- }
2277
- catch (err) {
2278
- const msg = err instanceof Error ? err.message : String(err);
2279
- log("ERROR", "error", msg);
2280
- const isAbort = msg.includes("aborted") || msg.includes("abort");
2281
- setLiveItems((prev) => [
2282
- ...prev,
2283
- isAbort
2284
- ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2285
- : toErrorItem(err, getId()),
2286
- ]);
2287
- }
2288
- // Reload custom commands in case a setup command created new ones
2289
- reloadCustomCommands();
2044
+ setOverlay("goal");
2045
+ },
2046
+ })) {
2290
2047
  return;
2291
2048
  }
2292
2049
  // Check slash commands
@@ -2305,7 +2062,7 @@ export function App(props) {
2305
2062
  // ── Queue message if agent is already running ──
2306
2063
  if (agentLoop.isRunning) {
2307
2064
  log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
2308
- agentLoop.queueMessage(userContent);
2065
+ agentLoop.queueMessage(userContent, input);
2309
2066
  let displayText = input;
2310
2067
  if (hasImages) {
2311
2068
  const { cleanText } = await extractImagePaths(input, props.cwd);
@@ -2320,31 +2077,32 @@ export function App(props) {
2320
2077
  setLiveItems((prev) => [...prev, queuedItem]);
2321
2078
  return;
2322
2079
  }
2323
- // Move any remaining live items into history (Static) before starting a
2324
- // new turn. Must go through queueFlush so flushGeneration bumps and the
2325
- // drain effect actually runs — mutating pendingFlushRef directly here
2326
- // stashed items that nothing was signalled to pick up, so they sat in
2327
- // limbo until some unrelated later code path happened to call queueFlush.
2328
- setLiveItems((prev) => {
2329
- if (prev.length > 0) {
2330
- queueFlush(prev);
2331
- }
2332
- return [];
2333
- });
2334
2080
  // Build display text — strip image paths, show badges instead
2335
2081
  let displayText = input;
2336
2082
  if (hasImages) {
2337
2083
  const { cleanText } = await extractImagePaths(input, props.cwd);
2338
2084
  displayText = cleanText;
2339
2085
  }
2086
+ let imagePreviews;
2087
+ if (hasImages) {
2088
+ const built = await Promise.all(inputImages
2089
+ .filter((img) => img.kind === "image")
2090
+ .map(async (img) => {
2091
+ const downscaled = await downscaleForPreview(Buffer.from(img.data, "base64"));
2092
+ return { base64: downscaled.toString("base64"), mediaType: img.mediaType };
2093
+ }));
2094
+ imagePreviews = built.length > 0 ? built : undefined;
2095
+ }
2340
2096
  const userItem = {
2341
2097
  kind: "user",
2342
2098
  text: displayText,
2343
2099
  imageCount: hasImages ? inputImages.length : undefined,
2100
+ imagePreviews,
2344
2101
  pasteInfo,
2345
2102
  id: getId(),
2346
2103
  };
2347
2104
  setLastUserMessage(input);
2105
+ setScrollResetToken((token) => token + 1);
2348
2106
  setDoneStatus(null);
2349
2107
  // Clear stale plan progress if there's no active approved plan
2350
2108
  // (avoids lingering progress from a completed or abandoned plan run)
@@ -2352,7 +2110,18 @@ export function App(props) {
2352
2110
  planStepsRef.current = [];
2353
2111
  setPlanSteps([]);
2354
2112
  }
2355
- setLiveItems([userItem]);
2113
+ finalizeSubmittedUserItem(userItem, liveItems);
2114
+ // Open a per-turn checkpoint capturing the conversation position right
2115
+ // before this exchange, so /rewind can restore pre-turn code/chat state.
2116
+ if (props.checkpointStore) {
2117
+ rewindTurnRef.current += 1;
2118
+ await props.checkpointStore
2119
+ .openCheckpoint({
2120
+ turnIndex: rewindTurnRef.current,
2121
+ messageIndex: messagesRef.current.length,
2122
+ })
2123
+ .catch(() => { });
2124
+ }
2356
2125
  // Run agent
2357
2126
  try {
2358
2127
  await agentLoop.run(userContent);
@@ -2370,17 +2139,29 @@ export function App(props) {
2370
2139
  }
2371
2140
  }, [
2372
2141
  agentLoop,
2373
- props.onSlashCommand,
2374
2142
  compactConversation,
2143
+ currentModel,
2144
+ finalizeSubmittedUserItem,
2145
+ liveItems,
2146
+ props.cwd,
2147
+ props.onSlashCommand,
2148
+ props.resetUI,
2149
+ props.sessionStore,
2375
2150
  rebuildSystemPrompt,
2151
+ showSessionSummaryAndExit,
2152
+ reloadCustomCommands,
2376
2153
  replaceSystemPrompt,
2377
- refreshRepoMap,
2378
- stripRepoMapMessages,
2379
2154
  ]);
2380
- const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
2155
+ const handleDoubleExit = useDoublePress(setExitPending, showSessionSummaryAndExit);
2381
2156
  const handleAbort = useCallback(() => {
2382
2157
  if (agentLoop.isRunning) {
2383
- agentLoop.clearQueue();
2158
+ // Restore any unsent queued messages to the composer instead of dropping
2159
+ // them, so an interrupt never silently discards what the user typed.
2160
+ const queuedText = agentLoop.drainQueuedText();
2161
+ if (queuedText) {
2162
+ setLiveItems((prev) => prev.filter((item) => item.kind !== "queued"));
2163
+ setComposerInject({ text: queuedText, nonce: nextIdRef.current++ });
2164
+ }
2384
2165
  agentLoop.abort();
2385
2166
  }
2386
2167
  else if (compactionAbortRef.current) {
@@ -2389,22 +2170,22 @@ export function App(props) {
2389
2170
  else {
2390
2171
  handleDoubleExit();
2391
2172
  }
2392
- }, [agentLoop, handleDoubleExit]);
2173
+ }, [agentLoop, handleDoubleExit, setLiveItems]);
2393
2174
  const handleToggleThinking = useCallback(() => {
2394
- setThinkingEnabled((prev) => {
2395
- const next = !prev;
2396
- log("INFO", "thinking", `Thinking ${next ? "enabled" : "disabled"}`);
2397
- setLiveItems((items) => [
2398
- ...items,
2399
- { kind: "thinking_transition", active: next, id: getId() },
2400
- ]);
2175
+ setThinkingLevel((prev) => {
2176
+ const next = getNextThinkingLevel(currentProvider, currentModel, prev);
2177
+ log("INFO", "thinking", next ? `Thinking ${next}` : "Thinking disabled");
2401
2178
  if (props.settingsFile) {
2402
2179
  const sm = new SettingsManager(props.settingsFile);
2403
- sm.load().then(() => sm.set("thinkingEnabled", next));
2180
+ void sm.load().then(async () => {
2181
+ await sm.set("thinkingEnabled", !!next);
2182
+ if (next)
2183
+ await sm.set("thinkingLevel", next);
2184
+ });
2404
2185
  }
2405
2186
  return next;
2406
2187
  });
2407
- }, [props.settingsFile]);
2188
+ }, [currentProvider, currentModel, props.settingsFile]);
2408
2189
  const handleModelSelect = useCallback((value) => {
2409
2190
  setOverlay(null);
2410
2191
  const colonIdx = value.indexOf(":");
@@ -2413,6 +2194,9 @@ export function App(props) {
2413
2194
  const newProvider = value.slice(0, colonIdx);
2414
2195
  const newModelId = value.slice(colonIdx + 1);
2415
2196
  log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
2197
+ // Keep the ref in sync before any prompt rebuild so the identity (Claude
2198
+ // Code vs EZ Coder) reflects the newly selected provider immediately.
2199
+ currentProviderRef.current = newProvider;
2416
2200
  const rebuildPromptWithTools = (tools) => {
2417
2201
  currentToolsRef.current = tools;
2418
2202
  void replaceSystemPrompt({ tools });
@@ -2512,198 +2296,179 @@ export function App(props) {
2512
2296
  const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
2513
2297
  const fromPrompt = (name) => {
2514
2298
  const c = promptByName.get(name);
2515
- return c ? { name: c.name, aliases: c.aliases, description: c.description } : null;
2299
+ return c
2300
+ ? {
2301
+ name: c.name,
2302
+ aliases: c.aliases,
2303
+ description: c.description,
2304
+ sectionTitle: "workflows",
2305
+ }
2306
+ : null;
2516
2307
  };
2517
2308
  const promptOrder = [
2518
2309
  // Project audits / one-shot analysis
2519
- "goal",
2520
2310
  "init",
2521
- "research",
2522
- "scan",
2523
- "verify",
2524
2311
  "expand",
2525
2312
  "bullet-proof",
2526
- "simplify",
2527
2313
  "compare",
2528
- "batch",
2529
2314
  // Setup / installers
2530
- "setup-lint",
2531
- "setup-tests",
2532
2315
  "setup-commit",
2533
- "setup-update",
2534
- "setup-eyes",
2535
- "eyes-improve",
2536
2316
  "setup-skills",
2537
2317
  ];
2538
2318
  const orderedPromptCommands = promptOrder
2539
2319
  .map(fromPrompt)
2540
2320
  .filter((c) => c !== null);
2541
2321
  const knownPromptNames = new Set(promptOrder);
2542
- const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({ name: c.name, aliases: c.aliases, description: c.description }));
2322
+ const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
2323
+ name: c.name,
2324
+ aliases: c.aliases,
2325
+ description: c.description,
2326
+ sectionTitle: "workflows",
2327
+ }));
2543
2328
  return [
2544
2329
  // Session actions (most frequent)
2545
- { name: "model", aliases: ["m"], description: "Switch model" },
2546
- { name: "compact", aliases: ["c"], description: "Compact conversation" },
2547
- { name: "clear", aliases: [], description: "Clear session and terminal" },
2548
- { name: "theme", aliases: ["t"], description: "Switch theme" },
2549
- { name: "plans", aliases: [], description: "Open plans pane" },
2330
+ { name: "model", aliases: ["m"], description: "Switch model", sectionTitle: "built-in" },
2331
+ { name: "compact", aliases: ["c"], description: "Compact context", sectionTitle: "built-in" },
2332
+ { name: "clear", aliases: [], description: "Clear session", sectionTitle: "built-in" },
2333
+ { name: "theme", aliases: ["t"], description: "Switch theme", sectionTitle: "built-in" },
2334
+ {
2335
+ name: idealReviewEnabled ? "ideal-off" : "ideal-on",
2336
+ aliases: [],
2337
+ description: idealReviewEnabled
2338
+ ? "Disable pre-final ideal review"
2339
+ : "Enable pre-final ideal review",
2340
+ sectionTitle: "built-in",
2341
+ },
2342
+ {
2343
+ name: "rewind",
2344
+ aliases: [],
2345
+ description: "Restore files/conversation to a checkpoint",
2346
+ sectionTitle: "built-in",
2347
+ },
2550
2348
  ...orderedPromptCommands,
2551
2349
  ...remainingPromptCommands,
2552
2350
  ...customCommands.map((cmd) => ({
2553
2351
  name: cmd.name,
2554
2352
  aliases: [],
2555
2353
  description: cmd.description,
2354
+ sectionTitle: "custom",
2556
2355
  })),
2557
- { name: "quit", aliases: ["q", "exit"], description: "Exit the agent" },
2356
+ {
2357
+ name: "quit",
2358
+ aliases: ["q", "exit"],
2359
+ description: "Exit ezcoder",
2360
+ sectionTitle: "built-in",
2361
+ },
2558
2362
  ];
2559
- }, [customCommands]);
2560
- const renderItem = (item) => {
2561
- switch (item.kind) {
2562
- case "tombstone":
2563
- return null;
2564
- case "banner":
2565
- return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
2566
- case "user":
2567
- return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
2568
- case "task":
2569
- return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Task: " }), _jsx(Text, { color: theme.success, children: item.title })] }) }, item.id));
2570
- case "goal":
2571
- return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Goal: " }), _jsx(Text, { color: theme.success, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }) }, item.id));
2572
- case "goal_progress": {
2573
- const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
2574
- const color = isError
2575
- ? theme.error
2576
- : item.phase === "worker_finished"
2577
- ? theme.success
2578
- : item.phase === "verifier_finished"
2579
- ? theme.accent
2580
- : item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
2581
- ? theme.secondary
2582
- : item.phase === "continuing"
2583
- ? theme.warning
2584
- : item.phase === "verifier_started"
2585
- ? theme.accent
2586
- : item.phase === "worker_started"
2587
- ? theme.primary
2588
- : item.phase === "terminal"
2589
- ? theme.success
2590
- : theme.primary;
2591
- const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
2592
- ? "✓ "
2593
- : item.phase === "terminal"
2594
- ? item.status === "passed"
2595
- ? "◆ "
2596
- : "! "
2597
- : "↻ ";
2598
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null, item.summaryRows && item.summaryRows.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, flexShrink: 1, children: item.summaryRows.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(10) }), _jsx(Text, { color: theme.text, children: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", row.detail] }) : null] }, row.label))) })) : null] }, item.id));
2599
- }
2600
- case "style_pack": {
2601
- const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
2602
- const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
2603
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: headerLabel })] }), _jsx(Text, { color: theme.text, bold: true, wrap: "wrap", children: names.join(", ") }), item.showSetupHint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit this project against the active pack(s)" })] }) }))] }, item.id));
2604
- }
2605
- case "setup_hint":
2606
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: "NO STYLE PACKS DETECTED" })] }), _jsx(Text, { color: theme.textMuted, wrap: "wrap", children: "This directory has no recognized language manifest at its root." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit project hygiene or bootstrap a new project from scratch" })] }) })] }, item.id));
2607
- case "assistant":
2608
- return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
2609
- case "tool_start":
2610
- return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
2611
- case "tool_done":
2612
- return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
2613
- case "tool_group":
2614
- return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
2615
- case "server_tool_start":
2616
- return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
2617
- case "server_tool_done":
2618
- return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
2619
- case "error": {
2620
- const showMessage = item.message && item.message !== item.headline;
2621
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { color: theme.error, wrap: "wrap", children: ["✗ ", item.headline] }), showMessage && (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.message}` })), _jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` → ${item.guidance}` })] }, item.id));
2622
- }
2623
- case "info":
2624
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
2625
- case "update_notice":
2626
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.success, paddingX: 1, children: _jsxs(Text, { color: theme.success, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }, item.id));
2627
- case "plan_transition":
2628
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
2629
- case "thinking_transition": {
2630
- const glyphColor = item.active ? THINKING_BORDER_COLORS[0] : theme.textDim;
2631
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "✻ " }), _jsx(Text, { color: item.active ? theme.accent : theme.textDim, bold: true, children: item.active ? "Thinking ON" : "Thinking OFF" })] }, item.id));
2632
- }
2633
- case "model_transition": {
2634
- const glyphColor = THINKING_BORDER_COLORS[0];
2635
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "▸ " }), _jsx(Text, { color: theme.textDim, children: "Switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.modelName })] }, item.id));
2636
- }
2637
- case "theme_transition": {
2638
- const glyphColor = THINKING_BORDER_COLORS[0];
2639
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "◐ " }), _jsx(Text, { color: theme.textDim, children: "Theme switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.themeName })] }, item.id));
2640
- }
2641
- case "plan_event": {
2642
- // Plan-domain status changes (approve / reject / dismiss). Uses
2643
- // theme.planPrimary to match the existing plan_transition family,
2644
- // distinct from the model/thinking gradient.
2645
- const label = item.event === "approved"
2646
- ? "Plan approved"
2647
- : item.event === "rejected"
2648
- ? "Plan rejected"
2649
- : "Plan dismissed";
2650
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsxs(Text, { color: theme.planPrimary, bold: true, children: ["○ ", label] }), item.detail ? _jsx(Text, { color: theme.textDim, children: ` — "${item.detail}"` }) : null] }, item.id));
2651
- }
2652
- case "stopped":
2653
- // Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
2654
- // Muted dim treatment — this is an ack, not a state change worth a
2655
- // gradient. Glyph `⊘` reads as "stop" without being alarming.
2656
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.textDim, bold: true, children: ["⊘ ", item.text] }) }, item.id));
2657
- case "step_done":
2658
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "✓ " }), _jsx(Text, { color: theme.success, bold: true, children: `Step ${item.stepNum} done` }), item.description ? (_jsx(Text, { color: theme.textDim, children: ` — ${item.description}` })) : null] }) }, item.id));
2659
- case "queued":
2660
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "⏳ Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
2661
- ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
2662
- : ""] })] }, item.id));
2663
- case "compacting":
2664
- return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
2665
- case "compacted":
2666
- return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
2667
- case "duration":
2668
- return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
2669
- case "subagent_group":
2670
- return _jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id);
2363
+ }, [customCommands, idealReviewEnabled]);
2364
+ const renderItem = (item, index, items) => renderTranscriptItem({
2365
+ item,
2366
+ index,
2367
+ items,
2368
+ pendingHistoryFlushLastItem: index === 0 ? lastVisibleTranscriptItem(pendingHistoryFlushRef.current) : undefined,
2369
+ historyLastItem: index === 0 ? lastVisibleTranscriptItem(history) : undefined,
2370
+ version: props.version,
2371
+ currentModel,
2372
+ currentProvider,
2373
+ displayedCwd,
2374
+ columns,
2375
+ theme,
2376
+ renderMarkdown,
2377
+ measuredLiveAreaRows,
2378
+ });
2379
+ const openOverlay = useCallback((kind) => {
2380
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2381
+ props.sessionStore.overlay = kind;
2382
+ if (kind !== "plan")
2383
+ props.sessionStore.planAutoExpand = false;
2384
+ if (kind !== "goal")
2385
+ props.sessionStore.goalAutoExpand = false;
2386
+ props.resetUI();
2671
2387
  }
2672
- };
2673
- // ── Start a task (shared by manual "work on it" and run-all) ──
2674
- const startTask = useCallback((title, prompt, taskId) => {
2675
- setTaskCount(getTaskCount(props.cwd));
2676
- const shortId = taskId.slice(0, 8);
2677
- const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2678
- `tasks({ action: "done", id: "${shortId}" })`;
2679
- const fullPrompt = prompt + completionHint;
2680
- if (props.resetUI && props.sessionStore) {
2681
- // Preserve the current system prompt (may differ from the launch
2682
- // config — e.g. plan mode toggled or skills changed).
2388
+ else {
2389
+ if (props.sessionStore) {
2390
+ props.sessionStore.overlay = kind;
2391
+ if (kind !== "plan")
2392
+ props.sessionStore.planAutoExpand = false;
2393
+ if (kind !== "goal")
2394
+ props.sessionStore.goalAutoExpand = false;
2395
+ if (agentLoop.isRunning && kind !== "plan") {
2396
+ props.sessionStore.pendingResetUI = true;
2397
+ }
2398
+ }
2399
+ if (kind !== "plan")
2400
+ setPlanAutoExpand(false);
2401
+ if (kind !== "goal")
2402
+ setGoalAutoExpand(false);
2403
+ setOverlay(kind);
2404
+ }
2405
+ }, [agentLoop.isRunning, props]);
2406
+ useEffect(() => {
2407
+ runAllTasksRef.current = runAllTasks;
2408
+ if (props.sessionStore)
2409
+ props.sessionStore.runAllTasks = runAllTasks;
2410
+ }, [runAllTasks, props.sessionStore]);
2411
+ useEffect(() => {
2412
+ agentRunningRef.current = agentLoop.isRunning;
2413
+ }, [agentLoop.isRunning]);
2414
+ const { startPixelFix, setRunAllPixel } = usePixelFixFlow({
2415
+ agentLoop,
2416
+ cwd: props.cwd,
2417
+ currentProvider,
2418
+ currentModel,
2419
+ rebuildToolsForCwd: props.rebuildToolsForCwd,
2420
+ sessionStore: props.sessionStore,
2421
+ currentPixelFixRef,
2422
+ runAllPixelRef,
2423
+ startPixelFixRef,
2424
+ cwdRef,
2425
+ currentToolsRef,
2426
+ injectedLanguagesRef,
2427
+ setupHintShownRef,
2428
+ messagesRef,
2429
+ persistedIndexRef,
2430
+ sessionManagerRef,
2431
+ sessionPathRef,
2432
+ setDisplayedCwd,
2433
+ setCurrentTools,
2434
+ setHistory,
2435
+ setLiveItems,
2436
+ setLastUserMessage,
2437
+ setDoneStatus,
2438
+ rebuildSystemPrompt,
2439
+ clearPendingHistory,
2440
+ getId,
2441
+ initialRunAllPixel: props.sessionStore?.runAllPixel ?? false,
2442
+ });
2443
+ // Starts a single task: opens a fresh session + chat and runs the task
2444
+ // prompt through the agent loop. Wired into startTaskRef so both the task
2445
+ // picker (Enter = start one, r = run all) and the run-all auto-advance in
2446
+ // onDone can invoke it from stale closures.
2447
+ const startTask = useCallback((title, prompt, taskId) => {
2448
+ const taskCwd = cwdRef.current;
2449
+ const shortId = taskId.slice(0, 8);
2450
+ const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2451
+ `tasks({ action: "done", id: "${shortId}" })`;
2452
+ const fullPrompt = prompt + completionHint;
2453
+ if (props.resetUI && props.sessionStore) {
2683
2454
  const sysMsg = messagesRef.current[0];
2684
2455
  const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
2685
- const taskItem = { kind: "task", title, id: String(nextIdRef.current++) };
2456
+ const taskItem = { kind: "task", title, id: getId() };
2686
2457
  const sm = sessionManagerRef.current;
2687
2458
  void (async () => {
2688
2459
  let newSessionPath;
2689
2460
  if (sm) {
2690
2461
  try {
2691
- const s = await sm.create(props.cwd, currentProvider, currentModel);
2692
- newSessionPath = s.path;
2693
- log("INFO", "tasks", "New session for task", { path: s.path });
2462
+ const session = await sm.create(taskCwd, currentProvider, currentModel);
2463
+ newSessionPath = session.path;
2464
+ log("INFO", "tasks", "New session for task", { path: session.path });
2694
2465
  }
2695
2466
  catch {
2696
- // session creation is best-effort
2467
+ // Session creation is best-effort.
2697
2468
  }
2698
2469
  }
2699
- if (props.sessionStore) {
2470
+ if (props.sessionStore)
2700
2471
  props.sessionStore.overlay = null;
2701
- // Mirror through sessionStore so the post-resetUI remount can
2702
- // re-seed the ref. markTaskDone() in the run-all onDone branch
2703
- // reads currentTaskIdRef.current, which is seeded from this.
2704
- props.sessionStore.currentTaskId = taskId;
2705
- }
2706
- currentTaskIdRef.current = taskId;
2707
2472
  props.resetUI?.({
2708
2473
  wipeSession: true,
2709
2474
  messages: newMessages,
@@ -2714,10 +2479,7 @@ export function App(props) {
2714
2479
  })();
2715
2480
  return;
2716
2481
  }
2717
- // Fallback path (resetUI not wired — tests).
2718
- currentTaskIdRef.current = taskId;
2719
- if (props.sessionStore)
2720
- props.sessionStore.currentTaskId = taskId;
2482
+ clearPendingHistory();
2721
2483
  setHistory([{ kind: "banner", id: "banner" }]);
2722
2484
  setLiveItems([]);
2723
2485
  messagesRef.current = messagesRef.current.slice(0, 1);
@@ -2725,940 +2487,585 @@ export function App(props) {
2725
2487
  persistedIndexRef.current = messagesRef.current.length;
2726
2488
  const sm = sessionManagerRef.current;
2727
2489
  if (sm) {
2728
- void sm.create(props.cwd, currentProvider, currentModel).then((s) => {
2729
- sessionPathRef.current = s.path;
2730
- log("INFO", "tasks", "New session for task", { path: s.path });
2490
+ void sm.create(taskCwd, currentProvider, currentModel).then((session) => {
2491
+ sessionPathRef.current = session.path;
2492
+ log("INFO", "tasks", "New session for task", { path: session.path });
2731
2493
  });
2732
2494
  }
2733
2495
  const taskItem = { kind: "task", title, id: getId() };
2734
2496
  setLastUserMessage(title);
2735
2497
  setDoneStatus(null);
2736
2498
  setLiveItems([taskItem]);
2499
+ void agentLoop.run(fullPrompt).catch((err) => {
2500
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2501
+ });
2502
+ }, [agentLoop, currentModel, currentProvider, props]);
2503
+ // Keep the ref in sync so stale closures (task picker, onDone run-all) can
2504
+ // start a task without being recreated each render.
2505
+ startTaskRef.current = startTask;
2506
+ // Reset the live tool feed at the start of each run so the pinned panel only
2507
+ // ever reflects the current turn's activity, not the previous one's.
2508
+ const wasRunningRef = useRef(false);
2509
+ useEffect(() => {
2510
+ if (agentLoop.isRunning && !wasRunningRef.current) {
2511
+ setLiveToolFeed([]);
2512
+ }
2513
+ wasRunningRef.current = agentLoop.isRunning;
2514
+ }, [agentLoop.isRunning]);
2515
+ const isSkillsView = overlay === "skills";
2516
+ const isPlanView = overlay === "plan";
2517
+ const { footerStatusLayout, activityVisible, stallStatusVisible, statusSlotVisible, mainControlsRef, measuredLiveAreaRows, viewportRows, } = useChatLayoutMeasurements({
2518
+ rows,
2519
+ columns,
2520
+ backgroundTaskCount: bgTasks.length,
2521
+ updatePending,
2522
+ agentRunning: agentLoop.isRunning,
2523
+ activityPhase: agentLoop.activityPhase,
2524
+ stallError: agentLoop.stallError,
2525
+ doneStatus,
2526
+ currentModel,
2527
+ contextUsed: agentLoop.contextUsed,
2528
+ contextWindowOptions,
2529
+ displayedCwd,
2530
+ gitBranch,
2531
+ thinkingLevel,
2532
+ exitPending,
2533
+ taskBarExpanded,
2534
+ liveToolFeedCount: liveToolFeed.length,
2535
+ });
2536
+ const isPixelView = overlay === "pixel";
2537
+ const hasLiveAssistantItem = liveItems.some((item) => item.kind === "assistant");
2538
+ const rawVisibleStreamingText = hasLiveAssistantItem ? "" : agentLoop.streamingText;
2539
+ // Compute the prospective paragraph flush DURING render so the live frame
2540
+ // immediately drops the prefix that is about to be written to scrollback.
2541
+ // The queueFlush below runs in an effect (after paint), so if the live text
2542
+ // were sliced only by the already-committed `flushedChars`, the just-flushed
2543
+ // paragraph would render BOTH in scrollback and live for one frame — that
2544
+ // transient extra height is what shoves the footer up and then back down on
2545
+ // every chunk boundary. Slicing by the prospective flush here keeps the live
2546
+ // frame height monotonic, so the footer never bounces.
2547
+ const alreadyFlushedChars = streamedAssistantFlushRef.current.flushedChars;
2548
+ const pendingFlushChars = rawVisibleStreamingText
2549
+ ? splitAssistantStreamingText(rawVisibleStreamingText.slice(alreadyFlushedChars)).flushedText
2550
+ .length
2551
+ : 0;
2552
+ useEffect(() => {
2553
+ if (!rawVisibleStreamingText) {
2554
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
2555
+ return;
2556
+ }
2557
+ if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
2558
+ return;
2559
+ const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
2560
+ const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
2561
+ const split = splitAssistantStreamingText(unflushedText);
2562
+ if (split.flushedText.length > 0) {
2563
+ queueFlush([
2564
+ {
2565
+ kind: "assistant",
2566
+ text: stripDoneMarkers(split.flushedText),
2567
+ continuation: streamedAssistantFlushRef.current.flushedChars > 0,
2568
+ id: getId(),
2569
+ },
2570
+ ]);
2571
+ streamedAssistantFlushRef.current = {
2572
+ flushedChars: alreadyFlushed + split.flushedText.length,
2573
+ text: rawVisibleStreamingText,
2574
+ };
2575
+ return;
2576
+ }
2577
+ streamedAssistantFlushRef.current = {
2578
+ ...streamedAssistantFlushRef.current,
2579
+ text: rawVisibleStreamingText,
2580
+ };
2581
+ }, [rawVisibleStreamingText, queueFlush]);
2582
+ const visibleStreamingText = stripDoneMarkers(rawVisibleStreamingText.slice(alreadyFlushedChars + pendingFlushChars));
2583
+ const lastLiveItem = liveItems.at(-1);
2584
+ // For spacing decisions, the previous row is the last item that actually
2585
+ // RENDERS. Panel-replaced tool items (now shown only in the LiveToolPanel)
2586
+ // render null, so counting them as the boundary inserts a blank separator
2587
+ // above the streamed response with nothing visible above it.
2588
+ const lastVisibleLiveItem = lastVisibleTranscriptItem(liveItems);
2589
+ const lastPendingHistoryItem = pendingHistoryFlushRef.current.at(-1);
2590
+ const lastHistoryItem = history.at(-1);
2591
+ // Spacing variants: flushed tool rows render null (LiveToolPanel owns them), so
2592
+ // the streamed/first-live boundary must look past them to the last row that
2593
+ // actually printed — otherwise a tool→assistant separator leaves a phantom gap.
2594
+ const lastVisiblePendingHistoryItem = lastVisibleTranscriptItem(pendingHistoryFlushRef.current);
2595
+ const lastVisibleHistoryItem = lastVisibleTranscriptItem(history);
2596
+ const previousTranscriptItem = lastPendingHistoryItem ?? lastHistoryItem;
2597
+ const isAwaitingAssistantAfterUser = agentLoop.isRunning &&
2598
+ !hasLiveAssistantItem &&
2599
+ visibleStreamingText.trim().length === 0 &&
2600
+ (lastLiveItem?.kind === "user" || (!lastLiveItem && previousTranscriptItem?.kind === "user"));
2601
+ const shouldReserveStreamingSpacing = agentLoop.isRunning &&
2602
+ !hasLiveAssistantItem &&
2603
+ (visibleStreamingText.trim().length > 0 ||
2604
+ liveItems.some(isTranscriptSpacingItem) ||
2605
+ isAwaitingAssistantAfterUser);
2606
+ const shouldTopSpaceStreamingText = shouldTopSpaceStreamingAssistant({
2607
+ visibleStreamingText,
2608
+ lastLiveItem: lastVisibleLiveItem,
2609
+ lastPendingHistoryItem: lastVisiblePendingHistoryItem,
2610
+ lastHistoryItem: lastVisibleHistoryItem,
2611
+ });
2612
+ // When earlier paragraphs of THIS response were already flushed to scrollback
2613
+ // mid-stream, the live remainder is the next paragraph — re-insert the blank
2614
+ // line that separated them so the live tail lines up with the flushed history.
2615
+ const streamingContinuesFlushed = alreadyFlushedChars + pendingFlushChars > 0;
2616
+ // ── Fullscreen alt-screen transcript ───────────────────
2617
+ // Flatten history + live items + in-flight streaming into the flat ANSI line
2618
+ // buffer the viewport renders. Reuses the same serializer the legacy
2619
+ // scrollback printer used, so the transcript looks identical. Only computed
2620
+ // when fullscreen is active (the legacy path renders items through Ink).
2621
+ const transcriptContext = useMemo(() => ({
2622
+ theme,
2623
+ columns,
2624
+ version: props.version,
2625
+ model: currentModel,
2626
+ provider: currentProvider,
2627
+ cwd: displayedCwd,
2628
+ }), [theme, columns, props.version, currentModel, currentProvider, displayedCwd]);
2629
+ const transcriptLines = useMemo(() => {
2630
+ if (!props.fullscreen)
2631
+ return [];
2632
+ const items = [...history, ...uniqueItemsById(liveItems)];
2633
+ const hasStreaming = visibleStreamingText.length > 0 || agentLoop.streamingThinking.length > 0;
2634
+ if (hasStreaming) {
2635
+ items.push({
2636
+ kind: "assistant",
2637
+ text: visibleStreamingText,
2638
+ thinking: agentLoop.streamingThinking,
2639
+ thinkingMs: agentLoop.thinkingMs,
2640
+ continuation: streamingContinuesFlushed,
2641
+ id: "__streaming__",
2642
+ });
2643
+ }
2644
+ return buildTranscriptLines(items, transcriptContext);
2645
+ }, [
2646
+ props.fullscreen,
2647
+ history,
2648
+ liveItems,
2649
+ visibleStreamingText,
2650
+ agentLoop.streamingThinking,
2651
+ agentLoop.thinkingMs,
2652
+ streamingContinuesFlushed,
2653
+ transcriptContext,
2654
+ ]);
2655
+ // Keyboard + bounds controller. The offset itself lives in the external
2656
+ // transcript-scroll store; the viewport subscribes to it directly so scroll
2657
+ // re-renders only the viewport, not this whole component.
2658
+ useTranscriptScroll({
2659
+ totalLines: transcriptLines.length,
2660
+ viewportRows,
2661
+ active: !!props.fullscreen && !overlay && !taskBarFocused,
2662
+ resetToken: scrollResetToken,
2663
+ });
2664
+ const visibleQueuedCount = liveItems.filter((item) => item.kind === "queued").length;
2665
+ const hiddenQueuedCount = Math.max(0, agentLoop.queuedCount - visibleQueuedCount);
2666
+ const shouldTopSpaceQueueIndicator = hiddenQueuedCount > 0 &&
2667
+ shouldTopSpaceAfterPrintedAgentBoundary({
2668
+ currentKind: "queued",
2669
+ previousLiveItem: lastLiveItem,
2670
+ lastPendingHistoryItem,
2671
+ lastHistoryItem,
2672
+ });
2673
+ const handleRewindCancel = useCallback(() => setRewindCheckpoints(null), []);
2674
+ const handleRewindRestore = useCallback((id, mode) => {
2675
+ const store = props.checkpointStore;
2676
+ setRewindCheckpoints(null);
2677
+ if (!store)
2678
+ return;
2737
2679
  void (async () => {
2738
2680
  try {
2739
- await agentLoop.run(fullPrompt);
2681
+ const result = await store.restore(id, mode);
2682
+ const turnNum = id.replace(/^cp-0*/, "") || id;
2683
+ const detailParts = [];
2684
+ if (mode === "code" || mode === "both") {
2685
+ detailParts.push(`${result.filesRestored} file${result.filesRestored === 1 ? "" : "s"} restored`);
2686
+ }
2687
+ if (mode === "conversation" || mode === "both")
2688
+ detailParts.push("conversation rewound");
2689
+ const infoText = `Rewound to checkpoint #${turnNum} (${detailParts.join(", ")}).`;
2690
+ if (mode === "conversation" || mode === "both") {
2691
+ const truncated = messagesRef.current.slice(0, Math.max(1, result.messageIndex));
2692
+ messagesRef.current = truncated;
2693
+ persistedIndexRef.current = truncated.length;
2694
+ agentLoop.reset();
2695
+ // Remount in lockstep so the banner + confirmation re-render cleanly
2696
+ // after the conversation context is truncated (CLAUDE.md pattern).
2697
+ if (props.resetUI) {
2698
+ props.resetUI({
2699
+ wipeSession: true,
2700
+ messages: truncated,
2701
+ history: [
2702
+ { kind: "banner", id: "banner" },
2703
+ { kind: "info", text: infoText, id: getId() },
2704
+ ],
2705
+ });
2706
+ return;
2707
+ }
2708
+ }
2709
+ setLiveItems((prev) => [...prev, { kind: "info", text: infoText, id: getId() }]);
2740
2710
  }
2741
2711
  catch (err) {
2742
2712
  const msg = err instanceof Error ? err.message : String(err);
2743
- log("ERROR", "error", msg);
2744
- const isAbort = msg.includes("aborted") || msg.includes("abort");
2745
2713
  setLiveItems((prev) => [
2746
2714
  ...prev,
2747
- isAbort
2748
- ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2749
- : toErrorItem(err, getId()),
2715
+ { kind: "info", text: `Rewind failed: ${msg}`, id: getId() },
2750
2716
  ]);
2751
- setRunAllTasks(false);
2752
2717
  }
2753
2718
  })();
2754
- }, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
2755
- const openOverlay = useCallback((kind) => {
2719
+ }, [props.checkpointStore, props.resetUI, agentLoop, messagesRef, persistedIndexRef]);
2720
+ const handleCloseRemountableOverlay = () => {
2756
2721
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2757
- props.sessionStore.overlay = kind;
2758
- if (kind !== "plan")
2759
- props.sessionStore.planAutoExpand = false;
2722
+ props.sessionStore.overlay = null;
2760
2723
  props.resetUI();
2724
+ return;
2761
2725
  }
2762
- else {
2763
- if (props.sessionStore) {
2764
- props.sessionStore.overlay = kind;
2765
- if (kind !== "plan")
2766
- props.sessionStore.planAutoExpand = false;
2767
- if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
2768
- props.sessionStore.pendingResetUI = true;
2769
- }
2770
- }
2771
- if (kind !== "plan")
2772
- setPlanAutoExpand(false);
2773
- setOverlay(kind);
2726
+ if (props.sessionStore) {
2727
+ props.sessionStore.overlay = null;
2728
+ if (agentLoop.isRunning)
2729
+ props.sessionStore.pendingResetUI = true;
2774
2730
  }
2775
- }, [agentLoop.isRunning, props]);
2776
- const closeOverlay = useCallback(() => {
2731
+ setOverlay(null);
2732
+ };
2733
+ const handleEnterPlanMode = useCallback(async (reason) => {
2734
+ await setPlanModeAndPrompt(true);
2735
+ setLiveItems((prev) => [
2736
+ ...prev,
2737
+ { kind: "plan_transition", text: reason ?? "", id: getId(), active: true },
2738
+ ]);
2739
+ }, [setPlanModeAndPrompt]);
2740
+ const handleExitPlanMode = useCallback(async (_planPath) => {
2741
+ await setPlanModeAndPrompt(false);
2742
+ planOverlayPendingRef.current = true;
2743
+ setPlanAutoExpand(true);
2744
+ if (props.sessionStore) {
2745
+ props.sessionStore.overlay = "plan";
2746
+ props.sessionStore.planAutoExpand = true;
2747
+ }
2748
+ setOverlay("plan");
2749
+ return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
2750
+ }, [props.sessionStore, setPlanModeAndPrompt]);
2751
+ useEffect(() => {
2752
+ if (!props.planCallbacks)
2753
+ return;
2754
+ props.planCallbacks.onEnterPlan = handleEnterPlanMode;
2755
+ props.planCallbacks.onExitPlan = handleExitPlanMode;
2756
+ }, [handleEnterPlanMode, handleExitPlanMode, props.planCallbacks]);
2757
+ const handleCloseGoalOverlay = () => {
2777
2758
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2778
2759
  props.sessionStore.overlay = null;
2760
+ props.sessionStore.goalAutoExpand = false;
2779
2761
  props.resetUI();
2762
+ return;
2780
2763
  }
2781
- else {
2782
- if (props.sessionStore) {
2783
- props.sessionStore.overlay = null;
2784
- }
2785
- setOverlay(null);
2764
+ if (props.sessionStore) {
2765
+ props.sessionStore.overlay = null;
2766
+ props.sessionStore.goalAutoExpand = false;
2767
+ if (agentLoop.isRunning)
2768
+ props.sessionStore.pendingResetUI = true;
2786
2769
  }
2787
- }, [agentLoop.isRunning, overlay, props]);
2788
- const runGoalSyntheticEvent = useCallback((eventText) => {
2789
- const eventInfo = parseGoalSyntheticEvent(eventText);
2790
- const detail = eventInfo?.kind === "worker"
2791
- ? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
2792
- : `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
2793
- if (agentRunningRef.current) {
2770
+ setGoalAutoExpand(false);
2771
+ setOverlay(null);
2772
+ };
2773
+ const handleRunGoal = (run) => {
2774
+ if (goalHasBlockingPrerequisites(run)) {
2775
+ upsertGoalStatusEntry({
2776
+ runId: run.id,
2777
+ label: run.title,
2778
+ phase: "failed",
2779
+ detail: formatGoalBlockingPrerequisites(run),
2780
+ startedAt: Date.now(),
2781
+ });
2794
2782
  appendGoalProgress({
2795
2783
  kind: "goal_progress",
2796
- phase: "orchestrator_reviewing",
2797
- title: "Goal update queued for orchestrator",
2798
- detail: `${detail} It will report back after the current turn.`,
2799
- workerId: eventInfo?.worker,
2800
- status: eventInfo?.status,
2784
+ phase: "terminal",
2785
+ title: `Goal blocked: ${run.title}`,
2786
+ detail: formatGoalBlockingPrerequisites(run),
2787
+ summaryRows: buildGoalSummaryRows(run),
2788
+ status: "blocked",
2801
2789
  });
2802
- agentLoop.queueMessage(eventText);
2803
2790
  return;
2804
2791
  }
2805
- appendGoalProgress({
2806
- kind: "goal_progress",
2807
- phase: "orchestrator_reviewing",
2808
- title: "Orchestrator reviewing Goal update",
2809
- detail,
2810
- workerId: eventInfo?.worker,
2811
- status: eventInfo?.status,
2812
- });
2813
- setLastUserMessage("");
2814
- setDoneStatus(null);
2815
- void agentLoop.run(eventText).catch((err) => {
2816
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2817
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2818
- });
2819
- }, [agentLoop, appendGoalProgress]);
2820
- const continueGoalRun = useCallback((runId) => {
2821
2792
  void (async () => {
2822
- const latestRun = await reconcileActiveGoalRuns(props.cwd, {
2823
- isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
2824
- }).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
2825
- if (!latestRun) {
2826
- runningGoalIdsRef.current.delete(runId);
2827
- clearGoalStatusEntry(runId);
2828
- return;
2829
- }
2830
- const decision = decideGoalNextAction(latestRun);
2831
- if (decision.kind === "wait")
2832
- return;
2833
- if (decision.kind === "terminal" ||
2834
- decision.kind === "blocked" ||
2835
- decision.kind === "pause") {
2836
- const status = decision.kind === "terminal"
2837
- ? decision.status
2838
- : decision.kind === "blocked"
2839
- ? "blocked"
2840
- : "paused";
2841
- const nextRun = {
2842
- ...latestRun,
2843
- status,
2844
- continueRequestedAt: undefined,
2845
- blockers: decision.kind === "blocked" || decision.kind === "pause"
2846
- ? Array.from(new Set([...latestRun.blockers, decision.reason]))
2847
- : latestRun.blockers,
2848
- };
2849
- await upsertGoalRun(props.cwd, nextRun);
2850
- await appendGoalDecision(props.cwd, latestRun.id, {
2851
- kind: "continuation_stopped",
2852
- reason: decision.reason,
2853
- content: `terminal=${status}`,
2854
- });
2855
- const terminalProgress = formatGoalTerminalProgress(nextRun);
2856
- if (terminalProgress)
2857
- appendGoalProgress(terminalProgress);
2858
- runningGoalIdsRef.current.delete(runId);
2859
- clearGoalStatusEntry(runId);
2860
- return;
2861
- }
2862
- let runForNextAction = latestRun;
2863
- if (latestRun.continueRequestedAt &&
2864
- !listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
2865
- activeVerifierRunIdsRef.current.size === 0) {
2866
- await appendGoalDecision(props.cwd, latestRun.id, {
2867
- kind: "continuation_consumed",
2868
- reason: `Continuation request consumed by ${decision.kind}.`,
2869
- });
2870
- runForNextAction = await upsertGoalRun(props.cwd, {
2871
- ...latestRun,
2872
- continueRequestedAt: undefined,
2873
- });
2874
- }
2875
- appendGoalProgress({
2876
- kind: "goal_progress",
2877
- phase: "continuing",
2878
- title: `Choosing next Goal step: ${latestRun.title}`,
2879
- detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
2880
- status: latestRun.status,
2881
- });
2882
- upsertGoalStatusEntry({
2883
- runId: latestRun.id,
2884
- label: latestRun.title,
2885
- phase: "orchestrating",
2886
- startedAt: Date.now(),
2887
- detail: "choosing next step",
2888
- });
2889
- startGoalRunRef.current(runForNextAction);
2890
- })().catch((err) => {
2891
- runningGoalIdsRef.current.delete(runId);
2892
- clearGoalStatusEntry(runId);
2893
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2894
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2895
- });
2896
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
2897
- const handleGoalWorkerComplete = useCallback((run, completion) => {
2898
- const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
2899
- completion.worker.goalTaskId;
2900
- const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
2901
- void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
2902
- appendGoalProgress({
2903
- kind: "goal_progress",
2904
- phase: "worker_finished",
2905
- title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
2906
- detail: summarizeGoalCompletion(completion.summary),
2907
- workerId: completion.worker.id,
2908
- status: completion.status,
2909
- });
2910
- upsertGoalStatusEntry({
2911
- runId: run.id,
2912
- label: taskTitle,
2913
- phase: completion.status === "done" ? "reviewing" : "failed",
2914
- startedAt: Date.now(),
2915
- detail: completion.status === "done" ? "reviewing result" : "task failed",
2916
- workerId: completion.worker.id,
2917
- goalNumber: goalNumberForRun(run.id),
2918
- });
2919
- runGoalSyntheticEvent(eventText);
2920
- void (async () => {
2921
- if (listGoalWorkers(completion.worker.cwd).some((worker) => worker.status === "running"))
2922
- return;
2923
- if (activeVerifierRunIdsRef.current.size > 0)
2924
- return;
2925
- const runs = await loadGoalRuns(completion.worker.cwd);
2926
- const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
2927
- if (queued)
2928
- setTimeout(() => continueGoalRun(queued.id), 750);
2929
- })().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
2930
- }, [
2931
- appendGoalProgress,
2932
- continueGoalRun,
2933
- goalNumberForRun,
2934
- runGoalSyntheticEvent,
2935
- upsertGoalStatusEntry,
2936
- ]);
2937
- useEffect(() => {
2938
- return subscribeGoalWorkerCompletions((completion) => {
2939
- void (async () => {
2940
- const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
2941
- if (!latestRun) {
2942
- log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
2943
- return;
2944
- }
2945
- runningGoalIdsRef.current.add(latestRun.id);
2946
- handleGoalWorkerComplete(latestRun, completion);
2947
- })().catch((err) => {
2948
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2949
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2950
- });
2951
- }, props.cwd);
2952
- }, [handleGoalWorkerComplete, props.cwd]);
2953
- const startGoalRun = useCallback((run) => {
2954
- runningGoalIdsRef.current.add(run.id);
2955
- void (async () => {
2956
- if (goalHasBlockingPrerequisites(run)) {
2957
- setOverlay(null);
2958
- const detail = formatGoalBlockingPrerequisites(run);
2959
- await upsertGoalRun(props.cwd, {
2960
- ...run,
2961
- status: "blocked",
2962
- blockers: Array.from(new Set([...run.blockers, detail])),
2963
- });
2964
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2965
- appendGoalProgress({
2966
- kind: "goal_progress",
2967
- phase: "terminal",
2968
- title: `Goal blocked: ${run.title}`,
2969
- detail,
2970
- status: "blocked",
2971
- });
2972
- runningGoalIdsRef.current.delete(run.id);
2973
- clearGoalStatusEntry(run.id);
2974
- return;
2975
- }
2976
- const decision = decideGoalNextAction(run);
2977
- await appendGoalDecision(props.cwd, run.id, decision);
2978
- if (decision.kind === "terminal") {
2979
- const terminalProgress = formatGoalTerminalProgress(run);
2980
- if (terminalProgress)
2981
- appendGoalProgress(terminalProgress);
2982
- runningGoalIdsRef.current.delete(run.id);
2983
- clearGoalStatusEntry(run.id);
2984
- return;
2985
- }
2986
- if (decision.kind === "wait") {
2987
- appendGoalProgress({
2988
- kind: "goal_progress",
2989
- phase: "worker_started",
2990
- title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
2991
- detail: decision.reason,
2992
- workerId: decision.workerId,
2993
- });
2793
+ const checked = await runGoalPrerequisiteChecks(props.cwd, run);
2794
+ const current = await upsertGoalRun(props.cwd, checked.run);
2795
+ if (goalHasBlockingPrerequisites(current)) {
2994
2796
  upsertGoalStatusEntry({
2995
- runId: run.id,
2996
- label: run.title,
2997
- phase: decision.workerId ? "worker" : "orchestrating",
2797
+ runId: current.id,
2798
+ label: current.title,
2799
+ phase: "failed",
2800
+ detail: formatGoalBlockingPrerequisites(current),
2998
2801
  startedAt: Date.now(),
2999
- detail: decision.reason,
3000
- workerId: decision.workerId,
3001
- goalNumber: goalNumberForRun(run.id),
3002
- });
3003
- return;
3004
- }
3005
- if (decision.kind === "complete") {
3006
- await upsertGoalRun(props.cwd, { ...run, status: "passed" });
3007
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3008
- appendGoalProgress({
3009
- kind: "goal_progress",
3010
- phase: "terminal",
3011
- title: `Goal passed: ${run.title}`,
3012
- detail: decision.reason,
3013
- status: "passed",
3014
- });
3015
- runningGoalIdsRef.current.delete(run.id);
3016
- clearGoalStatusEntry(run.id);
3017
- return;
3018
- }
3019
- if (decision.kind === "run_verifier") {
3020
- await verifyGoalRun(run);
3021
- return;
3022
- }
3023
- if (decision.kind === "create_task") {
3024
- await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
3025
- title: decision.title,
3026
- prompt: decision.prompt,
3027
- status: "pending",
3028
- });
3029
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3030
- await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
3031
- setTimeout(() => continueGoalRun(run.id), 250);
3032
- return;
3033
- }
3034
- if (decision.kind === "blocked") {
3035
- await upsertGoalRun(props.cwd, {
3036
- ...run,
3037
- status: "blocked",
3038
- blockers: [...run.blockers, decision.reason],
3039
2802
  });
3040
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3041
2803
  appendGoalProgress({
3042
2804
  kind: "goal_progress",
3043
2805
  phase: "terminal",
3044
- title: `Goal blocked: ${run.title}`,
3045
- detail: decision.reason,
2806
+ title: `Goal blocked: ${current.title}`,
2807
+ detail: formatGoalBlockingPrerequisites(current),
2808
+ summaryRows: buildGoalSummaryRows(current),
3046
2809
  status: "blocked",
3047
2810
  });
3048
- runningGoalIdsRef.current.delete(run.id);
3049
- clearGoalStatusEntry(run.id);
3050
- return;
3051
- }
3052
- if (decision.kind === "pause") {
3053
- await updateGoalTask(props.cwd, run.id, decision.task.id, {
3054
- status: "blocked",
3055
- attempts: decision.attempts,
3056
- lastSummary: "Paused after worker attempt limit.",
3057
- });
3058
- await upsertGoalRun(props.cwd, {
3059
- ...run,
3060
- status: "paused",
3061
- blockers: [...run.blockers, decision.reason],
3062
- });
3063
- await appendGoalEvidence(props.cwd, run.id, {
3064
- kind: "summary",
3065
- label: "Goal paused",
3066
- content: decision.reason,
3067
- });
3068
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3069
- appendGoalProgress({
3070
- kind: "goal_progress",
3071
- phase: "terminal",
3072
- title: `Goal paused: ${run.title}`,
3073
- detail: decision.reason,
3074
- status: "paused",
3075
- });
3076
- runningGoalIdsRef.current.delete(run.id);
3077
- clearGoalStatusEntry(run.id);
3078
2811
  return;
3079
2812
  }
3080
- await updateGoalTask(props.cwd, run.id, decision.task.id, { attempts: decision.attempts });
3081
- const worker = await startGoalWorker({
3082
- cwd: props.cwd,
3083
- provider: currentProvider,
3084
- model: currentModel,
3085
- goalRunId: run.id,
3086
- goalTaskId: decision.task.id,
3087
- taskTitle: decision.task.title,
3088
- prompt: decision.task.prompt,
2813
+ const resumed = await upsertGoalRun(props.cwd, {
2814
+ ...current,
2815
+ status: current.status === "draft" ? "ready" : current.status,
2816
+ continueRequestedAt: new Date().toISOString(),
3089
2817
  });
3090
- await upsertGoalRun(props.cwd, {
3091
- ...run,
3092
- status: "running",
3093
- activeWorkerId: worker.id,
3094
- continueRequestedAt: undefined,
2818
+ setGoalAutoExpand(true);
2819
+ upsertGoalStatusEntry({
2820
+ runId: resumed.id,
2821
+ label: resumed.title,
2822
+ phase: "orchestrating",
2823
+ detail: "Choosing next Goal action",
2824
+ startedAt: Date.now(),
3095
2825
  });
3096
- setOverlay(null);
3097
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3098
2826
  appendGoalProgress({
3099
2827
  kind: "goal_progress",
3100
- phase: "worker_started",
3101
- title: `Worker started: ${decision.task.title}`,
3102
- detail: "Task is running in the background.",
3103
- workerId: worker.id,
3104
- status: worker.status,
3105
- });
3106
- upsertGoalStatusEntry({
3107
- runId: run.id,
3108
- label: decision.task.title,
3109
- phase: "worker",
3110
- startedAt: Date.now(),
3111
- detail: "background worker running",
3112
- workerId: worker.id,
3113
- goalNumber: goalNumberForRun(run.id),
2828
+ phase: "orchestrator_reviewing",
2829
+ title: `Goal ${goalNumberForRun(resumed.id)} continuing`,
2830
+ detail: resumed.title,
2831
+ status: resumed.status,
3114
2832
  });
3115
- })().catch((err) => {
3116
- clearGoalStatusEntry(run.id);
3117
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3118
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3119
- });
3120
- }, [
3121
- props.cwd,
3122
- currentProvider,
3123
- currentModel,
3124
- appendGoalProgress,
3125
- clearGoalStatusEntry,
3126
- goalNumberForRun,
3127
- upsertGoalStatusEntry,
3128
- ]);
3129
- const verifyGoalRun = useCallback(async (run) => {
2833
+ continueGoalRun(resumed);
2834
+ })();
2835
+ };
2836
+ startGoalRunRef.current = handleRunGoal;
2837
+ const handleVerifyGoal = (run) => {
3130
2838
  if (!run.verifier?.command) {
3131
- await appendGoalEvidence(props.cwd, run.id, {
3132
- kind: "summary",
3133
- label: "Missing verifier",
3134
- content: "No verifier command is configured.",
3135
- });
3136
- await upsertGoalRun(props.cwd, {
3137
- ...run,
3138
- status: "blocked",
3139
- blockers: [...run.blockers, "No verifier command configured."],
3140
- });
3141
2839
  appendGoalProgress({
3142
2840
  kind: "goal_progress",
3143
2841
  phase: "terminal",
3144
- title: `Goal blocked: ${run.title}`,
3145
- detail: "No verifier command is configured.",
2842
+ title: `Goal verifier missing: ${run.title}`,
2843
+ detail: "Define a verifier command before running verification.",
3146
2844
  status: "blocked",
3147
2845
  });
3148
- runningGoalIdsRef.current.delete(run.id);
3149
- clearGoalStatusEntry(run.id);
3150
2846
  return;
3151
2847
  }
3152
- activeVerifierRunIdsRef.current.add(run.id);
3153
- await upsertGoalRun(props.cwd, {
3154
- ...run,
3155
- status: "verifying",
3156
- continueRequestedAt: undefined,
3157
- });
3158
- appendGoalProgress({
3159
- kind: "goal_progress",
3160
- phase: "verifier_started",
3161
- title: `Verifier started: ${run.title}`,
3162
- detail: run.verifier.command,
3163
- status: "verifying",
3164
- });
3165
- const startedAt = Date.now();
3166
- const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
3167
- upsertGoalStatusEntry({
3168
- runId: run.id,
3169
- label: run.title,
3170
- phase: "verifier",
3171
- startedAt,
3172
- detail: run.verifier.command,
3173
- goalNumber: goalNumberForRun(run.id),
3174
- });
3175
- void runGoalVerifierCommand({
3176
- cwd: props.cwd,
3177
- runId: run.id,
3178
- command: run.verifier.command,
3179
- timeoutMs: verifierTimeoutMs,
3180
- now: () => startedAt,
3181
- })
3182
- .then(async ({ verification, failureClass, durationMs }) => {
3183
- activeVerifierRunIdsRef.current.delete(run.id);
3184
- const status = verification.status;
3185
- const summary = verification.summary;
3186
- const outputPath = verification.outputPath;
3187
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3188
- const runWithVerifier = {
3189
- ...latestRun,
2848
+ void (async () => {
2849
+ const verifying = await upsertGoalRun(props.cwd, {
2850
+ ...run,
2851
+ status: "ready",
2852
+ tasks: run.tasks.map((task) => task.status === "pending" || task.status === "failed"
2853
+ ? { ...task, status: "done" }
2854
+ : task),
3190
2855
  verifier: {
3191
- ...latestRun.verifier,
3192
- description: latestRun.verifier?.description ?? "Goal verifier",
3193
- command: run.verifier?.command,
3194
- lastResult: verification,
2856
+ ...run.verifier,
2857
+ lastResult: undefined,
3195
2858
  },
3196
- };
3197
- const completionCheck = canCompleteGoalRun(runWithVerifier);
3198
- const verifiedRun = await upsertGoalRun(props.cwd, {
3199
- ...runWithVerifier,
3200
- continueRequestedAt: undefined,
3201
- status: status === "pass" && completionCheck.ok
3202
- ? "passed"
3203
- : status === "pass"
3204
- ? "ready"
3205
- : "failed",
3206
- });
3207
- await appendGoalEvidence(props.cwd, run.id, {
3208
- kind: "command",
3209
- label: `Verifier ${status}`,
3210
- content: `${failureClass}: ${summary}`.slice(0, 4000),
3211
- path: outputPath,
3212
- });
3213
- await appendGoalDecision(props.cwd, run.id, {
3214
- kind: `verifier_${status}`,
3215
- reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
3216
- content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
3217
2859
  });
3218
- if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
3219
- await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
3220
- title: "Fix verifier failure",
3221
- prompt: `Goal verifier failed after ${durationMs}ms. Original goal: ${run.goal}\n\n` +
3222
- `Verifier command: ${run.verifier?.command}\n\n` +
3223
- `Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
3224
- status: "pending",
3225
- });
3226
- const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
3227
- await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
2860
+ continueGoalRun(verifying);
2861
+ })();
2862
+ };
2863
+ const handlePauseGoal = (run) => {
2864
+ void (async () => {
2865
+ for (const worker of listGoalWorkers(props.cwd).filter((item) => item.goalRunId === run.id)) {
2866
+ await stopGoalWorker(worker.id);
3228
2867
  }
3229
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3230
- appendGoalProgress({
3231
- kind: "goal_progress",
3232
- phase: "verifier_finished",
3233
- title: `Verifier ${status}: ${run.title}`,
3234
- detail: summarizeGoalCompletion(summary),
3235
- status,
3236
- });
3237
- upsertGoalStatusEntry({
3238
- runId: run.id,
3239
- label: run.title,
3240
- phase: status === "pass" ? "reviewing" : "failed",
3241
- startedAt: Date.now(),
3242
- detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3243
- goalNumber: goalNumberForRun(run.id),
2868
+ const paused = await upsertGoalRun(props.cwd, {
2869
+ ...run,
2870
+ status: "paused",
2871
+ blockers: [...run.blockers, "Paused by user from Goal pane."],
3244
2872
  });
3245
- const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
3246
- runGoalSyntheticEvent(eventText);
3247
- const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3248
- if (continuationRun?.continueRequestedAt && status === "pass") {
3249
- setTimeout(() => continueGoalRun(run.id), 500);
3250
- }
3251
- })
3252
- .catch((err) => {
3253
- activeVerifierRunIdsRef.current.delete(run.id);
3254
2873
  clearGoalStatusEntry(run.id);
3255
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3256
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3257
- });
3258
- }, [
3259
- props.cwd,
3260
- appendGoalProgress,
3261
- clearGoalStatusEntry,
3262
- goalNumberForRun,
3263
- runGoalSyntheticEvent,
3264
- upsertGoalStatusEntry,
3265
- ]);
3266
- const pauseGoalRun = useCallback((run) => {
3267
- void (async () => {
3268
- runningGoalIdsRef.current.delete(run.id);
3269
- if (run.activeWorkerId)
3270
- await stopGoalWorker(run.activeWorkerId);
3271
- await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
3272
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3273
2874
  appendGoalProgress({
3274
2875
  kind: "goal_progress",
3275
2876
  phase: "terminal",
3276
- title: `Goal paused: ${run.title}`,
3277
- detail: "Auto-continuation stopped until resumed.",
2877
+ title: `Goal paused: ${paused.title}`,
2878
+ detail: "Paused by user from Goal pane.",
2879
+ summaryRows: buildGoalSummaryRows(paused),
3278
2880
  status: "paused",
3279
2881
  });
3280
- clearGoalStatusEntry(run.id);
3281
- })().catch((err) => {
3282
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3283
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3284
- });
3285
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
3286
- // Keep refs in sync for access from stale closures (onDone)
3287
- startTaskRef.current = startTask;
3288
- startGoalRunRef.current = startGoalRun;
3289
- useEffect(() => {
3290
- runAllTasksRef.current = runAllTasks;
2882
+ })();
2883
+ };
2884
+ const handleRefineGoal = (run, feedback) => {
2885
+ setOverlay(null);
2886
+ setGoalAutoExpand(false);
2887
+ setDoneStatus(null);
2888
+ appendGoalAgentTransition(`Goal refinement requested for ${run.title}`);
2889
+ void agentLoop
2890
+ .run(`Refine Goal run ${run.id} (${run.title}) using this user feedback:\n\n${feedback}\n\nUpdate the durable Goal plan with the goals tool. Do not start workers until the user approves/runs the Goal.`)
2891
+ .catch((err) => setLiveItems((prev) => [...prev, toErrorItem(err, getId())]));
2892
+ };
2893
+ const handleClosePlanOverlay = () => {
2894
+ planOverlayPendingRef.current = false;
2895
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2896
+ props.sessionStore.overlay = null;
2897
+ props.sessionStore.planAutoExpand = false;
2898
+ props.resetUI();
2899
+ return;
2900
+ }
3291
2901
  if (props.sessionStore) {
3292
- props.sessionStore.runAllTasks = runAllTasks;
3293
- // Keep currentTaskId in sync with the ref so a sessionStore-seeded
3294
- // remount picks up the same value the imperative sites wrote.
3295
- props.sessionStore.currentTaskId = currentTaskIdRef.current;
2902
+ props.sessionStore.overlay = null;
2903
+ props.sessionStore.planAutoExpand = false;
2904
+ if (agentLoop.isRunning)
2905
+ props.sessionStore.pendingResetUI = true;
3296
2906
  }
3297
- }, [runAllTasks, props.sessionStore]);
3298
- useEffect(() => {
3299
- agentRunningRef.current = agentLoop.isRunning;
3300
- }, [agentLoop.isRunning]);
3301
- const startPixelFix = useCallback((errorId) => {
2907
+ setPlanAutoExpand(false);
2908
+ setOverlay(null);
2909
+ };
2910
+ const handlePixelFixOne = (entry) => {
2911
+ setOverlay(null);
2912
+ startPixelFix(entry.errorId);
2913
+ };
2914
+ const handlePixelFixAll = (entries) => {
2915
+ const first = entries.find((entry) => entry.status === "open") ?? entries[0];
2916
+ if (!first)
2917
+ return;
2918
+ setOverlay(null);
2919
+ setRunAllPixel(true);
2920
+ startPixelFix(first.errorId);
2921
+ };
2922
+ const handleApprovePlan = (planPath) => {
2923
+ log("INFO", "plan", "Plan approved — transitioning to implementation", {
2924
+ planPath,
2925
+ });
2926
+ planOverlayPendingRef.current = false;
3302
2927
  void (async () => {
3303
2928
  try {
3304
- const { preparePixelFix } = await import("../core/pixel-fix.js");
3305
- const prep = await preparePixelFix(errorId);
3306
- currentPixelFixRef.current = prep;
3307
- // Move the agent into the error's project root. Four things must
3308
- // change in lockstep, otherwise the agent (or the chrome around
3309
- // it) shows the wrong project:
3310
- // 1. process.cwd — for any code reading it directly
3311
- // 2. cwd-bound tools (read/write/bash/grep/…) — baked at creation
3312
- // 3. the system prompt's "Working directory: …" line the only
3313
- // place the model itself learns where it is
3314
- // 4. displayedCwd state — Banner + Footer read this for display
3315
- try {
3316
- process.chdir(prep.projectPath);
3317
- }
3318
- catch (err) {
3319
- log("WARN", "pixel", `chdir failed: ${err.message}`);
2929
+ // Read plan steps for progress tracking — handed to the new
2930
+ // mount via sessionStore.planSteps below.
2931
+ const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
2932
+ const steps = extractPlanSteps(planContent);
2933
+ // Build the new system prompt with the approved plan baked in.
2934
+ const newPrompt = await rebuildSystemPrompt({
2935
+ approvedPlanPath: planPath,
2936
+ });
2937
+ // Create a new session file BEFORE remount so the new tree
2938
+ // picks it up via sessionStore.sessionPath.
2939
+ let newSessionPath;
2940
+ const sm = sessionManagerRef.current;
2941
+ if (sm) {
2942
+ const s = await sm.create(props.cwd, currentProvider, currentModel);
2943
+ newSessionPath = s.path;
3320
2944
  }
3321
- cwdRef.current = prep.projectPath;
3322
- repoMapDirtyRef.current = true;
3323
- repoMapMarkdownRef.current = "";
3324
- repoMapSnapshotRef.current = undefined;
3325
- repoMapChangedCountRef.current = 0;
3326
- repoMapCacheRef.current = createRepoMapCache();
3327
- props.repoMapChangedFilesRef?.current.clear();
3328
- props.repoMapReadFilesRef?.current.clear();
3329
- setDisplayedCwd(prep.projectPath);
3330
- let toolsForPixelFix = currentToolsRef.current;
3331
- if (props.rebuildToolsForCwd) {
3332
- toolsForPixelFix = props.rebuildToolsForCwd(prep.projectPath);
3333
- currentToolsRef.current = toolsForPixelFix;
3334
- setCurrentTools(toolsForPixelFix);
2945
+ if (props.resetUI && props.sessionStore) {
2946
+ // Clear the overlay so the new mount lands on the chat,
2947
+ // not back inside the plan pane.
2948
+ props.sessionStore.overlay = null;
2949
+ props.sessionStore.planAutoExpand = false;
2950
+ props.resetUI({
2951
+ wipeSession: true,
2952
+ messages: [{ role: "system", content: newPrompt }],
2953
+ approvedPlanPath: planPath,
2954
+ planSteps: steps,
2955
+ sessionPath: newSessionPath,
2956
+ pendingAction: {
2957
+ prompt: "The plan has been approved. Implement it now, following each step in order.",
2958
+ planEvent: { event: "approved" },
2959
+ },
2960
+ });
2961
+ return;
3335
2962
  }
3336
- // Pixel-fix swaps the project rootreset injected packs so the
3337
- // new project re-detects from scratch on the next tool call. Also
3338
- // reset the setup-hint flag so the new project's first badge re-
3339
- // surfaces the tip (different project, may need the reminder).
3340
- injectedLanguagesRef.current = new Set();
3341
- setupHintShownRef.current = false;
3342
- const detectedForPixelFix = detectLanguages(prep.projectPath);
3343
- injectedLanguagesRef.current = detectedForPixelFix;
3344
- const newSystemPrompt = await rebuildSystemPrompt({
3345
- cwd: prep.projectPath,
3346
- planMode: false,
3347
- clearApprovedPlan: true,
3348
- activeLanguages: detectedForPixelFix,
3349
- tools: toolsForPixelFix,
3350
- });
3351
- // Now that the cwd swap is committed, reset chat. Do not clear the
3352
- // terminal here; terminal clear sequences can erase saved scrollback.
2963
+ // Fallback path (resetUI not wiredtests). Mutate in place.
2964
+ approvedPlanPathRef.current = planPath;
2965
+ planStepsRef.current = steps;
2966
+ setPlanSteps(steps);
2967
+ clearPendingHistory();
3353
2968
  setHistory([{ kind: "banner", id: "banner" }]);
3354
2969
  setLiveItems([]);
3355
- messagesRef.current = messagesRef.current.slice(0, 1);
2970
+ setPlanAutoExpand(false);
2971
+ setOverlay(null);
2972
+ messagesRef.current = [{ role: "system", content: newPrompt }];
3356
2973
  agentLoop.reset();
3357
2974
  persistedIndexRef.current = messagesRef.current.length;
3358
- const sm = sessionManagerRef.current;
3359
- if (sm) {
3360
- void sm.create(prep.projectPath, currentProvider, currentModel).then((s) => {
3361
- sessionPathRef.current = s.path;
3362
- log("INFO", "pixel", "New session for pixel fix", { path: s.path });
3363
- });
3364
- }
3365
- if (messagesRef.current[0]?.role === "system") {
3366
- messagesRef.current[0] = { role: "system", content: newSystemPrompt };
3367
- }
3368
- else {
3369
- messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
3370
- }
3371
- const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
3372
- const taskItem = { kind: "task", title, id: getId() };
3373
- setLastUserMessage(title);
2975
+ if (newSessionPath)
2976
+ sessionPathRef.current = newSessionPath;
2977
+ setLiveItems([
2978
+ {
2979
+ kind: "info",
2980
+ text: "Plan approved — starting fresh session for implementation",
2981
+ id: getId(),
2982
+ },
2983
+ ]);
3374
2984
  setDoneStatus(null);
3375
- setLiveItems([taskItem]);
3376
- await agentLoop.run(prep.prompt);
2985
+ await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3377
2986
  }
3378
2987
  catch (err) {
3379
- const msg = err instanceof Error ? err.message : String(err);
3380
- log("ERROR", "pixel", msg);
3381
- currentPixelFixRef.current = null;
3382
- setRunAllPixel(false);
2988
+ const errMsg = err instanceof Error ? err.message : String(err);
2989
+ log("ERROR", "error", errMsg);
3383
2990
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3384
2991
  }
3385
2992
  })();
3386
- }, [props.cwd, agentLoop, currentProvider, currentModel]);
3387
- startPixelFixRef.current = startPixelFix;
3388
- // Seed from sessionStore so "Fix All" chaining survives a deferred
3389
- // resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
3390
- const [runAllPixel, setRunAllPixel] = useState(props.sessionStore?.runAllPixel ?? false);
3391
- useEffect(() => {
3392
- runAllPixelRef.current = runAllPixel;
3393
- if (props.sessionStore)
3394
- props.sessionStore.runAllPixel = runAllPixel;
3395
- }, [runAllPixel, props.sessionStore]);
3396
- const isTaskView = overlay === "tasks";
3397
- const isGoalView = overlay === "goal";
3398
- const isSkillsView = overlay === "skills";
3399
- const isPlanView = overlay === "plan";
3400
- const isEyesView = overlay === "eyes";
3401
- const footerStatusLayout = getFooterStatusLayoutDecision({
3402
- columns,
3403
- backgroundTaskCount: bgTasks.length,
3404
- eyesCount,
3405
- updatePending,
3406
- });
3407
- const isPixelView = overlay === "pixel";
3408
- const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
3409
- const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
3410
- const stabilizeOverlayPaneRerender = shouldStabilizeOverlayPaneRerender({
3411
- overlayPane: overlay,
3412
- isAgentRunning: agentLoop.isRunning,
3413
- });
3414
- const staticItems = shouldHideStaticItemsForOverlayView({
3415
- shouldHideHistoryForOverlay,
3416
- stabilizeOverlayPaneRerender,
3417
- })
3418
- ? []
3419
- : history;
3420
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3421
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3422
- props.sessionStore.overlay = null;
3423
- props.resetUI();
3424
- }
3425
- else {
3426
- if (props.sessionStore) {
3427
- props.sessionStore.overlay = null;
3428
- if (agentLoop.isRunning)
3429
- props.sessionStore.pendingResetUI = true;
3430
- }
3431
- setTaskCount(getTaskCount(props.cwd));
3432
- setOverlay(null);
3433
- }
3434
- }, onWorkOnTask: (title, prompt, taskId) => {
3435
- setOverlay(null);
3436
- startTask(title, prompt, taskId);
3437
- }, onRunAllTasks: () => {
3438
- setOverlay(null);
3439
- setRunAllTasks(true);
3440
- const next = getNextPendingTask(props.cwd);
3441
- if (next) {
3442
- markTaskInProgress(props.cwd, next.id);
3443
- startTask(next.title, next.prompt, next.id);
3444
- }
3445
- } })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3446
- void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
3447
- closeOverlay();
3448
- }, onRunGoal: (run) => {
3449
- setOverlay(null);
3450
- startGoalRun(run);
3451
- }, onVerifyGoal: (run) => {
3452
- void verifyGoalRun(run);
3453
- }, onPauseGoal: (run) => {
3454
- pauseGoalRun(run);
3455
- } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
3456
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3457
- props.sessionStore.overlay = null;
3458
- props.resetUI();
3459
- }
3460
- else {
3461
- if (props.sessionStore) {
3462
- props.sessionStore.overlay = null;
3463
- if (agentLoop.isRunning)
3464
- props.sessionStore.pendingResetUI = true;
3465
- }
3466
- setOverlay(null);
3467
- }
3468
- }, onFixOne: (entry) => {
3469
- setOverlay(null);
3470
- startPixelFix(entry.errorId);
3471
- }, onFixAll: (entries) => {
3472
- const first = entries.find((e) => e.status === "open") ?? entries[0];
3473
- if (!first)
3474
- return;
3475
- setOverlay(null);
3476
- setRunAllPixel(true);
3477
- startPixelFix(first.errorId);
3478
- } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
3479
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3480
- props.sessionStore.overlay = null;
3481
- props.resetUI();
3482
- }
3483
- else {
3484
- if (props.sessionStore) {
3485
- props.sessionStore.overlay = null;
3486
- if (agentLoop.isRunning)
3487
- props.sessionStore.pendingResetUI = true;
3488
- }
3489
- setOverlay(null);
3490
- }
3491
- } })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
3492
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3493
- props.sessionStore.overlay = null;
3494
- props.resetUI();
3495
- }
3496
- else {
3497
- if (props.sessionStore) {
3498
- props.sessionStore.overlay = null;
3499
- if (agentLoop.isRunning)
3500
- props.sessionStore.pendingResetUI = true;
3501
- }
3502
- setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
3503
- setOverlay(null);
3504
- }
3505
- }, onQueueMessage: (msg) => {
3506
- agentLoop.queueMessage(msg);
3507
- } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
3508
- planOverlayPendingRef.current = false;
3509
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3510
- props.sessionStore.overlay = null;
3511
- props.sessionStore.planAutoExpand = false;
3512
- props.resetUI();
3513
- }
3514
- else {
3515
- if (props.sessionStore) {
3516
- props.sessionStore.overlay = null;
3517
- props.sessionStore.planAutoExpand = false;
3518
- if (agentLoop.isRunning)
3519
- props.sessionStore.pendingResetUI = true;
3520
- }
3521
- setPlanAutoExpand(false);
3522
- setOverlay(null);
3523
- }
3524
- }, onApprove: (planPath) => {
3525
- log("INFO", "plan", "Plan approved — transitioning to implementation", {
3526
- planPath,
3527
- });
3528
- planOverlayPendingRef.current = false;
3529
- void (async () => {
3530
- try {
3531
- // Read plan steps for progress tracking — handed to the new
3532
- // mount via sessionStore.planSteps below.
3533
- const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
3534
- const steps = extractPlanSteps(planContent);
3535
- // Build the new system prompt with the approved plan baked in.
3536
- const newPrompt = await rebuildSystemPrompt({
3537
- planMode: false,
3538
- approvedPlanPath: planPath,
3539
- });
3540
- // Create a new session file BEFORE remount so the new tree
3541
- // picks it up via sessionStore.sessionPath.
3542
- let newSessionPath;
3543
- const sm = sessionManagerRef.current;
3544
- if (sm) {
3545
- const s = await sm.create(props.cwd, currentProvider, currentModel);
3546
- newSessionPath = s.path;
3547
- }
3548
- if (props.resetUI && props.sessionStore) {
3549
- // Clear the overlay so the new mount lands on the chat,
3550
- // not back inside the plan pane.
3551
- props.sessionStore.overlay = null;
3552
- props.sessionStore.planAutoExpand = false;
3553
- props.resetUI({
3554
- wipeSession: true,
3555
- messages: [{ role: "system", content: newPrompt }],
3556
- approvedPlanPath: planPath,
3557
- planSteps: steps,
3558
- sessionPath: newSessionPath,
3559
- pendingAction: {
3560
- prompt: "The plan has been approved. Implement it now, following each step in order.",
3561
- planEvent: { event: "approved" },
3562
- },
3563
- });
3564
- return;
3565
- }
3566
- // Fallback path (resetUI not wired — tests). Mutate in place.
3567
- approvedPlanPathRef.current = planPath;
3568
- planStepsRef.current = steps;
3569
- setPlanSteps(steps);
3570
- setHistory([{ kind: "banner", id: "banner" }]);
3571
- setLiveItems([]);
3572
- setPlanAutoExpand(false);
3573
- setOverlay(null);
3574
- messagesRef.current = [{ role: "system", content: newPrompt }];
3575
- agentLoop.reset();
3576
- persistedIndexRef.current = messagesRef.current.length;
3577
- if (newSessionPath)
3578
- sessionPathRef.current = newSessionPath;
3579
- setLiveItems([
3580
- {
3581
- kind: "info",
3582
- text: "Plan approved — starting fresh session for implementation",
3583
- id: getId(),
3584
- },
3585
- ]);
3586
- setDoneStatus(null);
3587
- await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3588
- }
3589
- catch (err) {
3590
- const errMsg = err instanceof Error ? err.message : String(err);
3591
- log("ERROR", "error", errMsg);
3592
- setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3593
- }
3594
- })();
3595
- }, onReject: (planPath, feedback) => {
3596
- planOverlayPendingRef.current = false;
3597
- const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
3598
- `Please revise the plan based on this feedback.`;
3599
- if (props.resetUI && props.sessionStore) {
3600
- props.sessionStore.overlay = null;
3601
- props.sessionStore.planAutoExpand = false;
3602
- // No wipeSession — keep history, messages, plan mode etc. The
3603
- // agent picks up the rejection mid-conversation.
3604
- props.resetUI({
3605
- pendingAction: {
3606
- prompt: rejectionMsg,
3607
- planEvent: { event: "rejected", detail: feedback },
3608
- },
3609
- });
3610
- return;
3611
- }
3612
- setPlanAutoExpand(false);
3613
- setOverlay(null);
3614
- setDoneStatus(null);
3615
- setLiveItems((prev) => [
3616
- ...prev,
3617
- { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3618
- ]);
3619
- void agentLoop.run(rejectionMsg).catch((err) => {
3620
- const errMsg = err instanceof Error ? err.message : String(err);
3621
- log("ERROR", "error", errMsg);
3622
- setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3623
- });
3624
- } })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 1, children: [liveItems.map((item) => renderItem(item)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: agentLoop.streamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking" ? THINKING_BORDER_COLORS[0] : "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, thinkingEnabled: thinkingEnabled, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
3625
- !agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
3626
- // Just flip the overlay state — Ink's log-update handles the
3627
- // live-area transition (chat input → TaskOverlay) natively, and
3628
- // the chat history above stays in scrollback. When the overlay
3629
- // closes, the history is still there (banner included).
3630
- openOverlay("tasks");
3631
- }, onToggleGoal: () => {
3632
- openOverlay("goal");
3633
- }, onToggleSkills: () => {
3634
- openOverlay("skills");
3635
- }, onTogglePixel: () => {
3636
- openOverlay("pixel");
3637
- }, onTogglePlanMode: () => {
3638
- const next = !planMode;
3639
- setPlanMode(next);
3640
- log("INFO", "plan", `Plan mode ${next ? "enabled" : "disabled"}`);
3641
- setLiveItems((items) => [
3642
- ...items,
3643
- {
3644
- kind: "plan_transition",
3645
- text: next ? "Plan Mode Activated" : "Plan Mode Deactivated",
3646
- active: next,
3647
- id: getId(),
3648
- },
3649
- ]);
3650
- }, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsxs(_Fragment, { children: [_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending }), !exitPending && _jsx(GoalStatusBar, { entries: goalStatusEntries })] })), (footerStatusLayout.hasBackgroundTasks ||
3651
- footerStatusLayout.hasEyesSignals ||
3652
- footerStatusLayout.hasUpdateNotice) && (_jsxs(Box, { flexDirection: footerStatusLayout.stack ? "column" : "row", width: columns, children: [footerStatusLayout.hasBackgroundTasks && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate, compact: footerStatusLayout.compactBackgroundTasks })), footerStatusLayout.hasEyesSignals && (_jsx(Box, { paddingLeft: footerStatusLayout.stack || bgTasks.length === 0 ? 1 : 2, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, wrap: "truncate", children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance EZ Coder` }) })), footerStatusLayout.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack ||
3653
- (!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
3654
- ? 1
3655
- : 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
3656
- }
3657
- function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
3658
- const status = enabled ? "on" : "off";
3659
- const prefix = refreshed
3660
- ? `Dynamic repo map refreshed · injection: ${status}`
3661
- : `Dynamic repo map · injection: ${status}`;
3662
- return `${prefix}\n\n${markdown}`;
2993
+ };
2994
+ const handleRejectPlan = (planPath, feedback) => {
2995
+ planOverlayPendingRef.current = false;
2996
+ const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
2997
+ `Please revise the plan based on this feedback.`;
2998
+ if (props.resetUI && props.sessionStore) {
2999
+ props.sessionStore.overlay = null;
3000
+ props.sessionStore.planAutoExpand = false;
3001
+ // No wipeSession — keep history and messages so the agent picks
3002
+ // up the rejection mid-conversation.
3003
+ props.resetUI({
3004
+ pendingAction: {
3005
+ prompt: rejectionMsg,
3006
+ planEvent: { event: "rejected", detail: feedback },
3007
+ },
3008
+ });
3009
+ return;
3010
+ }
3011
+ setPlanAutoExpand(false);
3012
+ setOverlay(null);
3013
+ setDoneStatus(null);
3014
+ setLiveItems((prev) => [
3015
+ ...prev,
3016
+ { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3017
+ ]);
3018
+ void agentLoop.run(rejectionMsg).catch((err) => {
3019
+ const errMsg = err instanceof Error ? err.message : String(err);
3020
+ log("ERROR", "error", errMsg);
3021
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3022
+ });
3023
+ };
3024
+ const handleToggleTasks = () => {
3025
+ taskPicker.toggle();
3026
+ };
3027
+ const fullScreenOverlay = isPixelView
3028
+ ? "pixel"
3029
+ : isSkillsView
3030
+ ? "skills"
3031
+ : isPlanView
3032
+ ? "plan"
3033
+ : null;
3034
+ if (quittingSummary) {
3035
+ return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: _jsx(SessionSummaryDisplay, { summary: quittingSummary }) }));
3036
+ }
3037
+ return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: rewindCheckpoints ? (_jsx(RewindOverlay, { checkpoints: rewindCheckpoints, onRestore: handleRewindRestore, onCancel: handleRewindCancel })) : fullScreenOverlay ? (_jsx(FullScreenOverlayRouter, { overlay: fullScreenOverlay, version: props.version, cwd: props.cwd, agentRunning: agentLoop.isRunning, planAutoExpand: planAutoExpand, onClosePixel: handleCloseRemountableOverlay, onPixelFixOne: handlePixelFixOne, onPixelFixAll: handlePixelFixAll, onCloseSkills: handleCloseRemountableOverlay, onClosePlan: handleClosePlanOverlay, onApprovePlan: handleApprovePlan, onRejectPlan: handleRejectPlan })) : (_jsx(ChatScreen, { columns: columns, liveItems: uniqueItemsById(liveItems), renderItem: renderItem, isRunning: agentLoop.isRunning, visibleStreamingText: visibleStreamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, reserveStreamingSpacing: shouldReserveStreamingSpacing, renderMarkdown: renderMarkdown, measuredLiveAreaRows: measuredLiveAreaRows, fullscreen: props.fullscreen, rows: rows, transcriptLines: transcriptLines, viewportRows: viewportRows, assistantMarginTop: shouldTopSpaceStreamingText || streamingContinuesFlushed ? 1 : 0, streamingContinuation: streamingContinuesFlushed, controlsRef: mainControlsRef, hiddenQueuedCount: hiddenQueuedCount, queueIndicatorMarginTop: shouldTopSpaceQueueIndicator ? 2 : 1, theme: theme, statusSlotVisible: statusSlotVisible, activityVisible: activityVisible, stallStatusVisible: stallStatusVisible, liveToolFeed: liveToolFeed, doneStatus: doneStatus, activityPhase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, isThinking: agentLoop.isThinking, thinkingLevel: thinkingLevel, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, lastUserMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, formatDuration: formatDuration, inputControls: {
3038
+ onSubmit: handleSubmit,
3039
+ onAbort: handleAbort,
3040
+ injectText: composerInject,
3041
+ inputActive: !taskBarFocused && !overlay,
3042
+ onDownAtEnd: handleFocusTaskBar,
3043
+ onShiftTab: handleToggleThinking,
3044
+ onToggleTasks: handleToggleTasks,
3045
+ onToggleSkills: () => openOverlay("skills"),
3046
+ onTogglePixel: () => openOverlay("pixel"),
3047
+ onToggleGoal: () => openOverlay("goal"),
3048
+ onToggleMarkdown: () => setRenderMarkdown((prev) => !prev),
3049
+ cwd: props.cwd,
3050
+ commands: allCommands,
3051
+ mouseScroll: props.fullscreen,
3052
+ onScroll: scrollTranscriptByLines,
3053
+ }, taskPicker: {
3054
+ open: taskPicker.open,
3055
+ tasks: taskPicker.tasks,
3056
+ onClose: taskPicker.close,
3057
+ onStart: taskPicker.start,
3058
+ onRunAll: taskPicker.runAll,
3059
+ onDelete: taskPicker.deleteTask,
3060
+ }, goalPane: {
3061
+ cwd: props.cwd,
3062
+ autoExpandNewest: goalAutoExpand,
3063
+ statusEntries: goalStatusEntries,
3064
+ onClose: handleCloseGoalOverlay,
3065
+ onRunGoal: handleRunGoal,
3066
+ onVerifyGoal: handleVerifyGoal,
3067
+ onPauseGoal: handlePauseGoal,
3068
+ onRefineGoal: handleRefineGoal,
3069
+ }, overlay: overlay, onModelSelect: handleModelSelect, onModelCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider, onThemeSelect: handleThemeSelect, onThemeCancel: () => setOverlay(null), currentTheme: theme.name, contextUsed: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, displayedCwd: displayedCwd, gitBranch: gitBranch, planMode: planMode, exitPending: exitPending, footerStatusLayout: footerStatusLayout, backgroundTasks: bgTasks, taskBarFocused: taskBarFocused, taskBarExpanded: taskBarExpanded, selectedTaskIndex: selectedTaskIndex, onTaskBarExpand: handleTaskBarExpand, onTaskBarCollapse: handleTaskBarCollapse, onTaskKill: handleTaskKill, onTaskBarExit: handleTaskBarExit, onTaskNavigate: handleTaskNavigate })) }));
3663
3070
  }
3664
3071
  //# sourceMappingURL=App.js.map