@kenkaiiii/ggcoder 4.3.211 → 4.3.213

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 (314) hide show
  1. package/README.md +5 -8
  2. package/dist/cli.d.ts +3 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +112 -61
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/continue-replay-inventory.test.d.ts +2 -0
  7. package/dist/core/continue-replay-inventory.test.d.ts.map +1 -0
  8. package/dist/core/continue-replay-inventory.test.js +42 -0
  9. package/dist/core/continue-replay-inventory.test.js.map +1 -0
  10. package/dist/core/goal-controller.d.ts +2 -0
  11. package/dist/core/goal-controller.d.ts.map +1 -1
  12. package/dist/core/goal-controller.js +283 -24
  13. package/dist/core/goal-controller.js.map +1 -1
  14. package/dist/core/goal-controller.test.js +413 -16
  15. package/dist/core/goal-controller.test.js.map +1 -1
  16. package/dist/core/goal-lifecycle-smoke.test.js +48 -6
  17. package/dist/core/goal-lifecycle-smoke.test.js.map +1 -1
  18. package/dist/core/goal-prerequisites.d.ts +23 -0
  19. package/dist/core/goal-prerequisites.d.ts.map +1 -0
  20. package/dist/core/goal-prerequisites.js +114 -0
  21. package/dist/core/goal-prerequisites.js.map +1 -0
  22. package/dist/core/goal-prerequisites.test.d.ts +2 -0
  23. package/dist/core/goal-prerequisites.test.d.ts.map +1 -0
  24. package/dist/core/goal-prerequisites.test.js +118 -0
  25. package/dist/core/goal-prerequisites.test.js.map +1 -0
  26. package/dist/core/goal-references.d.ts +14 -0
  27. package/dist/core/goal-references.d.ts.map +1 -0
  28. package/dist/core/goal-references.js +153 -0
  29. package/dist/core/goal-references.js.map +1 -0
  30. package/dist/core/goal-references.test.d.ts +2 -0
  31. package/dist/core/goal-references.test.d.ts.map +1 -0
  32. package/dist/core/goal-references.test.js +77 -0
  33. package/dist/core/goal-references.test.js.map +1 -0
  34. package/dist/core/goal-store.d.ts +25 -0
  35. package/dist/core/goal-store.d.ts.map +1 -1
  36. package/dist/core/goal-store.js +161 -38
  37. package/dist/core/goal-store.js.map +1 -1
  38. package/dist/core/goal-store.test.js +33 -8
  39. package/dist/core/goal-store.test.js.map +1 -1
  40. package/dist/core/goal-verifier.d.ts.map +1 -1
  41. package/dist/core/goal-verifier.js +4 -1
  42. package/dist/core/goal-verifier.js.map +1 -1
  43. package/dist/core/goal-verifier.test.js +43 -0
  44. package/dist/core/goal-verifier.test.js.map +1 -1
  45. package/dist/core/goal-worker.d.ts +2 -0
  46. package/dist/core/goal-worker.d.ts.map +1 -1
  47. package/dist/core/goal-worker.js +33 -9
  48. package/dist/core/goal-worker.js.map +1 -1
  49. package/dist/core/goal-worker.test.js +49 -1
  50. package/dist/core/goal-worker.test.js.map +1 -1
  51. package/dist/core/prompt-commands.d.ts.map +1 -1
  52. package/dist/core/prompt-commands.js +28 -845
  53. package/dist/core/prompt-commands.js.map +1 -1
  54. package/dist/core/prompt-commands.test.js +40 -74
  55. package/dist/core/prompt-commands.test.js.map +1 -1
  56. package/dist/core/runtime-mode.d.ts +14 -0
  57. package/dist/core/runtime-mode.d.ts.map +1 -0
  58. package/dist/core/runtime-mode.js +10 -0
  59. package/dist/core/runtime-mode.js.map +1 -0
  60. package/dist/core/session-restore-display.test.d.ts +2 -0
  61. package/dist/core/session-restore-display.test.d.ts.map +1 -0
  62. package/dist/core/session-restore-display.test.js +100 -0
  63. package/dist/core/session-restore-display.test.js.map +1 -0
  64. package/dist/core/verify-commands.js +4 -4
  65. package/dist/core/verify-commands.js.map +1 -1
  66. package/dist/system-prompt.d.ts +2 -1
  67. package/dist/system-prompt.d.ts.map +1 -1
  68. package/dist/system-prompt.js +51 -37
  69. package/dist/system-prompt.js.map +1 -1
  70. package/dist/system-prompt.test.js +147 -40
  71. package/dist/system-prompt.test.js.map +1 -1
  72. package/dist/tools/bash.d.ts +3 -2
  73. package/dist/tools/bash.d.ts.map +1 -1
  74. package/dist/tools/bash.js +11 -4
  75. package/dist/tools/bash.js.map +1 -1
  76. package/dist/tools/edit.d.ts +5 -3
  77. package/dist/tools/edit.d.ts.map +1 -1
  78. package/dist/tools/edit.js +14 -4
  79. package/dist/tools/edit.js.map +1 -1
  80. package/dist/tools/edit.test.js +0 -10
  81. package/dist/tools/edit.test.js.map +1 -1
  82. package/dist/tools/goal-mode.test.d.ts +2 -0
  83. package/dist/tools/goal-mode.test.d.ts.map +1 -0
  84. package/dist/tools/goal-mode.test.js +121 -0
  85. package/dist/tools/goal-mode.test.js.map +1 -0
  86. package/dist/tools/goals.d.ts +15 -3
  87. package/dist/tools/goals.d.ts.map +1 -1
  88. package/dist/tools/goals.js +385 -35
  89. package/dist/tools/goals.js.map +1 -1
  90. package/dist/tools/goals.test.js +389 -6
  91. package/dist/tools/goals.test.js.map +1 -1
  92. package/dist/tools/index.d.ts +7 -10
  93. package/dist/tools/index.d.ts.map +1 -1
  94. package/dist/tools/index.js +6 -19
  95. package/dist/tools/index.js.map +1 -1
  96. package/dist/tools/plan-mode.test.js +34 -224
  97. package/dist/tools/plan-mode.test.js.map +1 -1
  98. package/dist/tools/prompt-hints.d.ts.map +1 -1
  99. package/dist/tools/prompt-hints.js +2 -6
  100. package/dist/tools/prompt-hints.js.map +1 -1
  101. package/dist/tools/subagent.d.ts +3 -2
  102. package/dist/tools/subagent.d.ts.map +1 -1
  103. package/dist/tools/subagent.js +4 -9
  104. package/dist/tools/subagent.js.map +1 -1
  105. package/dist/tools/write.d.ts +5 -3
  106. package/dist/tools/write.d.ts.map +1 -1
  107. package/dist/tools/write.js +14 -13
  108. package/dist/tools/write.js.map +1 -1
  109. package/dist/tools/write.test.js +0 -16
  110. package/dist/tools/write.test.js.map +1 -1
  111. package/dist/ui/App.d.ts +146 -30
  112. package/dist/ui/App.d.ts.map +1 -1
  113. package/dist/ui/App.js +1202 -910
  114. package/dist/ui/App.js.map +1 -1
  115. package/dist/ui/activity-phrases.d.ts.map +1 -1
  116. package/dist/ui/activity-phrases.js +0 -2
  117. package/dist/ui/activity-phrases.js.map +1 -1
  118. package/dist/ui/app-state-persistence.test.js +181 -13
  119. package/dist/ui/app-state-persistence.test.js.map +1 -1
  120. package/dist/ui/chat-layout-pinning.test.d.ts +2 -0
  121. package/dist/ui/chat-layout-pinning.test.d.ts.map +1 -0
  122. package/dist/ui/chat-layout-pinning.test.js +407 -0
  123. package/dist/ui/chat-layout-pinning.test.js.map +1 -0
  124. package/dist/ui/components/ActivityIndicator.d.ts +1 -2
  125. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  126. package/dist/ui/components/ActivityIndicator.js +63 -94
  127. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  128. package/dist/ui/components/AssistantMessage.d.ts +6 -2
  129. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  130. package/dist/ui/components/AssistantMessage.js +9 -4
  131. package/dist/ui/components/AssistantMessage.js.map +1 -1
  132. package/dist/ui/components/AssistantMessage.test.d.ts +2 -0
  133. package/dist/ui/components/AssistantMessage.test.d.ts.map +1 -0
  134. package/dist/ui/components/AssistantMessage.test.js +369 -0
  135. package/dist/ui/components/AssistantMessage.test.js.map +1 -0
  136. package/dist/ui/components/BackgroundTasksBar.d.ts +1 -3
  137. package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
  138. package/dist/ui/components/BackgroundTasksBar.js +2 -4
  139. package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
  140. package/dist/ui/components/Banner.d.ts +1 -3
  141. package/dist/ui/components/Banner.d.ts.map +1 -1
  142. package/dist/ui/components/Banner.js +7 -3
  143. package/dist/ui/components/Banner.js.map +1 -1
  144. package/dist/ui/components/Footer.d.ts +26 -4
  145. package/dist/ui/components/Footer.d.ts.map +1 -1
  146. package/dist/ui/components/Footer.js +73 -21
  147. package/dist/ui/components/Footer.js.map +1 -1
  148. package/dist/ui/components/GoalOverlay.d.ts +28 -20
  149. package/dist/ui/components/GoalOverlay.d.ts.map +1 -1
  150. package/dist/ui/components/GoalOverlay.js +283 -253
  151. package/dist/ui/components/GoalOverlay.js.map +1 -1
  152. package/dist/ui/components/InputArea.d.ts +2 -6
  153. package/dist/ui/components/InputArea.d.ts.map +1 -1
  154. package/dist/ui/components/InputArea.js +40 -32
  155. package/dist/ui/components/InputArea.js.map +1 -1
  156. package/dist/ui/components/InputArea.test.js +11 -1
  157. package/dist/ui/components/InputArea.test.js.map +1 -1
  158. package/dist/ui/components/Markdown.d.ts +11 -11
  159. package/dist/ui/components/Markdown.d.ts.map +1 -1
  160. package/dist/ui/components/Markdown.js +25 -198
  161. package/dist/ui/components/Markdown.js.map +1 -1
  162. package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
  163. package/dist/ui/components/PlanOverlay.js +1 -1
  164. package/dist/ui/components/PlanOverlay.js.map +1 -1
  165. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  166. package/dist/ui/components/ServerToolExecution.js +3 -2
  167. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  168. package/dist/ui/components/SlashCommandMenu.d.ts +4 -3
  169. package/dist/ui/components/SlashCommandMenu.d.ts.map +1 -1
  170. package/dist/ui/components/SlashCommandMenu.js +38 -26
  171. package/dist/ui/components/SlashCommandMenu.js.map +1 -1
  172. package/dist/ui/components/StreamingArea.d.ts +11 -2
  173. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  174. package/dist/ui/components/StreamingArea.js +20 -23
  175. package/dist/ui/components/StreamingArea.js.map +1 -1
  176. package/dist/ui/components/StreamingArea.test.d.ts +2 -0
  177. package/dist/ui/components/StreamingArea.test.d.ts.map +1 -0
  178. package/dist/ui/components/StreamingArea.test.js +18 -0
  179. package/dist/ui/components/StreamingArea.test.js.map +1 -0
  180. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  181. package/dist/ui/components/ToolExecution.js +11 -27
  182. package/dist/ui/components/ToolExecution.js.map +1 -1
  183. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  184. package/dist/ui/components/ToolGroupExecution.js +9 -124
  185. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  186. package/dist/ui/components/UserMessage.d.ts.map +1 -1
  187. package/dist/ui/components/UserMessage.js +15 -10
  188. package/dist/ui/components/UserMessage.js.map +1 -1
  189. package/dist/ui/components/UserMessage.test.d.ts +2 -0
  190. package/dist/ui/components/UserMessage.test.d.ts.map +1 -0
  191. package/dist/ui/components/UserMessage.test.js +39 -0
  192. package/dist/ui/components/UserMessage.test.js.map +1 -0
  193. package/dist/ui/footer-status-layout.test.js +21 -7
  194. package/dist/ui/footer-status-layout.test.js.map +1 -1
  195. package/dist/ui/goal-events.d.ts +8 -0
  196. package/dist/ui/goal-events.d.ts.map +1 -1
  197. package/dist/ui/goal-events.js +28 -8
  198. package/dist/ui/goal-events.js.map +1 -1
  199. package/dist/ui/goal-events.test.js +40 -2
  200. package/dist/ui/goal-events.test.js.map +1 -1
  201. package/dist/ui/goal-lifecycle-orchestration.test.js +127 -34
  202. package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -1
  203. package/dist/ui/goal-overlay.test.js +122 -44
  204. package/dist/ui/goal-overlay.test.js.map +1 -1
  205. package/dist/ui/goal-summary.d.ts +14 -0
  206. package/dist/ui/goal-summary.d.ts.map +1 -0
  207. package/dist/ui/goal-summary.js +194 -0
  208. package/dist/ui/goal-summary.js.map +1 -0
  209. package/dist/ui/hooks/useAgentLoop.d.ts +8 -2
  210. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  211. package/dist/ui/hooks/useAgentLoop.js +20 -9
  212. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  213. package/dist/ui/hooks/useAgentLoop.test.d.ts +2 -0
  214. package/dist/ui/hooks/useAgentLoop.test.d.ts.map +1 -0
  215. package/dist/ui/hooks/useAgentLoop.test.js +8 -0
  216. package/dist/ui/hooks/useAgentLoop.test.js.map +1 -0
  217. package/dist/ui/hooks/useTerminalSize.d.ts +5 -9
  218. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  219. package/dist/ui/hooks/useTerminalSize.js +9 -14
  220. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  221. package/dist/ui/live-item-flush.d.ts +2 -2
  222. package/dist/ui/live-item-flush.d.ts.map +1 -1
  223. package/dist/ui/live-item-flush.js +8 -4
  224. package/dist/ui/live-item-flush.js.map +1 -1
  225. package/dist/ui/long-prompt-regression-harness.test.d.ts +2 -0
  226. package/dist/ui/long-prompt-regression-harness.test.d.ts.map +1 -0
  227. package/dist/ui/long-prompt-regression-harness.test.js +195 -0
  228. package/dist/ui/long-prompt-regression-harness.test.js.map +1 -0
  229. package/dist/ui/plan-overlay.test.js +7 -29
  230. package/dist/ui/plan-overlay.test.js.map +1 -1
  231. package/dist/ui/queued-message.test.d.ts.map +1 -1
  232. package/dist/ui/queued-message.test.js +76 -14
  233. package/dist/ui/queued-message.test.js.map +1 -1
  234. package/dist/ui/render.d.ts +21 -24
  235. package/dist/ui/render.d.ts.map +1 -1
  236. package/dist/ui/render.js +46 -28
  237. package/dist/ui/render.js.map +1 -1
  238. package/dist/ui/render.test.d.ts +2 -0
  239. package/dist/ui/render.test.d.ts.map +1 -0
  240. package/dist/ui/render.test.js +16 -0
  241. package/dist/ui/render.test.js.map +1 -0
  242. package/dist/ui/scroll-stabilization.test.js +1 -1
  243. package/dist/ui/scroll-stabilization.test.js.map +1 -1
  244. package/dist/ui/slash-command-images.test.js +79 -4
  245. package/dist/ui/slash-command-images.test.js.map +1 -1
  246. package/dist/ui/terminal-history.d.ts +26 -0
  247. package/dist/ui/terminal-history.d.ts.map +1 -0
  248. package/dist/ui/terminal-history.js +910 -0
  249. package/dist/ui/terminal-history.js.map +1 -0
  250. package/dist/ui/terminal-history.test.d.ts +2 -0
  251. package/dist/ui/terminal-history.test.d.ts.map +1 -0
  252. package/dist/ui/terminal-history.test.js +314 -0
  253. package/dist/ui/terminal-history.test.js.map +1 -0
  254. package/dist/ui/tool-group-summary.d.ts +16 -0
  255. package/dist/ui/tool-group-summary.d.ts.map +1 -0
  256. package/dist/ui/tool-group-summary.js +123 -0
  257. package/dist/ui/tool-group-summary.js.map +1 -0
  258. package/dist/ui/tui-history-parity.test.d.ts +2 -0
  259. package/dist/ui/tui-history-parity.test.d.ts.map +1 -0
  260. package/dist/ui/tui-history-parity.test.js +243 -0
  261. package/dist/ui/tui-history-parity.test.js.map +1 -0
  262. package/dist/ui/utils/assistant-stream-split.d.ts +6 -0
  263. package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -0
  264. package/dist/ui/utils/assistant-stream-split.js +37 -0
  265. package/dist/ui/utils/assistant-stream-split.js.map +1 -0
  266. package/dist/ui/utils/assistant-stream-split.test.d.ts +2 -0
  267. package/dist/ui/utils/assistant-stream-split.test.d.ts.map +1 -0
  268. package/dist/ui/utils/assistant-stream-split.test.js +58 -0
  269. package/dist/ui/utils/assistant-stream-split.test.js.map +1 -0
  270. package/dist/ui/utils/latex-to-unicode.d.ts +22 -0
  271. package/dist/ui/utils/latex-to-unicode.d.ts.map +1 -0
  272. package/dist/ui/utils/latex-to-unicode.js +538 -0
  273. package/dist/ui/utils/latex-to-unicode.js.map +1 -0
  274. package/dist/ui/utils/markdown-renderer.d.ts +20 -0
  275. package/dist/ui/utils/markdown-renderer.d.ts.map +1 -0
  276. package/dist/ui/utils/markdown-renderer.js +327 -0
  277. package/dist/ui/utils/markdown-renderer.js.map +1 -0
  278. package/dist/ui/utils/markdown-table.d.ts +9 -0
  279. package/dist/ui/utils/markdown-table.d.ts.map +1 -0
  280. package/dist/ui/utils/markdown-table.js +95 -0
  281. package/dist/ui/utils/markdown-table.js.map +1 -0
  282. package/dist/ui/utils/text-utils.d.ts +8 -0
  283. package/dist/ui/utils/text-utils.d.ts.map +1 -0
  284. package/dist/ui/utils/text-utils.js +16 -0
  285. package/dist/ui/utils/text-utils.js.map +1 -0
  286. package/dist/ui/utils/token-to-ansi.js +19 -9
  287. package/dist/ui/utils/token-to-ansi.js.map +1 -1
  288. package/dist/ui/utils/user-message-display.d.ts +7 -0
  289. package/dist/ui/utils/user-message-display.d.ts.map +1 -0
  290. package/dist/ui/utils/user-message-display.js +26 -0
  291. package/dist/ui/utils/user-message-display.js.map +1 -0
  292. package/dist/utils/format.js +0 -9
  293. package/dist/utils/format.js.map +1 -1
  294. package/package.json +9 -5
  295. package/dist/tools/enter-plan.d.ts +0 -8
  296. package/dist/tools/enter-plan.d.ts.map +0 -1
  297. package/dist/tools/enter-plan.js +0 -30
  298. package/dist/tools/enter-plan.js.map +0 -1
  299. package/dist/tools/exit-plan.d.ts +0 -8
  300. package/dist/tools/exit-plan.d.ts.map +0 -1
  301. package/dist/tools/exit-plan.js +0 -36
  302. package/dist/tools/exit-plan.js.map +0 -1
  303. package/dist/tools/tasks.d.ts +0 -16
  304. package/dist/tools/tasks.d.ts.map +0 -1
  305. package/dist/tools/tasks.js +0 -133
  306. package/dist/tools/tasks.js.map +0 -1
  307. package/dist/ui/components/EyesOverlay.d.ts +0 -10
  308. package/dist/ui/components/EyesOverlay.d.ts.map +0 -1
  309. package/dist/ui/components/EyesOverlay.js +0 -220
  310. package/dist/ui/components/EyesOverlay.js.map +0 -1
  311. package/dist/ui/components/TaskOverlay.d.ts +0 -10
  312. package/dist/ui/components/TaskOverlay.d.ts.map +0 -1
  313. package/dist/ui/components/TaskOverlay.js +0 -267
  314. package/dist/ui/components/TaskOverlay.js.map +0 -1
package/dist/ui/App.js CHANGED
@@ -1,39 +1,37 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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, Text, useStdout } from "ink";
4
4
  import { useTerminalSize } from "./hooks/useTerminalSize.js";
5
5
  import { useDoublePress } from "./hooks/useDoublePress.js";
6
6
  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";
7
+ import { writeFileSync } from "node:fs";
11
8
  import { playNotificationSound } from "../utils/sound.js";
12
9
  import { formatError, } from "@kenkaiiii/gg-ai";
13
10
  import { extractImagePaths } from "../utils/image.js";
11
+ import { buildGoalReferenceContext, formatGoalReferencesForPrompt, } from "../core/goal-references.js";
14
12
  import { useAgentLoop } from "./hooks/useAgentLoop.js";
15
- import { isEyesActive, journalCount } from "@kenkaiiii/ggcoder-eyes";
16
13
  import { UserMessage } from "./components/UserMessage.js";
17
14
  import { AssistantMessage } from "./components/AssistantMessage.js";
18
15
  import { ToolExecution } from "./components/ToolExecution.js";
16
+ import { ToolUseLoader } from "./components/ToolUseLoader.js";
19
17
  import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
20
18
  import { ServerToolExecution } from "./components/ServerToolExecution.js";
19
+ import { MessageResponse } from "./components/MessageResponse.js";
21
20
  import { SubAgentPanel } from "./components/SubAgentPanel.js";
22
21
  import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
23
22
  import { createWebSearchTool } from "../tools/web-search.js";
24
23
  import { StreamingArea } from "./components/StreamingArea.js";
25
24
  import { ActivityIndicator } from "./components/ActivityIndicator.js";
26
25
  import { InputArea } from "./components/InputArea.js";
27
- import { Footer } from "./components/Footer.js";
26
+ import { Footer, doesFooterFitOnOneLine } from "./components/Footer.js";
28
27
  import { GoalStatusBar, reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
29
28
  import { Banner } from "./components/Banner.js";
30
29
  import { PlanOverlay } from "./components/PlanOverlay.js";
31
30
  import { ModelSelector } from "./components/ModelSelector.js";
32
- import { TaskOverlay } from "./components/TaskOverlay.js";
33
31
  import { GoalOverlay } from "./components/GoalOverlay.js";
34
32
  import { PixelOverlay } from "./components/PixelOverlay.js";
33
+ import { buildGoalFinalSummarySections, buildGoalSummaryRows, goalPassedDetail, } from "./goal-summary.js";
35
34
  import { SkillsOverlay } from "./components/SkillsOverlay.js";
36
- import { EyesOverlay } from "./components/EyesOverlay.js";
37
35
  import { ThemeSelector } from "./components/ThemeSelector.js";
38
36
  import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
39
37
  import { useTheme, useSetTheme } from "./theme/theme.js";
@@ -59,8 +57,10 @@ import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMes
59
57
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
60
58
  import { getMCPServers } from "../core/mcp/index.js";
61
59
  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 } from "../core/goal-controller.js";
60
+ import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
61
+ import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
62
+ import { canCompleteGoalRun, decideGoalNextAction, } from "../core/goal-controller.js";
63
+ import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
64
64
  import { runGoalVerifierCommand } from "../core/goal-verifier.js";
65
65
  import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
66
66
  import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
@@ -116,6 +116,56 @@ export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS,
116
116
  fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
117
117
  };
118
118
  }
119
+ const GOAL_PLANNER_OUTPUT_MAX_CHARS = 2400;
120
+ function messageTextContent(message) {
121
+ if (typeof message.content === "string")
122
+ return message.content;
123
+ return message.content
124
+ .filter((part) => part.type === "text")
125
+ .map((part) => part.text)
126
+ .join("\n");
127
+ }
128
+ export function collectAssistantTextSince(messages, startIndex, maxChars = GOAL_PLANNER_OUTPUT_MAX_CHARS) {
129
+ const text = messages
130
+ .slice(startIndex)
131
+ .filter((message) => message.role === "assistant")
132
+ .map(messageTextContent)
133
+ .join("\n")
134
+ .trim();
135
+ if (text.length <= maxChars)
136
+ return text;
137
+ return text.slice(0, maxChars).trimEnd() + "\n[planner output truncated]";
138
+ }
139
+ export function buildGoalSetupPromptFromPlanner({ originalGoalPrompt, plannerOutput, }) {
140
+ const compactPlannerOutput = plannerOutput.trim() || "GOAL_PLAN\nresearch=none\nEND_GOAL_PLAN";
141
+ return (`${originalGoalPrompt.trim()}\n\n` +
142
+ `## Goal Planner Output\n\n${compactPlannerOutput}\n\n` +
143
+ `Use the original objective plus this planner output to create durable Goal setup only. ` +
144
+ `Do not redo planner research unless the planner output is unusable.`);
145
+ }
146
+ export function isGoalPromptCommandName(cmdName) {
147
+ return getPromptCommand(cmdName)?.name === "goal";
148
+ }
149
+ export async function runGoalPromptSetupSequence({ userContent, fullPrompt, messagesRef, setGoalModeAndPrompt, runAgent, onStage, }) {
150
+ onStage?.("Planning Goal setup");
151
+ await setGoalModeAndPrompt("planner");
152
+ const plannerStartIndex = messagesRef.current.length;
153
+ await runAgent(userContent);
154
+ const plannerOutput = collectAssistantTextSince(messagesRef.current, plannerStartIndex);
155
+ const setupPrompt = buildGoalSetupPromptFromPlanner({
156
+ originalGoalPrompt: fullPrompt,
157
+ plannerOutput,
158
+ });
159
+ await setGoalModeAndPrompt("setup");
160
+ onStage?.("Creating Goal run");
161
+ await runAgent(setupPrompt);
162
+ }
163
+ function buildGoalTaskPromptWithReferences(run, taskPrompt) {
164
+ if (taskPrompt.includes("## Goal References (MANDATORY)"))
165
+ return taskPrompt;
166
+ const references = formatGoalReferencesForPrompt(run.references ?? []);
167
+ return references ? `${references}\n\n${taskPrompt}` : taskPrompt;
168
+ }
119
169
  export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
120
170
  if (inputImages.length === 0)
121
171
  return text;
@@ -155,14 +205,13 @@ export function buildUserContentWithAttachments(text, inputImages, modelSupports
155
205
  // If only text parts remain after stripping images, simplify to plain string
156
206
  return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
157
207
  }
158
- /** Tools that get aggregated into a single compact group when concurrent. */
208
+ /** Tools that get aggregated into a single compact group when possible. */
159
209
  const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
160
210
  const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
161
211
  /**
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.
212
+ * Cap memory by replacing old finalized rows with tiny tombstones. The full
213
+ * transcript is already printed into terminal scrollback, so the in-memory copy
214
+ * only needs enough recent structure to survive remounts and session mirroring.
166
215
  */
167
216
  const MAX_LIVE_HISTORY = 200;
168
217
  function compactHistory(items) {
@@ -192,63 +241,30 @@ function summarizeGoalCompletion(summary) {
192
241
  function formatGoalWorkerFinishedTitle(taskTitle, status) {
193
242
  return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
194
243
  }
195
- function countGoalTasksByStatus(tasks, status) {
196
- return tasks.filter((task) => task.status === status).length;
197
- }
198
- function firstText(values) {
199
- return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
200
- }
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)}…`;
206
- }
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
- : {}),
235
- });
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
- });
244
+ function goalProgressLoaderStatus(item) {
245
+ if (item.status === "failed" || item.status === "fail" || item.status === "blocked")
246
+ return "error";
247
+ if (item.phase === "worker_finished" ||
248
+ item.phase === "verifier_finished" ||
249
+ item.phase === "terminal") {
250
+ return "done";
243
251
  }
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
- });
252
+ return "running";
253
+ }
254
+ function goalProgressColor(item, theme) {
255
+ const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
256
+ if (isError)
257
+ return theme.error;
258
+ if (item.phase === "worker_finished" || item.phase === "terminal")
259
+ return theme.success;
260
+ if (item.phase === "verifier_finished" || item.phase === "verifier_started")
261
+ return theme.accent;
262
+ if (item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working") {
263
+ return theme.secondary;
250
264
  }
251
- return rows.slice(0, 4);
265
+ if (item.phase === "continuing")
266
+ return theme.warning;
267
+ return theme.primary;
252
268
  }
253
269
  function goalTerminalProgressId(run) {
254
270
  return `goal-terminal-${run.id}`;
@@ -261,10 +277,20 @@ function goalTerminalRunIdFromItem(item) {
261
277
  return item.id.slice("goal-terminal-".length);
262
278
  }
263
279
  function goalProgressMatchesDraft(item, draft) {
264
- return (item.title === draft.title &&
280
+ return (item.phase === draft.phase &&
281
+ item.title === draft.title &&
265
282
  item.detail === draft.detail &&
283
+ item.workerId === draft.workerId &&
266
284
  item.status === draft.status &&
267
- JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []));
285
+ JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
286
+ JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
287
+ }
288
+ export function appendGoalProgressDraft(items, draft, makeId) {
289
+ const previous = items.at(-1);
290
+ if (previous?.kind === "goal_progress" && goalProgressMatchesDraft(previous, draft)) {
291
+ return items;
292
+ }
293
+ return [...items, { ...draft, id: makeId() }];
268
294
  }
269
295
  /**
270
296
  * Reconcile terminal Goal cards that are already visible in this UI session.
@@ -276,6 +302,16 @@ function goalProgressMatchesDraft(item, draft) {
276
302
  * event append that card first, then use this helper to tombstone stale older
277
303
  * cards for the same run.
278
304
  */
305
+ export function getNextGeneratedItemId(items) {
306
+ let max = -1;
307
+ for (const item of items) {
308
+ const raw = item.id.startsWith("ui-") ? item.id.slice(3) : item.id;
309
+ const n = Number(raw);
310
+ if (Number.isInteger(n) && n >= 0 && n > max)
311
+ max = n;
312
+ }
313
+ return max + 1;
314
+ }
279
315
  export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
280
316
  const runIds = new Set(runs.map((run) => run.id));
281
317
  const terminalByRun = new Map(runs
@@ -303,8 +339,9 @@ export function formatGoalTerminalProgress(run) {
303
339
  kind: "goal_progress",
304
340
  phase: "terminal",
305
341
  title: `Goal passed: ${run.title}`,
306
- detail: "Verifier evidence is recorded; auto-continuation stopped.",
342
+ detail: goalPassedDetail(run),
307
343
  summaryRows: buildGoalSummaryRows(run),
344
+ summarySections: buildGoalFinalSummarySections(run),
308
345
  status: run.status,
309
346
  };
310
347
  case "failed":
@@ -343,31 +380,187 @@ export function formatGoalTerminalProgress(run) {
343
380
  return null;
344
381
  }
345
382
  }
346
- export function shouldHideHistoryForOverlayView(_isOverlayView, _isAgentRunning) {
347
- // Ink Static is append-only. Passing [] for overlay panes rewrites the Static
348
- // accumulator and can destroy scrollback when the pane closes. Keep history
349
- // mounted and let overlays render below it.
350
- return false;
383
+ export function shouldHideHistoryForOverlayView(isOverlayView, _isAgentRunning) {
384
+ // Overlay panes are standalone full-screen states. Finalized chat rows are
385
+ // printed outside Ink, so overlays should never replay transcript UI behind them.
386
+ return isOverlayView;
351
387
  }
352
388
  export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
353
- return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
389
+ return isAgentRunning && overlayPane === "goal";
390
+ }
391
+ export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender: _stabilizeOverlayPaneRerender, }) {
392
+ return shouldHideHistoryForOverlay;
393
+ }
394
+ export function getDoneFlushDecision({ planOverlayPending, goalMode, goalAutoExpand, }) {
395
+ return {
396
+ showDoneStatus: !(planOverlayPending ||
397
+ goalMode === "planner" ||
398
+ goalMode === "setup" ||
399
+ goalAutoExpand),
400
+ flushLiveItems: true,
401
+ };
402
+ }
403
+ export function getGoalSetupFinishedPaneTransition() {
404
+ return {
405
+ overlay: "goal",
406
+ goalAutoExpand: true,
407
+ planAutoExpand: false,
408
+ suppressDoneStatus: true,
409
+ };
410
+ }
411
+ export function getGoalSetupPaneTransitionAfterRun({ isGoalSetupCommand, setupPanePending, }) {
412
+ return isGoalSetupCommand && setupPanePending ? getGoalSetupFinishedPaneTransition() : null;
413
+ }
414
+ export function shouldResetUIForSetupPaneTransition({ hasResetUI, hasSessionStore, }) {
415
+ // Opening a review pane is a full-screen state transition. A bare React state
416
+ // flip hides history in the virtual tree, but it does not reset Ink/log-update's
417
+ // already-written terminal frame, so the pane can render below prior chat.
418
+ return hasResetUI && hasSessionStore;
354
419
  }
355
- export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender, }) {
356
- return shouldHideHistoryForOverlay && !stabilizeOverlayPaneRerender;
420
+ export const shouldResetUIForGoalSetupPaneTransition = shouldResetUIForSetupPaneTransition;
421
+ export function getGoalActivationPaneTransition() {
422
+ return { overlay: null, goalAutoExpand: false, planAutoExpand: false, resetReviewScreen: true };
423
+ }
424
+ export function getGoalContinuationChoiceKey({ runId, decision, }) {
425
+ switch (decision.kind) {
426
+ case "create_task":
427
+ return `${runId}:create_task:${decision.title}:${decision.prompt}`;
428
+ case "start_worker":
429
+ case "pause":
430
+ return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
431
+ case "run_verifier":
432
+ return `${runId}:run_verifier:${decision.command}`;
433
+ case "blocked":
434
+ case "complete":
435
+ case "terminal":
436
+ case "wait":
437
+ return `${runId}:${decision.kind}:${decision.reason}`;
438
+ }
357
439
  }
358
- export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
359
- const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
440
+ export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, hasParagraphBreakLiveUserMessage = false, }) {
441
+ const shouldPreserveStatic = isUserScrolled || hasTallLiveUserMessage || hasParagraphBreakLiveUserMessage;
442
+ const shouldAutoFollow = !(isUserScrolled || hasTallLiveUserMessage);
360
443
  return {
361
- preserveStatic: shouldStabilize && hasNewOutput,
362
- autoFollow: !shouldStabilize,
444
+ preserveStatic: shouldPreserveStatic && hasNewOutput,
445
+ autoFollow: shouldAutoFollow,
363
446
  };
364
447
  }
448
+ export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
449
+ if (wasGoalSetupTurn)
450
+ return "off";
451
+ if (currentMode === "planner" || currentMode === "setup")
452
+ return currentMode;
453
+ if (queuedSyntheticEvents > 0)
454
+ return "coordinator";
455
+ if (activeContinuationFlights > 0)
456
+ return "coordinator";
457
+ if (currentMode === "coordinator" && runningGoalIds > 0)
458
+ return "coordinator";
459
+ return "off";
460
+ }
461
+ export function hasParagraphBreakLiveUserMessage(text) {
462
+ return /\n[ \t]*\n/.test(text);
463
+ }
365
464
  export function isTallLiveUserMessage(text, rows) {
366
465
  return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
367
466
  }
368
467
  export function getStaticHistoryKey({ resizeKey }) {
369
468
  return `${resizeKey}`;
370
469
  }
470
+ const MIN_LIVE_AREA_ROWS = 3;
471
+ const INPUT_AREA_ROWS = 3;
472
+ const STATUS_SLOT_ROWS = 2;
473
+ const FOOTER_ONE_LINE_ROWS = 1;
474
+ const FOOTER_TWO_LINE_ROWS = 2;
475
+ const GOAL_STATUS_ROWS = 1;
476
+ const COLLAPSED_FOOTER_STATUS_ROWS = 1;
477
+ const MAX_EXPANDED_BACKGROUND_TASK_ROWS = 7;
478
+ function isAgentSpacingKind(kind) {
479
+ return [
480
+ "assistant",
481
+ "queued",
482
+ "goal_progress",
483
+ "tool_start",
484
+ "tool_done",
485
+ "tool_group",
486
+ "server_tool_start",
487
+ "server_tool_done",
488
+ "subagent_group",
489
+ ].includes(kind);
490
+ }
491
+ function isToolBoundaryKind(kind) {
492
+ return [
493
+ "goal_progress",
494
+ "tool_start",
495
+ "tool_done",
496
+ "tool_group",
497
+ "server_tool_start",
498
+ "server_tool_done",
499
+ "subagent_group",
500
+ ].includes(kind);
501
+ }
502
+ function isAgentSpacingItem(item) {
503
+ return isAgentSpacingKind(item.kind);
504
+ }
505
+ export function shouldTopSpaceAfterPrintedAgentBoundary({ currentKind, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
506
+ const needsExternalSpacing = [
507
+ "goal_progress",
508
+ "tool_start",
509
+ "tool_group",
510
+ "assistant",
511
+ "queued",
512
+ ].includes(currentKind);
513
+ if (!needsExternalSpacing)
514
+ return false;
515
+ if (previousLiveItem !== undefined)
516
+ return false;
517
+ const previousKind = lastPendingHistoryItem?.kind ?? lastHistoryItem?.kind;
518
+ return previousKind !== undefined && isAgentSpacingKind(previousKind);
519
+ }
520
+ export function shouldTopSpaceAssistantAfterToolBoundary({ text, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
521
+ if (text.trim().length === 0)
522
+ return false;
523
+ if (shouldTopSpaceAfterPrintedAgentBoundary({
524
+ currentKind: "assistant",
525
+ previousLiveItem,
526
+ lastPendingHistoryItem,
527
+ lastHistoryItem,
528
+ })) {
529
+ return true;
530
+ }
531
+ const previousKind = previousLiveItem?.kind;
532
+ return previousKind !== undefined && isToolBoundaryKind(previousKind);
533
+ }
534
+ export function shouldTopSpaceStreamingAssistant({ visibleStreamingText, lastLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
535
+ return shouldTopSpaceAssistantAfterToolBoundary({
536
+ text: visibleStreamingText,
537
+ previousLiveItem: lastLiveItem,
538
+ lastPendingHistoryItem,
539
+ lastHistoryItem,
540
+ });
541
+ }
542
+ export function getChatControlsLayoutDecision({ rows, agentRunning, activityVisible, doneStatusVisible, stallStatusVisible, exitPending, footerStatusLayout, taskBarExpanded, goalStatusEntryCount, footerFitsOnOneLine, }) {
543
+ const statusRows = activityVisible || stallStatusVisible || doneStatusVisible || agentRunning
544
+ ? STATUS_SLOT_ROWS
545
+ : 0;
546
+ const footerRows = exitPending || footerFitsOnOneLine ? FOOTER_ONE_LINE_ROWS : FOOTER_TWO_LINE_ROWS;
547
+ const goalRows = !exitPending && goalStatusEntryCount > 0 ? GOAL_STATUS_ROWS : 0;
548
+ const footerStatusRows = footerStatusLayout.stack
549
+ ? Number(footerStatusLayout.hasBackgroundTasks) + Number(footerStatusLayout.hasUpdateNotice)
550
+ : footerStatusLayout.hasBackgroundTasks || footerStatusLayout.hasUpdateNotice
551
+ ? COLLAPSED_FOOTER_STATUS_ROWS
552
+ : 0;
553
+ const expandedTaskRows = taskBarExpanded && footerStatusLayout.hasBackgroundTasks
554
+ ? MAX_EXPANDED_BACKGROUND_TASK_ROWS - COLLAPSED_FOOTER_STATUS_ROWS
555
+ : 0;
556
+ const controlsRows = statusRows + INPUT_AREA_ROWS + footerRows + goalRows + footerStatusRows + expandedTaskRows;
557
+ const maxControlsRows = Math.max(1, rows - MIN_LIVE_AREA_ROWS);
558
+ const boundedControlsRows = Math.min(controlsRows, maxControlsRows);
559
+ return {
560
+ controlsRows: boundedControlsRows,
561
+ liveAreaRows: Math.max(MIN_LIVE_AREA_ROWS, rows - boundedControlsRows),
562
+ };
563
+ }
371
564
  // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
372
565
  /** Check whether an item is still active (running spinner, pending result). */
373
566
  export function isActiveItem(item) {
@@ -386,15 +579,17 @@ export function isActiveItem(item) {
386
579
  }
387
580
  }
388
581
  /**
389
- * Partition live items into completed (flushable to Static) and still-active.
582
+ * Partition live items into completed (flushable to finalized history) and still-active.
390
583
  * Completed items precede active ones — we flush the longest contiguous prefix
391
584
  * of completed items to keep ordering stable.
392
585
  */
393
- function partitionCompleted(items) {
394
- // Find the first active item — everything before it is safe to flush
586
+ export function partitionCompleted(items) {
587
+ // Find the first active item — everything before it is safe to flush as a
588
+ // single chronological prefix. Splitting assistant text out of that prefix
589
+ // lets later tool rows print to scrollback above the message that introduced
590
+ // them, so keep the prefix intact.
395
591
  const firstActiveIdx = items.findIndex(isActiveItem);
396
592
  if (firstActiveIdx === -1) {
397
- // All items are completed
398
593
  return { flushed: items, remaining: [] };
399
594
  }
400
595
  if (firstActiveIdx === 0) {
@@ -405,6 +600,29 @@ function partitionCompleted(items) {
405
600
  remaining: items.slice(firstActiveIdx),
406
601
  };
407
602
  }
603
+ function normalizeAssistantText(text) {
604
+ return stripDoneMarkers(text).trim();
605
+ }
606
+ function isSameAssistantText(item, text) {
607
+ return item.kind === "assistant" && normalizeAssistantText(item.text) === text;
608
+ }
609
+ export function pinStreamingTextBeforeToolBoundary({ items, visibleStreamingText, thinking, thinkingMs, makeId, }) {
610
+ const text = normalizeAssistantText(visibleStreamingText);
611
+ if (text.length === 0)
612
+ return items;
613
+ if (items.some((item) => item.kind === "assistant"))
614
+ return items;
615
+ return [
616
+ ...items,
617
+ {
618
+ kind: "assistant",
619
+ text,
620
+ thinking: thinking.length > 0 ? thinking : undefined,
621
+ thinkingMs: thinking.length > 0 ? thinkingMs : undefined,
622
+ id: makeId(),
623
+ },
624
+ ];
625
+ }
408
626
  // ── Duration summary ─────────────────────────────────────
409
627
  function formatDuration(ms) {
410
628
  const totalSec = Math.round(ms / 1000);
@@ -452,8 +670,8 @@ function pickDurationVerb(toolsUsed) {
452
670
  return "Ran & investigated for";
453
671
  if (has("bash"))
454
672
  return "Executed commands for";
455
- if (hasAny("tasks", "task-output", "task-stop"))
456
- return "Managed tasks for";
673
+ if (hasAny("task-output", "task-stop"))
674
+ return "Managed background processes for";
457
675
  if (has("grep") && has("read"))
458
676
  return "Investigated for";
459
677
  if (has("grep") && has("find"))
@@ -481,65 +699,16 @@ function pickDurationVerb(toolsUsed) {
481
699
  ];
482
700
  return phrases[Math.floor(Math.random() * phrases.length)];
483
701
  }
484
- // ── Animated thinking border ────────────────────────────────
485
- const THINKING_BORDER_COLORS = ["#60a5fa", "#818cf8", "#a78bfa", "#818cf8", "#60a5fa"];
486
- // ── Task count helper ───────────────────────────────────────
487
- function getTaskCount(cwd) {
488
- try {
489
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
490
- const data = readFileSync(join(homedir(), ".gg-tasks", "projects", hash, "tasks.json"), "utf-8");
491
- const tasks = JSON.parse(data);
492
- return tasks.filter((t) => t.status !== "done").length;
493
- }
494
- catch {
495
- return 0;
496
- }
497
- }
498
- function getNextPendingTask(cwd) {
499
- try {
500
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
501
- const data = readFileSync(join(homedir(), ".gg-tasks", "projects", hash, "tasks.json"), "utf-8");
502
- const tasks = JSON.parse(data);
503
- const pending = tasks.find((t) => t.status === "pending");
504
- if (!pending)
505
- return null;
506
- return {
507
- id: pending.id,
508
- title: pending.title,
509
- prompt: pending.prompt || pending.text || pending.title,
510
- };
511
- }
512
- catch {
513
- return null;
514
- }
515
- }
516
- function markTaskInProgress(cwd, taskId) {
517
- try {
518
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
519
- const filePath = join(homedir(), ".gg-tasks", "projects", hash, "tasks.json");
520
- const data = readFileSync(filePath, "utf-8");
521
- const tasks = JSON.parse(data);
522
- const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "in-progress" } : t));
523
- writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
524
- }
525
- catch {
526
- // ignore
527
- }
528
- }
529
702
  // ── App Component ──────────────────────────────────────────
530
703
  export function App(props) {
531
704
  const theme = useTheme();
532
705
  const switchTheme = useSetTheme();
533
- const { columns, resizeKey } = useTerminalSize();
706
+ const { write: writeStdout } = useStdout();
707
+ const { columns, rows } = useTerminalSize();
534
708
  // Hoisted before terminal title hook so it can reference them
535
709
  const [lastUserMessage, setLastUserMessage] = useState("");
536
710
  const [exitPending, setExitPending] = useState(false);
537
- // Initialize from planModeRef (lives outside React in cli.ts) so plan
538
- // mode survives /clear's unmount/remount, matching the prior behavior
539
- // where /clear didn't toggle plan mode off.
540
- const [planMode, setPlanMode] = useState(props.planModeRef?.current ?? false);
541
- const planModeLocalRef = useRef(false);
542
- planModeLocalRef.current = planMode;
711
+ const [goalMode, setGoalMode] = useState(props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off");
543
712
  // Terminal title — updated later after agentLoop is created
544
713
  // (hoisted here so the hook is always called in the same order)
545
714
  const [titleRunning, setTitleRunning] = useState(false);
@@ -549,14 +718,11 @@ export function App(props) {
549
718
  isRunning: titleRunning,
550
719
  sessionTitle,
551
720
  });
552
- // Items scrolled into Static (history). For restored sessions, seed the
553
- // initial array directly matches how every other Ink chat agent passes
554
- // messages to <Static> (cat-code, harness, p90-cli, openai-chatgpt, lms,
555
- // gatsby). Ink's Static (build/components/Static.js) starts with index=0
556
- // so slice(0) returns the full array regardless of length.
721
+ // Completed transcript rows are kept as durable session data but are no longer
722
+ // rendered through Ink history. They are serialized once into real terminal
723
+ // scrollback via terminalHistoryPrinter, while Ink owns only live rows and
724
+ // controls. This avoids Static/log-update replay drift on resize/remount.
557
725
  const [history, setHistory] = useState(() => {
558
- // sessionStore wins (lives across remount). Falls back to initialHistory
559
- // (loaded from a session file at startup), then a fresh banner-only list.
560
726
  const stored = props.sessionStore?.history;
561
727
  if (stored && stored.length > 0)
562
728
  return stored;
@@ -567,24 +733,19 @@ export function App(props) {
567
733
  });
568
734
  // Items from the current/last turn — rendered in the live area so they stay visible.
569
735
  // Seed from sessionStore so Goal progress/completion rows and other live output
570
- // survive pane/overlay/resize remounts before they are flushed to <Static>.
736
+ // survive pane/overlay/resize remounts before they are finalized.
571
737
  const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
572
738
  // overlay seeded from sessionStore (lives across remount). Falls back to
573
739
  // props.initialOverlay (CLI launched with one), then null.
574
740
  const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
575
- const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
576
- const [goalCount, setGoalCount] = useState(0);
577
741
  const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
578
- const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
579
742
  const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
580
- // Seed from sessionStore so "Run All" chaining survives the resetUI()
581
- // remount that startTask() triggers between tasks.
582
- const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
583
- const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
584
- const startTaskRef = useRef(() => { });
585
743
  const agentRunningRef = useRef(false);
586
744
  const runningGoalIdsRef = useRef(new Set());
587
745
  const activeVerifierRunIdsRef = useRef(new Set());
746
+ const queuedGoalSyntheticEventsRef = useRef(0);
747
+ const goalContinuationFlightsRef = useRef(new Set());
748
+ const goalContinuationRecentChoicesRef = useRef(new Map());
588
749
  const startGoalRunRef = useRef(() => { });
589
750
  const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
590
751
  const currentPixelFixRef = useRef(null);
@@ -594,12 +755,14 @@ export function App(props) {
594
755
  const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
595
756
  // Suppress "done" status when a plan overlay is about to open
596
757
  const planOverlayPendingRef = useRef(false);
758
+ const goalSetupPanePendingRef = useRef(false);
597
759
  const [gitBranch, setGitBranch] = useState(null);
598
760
  const [currentModel, setCurrentModel] = useState(props.model);
599
761
  const [currentProvider, setCurrentProvider] = useState(props.provider);
600
762
  const [currentTools, setCurrentTools] = useState(props.tools);
601
763
  const currentToolsRef = useRef(props.tools);
602
764
  const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
765
+ const [renderMarkdown, setRenderMarkdown] = useState(true);
603
766
  const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
604
767
  const repoMapInjectionEnabledRef = useRef(true);
605
768
  const repoMapDirtyRef = useRef(true);
@@ -608,10 +771,12 @@ export function App(props) {
608
771
  const repoMapChangedCountRef = useRef(0);
609
772
  const repoMapCacheRef = useRef(createRepoMapCache());
610
773
  const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
774
+ const [goalAutoExpand, setGoalAutoExpand] = useState(props.sessionStore?.goalAutoExpand ?? false);
775
+ const goalAutoExpandRef = useRef(props.sessionStore?.goalAutoExpand ?? false);
611
776
  const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
612
777
  const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
613
778
  const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
614
- const planModeStateRef = useRef(planMode);
779
+ const goalModeStateRef = useRef(goalMode);
615
780
  // Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
616
781
  // times we've nudged the agent to continue the same step. Reset whenever a
617
782
  // new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
@@ -619,23 +784,14 @@ export function App(props) {
619
784
  const followUpNudgesRef = useRef({ step: 0, count: 0 });
620
785
  // Seed the per-item ID counter so it doesn't collide with IDs already in
621
786
  // sessionStore.history (which survives remount). Without this, a remount
622
- // (resize, overlay toggle, etc.) starts the counter at 0 and new items
623
- // generate ids "0", "1", "2"… that collide with the same ids from the
624
- // previous mount, triggering React's duplicate-key warning and causing
625
- // duplicate/omitted renders.
626
- const nextIdRef = useRef((() => {
627
- const items = [
628
- ...(props.sessionStore?.history ?? props.initialHistory ?? []),
629
- ...(props.sessionStore?.liveItems ?? []),
630
- ];
631
- let max = -1;
632
- for (const item of items) {
633
- const n = Number(item.id);
634
- if (Number.isFinite(n) && n > max)
635
- max = n;
636
- }
637
- return max + 1;
638
- })());
787
+ // (resize, overlay toggle, goal pane open, etc.) starts the counter at 0
788
+ // and new items generate ids "ui-0", "ui-1", "ui-2"… that collide with
789
+ // the same ids from the previous mount, triggering React's duplicate-key
790
+ // warning and causing duplicate/omitted renders.
791
+ const nextIdRef = useRef(getNextGeneratedItemId([
792
+ ...(props.sessionStore?.history ?? props.initialHistory ?? []),
793
+ ...(props.sessionStore?.liveItems ?? []),
794
+ ]));
639
795
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
640
796
  const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
641
797
  const persistedIndexRef = useRef(messagesRef.current.length);
@@ -666,8 +822,11 @@ export function App(props) {
666
822
  */
667
823
  const triggerAutoSetupRef = useRef(async () => { });
668
824
  const getId = () => `ui-${nextIdRef.current++}`;
825
+ const appendGoalAgentTransition = useCallback((text) => {
826
+ setLiveItems((prev) => [...prev, { kind: "goal_agent_transition", text, id: getId() }]);
827
+ }, []);
669
828
  const appendGoalProgress = useCallback((item) => {
670
- setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
829
+ setLiveItems((prev) => appendGoalProgressDraft(prev, item, getId));
671
830
  }, []);
672
831
  const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
673
832
  const clearGoalStatusEntry = useCallback((runId) => {
@@ -686,21 +845,59 @@ export function App(props) {
686
845
  return next;
687
846
  });
688
847
  }, [props.sessionStore]);
689
- // Two-phase flush: items waiting to be moved to Static history after the
690
- // live area has been cleared and Ink has committed the smaller output.
691
- const pendingFlushRef = useRef([]);
692
- const [flushGeneration, setFlushGeneration] = useState(0);
693
- /** Queue items for two-phase flush and signal the drain effect. */
848
+ const sessionStore = props.sessionStore;
849
+ const terminalHistoryContextRef = useRef({
850
+ theme,
851
+ columns,
852
+ version: props.version,
853
+ model: currentModel,
854
+ provider: currentProvider,
855
+ cwd: displayedCwd,
856
+ });
857
+ useEffect(() => {
858
+ terminalHistoryContextRef.current = {
859
+ theme,
860
+ columns,
861
+ version: props.version,
862
+ model: currentModel,
863
+ provider: currentProvider,
864
+ cwd: displayedCwd,
865
+ };
866
+ }, [theme, columns, props.version, currentModel, currentProvider, displayedCwd]);
867
+ const printHistoryItems = useCallback((items, options) => {
868
+ if (!props.terminalHistoryPrinter || items.length === 0)
869
+ return;
870
+ props.terminalHistoryPrinter.print(items, terminalHistoryContextRef.current, {
871
+ ...options,
872
+ write: writeStdout,
873
+ });
874
+ }, [props.terminalHistoryPrinter, writeStdout]);
875
+ const pendingHistoryFlushRef = useRef([]);
876
+ const streamedAssistantFlushRef = useRef({
877
+ flushedChars: 0,
878
+ text: "",
879
+ });
880
+ const [historyFlushGeneration, setHistoryFlushGeneration] = useState(0);
694
881
  const queueFlush = useCallback((items) => {
695
- if (items.length === 0)
882
+ const flushed = trimFlushedItems(items);
883
+ if (flushed.length === 0)
696
884
  return;
697
- pendingFlushRef.current = [...pendingFlushRef.current, ...items];
698
- if (props.sessionStore) {
885
+ pendingHistoryFlushRef.current = [...pendingHistoryFlushRef.current, ...flushed];
886
+ if (sessionStore) {
699
887
  const queuedIds = new Set(items.map((item) => item.id));
700
- props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
888
+ sessionStore.liveItems = (sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
701
889
  }
702
- setFlushGeneration((g) => g + 1);
703
- }, [props.sessionStore]);
890
+ setHistoryFlushGeneration((generation) => generation + 1);
891
+ }, [sessionStore]);
892
+ const finalizeSubmittedUserItem = useCallback((item) => {
893
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
894
+ setLiveItems((prev) => {
895
+ if (prev.length > 0)
896
+ queueFlush(prev);
897
+ queueFlush([item]);
898
+ return [];
899
+ });
900
+ }, [queueFlush]);
704
901
  // Mirror runtime state choices (model/provider/thinking) into renderApp's
705
902
  // closure so unmount/remount preserves them.
706
903
  const onRuntimeStateChange = props.onRuntimeStateChange;
@@ -715,12 +912,35 @@ export function App(props) {
715
912
  thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
716
913
  });
717
914
  }, [thinkingEnabled, currentModel, onRuntimeStateChange]);
915
+ useEffect(() => {
916
+ printHistoryItems(history);
917
+ }, [history, printHistoryItems]);
918
+ useEffect(() => {
919
+ const flushed = pendingHistoryFlushRef.current;
920
+ if (flushed.length === 0)
921
+ return;
922
+ pendingHistoryFlushRef.current = [];
923
+ printHistoryItems(flushed);
924
+ const flushedIds = new Set(flushed.map((item) => item.id));
925
+ setLiveItems((prev) => prev.filter((item) => !flushedIds.has(item.id)));
926
+ setHistory((prev) => {
927
+ const existingIds = new Set(prev.map((item) => item.id));
928
+ const nextItems = flushed.filter((item) => !existingIds.has(item.id));
929
+ if (nextItems.length === 0)
930
+ return prev;
931
+ const next = compactHistory([...prev, ...nextItems]);
932
+ if (sessionStore)
933
+ sessionStore.history = next;
934
+ return next;
935
+ });
936
+ }, [historyFlushGeneration, printHistoryItems, sessionStore]);
718
937
  // Mirror session state into renderApp's closure so resetUI() can re-seed
719
938
  // the conversation on remount. Each panel that previously did a bare ANSI
720
- // screen clear (overlay open/close, plan accept/reject, /clear, startTask)
939
+ // screen clear (overlay open/close, plan accept/reject, /clear)
721
940
  // now goes through resetUI; without these mirrors, the chat would vanish.
722
- const sessionStore = props.sessionStore;
941
+ const historyRef = useRef(history);
723
942
  useEffect(() => {
943
+ historyRef.current = history;
724
944
  if (sessionStore)
725
945
  sessionStore.history = history;
726
946
  }, [history, sessionStore]);
@@ -744,10 +964,19 @@ export function App(props) {
744
964
  if (sessionStore)
745
965
  sessionStore.overlay = overlay;
746
966
  }, [overlay, sessionStore]);
967
+ useEffect(() => {
968
+ goalAutoExpandRef.current = goalAutoExpand;
969
+ if (sessionStore)
970
+ sessionStore.goalAutoExpand = goalAutoExpand;
971
+ }, [goalAutoExpand, sessionStore]);
747
972
  useEffect(() => {
748
973
  if (sessionStore)
749
974
  sessionStore.goalStatusEntries = goalStatusEntries;
750
975
  }, [goalStatusEntries, sessionStore]);
976
+ useEffect(() => {
977
+ if (sessionStore)
978
+ sessionStore.goalMode = goalMode;
979
+ }, [goalMode, sessionStore]);
751
980
  // pendingAction is consumed via a useEffect AFTER agentLoop is created
752
981
  // — see below where useAgentLoop is set up.
753
982
  const pendingActionConsumedRef = useRef(false);
@@ -769,10 +998,8 @@ export function App(props) {
769
998
  void reconcileActiveGoalRuns(props.cwd, {
770
999
  isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
771
1000
  }).then(({ runs }) => {
772
- const counts = summarizeGoalCountsFromRuns(runs);
773
1001
  if (cancelled)
774
1002
  return;
775
- setGoalCount(counts.active);
776
1003
  setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
777
1004
  setGoalStatusEntries((prev) => {
778
1005
  const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
@@ -813,19 +1040,23 @@ export function App(props) {
813
1040
  useEffect(() => {
814
1041
  currentToolsRef.current = currentTools;
815
1042
  }, [currentTools]);
816
- // ── Plan mode wiring ─────────────────────────────────────
817
- // Sync planModeRef with React state
1043
+ // ── Runtime mode wiring ──────────────────────────────────
1044
+ // Sync runtime mode refs with React state.
818
1045
  useEffect(() => {
819
- planModeStateRef.current = planMode;
820
- if (props.planModeRef) {
821
- props.planModeRef.current = planMode;
822
- }
823
- }, [planMode, props.planModeRef]);
1046
+ goalModeStateRef.current = goalMode;
1047
+ if (props.goalModeRef) {
1048
+ props.goalModeRef.current = goalMode;
1049
+ }
1050
+ }, [goalMode, props.goalModeRef]);
1051
+ const setActiveGoalReferences = useCallback((references) => {
1052
+ if (props.goalReferencesRef)
1053
+ props.goalReferencesRef.current = references;
1054
+ }, [props.goalReferencesRef]);
824
1055
  const rebuildSystemPrompt = useCallback(async (options) => {
825
1056
  const approvedPlanPath = options?.clearApprovedPlan
826
1057
  ? undefined
827
1058
  : (options?.approvedPlanPath ?? approvedPlanPathRef.current);
828
- 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);
1059
+ return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills, false, approvedPlanPath, (options?.tools ?? currentToolsRef.current).map((tool) => tool.name), options?.activeLanguages ?? injectedLanguagesRef.current, options?.goalMode ?? goalModeStateRef.current);
829
1060
  }, [props.skills]);
830
1061
  const replaceSystemPrompt = useCallback(async (options) => {
831
1062
  const newPrompt = await rebuildSystemPrompt(options);
@@ -834,6 +1065,28 @@ export function App(props) {
834
1065
  }
835
1066
  return newPrompt;
836
1067
  }, [rebuildSystemPrompt]);
1068
+ const setGoalModeAndPrompt = useCallback(async (nextMode, options) => {
1069
+ goalModeStateRef.current = nextMode;
1070
+ if (props.goalModeRef)
1071
+ props.goalModeRef.current = nextMode;
1072
+ if (props.sessionStore)
1073
+ props.sessionStore.goalMode = nextMode;
1074
+ setGoalMode(nextMode);
1075
+ await replaceSystemPrompt({ ...options, goalMode: nextMode });
1076
+ }, [props.goalModeRef, props.sessionStore, replaceSystemPrompt]);
1077
+ const clearGoalModeIfIdle = useCallback(() => {
1078
+ setTimeout(() => {
1079
+ if (goalModeStateRef.current === "off")
1080
+ return;
1081
+ if (runningGoalIdsRef.current.size > 0)
1082
+ return;
1083
+ if (activeVerifierRunIdsRef.current.size > 0)
1084
+ return;
1085
+ if (queuedGoalSyntheticEventsRef.current > 0)
1086
+ return;
1087
+ void setGoalModeAndPrompt("off");
1088
+ }, 0);
1089
+ }, [setGoalModeAndPrompt]);
837
1090
  /**
838
1091
  * Unified "apply detection result" pipeline. Called from three sites:
839
1092
  * 1. Initial mount (existing project at startup).
@@ -918,53 +1171,6 @@ export function App(props) {
918
1171
  useEffect(() => {
919
1172
  void applyLanguageDetectionRef.current("initial");
920
1173
  }, []);
921
- // Rebuild system prompt when plan mode changes
922
- useEffect(() => {
923
- void replaceSystemPrompt({ planMode });
924
- }, [planMode, replaceSystemPrompt]);
925
- // Wire onEnterPlan callback ref
926
- useEffect(() => {
927
- if (props.onEnterPlanRef) {
928
- props.onEnterPlanRef.current = (reason) => {
929
- setPlanMode(true);
930
- const msg = reason ? `Plan Mode Activated — ${reason}` : "Plan Mode Activated";
931
- setLiveItems((prev) => [
932
- ...prev,
933
- { kind: "plan_transition", text: msg, active: true, id: getId() },
934
- ]);
935
- };
936
- }
937
- }, [props.onEnterPlanRef]);
938
- // Wire onExitPlan callback ref
939
- useEffect(() => {
940
- if (props.onExitPlanRef) {
941
- props.onExitPlanRef.current = async (planPath) => {
942
- // Deactivate plan mode, store approved plan path, open pane
943
- planModeStateRef.current = false;
944
- setPlanMode(false);
945
- approvedPlanPathRef.current = planPath;
946
- await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
947
- // Use setTimeout to open pane after the current tool execution completes,
948
- // so the turn can finish and the UI transitions cleanly
949
- // Flag that the plan overlay is about to open — suppresses the
950
- // premature "done" status that fires when the agent loop finishes
951
- planOverlayPendingRef.current = true;
952
- setTimeout(() => {
953
- setPlanAutoExpand(true);
954
- setOverlay("plan");
955
- // Don't clear planOverlayPendingRef here — keep it true until
956
- // the user actually approves/rejects the plan. Clearing it on a
957
- // timer causes a race where agent_done fires after the 300ms
958
- // timeout but before the user interacts, triggering a premature
959
- // completion sound.
960
- }, 300);
961
- return ("Plan submitted. Exiting plan mode.\n" +
962
- "The plan pane is opening for user review.\n" +
963
- "Plan saved at: " +
964
- planPath);
965
- };
966
- }
967
- }, [props.onExitPlanRef, replaceSystemPrompt]);
968
1174
  const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
969
1175
  const sm = sessionManagerRef.current;
970
1176
  if (!sm)
@@ -1011,7 +1217,7 @@ export function App(props) {
1011
1217
  * Other tool kinds skip detection entirely to avoid wasted filesystem stats.
1012
1218
  *
1013
1219
  * No restart required: the system prompt is mutated in place, same mechanism
1014
- * already used for plan mode + pixel-fix chdir.
1220
+ * used for pixel-fix chdir.
1015
1221
  *
1016
1222
  * Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
1017
1223
  * empty to keep agent-loop options stable) can call the freshest version.
@@ -1312,7 +1518,6 @@ export function App(props) {
1312
1518
  }, [
1313
1519
  persistNewMessages,
1314
1520
  stripRepoMapMessages,
1315
- planMode,
1316
1521
  props.cwd,
1317
1522
  props.skills,
1318
1523
  currentProvider,
@@ -1322,6 +1527,11 @@ export function App(props) {
1322
1527
  resolveCredentials,
1323
1528
  ]),
1324
1529
  onTurnText: useCallback((text, thinking, thinkingMs) => {
1530
+ if (goalModeStateRef.current === "planner") {
1531
+ return;
1532
+ }
1533
+ const hadStreamedAssistantFlush = streamedAssistantFlushRef.current.flushedChars > 0;
1534
+ const unflushedAssistantText = text.slice(streamedAssistantFlushRef.current.flushedChars);
1325
1535
  // Track [DONE:n] markers for plan step progress
1326
1536
  if (planStepsRef.current.length > 0) {
1327
1537
  const completed = findCompletedMarkers(text);
@@ -1336,15 +1546,9 @@ export function App(props) {
1336
1546
  followUpNudgesRef.current = { step: 0, count: 0 };
1337
1547
  }
1338
1548
  }
1339
- // Flush all completed items from the previous turn to Static history.
1340
- // This keeps liveItems bounded per-turn, preventing Ink's live area from
1341
- // growing unbounded, which makes Ink's live-area re-renders expensive.
1342
- //
1343
- // Items are queued in pendingFlushRef (not sent to setHistory directly)
1344
- // so the Static write happens in a SEPARATE render cycle from the
1345
- // live-area change — avoiding both Ink cursor-math clipping and the
1346
- // brief duplicate that occurred when setHistory was nested inside the
1347
- // setLiveItems updater.
1549
+ // Flush completed rows from the previous turn to finalized terminal
1550
+ // history. Ink keeps only the active turn, preventing live-area growth
1551
+ // and avoiding Static/log-update replay during resize/remount churn.
1348
1552
  setLiveItems((prev) => {
1349
1553
  const flushed = flushOnTurnText(prev);
1350
1554
  if (flushed.length > 0) {
@@ -1353,10 +1557,7 @@ export function App(props) {
1353
1557
  // Split text on [DONE:N] markers so each marker renders inline as
1354
1558
  // a styled "✓ Step N: <description>" item at the position the
1355
1559
  // agent emitted it, instead of vanishing into stripped whitespace.
1356
- // Falls back to a single assistant item containing the
1357
- // marker-stripped text when there are no markers (keeps the
1358
- // common case zero-cost).
1359
- const segments = segmentDisplayText(text, planStepsRef.current);
1560
+ const segments = segmentDisplayText(unflushedAssistantText, planStepsRef.current);
1360
1561
  const items = [];
1361
1562
  let thinkingAttached = false;
1362
1563
  for (const seg of segments) {
@@ -1369,7 +1570,7 @@ export function App(props) {
1369
1570
  // contains multiple text chunks split by markers.
1370
1571
  thinking: thinkingAttached ? undefined : thinking,
1371
1572
  thinkingMs: thinkingAttached ? undefined : thinkingMs,
1372
- planMode: planModeLocalRef.current,
1573
+ continuation: hadStreamedAssistantFlush,
1373
1574
  id: getId(),
1374
1575
  });
1375
1576
  thinkingAttached = true;
@@ -1384,36 +1585,51 @@ export function App(props) {
1384
1585
  }
1385
1586
  }
1386
1587
  // No segments at all (text was empty/whitespace, no markers).
1387
- // Still emit an assistant item so a thinking block renders if
1388
- // there was thinking content for this turn.
1588
+ // Still persist an assistant item so a thinking block renders in
1589
+ // terminal history if there was thinking content for this turn.
1389
1590
  if (items.length === 0) {
1390
1591
  items.push({
1391
1592
  kind: "assistant",
1392
1593
  text: "",
1393
1594
  thinking,
1394
1595
  thinkingMs,
1395
- planMode: planModeLocalRef.current,
1396
1596
  id: getId(),
1397
1597
  });
1398
1598
  }
1399
- return items;
1599
+ const assistantItems = prev.filter((item) => item.kind === "assistant");
1600
+ const newAssistantText = normalizeAssistantText(unflushedAssistantText);
1601
+ const duplicatePinnedText = newAssistantText.length > 0 &&
1602
+ [...assistantItems, ...pendingHistoryFlushRef.current, ...historyRef.current].some((item) => isSameAssistantText(item, newAssistantText));
1603
+ const nextItems = duplicatePinnedText
1604
+ ? items.filter((item) => !isSameAssistantText(item, newAssistantText))
1605
+ : items;
1606
+ const flushablePrev = prev.filter((item) => item.kind !== "assistant");
1607
+ if (flushablePrev.length > 0)
1608
+ queueFlush(flushablePrev);
1609
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
1610
+ return [...assistantItems, ...nextItems];
1400
1611
  });
1401
- }, []),
1402
- onToolStart: useCallback((toolCallId, name, args) => {
1612
+ }, [queueFlush]),
1613
+ onToolStart: useCallback((toolCallId, name, args, stream) => {
1403
1614
  log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
1404
1615
  const startedAt = Date.now();
1405
1616
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1406
- // Flush completed items (assistant text, finished tools) to Static
1407
- // before adding tool UI. Keeping both in the live area makes it tall
1408
- // and causes Ink's cursor math to clip the top.
1409
- setLiveItems((prev) => {
1410
- const { flushed, remaining } = partitionCompleted(prev);
1617
+ const appendToolStart = (prev) => {
1618
+ const visible = pinStreamingTextBeforeToolBoundary({
1619
+ items: prev,
1620
+ visibleStreamingText: stream.text,
1621
+ thinking: stream.thinking,
1622
+ thinkingMs: stream.thinkingMs,
1623
+ makeId: getId,
1624
+ });
1625
+ const { flushed, remaining } = partitionCompleted(visible);
1411
1626
  if (flushed.length > 0) {
1412
1627
  queueFlush(flushed);
1413
1628
  }
1414
1629
  return remaining;
1415
- });
1630
+ };
1416
1631
  if (name === "subagent") {
1632
+ setLiveItems(appendToolStart);
1417
1633
  // Create or update the sub-agent group item
1418
1634
  const newAgent = {
1419
1635
  toolCallId,
@@ -1438,25 +1654,32 @@ export function App(props) {
1438
1654
  });
1439
1655
  }
1440
1656
  else if (AGGREGATABLE_TOOLS.has(name)) {
1441
- // Group concurrent read-only tools into a single compact item
1442
1657
  setLiveItems((prev) => {
1443
- // Find an active tool group (has at least one running tool)
1444
- const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1445
- item.tools.some((t) => t.status === "running"));
1446
- if (groupIdx !== -1) {
1447
- const group = prev[groupIdx];
1448
- const next = [...prev];
1449
- next[groupIdx] = {
1450
- ...group,
1451
- tools: [
1452
- ...group.tools,
1453
- { toolCallId, name, args, status: "running", animateUntil },
1454
- ],
1455
- };
1456
- return next;
1658
+ const reusableGroupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1659
+ item.tools.every((tool) => tool.name === name && !tool.isError));
1660
+ const prior = reusableGroupIdx === -1 ? [] : prev.slice(0, reusableGroupIdx);
1661
+ if (reusableGroupIdx !== -1 && prior.every((item) => !isActiveItem(item))) {
1662
+ const flushablePrior = prior.filter((item) => item.kind !== "assistant");
1663
+ if (flushablePrior.length > 0)
1664
+ queueFlush(flushablePrior);
1665
+ const pinnedPrior = prior.filter((item) => item.kind === "assistant");
1666
+ const candidates = prev.slice(reusableGroupIdx);
1667
+ const group = candidates[0];
1668
+ return [
1669
+ ...pinnedPrior,
1670
+ {
1671
+ ...group,
1672
+ tools: [
1673
+ ...group.tools,
1674
+ { toolCallId, name, args, status: "running", animateUntil },
1675
+ ],
1676
+ },
1677
+ ...candidates.slice(1),
1678
+ ];
1457
1679
  }
1680
+ const remaining = appendToolStart(prev);
1458
1681
  return [
1459
- ...prev,
1682
+ ...remaining,
1460
1683
  {
1461
1684
  kind: "tool_group",
1462
1685
  tools: [{ toolCallId, name, args, status: "running", animateUntil }],
@@ -1467,11 +1690,11 @@ export function App(props) {
1467
1690
  }
1468
1691
  else {
1469
1692
  setLiveItems((prev) => [
1470
- ...prev,
1693
+ ...appendToolStart(prev),
1471
1694
  { kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
1472
1695
  ]);
1473
1696
  }
1474
- }, []),
1697
+ }, [queueFlush]),
1475
1698
  onToolUpdate: useCallback((toolCallId, update) => {
1476
1699
  const u = update;
1477
1700
  // Bash progress streaming — append output to tool_start item
@@ -1544,7 +1767,7 @@ export function App(props) {
1544
1767
  };
1545
1768
  const next = [...prev];
1546
1769
  next[groupIdx] = { ...group, agents: updatedAgents };
1547
- // Flush completed items to Static to keep the live area small
1770
+ // Flush completed items to finalized history to keep the live area small
1548
1771
  const { flushed, remaining } = partitionCompleted(next);
1549
1772
  if (flushed.length > 0) {
1550
1773
  queueFlush(flushed);
@@ -1603,7 +1826,7 @@ export function App(props) {
1603
1826
  ];
1604
1827
  }
1605
1828
  }
1606
- // Flush completed items to Static to keep the live area small
1829
+ // Flush completed items to finalized history to keep the live area small
1607
1830
  const { flushed, remaining } = partitionCompleted(updated);
1608
1831
  if (flushed.length > 0) {
1609
1832
  queueFlush(flushed);
@@ -1619,14 +1842,21 @@ export function App(props) {
1619
1842
  });
1620
1843
  }
1621
1844
  }, []),
1622
- onServerToolCall: useCallback((id, name, input) => {
1845
+ onServerToolCall: useCallback((id, name, input, stream) => {
1623
1846
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
1624
1847
  const startedAt = Date.now();
1625
1848
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1626
- // Flush completed items (including assistant text) to Static before
1627
- // adding server tool UI — same rationale as onToolStart.
1849
+ // Flush completed items (including assistant text) before adding server
1850
+ // tool UI — same rationale as onToolStart.
1628
1851
  setLiveItems((prev) => {
1629
- const { flushed, remaining } = partitionCompleted(prev);
1852
+ const visible = pinStreamingTextBeforeToolBoundary({
1853
+ items: prev,
1854
+ visibleStreamingText: stream.text,
1855
+ thinking: stream.thinking,
1856
+ thinkingMs: stream.thinkingMs,
1857
+ makeId: getId,
1858
+ });
1859
+ const { flushed, remaining } = partitionCompleted(visible);
1630
1860
  if (flushed.length > 0) {
1631
1861
  queueFlush(flushed);
1632
1862
  }
@@ -1643,7 +1873,7 @@ export function App(props) {
1643
1873
  },
1644
1874
  ];
1645
1875
  });
1646
- }, []),
1876
+ }, [queueFlush]),
1647
1877
  onServerToolResult: useCallback((toolUseId, resultType, data) => {
1648
1878
  log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
1649
1879
  setLiveItems((prev) => {
@@ -1677,14 +1907,14 @@ export function App(props) {
1677
1907
  },
1678
1908
  ];
1679
1909
  }
1680
- // Flush completed items to Static
1910
+ // Flush completed items to finalized history
1681
1911
  const { flushed, remaining } = partitionCompleted(updated);
1682
1912
  if (flushed.length > 0) {
1683
1913
  queueFlush(flushed);
1684
1914
  }
1685
1915
  return remaining;
1686
1916
  });
1687
- }, []),
1917
+ }, [queueFlush]),
1688
1918
  onTurnEnd: useCallback((turn, stopReason, usage) => {
1689
1919
  log("INFO", "turn", `Turn ${turn} ended`, {
1690
1920
  stopReason,
@@ -1700,8 +1930,8 @@ export function App(props) {
1700
1930
  lastActualTokensRef.current =
1701
1931
  currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
1702
1932
  lastActualTokensTimestampRef.current = Date.now();
1703
- // For tool-only turns (no text), flush completed items to Static so
1704
- // liveItems doesn't grow unbounded across consecutive tool-only turns.
1933
+ // For tool-only turns (no text), flush completed items to finalized
1934
+ // history so liveItems doesn't grow unbounded across consecutive turns.
1705
1935
  setLiveItems((prev) => {
1706
1936
  const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
1707
1937
  if (flushed.length > 0) {
@@ -1709,44 +1939,42 @@ export function App(props) {
1709
1939
  }
1710
1940
  return remaining;
1711
1941
  });
1712
- }, []),
1942
+ }, [queueFlush]),
1713
1943
  onDone: useCallback((durationMs, toolsUsed) => {
1714
1944
  log("INFO", "agent", `Agent done`, {
1715
1945
  duration: `${durationMs}ms`,
1716
1946
  toolsUsed: toolsUsed.join(",") || "none",
1717
1947
  });
1718
- // Don't show "done" status when plan overlay is about to open —
1719
- // the agent loop finished but we're waiting for user plan review
1720
- if (planOverlayPendingRef.current)
1721
- return;
1722
- setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1723
- playNotificationSound();
1724
- // Two-phase flush to avoid Ink text clipping.
1725
- // Phase 1 (here): clear the live area so Ink commits a render with
1726
- // the smaller output and updates its internal line counter.
1727
- // Phase 2 (useEffect below): push items to Static history in a
1728
- // separate render cycle so the Static write never coincides with
1729
- // a live-area height change in the same frame.
1730
- setLiveItems((prev) => {
1731
- if (prev.length > 0)
1732
- queueFlush(prev);
1733
- return [];
1948
+ const doneDecision = getDoneFlushDecision({
1949
+ planOverlayPending: planOverlayPendingRef.current,
1950
+ goalMode: goalModeStateRef.current,
1951
+ goalAutoExpand: goalAutoExpandRef.current,
1734
1952
  });
1735
- // Run-all: auto-start next pending task after a short delay
1736
- // (allow the two-phase flush to complete first)
1737
- if (runAllTasksRef.current) {
1738
- setTimeout(() => {
1739
- const cwd = cwdRef.current;
1740
- const next = getNextPendingTask(cwd);
1741
- if (next) {
1742
- markTaskInProgress(cwd, next.id);
1743
- startTaskRef.current(next.title, next.prompt, next.id);
1744
- }
1745
- else {
1746
- setRunAllTasks(false);
1747
- log("INFO", "tasks", "Run-all complete — no more pending tasks");
1748
- }
1749
- }, 500);
1953
+ // Don't show "done" status when plan/goal review panes are about to open —
1954
+ // the agent loop finished but we're waiting for user approval/review.
1955
+ // Still flush live transcript rows before the pane remounts; otherwise
1956
+ // setup output remains in ephemeral liveItems and appears to vanish.
1957
+ if (doneDecision.showDoneStatus) {
1958
+ setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1959
+ playNotificationSound();
1960
+ }
1961
+ // Finalize rows now; the sink writes them outside Ink and then the
1962
+ // live area is cleared, so there is no Static/live repaint race.
1963
+ if (doneDecision.flushLiveItems) {
1964
+ setLiveItems((prev) => {
1965
+ if (prev.length > 0)
1966
+ queueFlush(prev);
1967
+ return [];
1968
+ });
1969
+ }
1970
+ const nextGoalMode = nextGoalModeAfterAgentDone({
1971
+ currentMode: goalModeStateRef.current,
1972
+ runningGoalIds: runningGoalIdsRef.current.size,
1973
+ queuedSyntheticEvents: queuedGoalSyntheticEventsRef.current,
1974
+ activeContinuationFlights: goalContinuationFlightsRef.current.size,
1975
+ });
1976
+ if (nextGoalMode !== goalModeStateRef.current) {
1977
+ void setGoalModeAndPrompt(nextGoalMode);
1750
1978
  }
1751
1979
  // Goal loop: after the orchestrator handles a worker/verifier event,
1752
1980
  // continue the same Goal automatically until it reaches a terminal state.
@@ -1788,12 +2016,16 @@ export function App(props) {
1788
2016
  }
1789
2017
  })();
1790
2018
  }
1791
- }, []),
2019
+ }, [setGoalModeAndPrompt]),
1792
2020
  onAborted: useCallback(() => {
1793
2021
  log("WARN", "agent", "Agent run aborted by user");
1794
- setRunAllTasks(false);
1795
2022
  setRunAllPixel(false);
1796
2023
  currentPixelFixRef.current = null;
2024
+ queuedGoalSyntheticEventsRef.current = 0;
2025
+ goalSetupPanePendingRef.current = false;
2026
+ setActiveGoalReferences(undefined);
2027
+ if (goalModeStateRef.current !== "off")
2028
+ void setGoalModeAndPrompt("off");
1797
2029
  setDoneStatus(null);
1798
2030
  setLiveItems((prev) => {
1799
2031
  const next = prev.map((item) => {
@@ -1836,7 +2068,7 @@ export function App(props) {
1836
2068
  });
1837
2069
  return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
1838
2070
  });
1839
- }, []),
2071
+ }, [setActiveGoalReferences, setGoalModeAndPrompt]),
1840
2072
  onQueuedStart: useCallback((content) => {
1841
2073
  // When a queued message starts processing, show it as a UserItem
1842
2074
  // and flush prior items to history. Synthetic system events are hidden
@@ -1848,6 +2080,8 @@ export function App(props) {
1848
2080
  .map((c) => c.text)
1849
2081
  .join("\n");
1850
2082
  if (isGoalSyntheticEvent(displayText)) {
2083
+ queuedGoalSyntheticEventsRef.current = Math.max(0, queuedGoalSyntheticEventsRef.current - 1);
2084
+ void setGoalModeAndPrompt("coordinator");
1851
2085
  const eventInfo = parseGoalSyntheticEvent(displayText);
1852
2086
  setLiveItems((prev) => {
1853
2087
  if (prev.length > 0)
@@ -1870,11 +2104,6 @@ export function App(props) {
1870
2104
  const imageCount = typeof content === "string"
1871
2105
  ? undefined
1872
2106
  : content.filter((c) => c.type === "image").length || undefined;
1873
- setLiveItems((prev) => {
1874
- if (prev.length > 0)
1875
- queueFlush(prev);
1876
- return [];
1877
- });
1878
2107
  const userItem = {
1879
2108
  kind: "user",
1880
2109
  text: displayText,
@@ -1883,8 +2112,8 @@ export function App(props) {
1883
2112
  };
1884
2113
  setLastUserMessage(displayText);
1885
2114
  setDoneStatus(null);
1886
- setLiveItems([userItem]);
1887
- }, []),
2115
+ finalizeSubmittedUserItem(userItem);
2116
+ }, [appendGoalProgress, finalizeSubmittedUserItem, setGoalModeAndPrompt]),
1888
2117
  // Inject a "continue with the next step" follow-up when the agent
1889
2118
  // would otherwise stop mid-plan. The prompt-only instruction wasn't
1890
2119
  // enough — some models (notably Opus) treat each [DONE:n] as a
@@ -1954,27 +2183,6 @@ export function App(props) {
1954
2183
  ]);
1955
2184
  }
1956
2185
  };
1957
- // Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
1958
- // and Ink renders the smaller live area (updating its internal line
1959
- // counter), this effect pushes the stashed items into Static history.
1960
- // Because the Static write happens in a SEPARATE render cycle from the
1961
- // live-area shrink, Ink's log-update never needs to erase the old tall
1962
- // live area AND write Static content in the same frame — avoiding the
1963
- // cursor-math mismatch that caused text clipping.
1964
- useEffect(() => {
1965
- if (pendingFlushRef.current.length > 0) {
1966
- const items = pendingFlushRef.current;
1967
- pendingFlushRef.current = [];
1968
- setHistory((h) => {
1969
- const next = compactHistory([...h, ...trimFlushedItems(items)]);
1970
- if (sessionStore)
1971
- sessionStore.history = next;
1972
- return next;
1973
- });
1974
- if (sessionStore)
1975
- sessionStore.liveItems = liveItems;
1976
- }
1977
- }, [flushGeneration]);
1978
2186
  // Sync terminal title with agent loop state
1979
2187
  useEffect(() => {
1980
2188
  setTitleRunning(agentLoop.isRunning);
@@ -2002,19 +2210,29 @@ export function App(props) {
2002
2210
  return () => clearTimeout(timer);
2003
2211
  }
2004
2212
  }, [agentLoop.isRunning, sessionStore, props.resetUI]);
2005
- // Consume sessionStore.pendingAction once on mount. Set by resetUI options
2006
- // for paths that remount AND immediately drive the agent (plan accept,
2007
- // plan reject, startTask, pixel fix). The action survives the unmount
2008
- // because it lives in renderApp's closure (sessionStore), not React state.
2213
+ // Consume pending post-remount work once on mount. Set by resetUI options
2214
+ // for paths that remount AND immediately drive work (plan accept/reject,
2215
+ // pixel fix, Goal approval). The work survives the unmount because
2216
+ // it lives in renderApp's closure (sessionStore), not React state.
2009
2217
  useEffect(() => {
2010
2218
  if (pendingActionConsumedRef.current)
2011
2219
  return;
2012
2220
  const action = sessionStore?.pendingAction;
2013
- if (!action)
2221
+ const pendingGoalRun = sessionStore?.pendingGoalRun;
2222
+ if (!action && !pendingGoalRun)
2014
2223
  return;
2015
2224
  pendingActionConsumedRef.current = true;
2016
- if (sessionStore)
2225
+ if (sessionStore) {
2017
2226
  sessionStore.pendingAction = undefined;
2227
+ sessionStore.pendingGoalRun = undefined;
2228
+ }
2229
+ setDoneStatus(null);
2230
+ if (pendingGoalRun) {
2231
+ startGoalRunRef.current(pendingGoalRun);
2232
+ return;
2233
+ }
2234
+ if (!action)
2235
+ return;
2018
2236
  if (action.planEvent) {
2019
2237
  const ev = action.planEvent;
2020
2238
  setLiveItems((prev) => [
@@ -2028,7 +2246,6 @@ export function App(props) {
2028
2246
  { kind: "info", text: action.infoText, id: getId() },
2029
2247
  ]);
2030
2248
  }
2031
- setDoneStatus(null);
2032
2249
  void agentLoop.run(action.prompt).catch((err) => {
2033
2250
  const errMsg = err instanceof Error ? err.message : String(err);
2034
2251
  log("ERROR", "error", errMsg);
@@ -2036,14 +2253,6 @@ export function App(props) {
2036
2253
  });
2037
2254
  // Intentional one-shot: run once on mount, never re-fire on re-render.
2038
2255
  }, []);
2039
- // Refresh eyes badge count when the agent settles (end of a turn) — a turn
2040
- // may have logged new rough/wish/blocked signals. Also covers the case where
2041
- // /eyes was run for the first time (manifest now exists).
2042
- useEffect(() => {
2043
- if (!agentLoop.isRunning) {
2044
- setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
2045
- }
2046
- }, [agentLoop.isRunning, props.cwd]);
2047
2256
  const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
2048
2257
  const trimmed = input.trim();
2049
2258
  if (trimmed.startsWith("/")) {
@@ -2098,7 +2307,8 @@ export function App(props) {
2098
2307
  }
2099
2308
  // Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
2100
2309
  // React state in place without touching terminal scrollback.
2101
- pendingFlushRef.current = [];
2310
+ pendingHistoryFlushRef.current = [];
2311
+ props.terminalHistoryPrinter?.clear();
2102
2312
  setHistory([{ kind: "banner", id: "banner" }]);
2103
2313
  setLiveItems([]);
2104
2314
  setDoneStatus(null);
@@ -2121,44 +2331,19 @@ export function App(props) {
2121
2331
  setOverlay("theme");
2122
2332
  return;
2123
2333
  }
2124
- // Open the Eyes pane read-only review of installed probes + open signals.
2125
- // Gated by the ggcoder-eyes manifest: in projects without /eyes set up,
2126
- // there's nothing useful to show.
2127
- if (trimmed === "/eyes-view" || trimmed === "/ev") {
2128
- if (!isEyesActive(props.cwd)) {
2129
- setLiveItems((prev) => [
2130
- ...prev,
2334
+ // Handle /markdownGemini-style rendered/raw markdown toggle
2335
+ if (trimmed === "/markdown" || trimmed === "/md") {
2336
+ setRenderMarkdown((prev) => {
2337
+ const next = !prev;
2338
+ setLiveItems([
2131
2339
  {
2132
2340
  kind: "info",
2133
- text: "Eyes not set up in this project. Run /setup-eyes to get started.",
2341
+ text: next ? "Rendered markdown mode." : "Raw markdown mode.",
2134
2342
  id: getId(),
2135
2343
  },
2136
2344
  ]);
2137
- return;
2138
- }
2139
- setOverlay("eyes");
2140
- return;
2141
- }
2142
- // Handle /plan — toggle plan mode
2143
- if (trimmed === "/plan" || trimmed === "/plan on") {
2144
- setPlanMode(true);
2145
- setLiveItems((prev) => [
2146
- ...prev,
2147
- { kind: "plan_transition", text: "Plan Mode Activated", active: true, id: getId() },
2148
- ]);
2149
- return;
2150
- }
2151
- if (trimmed === "/plan off") {
2152
- setPlanMode(false);
2153
- setLiveItems((prev) => [
2154
- ...prev,
2155
- {
2156
- kind: "plan_transition",
2157
- text: "Plan Mode Deactivated",
2158
- active: false,
2159
- id: getId(),
2160
- },
2161
- ]);
2345
+ return next;
2346
+ });
2162
2347
  return;
2163
2348
  }
2164
2349
  // Handle /clearplan — dismiss the approved plan
@@ -2217,55 +2402,45 @@ export function App(props) {
2217
2402
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2218
2403
  props.sessionStore.overlay = "goal";
2219
2404
  props.sessionStore.planAutoExpand = false;
2405
+ props.sessionStore.goalAutoExpand = false;
2220
2406
  props.resetUI();
2221
2407
  }
2222
2408
  else {
2223
2409
  if (props.sessionStore) {
2224
2410
  props.sessionStore.overlay = "goal";
2225
2411
  props.sessionStore.planAutoExpand = false;
2412
+ props.sessionStore.goalAutoExpand = false;
2226
2413
  if (agentLoop.isRunning)
2227
2414
  props.sessionStore.pendingResetUI = true;
2228
2415
  }
2229
2416
  setPlanAutoExpand(false);
2417
+ setGoalAutoExpand(false);
2230
2418
  setOverlay("goal");
2231
2419
  }
2232
2420
  return;
2233
2421
  }
2234
- // Handle /plans — open plan pane
2235
- if (trimmed === "/plans") {
2236
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2237
- props.sessionStore.overlay = "plan";
2238
- props.sessionStore.planAutoExpand = false;
2239
- props.resetUI();
2240
- }
2241
- else {
2242
- if (props.sessionStore) {
2243
- props.sessionStore.overlay = "plan";
2244
- props.sessionStore.planAutoExpand = false;
2245
- if (agentLoop.isRunning)
2246
- props.sessionStore.pendingResetUI = true;
2247
- }
2248
- setPlanAutoExpand(false);
2249
- setOverlay("plan");
2250
- }
2251
- return;
2252
- }
2253
2422
  // Handle prompt-template commands (built-in + custom from .gg/commands/)
2254
2423
  const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
2255
2424
  if (promptCommandRoute) {
2256
2425
  const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
2257
2426
  log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
2258
- // Move live items into history before starting
2259
- setLiveItems((prev) => {
2260
- if (prev.length > 0) {
2261
- pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
2262
- }
2263
- return [];
2264
- });
2265
2427
  const hasImages = inputImages.length > 0;
2428
+ const isGoalSetupCommand = isGoalPromptCommandName(cmdName);
2429
+ let promptForAgent = fullPrompt;
2430
+ if (isGoalSetupCommand) {
2431
+ const referenceContext = await buildGoalReferenceContext({
2432
+ cwd: props.cwd,
2433
+ originalGoalPrompt: fullPrompt,
2434
+ attachments: inputImages,
2435
+ });
2436
+ setActiveGoalReferences(referenceContext.references);
2437
+ promptForAgent = referenceContext.promptSection
2438
+ ? `${fullPrompt}\n\n${referenceContext.promptSection}`
2439
+ : fullPrompt;
2440
+ }
2266
2441
  const modelInfo = getModel(currentModel);
2267
2442
  const modelSupportsImages = modelInfo?.supportsImages ?? true;
2268
- const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
2443
+ const userContent = buildUserContentWithAttachments(promptForAgent, inputImages, modelSupportsImages);
2269
2444
  // Show the typed command as the user message
2270
2445
  const userItem = {
2271
2446
  kind: "user",
@@ -2275,15 +2450,30 @@ export function App(props) {
2275
2450
  };
2276
2451
  setLastUserMessage(trimmed);
2277
2452
  setDoneStatus(null);
2278
- setLiveItems([userItem]);
2453
+ finalizeSubmittedUserItem(userItem);
2279
2454
  // Send the full prompt to the agent, with user args appended if provided
2280
2455
  try {
2281
- await agentLoop.run(userContent);
2456
+ if (isGoalSetupCommand) {
2457
+ goalSetupPanePendingRef.current = true;
2458
+ await runGoalPromptSetupSequence({
2459
+ userContent,
2460
+ fullPrompt: promptForAgent,
2461
+ messagesRef,
2462
+ setGoalModeAndPrompt,
2463
+ runAgent: (content) => agentLoop.run(content),
2464
+ onStage: appendGoalAgentTransition,
2465
+ });
2466
+ }
2467
+ else {
2468
+ await agentLoop.run(userContent);
2469
+ }
2282
2470
  }
2283
2471
  catch (err) {
2284
2472
  const msg = err instanceof Error ? err.message : String(err);
2285
2473
  log("ERROR", "error", msg);
2286
2474
  const isAbort = msg.includes("aborted") || msg.includes("abort");
2475
+ if (isGoalSetupCommand)
2476
+ goalSetupPanePendingRef.current = false;
2287
2477
  setLiveItems((prev) => [
2288
2478
  ...prev,
2289
2479
  isAbort
@@ -2291,6 +2481,41 @@ export function App(props) {
2291
2481
  : toErrorItem(err, getId()),
2292
2482
  ]);
2293
2483
  }
2484
+ finally {
2485
+ if (isGoalSetupCommand) {
2486
+ setActiveGoalReferences(undefined);
2487
+ const paneTransition = getGoalSetupPaneTransitionAfterRun({
2488
+ isGoalSetupCommand,
2489
+ setupPanePending: goalSetupPanePendingRef.current,
2490
+ });
2491
+ goalSetupPanePendingRef.current = false;
2492
+ if (goalModeStateRef.current !== "off") {
2493
+ await setGoalModeAndPrompt("off");
2494
+ }
2495
+ if (paneTransition) {
2496
+ goalAutoExpandRef.current = paneTransition.goalAutoExpand;
2497
+ setTimeout(() => {
2498
+ const resetUI = props.resetUI;
2499
+ const sessionStore = props.sessionStore;
2500
+ if (shouldResetUIForGoalSetupPaneTransition({
2501
+ hasResetUI: resetUI !== undefined,
2502
+ hasSessionStore: sessionStore !== undefined,
2503
+ }) &&
2504
+ resetUI &&
2505
+ sessionStore) {
2506
+ sessionStore.overlay = paneTransition.overlay;
2507
+ sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
2508
+ sessionStore.planAutoExpand = paneTransition.planAutoExpand;
2509
+ resetUI();
2510
+ return;
2511
+ }
2512
+ setGoalAutoExpand(paneTransition.goalAutoExpand);
2513
+ setPlanAutoExpand(paneTransition.planAutoExpand);
2514
+ setOverlay(paneTransition.overlay);
2515
+ }, 300);
2516
+ }
2517
+ }
2518
+ }
2294
2519
  // Reload custom commands in case a setup command created new ones
2295
2520
  reloadCustomCommands();
2296
2521
  return;
@@ -2326,17 +2551,6 @@ export function App(props) {
2326
2551
  setLiveItems((prev) => [...prev, queuedItem]);
2327
2552
  return;
2328
2553
  }
2329
- // Move any remaining live items into history (Static) before starting a
2330
- // new turn. Must go through queueFlush so flushGeneration bumps and the
2331
- // drain effect actually runs — mutating pendingFlushRef directly here
2332
- // stashed items that nothing was signalled to pick up, so they sat in
2333
- // limbo until some unrelated later code path happened to call queueFlush.
2334
- setLiveItems((prev) => {
2335
- if (prev.length > 0) {
2336
- queueFlush(prev);
2337
- }
2338
- return [];
2339
- });
2340
2554
  // Build display text — strip image paths, show badges instead
2341
2555
  let displayText = input;
2342
2556
  if (hasImages) {
@@ -2358,7 +2572,7 @@ export function App(props) {
2358
2572
  planStepsRef.current = [];
2359
2573
  setPlanSteps([]);
2360
2574
  }
2361
- setLiveItems([userItem]);
2575
+ finalizeSubmittedUserItem(userItem);
2362
2576
  // Run agent
2363
2577
  try {
2364
2578
  await agentLoop.run(userContent);
@@ -2376,11 +2590,20 @@ export function App(props) {
2376
2590
  }
2377
2591
  }, [
2378
2592
  agentLoop,
2379
- props.onSlashCommand,
2593
+ appendGoalAgentTransition,
2380
2594
  compactConversation,
2595
+ currentModel,
2596
+ finalizeSubmittedUserItem,
2597
+ props.cwd,
2598
+ props.onSlashCommand,
2599
+ props.resetUI,
2600
+ props.sessionStore,
2381
2601
  rebuildSystemPrompt,
2382
- replaceSystemPrompt,
2383
2602
  refreshRepoMap,
2603
+ reloadCustomCommands,
2604
+ replaceSystemPrompt,
2605
+ setActiveGoalReferences,
2606
+ setGoalModeAndPrompt,
2384
2607
  stripRepoMapMessages,
2385
2608
  ]);
2386
2609
  const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
@@ -2518,90 +2741,96 @@ export function App(props) {
2518
2741
  const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
2519
2742
  const fromPrompt = (name) => {
2520
2743
  const c = promptByName.get(name);
2521
- return c ? { name: c.name, aliases: c.aliases, description: c.description } : null;
2744
+ return c
2745
+ ? {
2746
+ name: c.name,
2747
+ aliases: c.aliases,
2748
+ description: c.description,
2749
+ sectionTitle: "workflows",
2750
+ }
2751
+ : null;
2522
2752
  };
2523
2753
  const promptOrder = [
2524
2754
  // Project audits / one-shot analysis
2525
2755
  "goal",
2526
2756
  "init",
2527
- "research",
2528
- "scan",
2529
- "verify",
2530
2757
  "expand",
2531
2758
  "bullet-proof",
2532
- "simplify",
2533
2759
  "compare",
2534
- "batch",
2535
2760
  // Setup / installers
2536
- "setup-lint",
2537
- "setup-tests",
2538
2761
  "setup-commit",
2539
- "setup-update",
2540
- "setup-eyes",
2541
- "eyes-improve",
2542
2762
  "setup-skills",
2543
2763
  ];
2544
2764
  const orderedPromptCommands = promptOrder
2545
2765
  .map(fromPrompt)
2546
2766
  .filter((c) => c !== null);
2547
2767
  const knownPromptNames = new Set(promptOrder);
2548
- const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({ name: c.name, aliases: c.aliases, description: c.description }));
2768
+ const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
2769
+ name: c.name,
2770
+ aliases: c.aliases,
2771
+ description: c.description,
2772
+ sectionTitle: "workflows",
2773
+ }));
2549
2774
  return [
2550
2775
  // Session actions (most frequent)
2551
- { name: "model", aliases: ["m"], description: "Switch model" },
2552
- { name: "compact", aliases: ["c"], description: "Compact conversation" },
2553
- { name: "clear", aliases: [], description: "Clear session and terminal" },
2554
- { name: "theme", aliases: ["t"], description: "Switch theme" },
2555
- { name: "plans", aliases: [], description: "Open plans pane" },
2776
+ { name: "model", aliases: ["m"], description: "Switch model", sectionTitle: "built-in" },
2777
+ { name: "compact", aliases: ["c"], description: "Compact context", sectionTitle: "built-in" },
2778
+ { name: "clear", aliases: [], description: "Clear session", sectionTitle: "built-in" },
2779
+ { name: "theme", aliases: ["t"], description: "Switch theme", sectionTitle: "built-in" },
2556
2780
  ...orderedPromptCommands,
2557
2781
  ...remainingPromptCommands,
2558
2782
  ...customCommands.map((cmd) => ({
2559
2783
  name: cmd.name,
2560
2784
  aliases: [],
2561
2785
  description: cmd.description,
2786
+ sectionTitle: "custom",
2562
2787
  })),
2563
- { name: "quit", aliases: ["q", "exit"], description: "Exit the agent" },
2788
+ {
2789
+ name: "quit",
2790
+ aliases: ["q", "exit"],
2791
+ description: "Exit ggcoder",
2792
+ sectionTitle: "built-in",
2793
+ },
2564
2794
  ];
2565
2795
  }, [customCommands]);
2566
- const renderItem = (item) => {
2796
+ const normalizeStatusText = (text) => text.replace(/\\n/g, "\n").replace(/^\n+|\n+$/g, "");
2797
+ const renderStatusMessage = (key, glyph, content, glyphColor = theme.commandColor, options = {}) => (_jsxs(Box, { flexDirection: "row", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: glyphColor, bold: options.bold ?? true, children: glyph }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(Text, { color: options.muted ? theme.textDim : theme.commandColor, bold: options.bold, wrap: "wrap", children: content }) })] }, key));
2798
+ const renderItem = (item, index, items) => {
2799
+ const previousLiveItem = index > 0 ? items[index - 1] : undefined;
2800
+ const shouldTopSpacePrintedBoundary = shouldTopSpaceAfterPrintedAgentBoundary({
2801
+ currentKind: item.kind,
2802
+ previousLiveItem,
2803
+ lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
2804
+ lastHistoryItem: history.at(-1),
2805
+ });
2806
+ const assistantMarginTop = item.kind === "assistant" &&
2807
+ (shouldTopSpacePrintedBoundary ||
2808
+ shouldTopSpaceAssistantAfterToolBoundary({
2809
+ text: item.text,
2810
+ previousLiveItem,
2811
+ lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
2812
+ lastHistoryItem: history.at(-1),
2813
+ }))
2814
+ ? 1
2815
+ : 0;
2816
+ const withPrintedBoundarySpacing = (node) => shouldTopSpacePrintedBoundary ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: node }, `${item.id}-printed-boundary`)) : (node);
2567
2817
  switch (item.kind) {
2568
2818
  case "tombstone":
2569
2819
  return null;
2570
2820
  case "banner":
2571
- return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
2821
+ return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd }, item.id));
2572
2822
  case "user":
2573
2823
  return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
2574
- case "task":
2575
- 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));
2576
2824
  case "goal":
2577
2825
  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));
2578
2826
  case "goal_progress": {
2579
- const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
2580
- const color = isError
2581
- ? theme.error
2582
- : item.phase === "worker_finished"
2583
- ? theme.success
2584
- : item.phase === "verifier_finished"
2585
- ? theme.accent
2586
- : item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
2587
- ? theme.secondary
2588
- : item.phase === "continuing"
2589
- ? theme.warning
2590
- : item.phase === "verifier_started"
2591
- ? theme.accent
2592
- : item.phase === "worker_started"
2593
- ? theme.primary
2594
- : item.phase === "terminal"
2595
- ? theme.success
2596
- : theme.primary;
2597
- const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
2598
- ? "✓ "
2599
- : item.phase === "terminal"
2600
- ? item.status === "passed"
2601
- ? "◆ "
2602
- : "! "
2603
- : "↻ ";
2604
- 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));
2827
+ const color = goalProgressColor(item, theme);
2828
+ const loaderStatus = goalProgressLoaderStatus(item);
2829
+ const hasBody = !!item.detail ||
2830
+ (item.summaryRows !== undefined && item.summaryRows.length > 0) ||
2831
+ (item.summarySections !== undefined && item.summarySections.length > 0);
2832
+ const headerContentWidth = Math.max(10, columns - 3);
2833
+ return withPrintedBoundarySpacing(_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(ToolUseLoader, { status: loaderStatus, staticDisplay: true }), _jsx(Box, { flexGrow: 1, width: headerContentWidth, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? (_jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] })) : null] }) })] }), hasBody ? (_jsx(MessageResponse, { children: _jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.detail })) : null, item.summaryRows?.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(12) }), _jsx(Text, { color: theme.text, children: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", row.detail] }) : null] }, row.label))), item.summarySections?.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: theme.textDim, bold: true, children: section.title }), section.lines.map((line, sectionLineIndex) => (_jsx(Text, { color: theme.text, wrap: "wrap", children: `• ${line}` }, `${section.title}-${sectionLineIndex}`)))] }, section.title)))] }) })) : null] }, item.id));
2605
2834
  }
2606
2835
  case "style_pack": {
2607
2836
  const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
@@ -2611,61 +2840,60 @@ export function App(props) {
2611
2840
  case "setup_hint":
2612
2841
  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));
2613
2842
  case "assistant":
2614
- return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
2843
+ return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, renderMarkdown: renderMarkdown, availableTerminalHeight: measuredLiveAreaRows, marginTop: assistantMarginTop }, item.id));
2615
2844
  case "tool_start":
2616
- return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
2845
+ return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
2617
2846
  case "tool_done":
2618
- return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
2847
+ return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
2619
2848
  case "tool_group":
2620
- return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
2849
+ return withPrintedBoundarySpacing(_jsx(ToolGroupExecution, { tools: item.tools }, item.id));
2621
2850
  case "server_tool_start":
2622
- return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
2851
+ return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
2623
2852
  case "server_tool_done":
2624
- return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
2853
+ return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
2625
2854
  case "error": {
2626
2855
  const showMessage = item.message && item.message !== item.headline;
2627
- 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));
2856
+ return (_jsxs(Box, { flexDirection: "row", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: theme.error, bold: true, children: "✗ " }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(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));
2628
2857
  }
2629
2858
  case "info":
2630
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
2859
+ return renderStatusMessage(item.id, "○ ", item.text, theme.commandColor, { muted: true });
2631
2860
  case "update_notice":
2632
- 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));
2861
+ return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.commandColor, paddingX: 1, children: _jsxs(Text, { color: theme.commandColor, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }, item.id));
2633
2862
  case "plan_transition":
2634
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
2863
+ return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2864
+ case "goal_agent_transition":
2865
+ return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2635
2866
  case "thinking_transition": {
2636
- const glyphColor = item.active ? THINKING_BORDER_COLORS[0] : theme.textDim;
2637
- 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));
2638
- }
2639
- case "model_transition": {
2640
- const glyphColor = THINKING_BORDER_COLORS[0];
2641
- 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));
2642
- }
2643
- case "theme_transition": {
2644
- const glyphColor = THINKING_BORDER_COLORS[0];
2645
- 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));
2867
+ const glyphColor = item.active ? theme.commandColor : theme.textDim;
2868
+ return renderStatusMessage(item.id, "✻ ", item.active ? "Thinking ON" : "Thinking OFF", glyphColor, { bold: true, muted: !item.active });
2646
2869
  }
2870
+ case "model_transition":
2871
+ return renderStatusMessage(item.id, "▸ ", _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: "Switched to " }), _jsx(Text, { color: theme.commandColor, bold: true, children: item.modelName })] }), theme.commandColor, { bold: true });
2872
+ case "theme_transition":
2873
+ return renderStatusMessage(item.id, "◐ ", _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: "Theme switched to " }), _jsx(Text, { color: theme.commandColor, bold: true, children: item.themeName })] }), theme.commandColor, { bold: true });
2647
2874
  case "plan_event": {
2648
- // Plan-domain status changes (approve / reject / dismiss). Uses
2649
- // theme.planPrimary to match the existing plan_transition family,
2650
- // distinct from the model/thinking gradient.
2875
+ // Plan-domain status changes (approve / reject / dismiss). Use the
2876
+ // command accent so transient TUI status rows share one purple voice.
2651
2877
  const label = item.event === "approved"
2652
2878
  ? "Plan approved"
2653
2879
  : item.event === "rejected"
2654
2880
  ? "Plan rejected"
2655
2881
  : "Plan dismissed";
2656
- 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));
2882
+ return renderStatusMessage(item.id, "○ ", _jsxs(_Fragment, { children: [_jsx(Text, { children: label }), item.detail ? _jsx(Text, { color: theme.textDim, children: ` — "${item.detail}"` }) : null] }), theme.commandColor, { bold: true });
2657
2883
  }
2658
2884
  case "stopped":
2659
2885
  // Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
2660
2886
  // Muted dim treatment — this is an ack, not a state change worth a
2661
2887
  // gradient. Glyph `⊘` reads as "stop" without being alarming.
2662
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.textDim, bold: true, children: ["⊘ ", item.text] }) }, item.id));
2888
+ return renderStatusMessage(item.id, "⊘ ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2663
2889
  case "step_done":
2664
2890
  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));
2665
- case "queued":
2666
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.warning, bold: true, children: "• " }), _jsx(Text, { color: theme.textDim, children: "Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
2667
- ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
2668
- : ""] })] }, item.id));
2891
+ case "queued": {
2892
+ const suffix = item.imageCount
2893
+ ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
2894
+ : "";
2895
+ return withPrintedBoundarySpacing(_jsxs(Box, { flexDirection: "row", paddingLeft: 1, marginTop: 1, flexShrink: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: theme.warning, bold: true, children: "• " }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Text, { color: theme.text, wrap: "wrap", children: [_jsx(Text, { color: theme.textDim, children: "Queued: " }), item.text || "(empty)", suffix] }) })] }, item.id));
2896
+ }
2669
2897
  case "compacting":
2670
2898
  return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
2671
2899
  case "compacted":
@@ -2673,87 +2901,16 @@ export function App(props) {
2673
2901
  case "duration":
2674
2902
  return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
2675
2903
  case "subagent_group":
2676
- return _jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id);
2904
+ return withPrintedBoundarySpacing(_jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id));
2677
2905
  }
2678
2906
  };
2679
- // ── Start a task (shared by manual "work on it" and run-all) ──
2680
- const startTask = useCallback((title, prompt, taskId) => {
2681
- setTaskCount(getTaskCount(props.cwd));
2682
- const shortId = taskId.slice(0, 8);
2683
- const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2684
- `tasks({ action: "done", id: "${shortId}" })`;
2685
- const fullPrompt = prompt + completionHint;
2686
- if (props.resetUI && props.sessionStore) {
2687
- // Preserve the current system prompt (may differ from the launch
2688
- // config — e.g. plan mode toggled or skills changed).
2689
- const sysMsg = messagesRef.current[0];
2690
- const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
2691
- const taskItem = { kind: "task", title, id: String(nextIdRef.current++) };
2692
- const sm = sessionManagerRef.current;
2693
- void (async () => {
2694
- let newSessionPath;
2695
- if (sm) {
2696
- try {
2697
- const s = await sm.create(props.cwd, currentProvider, currentModel);
2698
- newSessionPath = s.path;
2699
- log("INFO", "tasks", "New session for task", { path: s.path });
2700
- }
2701
- catch {
2702
- // session creation is best-effort
2703
- }
2704
- }
2705
- if (props.sessionStore)
2706
- props.sessionStore.overlay = null;
2707
- props.resetUI?.({
2708
- wipeSession: true,
2709
- messages: newMessages,
2710
- history: [{ kind: "banner", id: "banner" }, taskItem],
2711
- sessionPath: newSessionPath,
2712
- pendingAction: { prompt: fullPrompt },
2713
- });
2714
- })();
2715
- return;
2716
- }
2717
- // Fallback path (resetUI not wired — tests).
2718
- setHistory([{ kind: "banner", id: "banner" }]);
2719
- setLiveItems([]);
2720
- messagesRef.current = messagesRef.current.slice(0, 1);
2721
- agentLoop.reset();
2722
- persistedIndexRef.current = messagesRef.current.length;
2723
- const sm = sessionManagerRef.current;
2724
- if (sm) {
2725
- void sm.create(props.cwd, currentProvider, currentModel).then((s) => {
2726
- sessionPathRef.current = s.path;
2727
- log("INFO", "tasks", "New session for task", { path: s.path });
2728
- });
2729
- }
2730
- const taskItem = { kind: "task", title, id: getId() };
2731
- setLastUserMessage(title);
2732
- setDoneStatus(null);
2733
- setLiveItems([taskItem]);
2734
- void (async () => {
2735
- try {
2736
- await agentLoop.run(fullPrompt);
2737
- }
2738
- catch (err) {
2739
- const msg = err instanceof Error ? err.message : String(err);
2740
- log("ERROR", "error", msg);
2741
- const isAbort = msg.includes("aborted") || msg.includes("abort");
2742
- setLiveItems((prev) => [
2743
- ...prev,
2744
- isAbort
2745
- ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2746
- : toErrorItem(err, getId()),
2747
- ]);
2748
- setRunAllTasks(false);
2749
- }
2750
- })();
2751
- }, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
2752
2907
  const openOverlay = useCallback((kind) => {
2753
2908
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2754
2909
  props.sessionStore.overlay = kind;
2755
2910
  if (kind !== "plan")
2756
2911
  props.sessionStore.planAutoExpand = false;
2912
+ if (kind !== "goal")
2913
+ props.sessionStore.goalAutoExpand = false;
2757
2914
  props.resetUI();
2758
2915
  }
2759
2916
  else {
@@ -2761,12 +2918,16 @@ export function App(props) {
2761
2918
  props.sessionStore.overlay = kind;
2762
2919
  if (kind !== "plan")
2763
2920
  props.sessionStore.planAutoExpand = false;
2921
+ if (kind !== "goal")
2922
+ props.sessionStore.goalAutoExpand = false;
2764
2923
  if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
2765
2924
  props.sessionStore.pendingResetUI = true;
2766
2925
  }
2767
2926
  }
2768
2927
  if (kind !== "plan")
2769
2928
  setPlanAutoExpand(false);
2929
+ if (kind !== "goal")
2930
+ setGoalAutoExpand(false);
2770
2931
  setOverlay(kind);
2771
2932
  }
2772
2933
  }, [agentLoop.isRunning, props]);
@@ -2788,6 +2949,8 @@ export function App(props) {
2788
2949
  ? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
2789
2950
  : `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
2790
2951
  if (agentRunningRef.current) {
2952
+ queuedGoalSyntheticEventsRef.current += 1;
2953
+ void setGoalModeAndPrompt("coordinator");
2791
2954
  appendGoalProgress({
2792
2955
  kind: "goal_progress",
2793
2956
  phase: "orchestrator_reviewing",
@@ -2809,12 +2972,19 @@ export function App(props) {
2809
2972
  });
2810
2973
  setLastUserMessage("");
2811
2974
  setDoneStatus(null);
2812
- void agentLoop.run(eventText).catch((err) => {
2975
+ void (async () => {
2976
+ await setGoalModeAndPrompt("coordinator");
2977
+ await agentLoop.run(eventText);
2978
+ })().catch((err) => {
2813
2979
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2814
2980
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2981
+ clearGoalModeIfIdle();
2815
2982
  });
2816
- }, [agentLoop, appendGoalProgress]);
2983
+ }, [agentLoop, appendGoalProgress, clearGoalModeIfIdle, setGoalModeAndPrompt]);
2817
2984
  const continueGoalRun = useCallback((runId) => {
2985
+ if (goalContinuationFlightsRef.current.has(runId))
2986
+ return;
2987
+ goalContinuationFlightsRef.current.add(runId);
2818
2988
  void (async () => {
2819
2989
  const latestRun = await reconcileActiveGoalRuns(props.cwd, {
2820
2990
  isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
@@ -2822,11 +2992,24 @@ export function App(props) {
2822
2992
  if (!latestRun) {
2823
2993
  runningGoalIdsRef.current.delete(runId);
2824
2994
  clearGoalStatusEntry(runId);
2995
+ clearGoalModeIfIdle();
2825
2996
  return;
2826
2997
  }
2827
2998
  const decision = decideGoalNextAction(latestRun);
2828
2999
  if (decision.kind === "wait")
2829
3000
  return;
3001
+ const choiceKey = getGoalContinuationChoiceKey({ runId: latestRun.id, decision });
3002
+ const now = Date.now();
3003
+ const recentChoiceAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
3004
+ if (recentChoiceAt !== undefined && now - recentChoiceAt < 5000)
3005
+ return;
3006
+ goalContinuationRecentChoicesRef.current.set(choiceKey, now);
3007
+ if (goalContinuationRecentChoicesRef.current.size > 100) {
3008
+ for (const [key, startedAt] of goalContinuationRecentChoicesRef.current) {
3009
+ if (now - startedAt > 60_000)
3010
+ goalContinuationRecentChoicesRef.current.delete(key);
3011
+ }
3012
+ }
2830
3013
  if (decision.kind === "terminal" ||
2831
3014
  decision.kind === "blocked" ||
2832
3015
  decision.kind === "pause") {
@@ -2856,6 +3039,7 @@ export function App(props) {
2856
3039
  }
2857
3040
  runningGoalIdsRef.current.delete(runId);
2858
3041
  clearGoalStatusEntry(runId);
3042
+ clearGoalModeIfIdle();
2859
3043
  return;
2860
3044
  }
2861
3045
  let runForNextAction = latestRun;
@@ -2886,18 +3070,28 @@ export function App(props) {
2886
3070
  detail: "choosing next step",
2887
3071
  });
2888
3072
  startGoalRunRef.current(runForNextAction);
2889
- })().catch((err) => {
3073
+ })()
3074
+ .catch((err) => {
2890
3075
  runningGoalIdsRef.current.delete(runId);
2891
3076
  clearGoalStatusEntry(runId);
2892
3077
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2893
3078
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3079
+ })
3080
+ .finally(() => {
3081
+ goalContinuationFlightsRef.current.delete(runId);
3082
+ clearGoalModeIfIdle();
2894
3083
  });
2895
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
3084
+ }, [
3085
+ appendGoalProgress,
3086
+ clearGoalModeIfIdle,
3087
+ clearGoalStatusEntry,
3088
+ props.cwd,
3089
+ upsertGoalStatusEntry,
3090
+ ]);
2896
3091
  const handleGoalWorkerComplete = useCallback((run, completion) => {
2897
3092
  const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
2898
3093
  completion.worker.goalTaskId;
2899
3094
  const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
2900
- void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
2901
3095
  appendGoalProgress({
2902
3096
  kind: "goal_progress",
2903
3097
  phase: "worker_finished",
@@ -2951,112 +3145,131 @@ export function App(props) {
2951
3145
  }, [handleGoalWorkerComplete, props.cwd]);
2952
3146
  const startGoalRun = useCallback((run) => {
2953
3147
  runningGoalIdsRef.current.add(run.id);
3148
+ upsertGoalStatusEntry({
3149
+ runId: run.id,
3150
+ label: run.title,
3151
+ phase: "orchestrating",
3152
+ startedAt: Date.now(),
3153
+ detail: "choosing next step",
3154
+ goalNumber: goalNumberForRun(run.id),
3155
+ });
2954
3156
  void (async () => {
2955
- if (goalHasBlockingPrerequisites(run)) {
2956
- setOverlay(null);
2957
- const detail = formatGoalBlockingPrerequisites(run);
3157
+ await setGoalModeAndPrompt("coordinator");
3158
+ const currentRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3159
+ const prereqCheck = await runGoalPrerequisiteChecks(props.cwd, currentRun);
3160
+ const checkedRun = prereqCheck.checkedCount > 0
3161
+ ? await upsertGoalRun(props.cwd, {
3162
+ ...prereqCheck.run,
3163
+ status: goalHasBlockingPrerequisites(prereqCheck.run) ? "blocked" : "ready",
3164
+ })
3165
+ : currentRun;
3166
+ if (goalHasBlockingPrerequisites(checkedRun)) {
3167
+ const detail = formatGoalBlockingPrerequisites(checkedRun);
2958
3168
  await upsertGoalRun(props.cwd, {
2959
- ...run,
3169
+ ...checkedRun,
2960
3170
  status: "blocked",
2961
- blockers: Array.from(new Set([...run.blockers, detail])),
3171
+ blockers: Array.from(new Set([...checkedRun.blockers, detail])),
2962
3172
  });
2963
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2964
3173
  appendGoalProgress({
2965
3174
  kind: "goal_progress",
2966
3175
  phase: "terminal",
2967
- title: `Goal blocked: ${run.title}`,
3176
+ title: `Goal blocked: ${checkedRun.title}`,
2968
3177
  detail,
2969
3178
  status: "blocked",
2970
3179
  });
2971
- runningGoalIdsRef.current.delete(run.id);
2972
- clearGoalStatusEntry(run.id);
3180
+ runningGoalIdsRef.current.delete(checkedRun.id);
3181
+ clearGoalStatusEntry(checkedRun.id);
3182
+ clearGoalModeIfIdle();
2973
3183
  return;
2974
3184
  }
2975
- const decision = decideGoalNextAction(run);
2976
- await appendGoalDecision(props.cwd, run.id, decision);
3185
+ const decision = decideGoalNextAction(checkedRun);
3186
+ await appendGoalDecision(props.cwd, checkedRun.id, decision);
2977
3187
  if (decision.kind === "terminal") {
2978
- const terminalProgress = formatGoalTerminalProgress(run);
3188
+ const terminalProgress = formatGoalTerminalProgress(checkedRun);
2979
3189
  if (terminalProgress) {
2980
- const item = { ...terminalProgress, id: goalTerminalProgressId(run) };
2981
- setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [run]));
3190
+ const item = { ...terminalProgress, id: goalTerminalProgressId(checkedRun) };
3191
+ setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [checkedRun]));
2982
3192
  }
2983
- runningGoalIdsRef.current.delete(run.id);
2984
- clearGoalStatusEntry(run.id);
3193
+ runningGoalIdsRef.current.delete(checkedRun.id);
3194
+ clearGoalStatusEntry(checkedRun.id);
3195
+ clearGoalModeIfIdle();
2985
3196
  return;
2986
3197
  }
2987
3198
  if (decision.kind === "wait") {
2988
3199
  appendGoalProgress({
2989
3200
  kind: "goal_progress",
2990
3201
  phase: "worker_started",
2991
- title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
3202
+ title: decision.workerId
3203
+ ? `Goal working: ${checkedRun.title}`
3204
+ : `Goal active: ${checkedRun.title}`,
2992
3205
  detail: decision.reason,
2993
3206
  workerId: decision.workerId,
2994
3207
  });
2995
3208
  upsertGoalStatusEntry({
2996
- runId: run.id,
2997
- label: run.title,
3209
+ runId: checkedRun.id,
3210
+ label: checkedRun.title,
2998
3211
  phase: decision.workerId ? "worker" : "orchestrating",
2999
3212
  startedAt: Date.now(),
3000
3213
  detail: decision.reason,
3001
3214
  workerId: decision.workerId,
3002
- goalNumber: goalNumberForRun(run.id),
3215
+ goalNumber: goalNumberForRun(checkedRun.id),
3003
3216
  });
3004
3217
  return;
3005
3218
  }
3006
3219
  if (decision.kind === "complete") {
3007
- await upsertGoalRun(props.cwd, { ...run, status: "passed" });
3008
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3220
+ await upsertGoalRun(props.cwd, { ...checkedRun, status: "passed" });
3009
3221
  appendGoalProgress({
3010
3222
  kind: "goal_progress",
3011
3223
  phase: "terminal",
3012
- title: `Goal passed: ${run.title}`,
3224
+ title: `Goal passed: ${checkedRun.title}`,
3013
3225
  detail: decision.reason,
3014
3226
  status: "passed",
3015
3227
  });
3016
- runningGoalIdsRef.current.delete(run.id);
3017
- clearGoalStatusEntry(run.id);
3228
+ runningGoalIdsRef.current.delete(checkedRun.id);
3229
+ clearGoalStatusEntry(checkedRun.id);
3230
+ clearGoalModeIfIdle();
3018
3231
  return;
3019
3232
  }
3020
3233
  if (decision.kind === "run_verifier") {
3021
- await verifyGoalRun(run);
3234
+ await verifyGoalRun(checkedRun);
3022
3235
  return;
3023
3236
  }
3024
3237
  if (decision.kind === "create_task") {
3025
- await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
3238
+ await updateGoalTask(props.cwd, checkedRun.id, `auto-${Date.now()}`, {
3026
3239
  title: decision.title,
3027
3240
  prompt: decision.prompt,
3028
3241
  status: "pending",
3029
3242
  });
3030
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3243
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ?? checkedRun;
3031
3244
  await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
3032
- setTimeout(() => continueGoalRun(run.id), 250);
3245
+ setTimeout(() => continueGoalRun(checkedRun.id), 250);
3033
3246
  return;
3034
3247
  }
3035
3248
  if (decision.kind === "blocked") {
3036
3249
  await upsertGoalRun(props.cwd, {
3037
- ...run,
3250
+ ...checkedRun,
3038
3251
  status: "blocked",
3039
- blockers: [...run.blockers, decision.reason],
3252
+ blockers: [...checkedRun.blockers, decision.reason],
3040
3253
  });
3041
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3042
3254
  appendGoalProgress({
3043
3255
  kind: "goal_progress",
3044
3256
  phase: "terminal",
3045
- title: `Goal blocked: ${run.title}`,
3257
+ title: `Goal blocked: ${checkedRun.title}`,
3046
3258
  detail: decision.reason,
3047
3259
  status: "blocked",
3048
3260
  });
3049
- runningGoalIdsRef.current.delete(run.id);
3050
- clearGoalStatusEntry(run.id);
3261
+ runningGoalIdsRef.current.delete(checkedRun.id);
3262
+ clearGoalStatusEntry(checkedRun.id);
3263
+ clearGoalModeIfIdle();
3051
3264
  return;
3052
3265
  }
3053
3266
  if (decision.kind === "pause") {
3054
- const runWithBlockedTask = (await updateGoalTask(props.cwd, run.id, decision.task.id, {
3267
+ const runWithBlockedTask = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
3055
3268
  status: "blocked",
3056
3269
  attempts: decision.attempts,
3057
3270
  lastSummary: "Paused after worker attempt limit.",
3058
- })) ?? run;
3059
- const runWithPauseEvidence = (await appendGoalEvidence(props.cwd, run.id, {
3271
+ })) ?? checkedRun;
3272
+ const runWithPauseEvidence = (await appendGoalEvidence(props.cwd, checkedRun.id, {
3060
3273
  kind: "summary",
3061
3274
  label: "Goal paused",
3062
3275
  content: decision.reason,
@@ -3067,31 +3280,32 @@ export function App(props) {
3067
3280
  continueRequestedAt: undefined,
3068
3281
  blockers: Array.from(new Set([...runWithPauseEvidence.blockers, decision.reason])),
3069
3282
  });
3070
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3071
3283
  appendGoalProgress({
3072
3284
  kind: "goal_progress",
3073
3285
  phase: "terminal",
3074
- title: `Goal paused: ${run.title}`,
3286
+ title: `Goal paused: ${checkedRun.title}`,
3075
3287
  detail: decision.reason,
3076
3288
  status: "paused",
3077
3289
  });
3078
- runningGoalIdsRef.current.delete(run.id);
3079
- clearGoalStatusEntry(run.id);
3290
+ runningGoalIdsRef.current.delete(checkedRun.id);
3291
+ clearGoalStatusEntry(checkedRun.id);
3292
+ clearGoalModeIfIdle();
3080
3293
  return;
3081
3294
  }
3082
- const runWithAttempt = (await updateGoalTask(props.cwd, run.id, decision.task.id, {
3295
+ const runWithAttempt = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
3083
3296
  attempts: decision.attempts,
3084
- })) ?? run;
3297
+ })) ?? checkedRun;
3085
3298
  const worker = await startGoalWorker({
3086
3299
  cwd: props.cwd,
3087
3300
  provider: currentProvider,
3088
3301
  model: currentModel,
3089
- goalRunId: run.id,
3302
+ goalRunId: checkedRun.id,
3090
3303
  goalTaskId: decision.task.id,
3091
3304
  taskTitle: decision.task.title,
3092
- prompt: decision.task.prompt,
3305
+ prompt: buildGoalTaskPromptWithReferences(checkedRun, decision.task.prompt),
3093
3306
  });
3094
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? runWithAttempt;
3307
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ??
3308
+ runWithAttempt;
3095
3309
  await upsertGoalRun(props.cwd, {
3096
3310
  ...latestRun,
3097
3311
  status: "running",
@@ -3101,8 +3315,6 @@ export function App(props) {
3101
3315
  ? { ...item, status: "running", workerId: worker.id, attempts: decision.attempts }
3102
3316
  : item),
3103
3317
  });
3104
- setOverlay(null);
3105
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3106
3318
  appendGoalProgress({
3107
3319
  kind: "goal_progress",
3108
3320
  phase: "worker_started",
@@ -3112,16 +3324,17 @@ export function App(props) {
3112
3324
  status: worker.status,
3113
3325
  });
3114
3326
  upsertGoalStatusEntry({
3115
- runId: run.id,
3327
+ runId: checkedRun.id,
3116
3328
  label: decision.task.title,
3117
3329
  phase: "worker",
3118
3330
  startedAt: Date.now(),
3119
3331
  detail: "background worker running",
3120
3332
  workerId: worker.id,
3121
- goalNumber: goalNumberForRun(run.id),
3333
+ goalNumber: goalNumberForRun(checkedRun.id),
3122
3334
  });
3123
3335
  })().catch((err) => {
3124
3336
  clearGoalStatusEntry(run.id);
3337
+ clearGoalModeIfIdle();
3125
3338
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3126
3339
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3127
3340
  });
@@ -3130,11 +3343,14 @@ export function App(props) {
3130
3343
  currentProvider,
3131
3344
  currentModel,
3132
3345
  appendGoalProgress,
3346
+ clearGoalModeIfIdle,
3133
3347
  clearGoalStatusEntry,
3134
3348
  goalNumberForRun,
3349
+ setGoalModeAndPrompt,
3135
3350
  upsertGoalStatusEntry,
3136
3351
  ]);
3137
3352
  const verifyGoalRun = useCallback(async (run) => {
3353
+ await setGoalModeAndPrompt("coordinator");
3138
3354
  if (!run.verifier?.command) {
3139
3355
  await appendGoalEvidence(props.cwd, run.id, {
3140
3356
  kind: "summary",
@@ -3155,6 +3371,7 @@ export function App(props) {
3155
3371
  });
3156
3372
  runningGoalIdsRef.current.delete(run.id);
3157
3373
  clearGoalStatusEntry(run.id);
3374
+ clearGoalModeIfIdle();
3158
3375
  return;
3159
3376
  }
3160
3377
  activeVerifierRunIdsRef.current.add(run.id);
@@ -3201,11 +3418,22 @@ export function App(props) {
3201
3418
  command: run.verifier?.command,
3202
3419
  lastResult: verification,
3203
3420
  },
3421
+ ...(status === "pass"
3422
+ ? {
3423
+ completionAudit: {
3424
+ status: "unknown",
3425
+ summary: "Final completion audit pending for latest verifier result.",
3426
+ checkedAt: verification.checkedAt,
3427
+ verifierCheckedAt: verification.checkedAt,
3428
+ ...(verification.outputPath ? { outputPath: verification.outputPath } : {}),
3429
+ },
3430
+ }
3431
+ : {}),
3204
3432
  };
3205
3433
  const completionCheck = canCompleteGoalRun(runWithVerifier);
3206
3434
  const verifiedRun = await upsertGoalRun(props.cwd, {
3207
3435
  ...runWithVerifier,
3208
- continueRequestedAt: status === "pass" && completionCheck.ok ? undefined : latestRun.continueRequestedAt,
3436
+ continueRequestedAt: latestRun.continueRequestedAt,
3209
3437
  status: status === "pass" && completionCheck.ok ? "passed" : "ready",
3210
3438
  });
3211
3439
  await appendGoalEvidence(props.cwd, run.id, {
@@ -3219,7 +3447,6 @@ export function App(props) {
3219
3447
  reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
3220
3448
  content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
3221
3449
  });
3222
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3223
3450
  appendGoalProgress({
3224
3451
  kind: "goal_progress",
3225
3452
  phase: "verifier_finished",
@@ -3238,22 +3465,25 @@ export function App(props) {
3238
3465
  const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
3239
3466
  runGoalSyntheticEvent(eventText);
3240
3467
  const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3241
- if (continuationRun?.continueRequestedAt || status === "fail") {
3468
+ if (continuationRun?.continueRequestedAt || status === "fail" || status === "pass") {
3242
3469
  setTimeout(() => continueGoalRun(run.id), 500);
3243
3470
  }
3244
3471
  })
3245
3472
  .catch((err) => {
3246
3473
  activeVerifierRunIdsRef.current.delete(run.id);
3247
3474
  clearGoalStatusEntry(run.id);
3475
+ clearGoalModeIfIdle();
3248
3476
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3249
3477
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3250
3478
  });
3251
3479
  }, [
3252
3480
  props.cwd,
3253
3481
  appendGoalProgress,
3482
+ clearGoalModeIfIdle,
3254
3483
  clearGoalStatusEntry,
3255
3484
  goalNumberForRun,
3256
3485
  runGoalSyntheticEvent,
3486
+ setGoalModeAndPrompt,
3257
3487
  upsertGoalStatusEntry,
3258
3488
  ]);
3259
3489
  const pauseGoalRun = useCallback((run) => {
@@ -3267,7 +3497,6 @@ export function App(props) {
3267
3497
  status: "paused",
3268
3498
  activeWorkerId: undefined,
3269
3499
  });
3270
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3271
3500
  appendGoalProgress({
3272
3501
  kind: "goal_progress",
3273
3502
  phase: "terminal",
@@ -3276,19 +3505,14 @@ export function App(props) {
3276
3505
  status: "paused",
3277
3506
  });
3278
3507
  clearGoalStatusEntry(run.id);
3508
+ clearGoalModeIfIdle();
3279
3509
  })().catch((err) => {
3280
3510
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3281
3511
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3282
3512
  });
3283
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
3513
+ }, [appendGoalProgress, clearGoalModeIfIdle, clearGoalStatusEntry, props.cwd]);
3284
3514
  // Keep refs in sync for access from stale closures (onDone)
3285
- startTaskRef.current = startTask;
3286
3515
  startGoalRunRef.current = startGoalRun;
3287
- useEffect(() => {
3288
- runAllTasksRef.current = runAllTasks;
3289
- if (props.sessionStore)
3290
- props.sessionStore.runAllTasks = runAllTasks;
3291
- }, [runAllTasks, props.sessionStore]);
3292
3516
  useEffect(() => {
3293
3517
  agentRunningRef.current = agentLoop.isRunning;
3294
3518
  }, [agentLoop.isRunning]);
@@ -3337,13 +3561,14 @@ export function App(props) {
3337
3561
  injectedLanguagesRef.current = detectedForPixelFix;
3338
3562
  const newSystemPrompt = await rebuildSystemPrompt({
3339
3563
  cwd: prep.projectPath,
3340
- planMode: false,
3341
3564
  clearApprovedPlan: true,
3342
3565
  activeLanguages: detectedForPixelFix,
3343
3566
  tools: toolsForPixelFix,
3344
3567
  });
3345
3568
  // Now that the cwd swap is committed, reset chat. Do not clear the
3346
3569
  // terminal here; terminal clear sequences can erase saved scrollback.
3570
+ pendingHistoryFlushRef.current = [];
3571
+ props.terminalHistoryPrinter?.clear();
3347
3572
  setHistory([{ kind: "banner", id: "banner" }]);
3348
3573
  setLiveItems([]);
3349
3574
  messagesRef.current = messagesRef.current.slice(0, 1);
@@ -3363,10 +3588,10 @@ export function App(props) {
3363
3588
  messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
3364
3589
  }
3365
3590
  const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
3366
- const taskItem = { kind: "task", title, id: getId() };
3591
+ const goalItem = { kind: "goal", title, id: getId() };
3367
3592
  setLastUserMessage(title);
3368
3593
  setDoneStatus(null);
3369
- setLiveItems([taskItem]);
3594
+ setLiveItems([goalItem]);
3370
3595
  await agentLoop.run(prep.prompt);
3371
3596
  }
3372
3597
  catch (err) {
@@ -3387,266 +3612,333 @@ export function App(props) {
3387
3612
  if (props.sessionStore)
3388
3613
  props.sessionStore.runAllPixel = runAllPixel;
3389
3614
  }, [runAllPixel, props.sessionStore]);
3390
- const isTaskView = overlay === "tasks";
3391
3615
  const isGoalView = overlay === "goal";
3392
3616
  const isSkillsView = overlay === "skills";
3393
3617
  const isPlanView = overlay === "plan";
3394
- const isEyesView = overlay === "eyes";
3395
3618
  const footerStatusLayout = getFooterStatusLayoutDecision({
3396
3619
  columns,
3397
3620
  backgroundTaskCount: bgTasks.length,
3398
- eyesCount,
3399
3621
  updatePending,
3400
3622
  });
3623
+ const activityVisible = agentLoop.isRunning && agentLoop.activityPhase !== "idle";
3624
+ const stallStatusVisible = !activityVisible && !!agentLoop.stallError;
3625
+ const doneStatusVisible = !activityVisible && !stallStatusVisible && !!doneStatus && !agentLoop.isRunning;
3626
+ const statusSlotVisible = activityVisible || stallStatusVisible || doneStatusVisible;
3627
+ const [controlsHeight, setControlsHeight] = useState(0);
3628
+ const controlsObserverRef = useRef(null);
3629
+ const mainControlsRef = useCallback((node) => {
3630
+ if (controlsObserverRef.current) {
3631
+ controlsObserverRef.current.disconnect();
3632
+ controlsObserverRef.current = null;
3633
+ }
3634
+ if (!node || typeof ResizeObserver === "undefined")
3635
+ return;
3636
+ const observer = new ResizeObserver((entries) => {
3637
+ const entry = entries[0];
3638
+ if (!entry)
3639
+ return;
3640
+ const roundedHeight = Math.round(entry.contentRect.height);
3641
+ setControlsHeight((prev) => (roundedHeight !== prev ? roundedHeight : prev));
3642
+ });
3643
+ observer.observe(node);
3644
+ controlsObserverRef.current = observer;
3645
+ }, []);
3646
+ useEffect(() => () => controlsObserverRef.current?.disconnect(), []);
3647
+ const footerFitsOnOneLine = doesFooterFitOnOneLine({
3648
+ columns,
3649
+ model: currentModel,
3650
+ tokensIn: agentLoop.contextUsed,
3651
+ contextWindowOptions,
3652
+ cwd: displayedCwd,
3653
+ gitBranch,
3654
+ thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
3655
+ goalMode,
3656
+ });
3657
+ const chatControlsLayout = getChatControlsLayoutDecision({
3658
+ rows,
3659
+ columns,
3660
+ agentRunning: agentLoop.isRunning,
3661
+ activityVisible,
3662
+ doneStatusVisible,
3663
+ stallStatusVisible,
3664
+ exitPending,
3665
+ footerStatusLayout,
3666
+ taskBarExpanded,
3667
+ goalStatusEntryCount: goalStatusEntries.length,
3668
+ footerFitsOnOneLine,
3669
+ });
3670
+ const stableControlsRows = controlsHeight > 0 ? controlsHeight : chatControlsLayout.controlsRows;
3671
+ const measuredLiveAreaRows = Math.max(MIN_LIVE_AREA_ROWS, rows - stableControlsRows - 1);
3401
3672
  const isPixelView = overlay === "pixel";
3402
- const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
3403
- const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
3404
- const stabilizeOverlayPaneRerender = shouldStabilizeOverlayPaneRerender({
3405
- overlayPane: overlay,
3406
- isAgentRunning: agentLoop.isRunning,
3673
+ const hasLiveAssistantItem = liveItems.some((item) => item.kind === "assistant");
3674
+ const rawVisibleStreamingText = goalModeStateRef.current === "planner" || hasLiveAssistantItem ? "" : agentLoop.streamingText;
3675
+ useEffect(() => {
3676
+ if (!rawVisibleStreamingText) {
3677
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
3678
+ return;
3679
+ }
3680
+ if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
3681
+ return;
3682
+ const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
3683
+ const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
3684
+ const split = splitAssistantStreamingText(unflushedText);
3685
+ if (split.flushedText.length > 0) {
3686
+ queueFlush([
3687
+ {
3688
+ kind: "assistant",
3689
+ text: split.flushedText,
3690
+ continuation: streamedAssistantFlushRef.current.flushedChars > 0,
3691
+ id: getId(),
3692
+ },
3693
+ ]);
3694
+ streamedAssistantFlushRef.current = {
3695
+ flushedChars: alreadyFlushed + split.flushedText.length,
3696
+ text: rawVisibleStreamingText,
3697
+ };
3698
+ return;
3699
+ }
3700
+ streamedAssistantFlushRef.current = {
3701
+ ...streamedAssistantFlushRef.current,
3702
+ text: rawVisibleStreamingText,
3703
+ };
3704
+ }, [rawVisibleStreamingText, queueFlush]);
3705
+ const visibleStreamingText = rawVisibleStreamingText.slice(streamedAssistantFlushRef.current.flushedChars);
3706
+ const shouldReserveStreamingSpacing = agentLoop.isRunning &&
3707
+ !hasLiveAssistantItem &&
3708
+ (visibleStreamingText.trim().length > 0 || liveItems.some(isAgentSpacingItem));
3709
+ const shouldTopSpaceStreamingText = shouldTopSpaceStreamingAssistant({
3710
+ visibleStreamingText,
3711
+ lastLiveItem: liveItems.at(-1),
3712
+ lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
3713
+ lastHistoryItem: history.at(-1),
3407
3714
  });
3408
- const staticItems = shouldHideStaticItemsForOverlayView({
3409
- shouldHideHistoryForOverlay,
3410
- stabilizeOverlayPaneRerender,
3411
- })
3412
- ? []
3413
- : history;
3414
- 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: () => {
3415
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3416
- props.sessionStore.overlay = null;
3417
- props.resetUI();
3715
+ return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, autoExpandNewest: goalAutoExpand, onClose: () => {
3716
+ goalAutoExpandRef.current = false;
3717
+ setGoalAutoExpand(false);
3718
+ if (props.sessionStore)
3719
+ props.sessionStore.goalAutoExpand = false;
3720
+ closeOverlay();
3721
+ }, onRunGoal: (run) => {
3722
+ const paneTransition = getGoalActivationPaneTransition();
3723
+ goalAutoExpandRef.current = paneTransition.goalAutoExpand;
3724
+ setGoalAutoExpand(paneTransition.goalAutoExpand);
3725
+ setPlanAutoExpand(paneTransition.planAutoExpand);
3726
+ if (props.sessionStore) {
3727
+ props.sessionStore.overlay = paneTransition.overlay;
3728
+ props.sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
3729
+ props.sessionStore.planAutoExpand = paneTransition.planAutoExpand;
3730
+ }
3731
+ if (paneTransition.resetReviewScreen && props.resetUI && props.sessionStore) {
3732
+ props.sessionStore.pendingGoalRun = run;
3733
+ props.resetUI();
3734
+ return;
3735
+ }
3736
+ setOverlay(paneTransition.overlay);
3737
+ startGoalRun(run);
3738
+ }, onVerifyGoal: (run) => {
3739
+ void verifyGoalRun(run);
3740
+ }, onPauseGoal: (run) => {
3741
+ pauseGoalRun(run);
3742
+ }, onRefineGoal: (run, feedback) => {
3743
+ goalAutoExpandRef.current = true;
3744
+ setGoalAutoExpand(true);
3745
+ void (async () => {
3746
+ try {
3747
+ await setGoalModeAndPrompt("setup");
3748
+ await agentLoop.run(`Refine Goal run ${run.id} (${run.title}) based on this user feedback. Update durable Goal setup only, then stop and reopen the Goal pane for review.\n\nFeedback: ${feedback}`);
3418
3749
  }
3419
- else {
3420
- if (props.sessionStore) {
3421
- props.sessionStore.overlay = null;
3422
- if (agentLoop.isRunning)
3423
- props.sessionStore.pendingResetUI = true;
3424
- }
3425
- setTaskCount(getTaskCount(props.cwd));
3426
- setOverlay(null);
3750
+ catch (err) {
3751
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3752
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3427
3753
  }
3428
- }, onWorkOnTask: (title, prompt, taskId) => {
3429
- setOverlay(null);
3430
- startTask(title, prompt, taskId);
3431
- }, onRunAllTasks: () => {
3432
- setOverlay(null);
3433
- setRunAllTasks(true);
3434
- const next = getNextPendingTask(props.cwd);
3435
- if (next) {
3436
- markTaskInProgress(props.cwd, next.id);
3437
- startTask(next.title, next.prompt, next.id);
3754
+ finally {
3755
+ await setGoalModeAndPrompt("off");
3756
+ const paneTransition = getGoalSetupFinishedPaneTransition();
3757
+ goalAutoExpandRef.current = paneTransition.goalAutoExpand;
3758
+ setTimeout(() => {
3759
+ const resetUI = props.resetUI;
3760
+ const sessionStore = props.sessionStore;
3761
+ if (shouldResetUIForGoalSetupPaneTransition({
3762
+ hasResetUI: resetUI !== undefined,
3763
+ hasSessionStore: sessionStore !== undefined,
3764
+ }) &&
3765
+ resetUI &&
3766
+ sessionStore) {
3767
+ sessionStore.overlay = paneTransition.overlay;
3768
+ sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
3769
+ sessionStore.planAutoExpand = paneTransition.planAutoExpand;
3770
+ resetUI();
3771
+ return;
3772
+ }
3773
+ setGoalAutoExpand(paneTransition.goalAutoExpand);
3774
+ setPlanAutoExpand(paneTransition.planAutoExpand);
3775
+ setOverlay(paneTransition.overlay);
3776
+ }, 300);
3438
3777
  }
3439
- } })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3440
- void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
3441
- closeOverlay();
3442
- }, onRunGoal: (run) => {
3443
- setOverlay(null);
3444
- startGoalRun(run);
3445
- }, onVerifyGoal: (run) => {
3446
- void verifyGoalRun(run);
3447
- }, onPauseGoal: (run) => {
3448
- pauseGoalRun(run);
3449
- } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
3450
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3778
+ })();
3779
+ } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
3780
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3781
+ props.sessionStore.overlay = null;
3782
+ props.resetUI();
3783
+ }
3784
+ else {
3785
+ if (props.sessionStore) {
3451
3786
  props.sessionStore.overlay = null;
3452
- props.resetUI();
3787
+ if (agentLoop.isRunning)
3788
+ props.sessionStore.pendingResetUI = true;
3453
3789
  }
3454
- else {
3455
- if (props.sessionStore) {
3456
- props.sessionStore.overlay = null;
3457
- if (agentLoop.isRunning)
3458
- props.sessionStore.pendingResetUI = true;
3459
- }
3460
- setOverlay(null);
3461
- }
3462
- }, onFixOne: (entry) => {
3463
- setOverlay(null);
3464
- startPixelFix(entry.errorId);
3465
- }, onFixAll: (entries) => {
3466
- const first = entries.find((e) => e.status === "open") ?? entries[0];
3467
- if (!first)
3468
- return;
3469
3790
  setOverlay(null);
3470
- setRunAllPixel(true);
3471
- startPixelFix(first.errorId);
3472
- } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
3473
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3474
- props.sessionStore.overlay = null;
3475
- props.resetUI();
3476
- }
3477
- else {
3478
- if (props.sessionStore) {
3479
- props.sessionStore.overlay = null;
3480
- if (agentLoop.isRunning)
3481
- props.sessionStore.pendingResetUI = true;
3482
- }
3483
- setOverlay(null);
3484
- }
3485
- } })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
3486
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3791
+ }
3792
+ }, onFixOne: (entry) => {
3793
+ setOverlay(null);
3794
+ startPixelFix(entry.errorId);
3795
+ }, onFixAll: (entries) => {
3796
+ const first = entries.find((e) => e.status === "open") ?? entries[0];
3797
+ if (!first)
3798
+ return;
3799
+ setOverlay(null);
3800
+ setRunAllPixel(true);
3801
+ startPixelFix(first.errorId);
3802
+ } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
3803
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3804
+ props.sessionStore.overlay = null;
3805
+ props.resetUI();
3806
+ }
3807
+ else {
3808
+ if (props.sessionStore) {
3487
3809
  props.sessionStore.overlay = null;
3488
- props.resetUI();
3810
+ if (agentLoop.isRunning)
3811
+ props.sessionStore.pendingResetUI = true;
3489
3812
  }
3490
- else {
3491
- if (props.sessionStore) {
3492
- props.sessionStore.overlay = null;
3493
- if (agentLoop.isRunning)
3494
- props.sessionStore.pendingResetUI = true;
3495
- }
3496
- setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
3497
- setOverlay(null);
3498
- }
3499
- }, onQueueMessage: (msg) => {
3500
- agentLoop.queueMessage(msg);
3501
- } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
3502
- planOverlayPendingRef.current = false;
3503
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3813
+ setOverlay(null);
3814
+ }
3815
+ } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
3816
+ planOverlayPendingRef.current = false;
3817
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3818
+ props.sessionStore.overlay = null;
3819
+ props.sessionStore.planAutoExpand = false;
3820
+ props.resetUI();
3821
+ }
3822
+ else {
3823
+ if (props.sessionStore) {
3504
3824
  props.sessionStore.overlay = null;
3505
3825
  props.sessionStore.planAutoExpand = false;
3506
- props.resetUI();
3826
+ if (agentLoop.isRunning)
3827
+ props.sessionStore.pendingResetUI = true;
3507
3828
  }
3508
- else {
3509
- if (props.sessionStore) {
3829
+ setPlanAutoExpand(false);
3830
+ setOverlay(null);
3831
+ }
3832
+ }, onApprove: (planPath) => {
3833
+ log("INFO", "plan", "Plan approved — transitioning to implementation", {
3834
+ planPath,
3835
+ });
3836
+ planOverlayPendingRef.current = false;
3837
+ void (async () => {
3838
+ try {
3839
+ // Read plan steps for progress tracking — handed to the new
3840
+ // mount via sessionStore.planSteps below.
3841
+ const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
3842
+ const steps = extractPlanSteps(planContent);
3843
+ // Build the new system prompt with the approved plan baked in.
3844
+ const newPrompt = await rebuildSystemPrompt({
3845
+ approvedPlanPath: planPath,
3846
+ });
3847
+ // Create a new session file BEFORE remount so the new tree
3848
+ // picks it up via sessionStore.sessionPath.
3849
+ let newSessionPath;
3850
+ const sm = sessionManagerRef.current;
3851
+ if (sm) {
3852
+ const s = await sm.create(props.cwd, currentProvider, currentModel);
3853
+ newSessionPath = s.path;
3854
+ }
3855
+ if (props.resetUI && props.sessionStore) {
3856
+ // Clear the overlay so the new mount lands on the chat,
3857
+ // not back inside the plan pane.
3510
3858
  props.sessionStore.overlay = null;
3511
3859
  props.sessionStore.planAutoExpand = false;
3512
- if (agentLoop.isRunning)
3513
- props.sessionStore.pendingResetUI = true;
3514
- }
3515
- setPlanAutoExpand(false);
3516
- setOverlay(null);
3517
- }
3518
- }, onApprove: (planPath) => {
3519
- log("INFO", "plan", "Plan approved — transitioning to implementation", {
3520
- planPath,
3521
- });
3522
- planOverlayPendingRef.current = false;
3523
- void (async () => {
3524
- try {
3525
- // Read plan steps for progress tracking — handed to the new
3526
- // mount via sessionStore.planSteps below.
3527
- const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
3528
- const steps = extractPlanSteps(planContent);
3529
- // Build the new system prompt with the approved plan baked in.
3530
- const newPrompt = await rebuildSystemPrompt({
3531
- planMode: false,
3860
+ props.resetUI({
3861
+ wipeSession: true,
3862
+ messages: [{ role: "system", content: newPrompt }],
3532
3863
  approvedPlanPath: planPath,
3533
- });
3534
- // Create a new session file BEFORE remount so the new tree
3535
- // picks it up via sessionStore.sessionPath.
3536
- let newSessionPath;
3537
- const sm = sessionManagerRef.current;
3538
- if (sm) {
3539
- const s = await sm.create(props.cwd, currentProvider, currentModel);
3540
- newSessionPath = s.path;
3541
- }
3542
- if (props.resetUI && props.sessionStore) {
3543
- // Clear the overlay so the new mount lands on the chat,
3544
- // not back inside the plan pane.
3545
- props.sessionStore.overlay = null;
3546
- props.sessionStore.planAutoExpand = false;
3547
- props.resetUI({
3548
- wipeSession: true,
3549
- messages: [{ role: "system", content: newPrompt }],
3550
- approvedPlanPath: planPath,
3551
- planSteps: steps,
3552
- sessionPath: newSessionPath,
3553
- pendingAction: {
3554
- prompt: "The plan has been approved. Implement it now, following each step in order.",
3555
- planEvent: { event: "approved" },
3556
- },
3557
- });
3558
- return;
3559
- }
3560
- // Fallback path (resetUI not wired — tests). Mutate in place.
3561
- approvedPlanPathRef.current = planPath;
3562
- planStepsRef.current = steps;
3563
- setPlanSteps(steps);
3564
- setHistory([{ kind: "banner", id: "banner" }]);
3565
- setLiveItems([]);
3566
- setPlanAutoExpand(false);
3567
- setOverlay(null);
3568
- messagesRef.current = [{ role: "system", content: newPrompt }];
3569
- agentLoop.reset();
3570
- persistedIndexRef.current = messagesRef.current.length;
3571
- if (newSessionPath)
3572
- sessionPathRef.current = newSessionPath;
3573
- setLiveItems([
3574
- {
3575
- kind: "info",
3576
- text: "Plan approved — starting fresh session for implementation",
3577
- id: getId(),
3864
+ planSteps: steps,
3865
+ sessionPath: newSessionPath,
3866
+ pendingAction: {
3867
+ prompt: "The plan has been approved. Implement it now, following each step in order.",
3868
+ planEvent: { event: "approved" },
3578
3869
  },
3579
- ]);
3580
- setDoneStatus(null);
3581
- await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3582
- }
3583
- catch (err) {
3584
- const errMsg = err instanceof Error ? err.message : String(err);
3585
- log("ERROR", "error", errMsg);
3586
- setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3870
+ });
3871
+ return;
3587
3872
  }
3588
- })();
3589
- }, onReject: (planPath, feedback) => {
3590
- planOverlayPendingRef.current = false;
3591
- const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
3592
- `Please revise the plan based on this feedback.`;
3593
- if (props.resetUI && props.sessionStore) {
3594
- props.sessionStore.overlay = null;
3595
- props.sessionStore.planAutoExpand = false;
3596
- // No wipeSession — keep history, messages, plan mode etc. The
3597
- // agent picks up the rejection mid-conversation.
3598
- props.resetUI({
3599
- pendingAction: {
3600
- prompt: rejectionMsg,
3601
- planEvent: { event: "rejected", detail: feedback },
3873
+ // Fallback path (resetUI not wired — tests). Mutate in place.
3874
+ approvedPlanPathRef.current = planPath;
3875
+ planStepsRef.current = steps;
3876
+ setPlanSteps(steps);
3877
+ pendingHistoryFlushRef.current = [];
3878
+ props.terminalHistoryPrinter?.clear();
3879
+ setHistory([{ kind: "banner", id: "banner" }]);
3880
+ setLiveItems([]);
3881
+ setPlanAutoExpand(false);
3882
+ setOverlay(null);
3883
+ messagesRef.current = [{ role: "system", content: newPrompt }];
3884
+ agentLoop.reset();
3885
+ persistedIndexRef.current = messagesRef.current.length;
3886
+ if (newSessionPath)
3887
+ sessionPathRef.current = newSessionPath;
3888
+ setLiveItems([
3889
+ {
3890
+ kind: "info",
3891
+ text: "Plan approved — starting fresh session for implementation",
3892
+ id: getId(),
3602
3893
  },
3603
- });
3604
- return;
3894
+ ]);
3895
+ setDoneStatus(null);
3896
+ await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3605
3897
  }
3606
- setPlanAutoExpand(false);
3607
- setOverlay(null);
3608
- setDoneStatus(null);
3609
- setLiveItems((prev) => [
3610
- ...prev,
3611
- { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3612
- ]);
3613
- void agentLoop.run(rejectionMsg).catch((err) => {
3898
+ catch (err) {
3614
3899
  const errMsg = err instanceof Error ? err.message : String(err);
3615
3900
  log("ERROR", "error", errMsg);
3616
3901
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3902
+ }
3903
+ })();
3904
+ }, onReject: (planPath, feedback) => {
3905
+ planOverlayPendingRef.current = false;
3906
+ const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
3907
+ `Please revise the plan based on this feedback.`;
3908
+ if (props.resetUI && props.sessionStore) {
3909
+ props.sessionStore.overlay = null;
3910
+ props.sessionStore.planAutoExpand = false;
3911
+ // No wipeSession — keep history and messages so the agent picks
3912
+ // up the rejection mid-conversation.
3913
+ props.resetUI({
3914
+ pendingAction: {
3915
+ prompt: rejectionMsg,
3916
+ planEvent: { event: "rejected", detail: feedback },
3917
+ },
3617
3918
  });
3618
- } })) : (_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 &&
3619
- !agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.warning, bold: true, children: "• " }), _jsxs(Text, { color: theme.textDim, 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: () => {
3620
- // Just flip the overlay state — Ink's log-update handles the
3621
- // live-area transition (chat input → TaskOverlay) natively, and
3622
- // the chat history above stays in scrollback. When the overlay
3623
- // closes, the history is still there (banner included).
3624
- openOverlay("tasks");
3625
- }, onToggleGoal: () => {
3626
- openOverlay("goal");
3627
- }, onToggleSkills: () => {
3628
- openOverlay("skills");
3629
- }, onTogglePixel: () => {
3630
- openOverlay("pixel");
3631
- }, onTogglePlanMode: () => {
3632
- const next = !planMode;
3633
- setPlanMode(next);
3634
- log("INFO", "plan", `Plan mode ${next ? "enabled" : "disabled"}`);
3635
- setLiveItems((items) => [
3636
- ...items,
3637
- {
3638
- kind: "plan_transition",
3639
- text: next ? "Plan Mode Activated" : "Plan Mode Deactivated",
3640
- active: next,
3641
- id: getId(),
3642
- },
3643
- ]);
3644
- }, 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 ||
3645
- footerStatusLayout.hasEyesSignals ||
3646
- 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 GG Coder` }) })), footerStatusLayout.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack ||
3647
- (!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
3648
- ? 1
3649
- : 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
3919
+ return;
3920
+ }
3921
+ setPlanAutoExpand(false);
3922
+ setOverlay(null);
3923
+ setDoneStatus(null);
3924
+ setLiveItems((prev) => [
3925
+ ...prev,
3926
+ { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3927
+ ]);
3928
+ void agentLoop.run(rejectionMsg).catch((err) => {
3929
+ const errMsg = err instanceof Error ? err.message : String(err);
3930
+ log("ERROR", "error", errMsg);
3931
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3932
+ });
3933
+ } })) : (_jsxs(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: [_jsxs(Box, { flexDirection: "column", maxHeight: measuredLiveAreaRows, flexGrow: 0, flexShrink: 1, overflowY: "hidden", children: [liveItems.map((item, index, items) => renderItem(item, index, items)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: visibleStreamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, reserveSpacing: shouldReserveStreamingSpacing, renderMarkdown: renderMarkdown, availableTerminalHeight: measuredLiveAreaRows, assistantMarginTop: shouldTopSpaceStreamingText ? 1 : 0, continuation: streamedAssistantFlushRef.current.flushedChars > 0 })] }), _jsxs(Box, { ref: mainControlsRef, flexDirection: "column", flexShrink: 0, flexGrow: 0, children: [agentLoop.queuedCount > 0 && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.warning, bold: true, children: "• " }), _jsxs(Text, { color: theme.textDim, children: [agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] })] })), _jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: theme.textDim, width: columns, height: 0 }), _jsx(Box, { paddingLeft: 1, paddingRight: 1, width: columns, children: statusSlotVisible ? (activityVisible ? (_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), retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true })) : stallStatusVisible ? (_jsx(Text, { color: theme.warning, wrap: "truncate", children: "⚠ API provider stream interrupted — retries exhausted. Your conversation is preserved." })) : doneStatus ? (_jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] })) : (_jsxs(Text, { children: [_jsx(Text, { color: theme.commandColor, children: "⠿ " }), _jsx(Text, { color: theme.textDim, children: "Ready to go.." }), !renderMarkdown && (_jsx(Text, { color: theme.warning, children: " · raw markdown mode" }))] }))) : (_jsxs(Text, { children: [_jsx(Text, { color: theme.commandColor, children: "⠿ " }), _jsx(Text, { color: theme.textDim, children: "Ready to go.." })] })) })] }), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleGoal: () => {
3934
+ openOverlay("goal");
3935
+ }, onToggleSkills: () => {
3936
+ openOverlay("skills");
3937
+ }, onTogglePixel: () => {
3938
+ openOverlay("pixel");
3939
+ }, onToggleMarkdown: () => {
3940
+ setRenderMarkdown((prev) => !prev);
3941
+ }, cwd: props.cwd, commands: allCommands }), 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, goalMode: goalMode, exitPending: exitPending, renderMarkdown: renderMarkdown }), !exitPending && _jsx(GoalStatusBar, { entries: goalStatusEntries })] })), (footerStatusLayout.hasBackgroundTasks || 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.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack || !footerStatusLayout.hasBackgroundTasks ? 1 : 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] })] })) }));
3650
3942
  }
3651
3943
  function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
3652
3944
  const status = enabled ? "on" : "off";