@kenkaiiii/ggcoder 4.3.212 → 4.3.214

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 (312) 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 +5 -0
  19. package/dist/core/goal-prerequisites.d.ts.map +1 -1
  20. package/dist/core/goal-prerequisites.js +37 -0
  21. package/dist/core/goal-prerequisites.js.map +1 -1
  22. package/dist/core/goal-prerequisites.test.js +29 -1
  23. package/dist/core/goal-prerequisites.test.js.map +1 -1
  24. package/dist/core/goal-references.d.ts +14 -0
  25. package/dist/core/goal-references.d.ts.map +1 -0
  26. package/dist/core/goal-references.js +153 -0
  27. package/dist/core/goal-references.js.map +1 -0
  28. package/dist/core/goal-references.test.d.ts +2 -0
  29. package/dist/core/goal-references.test.d.ts.map +1 -0
  30. package/dist/core/goal-references.test.js +77 -0
  31. package/dist/core/goal-references.test.js.map +1 -0
  32. package/dist/core/goal-store.d.ts +25 -0
  33. package/dist/core/goal-store.d.ts.map +1 -1
  34. package/dist/core/goal-store.js +150 -36
  35. package/dist/core/goal-store.js.map +1 -1
  36. package/dist/core/goal-store.test.js +19 -2
  37. package/dist/core/goal-store.test.js.map +1 -1
  38. package/dist/core/goal-verifier.d.ts.map +1 -1
  39. package/dist/core/goal-verifier.js +4 -1
  40. package/dist/core/goal-verifier.js.map +1 -1
  41. package/dist/core/goal-verifier.test.js +43 -0
  42. package/dist/core/goal-verifier.test.js.map +1 -1
  43. package/dist/core/goal-worker.d.ts +2 -0
  44. package/dist/core/goal-worker.d.ts.map +1 -1
  45. package/dist/core/goal-worker.js +33 -9
  46. package/dist/core/goal-worker.js.map +1 -1
  47. package/dist/core/goal-worker.test.js +49 -1
  48. package/dist/core/goal-worker.test.js.map +1 -1
  49. package/dist/core/prompt-commands.d.ts.map +1 -1
  50. package/dist/core/prompt-commands.js +28 -846
  51. package/dist/core/prompt-commands.js.map +1 -1
  52. package/dist/core/prompt-commands.test.js +40 -78
  53. package/dist/core/prompt-commands.test.js.map +1 -1
  54. package/dist/core/runtime-mode.d.ts +14 -0
  55. package/dist/core/runtime-mode.d.ts.map +1 -0
  56. package/dist/core/runtime-mode.js +10 -0
  57. package/dist/core/runtime-mode.js.map +1 -0
  58. package/dist/core/session-restore-display.test.d.ts +2 -0
  59. package/dist/core/session-restore-display.test.d.ts.map +1 -0
  60. package/dist/core/session-restore-display.test.js +100 -0
  61. package/dist/core/session-restore-display.test.js.map +1 -0
  62. package/dist/core/verify-commands.js +4 -4
  63. package/dist/core/verify-commands.js.map +1 -1
  64. package/dist/system-prompt.d.ts +2 -1
  65. package/dist/system-prompt.d.ts.map +1 -1
  66. package/dist/system-prompt.js +51 -37
  67. package/dist/system-prompt.js.map +1 -1
  68. package/dist/system-prompt.test.js +147 -40
  69. package/dist/system-prompt.test.js.map +1 -1
  70. package/dist/tools/bash.d.ts +3 -2
  71. package/dist/tools/bash.d.ts.map +1 -1
  72. package/dist/tools/bash.js +11 -4
  73. package/dist/tools/bash.js.map +1 -1
  74. package/dist/tools/edit.d.ts +5 -3
  75. package/dist/tools/edit.d.ts.map +1 -1
  76. package/dist/tools/edit.js +14 -4
  77. package/dist/tools/edit.js.map +1 -1
  78. package/dist/tools/edit.test.js +0 -10
  79. package/dist/tools/edit.test.js.map +1 -1
  80. package/dist/tools/goal-mode.test.d.ts +2 -0
  81. package/dist/tools/goal-mode.test.d.ts.map +1 -0
  82. package/dist/tools/goal-mode.test.js +121 -0
  83. package/dist/tools/goal-mode.test.js.map +1 -0
  84. package/dist/tools/goals.d.ts +15 -3
  85. package/dist/tools/goals.d.ts.map +1 -1
  86. package/dist/tools/goals.js +336 -26
  87. package/dist/tools/goals.js.map +1 -1
  88. package/dist/tools/goals.test.js +346 -6
  89. package/dist/tools/goals.test.js.map +1 -1
  90. package/dist/tools/index.d.ts +7 -10
  91. package/dist/tools/index.d.ts.map +1 -1
  92. package/dist/tools/index.js +6 -19
  93. package/dist/tools/index.js.map +1 -1
  94. package/dist/tools/plan-mode.test.js +34 -224
  95. package/dist/tools/plan-mode.test.js.map +1 -1
  96. package/dist/tools/prompt-hints.d.ts.map +1 -1
  97. package/dist/tools/prompt-hints.js +2 -6
  98. package/dist/tools/prompt-hints.js.map +1 -1
  99. package/dist/tools/subagent.d.ts +3 -2
  100. package/dist/tools/subagent.d.ts.map +1 -1
  101. package/dist/tools/subagent.js +4 -9
  102. package/dist/tools/subagent.js.map +1 -1
  103. package/dist/tools/write.d.ts +5 -3
  104. package/dist/tools/write.d.ts.map +1 -1
  105. package/dist/tools/write.js +14 -13
  106. package/dist/tools/write.js.map +1 -1
  107. package/dist/tools/write.test.js +0 -16
  108. package/dist/tools/write.test.js.map +1 -1
  109. package/dist/ui/App.d.ts +145 -28
  110. package/dist/ui/App.d.ts.map +1 -1
  111. package/dist/ui/App.js +1153 -864
  112. package/dist/ui/App.js.map +1 -1
  113. package/dist/ui/activity-phrases.d.ts.map +1 -1
  114. package/dist/ui/activity-phrases.js +0 -2
  115. package/dist/ui/activity-phrases.js.map +1 -1
  116. package/dist/ui/app-state-persistence.test.js +173 -5
  117. package/dist/ui/app-state-persistence.test.js.map +1 -1
  118. package/dist/ui/chat-layout-pinning.test.d.ts +2 -0
  119. package/dist/ui/chat-layout-pinning.test.d.ts.map +1 -0
  120. package/dist/ui/chat-layout-pinning.test.js +407 -0
  121. package/dist/ui/chat-layout-pinning.test.js.map +1 -0
  122. package/dist/ui/components/ActivityIndicator.d.ts +1 -2
  123. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  124. package/dist/ui/components/ActivityIndicator.js +63 -94
  125. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  126. package/dist/ui/components/AssistantMessage.d.ts +6 -2
  127. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  128. package/dist/ui/components/AssistantMessage.js +9 -4
  129. package/dist/ui/components/AssistantMessage.js.map +1 -1
  130. package/dist/ui/components/AssistantMessage.test.d.ts +2 -0
  131. package/dist/ui/components/AssistantMessage.test.d.ts.map +1 -0
  132. package/dist/ui/components/AssistantMessage.test.js +369 -0
  133. package/dist/ui/components/AssistantMessage.test.js.map +1 -0
  134. package/dist/ui/components/BackgroundTasksBar.d.ts +1 -3
  135. package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
  136. package/dist/ui/components/BackgroundTasksBar.js +2 -4
  137. package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
  138. package/dist/ui/components/Banner.d.ts +1 -3
  139. package/dist/ui/components/Banner.d.ts.map +1 -1
  140. package/dist/ui/components/Banner.js +7 -3
  141. package/dist/ui/components/Banner.js.map +1 -1
  142. package/dist/ui/components/Footer.d.ts +26 -4
  143. package/dist/ui/components/Footer.d.ts.map +1 -1
  144. package/dist/ui/components/Footer.js +73 -21
  145. package/dist/ui/components/Footer.js.map +1 -1
  146. package/dist/ui/components/GoalOverlay.d.ts +28 -20
  147. package/dist/ui/components/GoalOverlay.d.ts.map +1 -1
  148. package/dist/ui/components/GoalOverlay.js +283 -253
  149. package/dist/ui/components/GoalOverlay.js.map +1 -1
  150. package/dist/ui/components/InputArea.d.ts +2 -6
  151. package/dist/ui/components/InputArea.d.ts.map +1 -1
  152. package/dist/ui/components/InputArea.js +40 -32
  153. package/dist/ui/components/InputArea.js.map +1 -1
  154. package/dist/ui/components/InputArea.test.js +11 -1
  155. package/dist/ui/components/InputArea.test.js.map +1 -1
  156. package/dist/ui/components/Markdown.d.ts +11 -11
  157. package/dist/ui/components/Markdown.d.ts.map +1 -1
  158. package/dist/ui/components/Markdown.js +25 -198
  159. package/dist/ui/components/Markdown.js.map +1 -1
  160. package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
  161. package/dist/ui/components/PlanOverlay.js +1 -1
  162. package/dist/ui/components/PlanOverlay.js.map +1 -1
  163. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  164. package/dist/ui/components/ServerToolExecution.js +3 -2
  165. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  166. package/dist/ui/components/SlashCommandMenu.d.ts +4 -3
  167. package/dist/ui/components/SlashCommandMenu.d.ts.map +1 -1
  168. package/dist/ui/components/SlashCommandMenu.js +38 -26
  169. package/dist/ui/components/SlashCommandMenu.js.map +1 -1
  170. package/dist/ui/components/StreamingArea.d.ts +11 -2
  171. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  172. package/dist/ui/components/StreamingArea.js +20 -23
  173. package/dist/ui/components/StreamingArea.js.map +1 -1
  174. package/dist/ui/components/StreamingArea.test.d.ts +2 -0
  175. package/dist/ui/components/StreamingArea.test.d.ts.map +1 -0
  176. package/dist/ui/components/StreamingArea.test.js +18 -0
  177. package/dist/ui/components/StreamingArea.test.js.map +1 -0
  178. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  179. package/dist/ui/components/ToolExecution.js +11 -27
  180. package/dist/ui/components/ToolExecution.js.map +1 -1
  181. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  182. package/dist/ui/components/ToolGroupExecution.js +9 -124
  183. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  184. package/dist/ui/components/UserMessage.d.ts.map +1 -1
  185. package/dist/ui/components/UserMessage.js +15 -10
  186. package/dist/ui/components/UserMessage.js.map +1 -1
  187. package/dist/ui/components/UserMessage.test.d.ts +2 -0
  188. package/dist/ui/components/UserMessage.test.d.ts.map +1 -0
  189. package/dist/ui/components/UserMessage.test.js +39 -0
  190. package/dist/ui/components/UserMessage.test.js.map +1 -0
  191. package/dist/ui/footer-status-layout.test.js +21 -7
  192. package/dist/ui/footer-status-layout.test.js.map +1 -1
  193. package/dist/ui/goal-events.d.ts +13 -0
  194. package/dist/ui/goal-events.d.ts.map +1 -1
  195. package/dist/ui/goal-events.js +81 -16
  196. package/dist/ui/goal-events.js.map +1 -1
  197. package/dist/ui/goal-events.test.js +76 -2
  198. package/dist/ui/goal-events.test.js.map +1 -1
  199. package/dist/ui/goal-lifecycle-orchestration.test.js +131 -34
  200. package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -1
  201. package/dist/ui/goal-overlay.test.js +121 -43
  202. package/dist/ui/goal-overlay.test.js.map +1 -1
  203. package/dist/ui/goal-summary.d.ts +14 -0
  204. package/dist/ui/goal-summary.d.ts.map +1 -0
  205. package/dist/ui/goal-summary.js +194 -0
  206. package/dist/ui/goal-summary.js.map +1 -0
  207. package/dist/ui/hooks/useAgentLoop.d.ts +8 -2
  208. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  209. package/dist/ui/hooks/useAgentLoop.js +20 -9
  210. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  211. package/dist/ui/hooks/useAgentLoop.test.d.ts +2 -0
  212. package/dist/ui/hooks/useAgentLoop.test.d.ts.map +1 -0
  213. package/dist/ui/hooks/useAgentLoop.test.js +8 -0
  214. package/dist/ui/hooks/useAgentLoop.test.js.map +1 -0
  215. package/dist/ui/hooks/useTerminalSize.d.ts +5 -9
  216. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  217. package/dist/ui/hooks/useTerminalSize.js +9 -14
  218. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  219. package/dist/ui/live-item-flush.d.ts +2 -2
  220. package/dist/ui/live-item-flush.d.ts.map +1 -1
  221. package/dist/ui/live-item-flush.js +8 -4
  222. package/dist/ui/live-item-flush.js.map +1 -1
  223. package/dist/ui/long-prompt-regression-harness.test.d.ts +2 -0
  224. package/dist/ui/long-prompt-regression-harness.test.d.ts.map +1 -0
  225. package/dist/ui/long-prompt-regression-harness.test.js +195 -0
  226. package/dist/ui/long-prompt-regression-harness.test.js.map +1 -0
  227. package/dist/ui/plan-overlay.test.js +7 -29
  228. package/dist/ui/plan-overlay.test.js.map +1 -1
  229. package/dist/ui/queued-message.test.d.ts.map +1 -1
  230. package/dist/ui/queued-message.test.js +76 -14
  231. package/dist/ui/queued-message.test.js.map +1 -1
  232. package/dist/ui/render.d.ts +21 -24
  233. package/dist/ui/render.d.ts.map +1 -1
  234. package/dist/ui/render.js +46 -28
  235. package/dist/ui/render.js.map +1 -1
  236. package/dist/ui/render.test.d.ts +2 -0
  237. package/dist/ui/render.test.d.ts.map +1 -0
  238. package/dist/ui/render.test.js +16 -0
  239. package/dist/ui/render.test.js.map +1 -0
  240. package/dist/ui/scroll-stabilization.test.js +1 -1
  241. package/dist/ui/scroll-stabilization.test.js.map +1 -1
  242. package/dist/ui/slash-command-images.test.js +79 -4
  243. package/dist/ui/slash-command-images.test.js.map +1 -1
  244. package/dist/ui/terminal-history.d.ts +26 -0
  245. package/dist/ui/terminal-history.d.ts.map +1 -0
  246. package/dist/ui/terminal-history.js +910 -0
  247. package/dist/ui/terminal-history.js.map +1 -0
  248. package/dist/ui/terminal-history.test.d.ts +2 -0
  249. package/dist/ui/terminal-history.test.d.ts.map +1 -0
  250. package/dist/ui/terminal-history.test.js +314 -0
  251. package/dist/ui/terminal-history.test.js.map +1 -0
  252. package/dist/ui/tool-group-summary.d.ts +16 -0
  253. package/dist/ui/tool-group-summary.d.ts.map +1 -0
  254. package/dist/ui/tool-group-summary.js +123 -0
  255. package/dist/ui/tool-group-summary.js.map +1 -0
  256. package/dist/ui/tui-history-parity.test.d.ts +2 -0
  257. package/dist/ui/tui-history-parity.test.d.ts.map +1 -0
  258. package/dist/ui/tui-history-parity.test.js +243 -0
  259. package/dist/ui/tui-history-parity.test.js.map +1 -0
  260. package/dist/ui/utils/assistant-stream-split.d.ts +6 -0
  261. package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -0
  262. package/dist/ui/utils/assistant-stream-split.js +37 -0
  263. package/dist/ui/utils/assistant-stream-split.js.map +1 -0
  264. package/dist/ui/utils/assistant-stream-split.test.d.ts +2 -0
  265. package/dist/ui/utils/assistant-stream-split.test.d.ts.map +1 -0
  266. package/dist/ui/utils/assistant-stream-split.test.js +58 -0
  267. package/dist/ui/utils/assistant-stream-split.test.js.map +1 -0
  268. package/dist/ui/utils/latex-to-unicode.d.ts +22 -0
  269. package/dist/ui/utils/latex-to-unicode.d.ts.map +1 -0
  270. package/dist/ui/utils/latex-to-unicode.js +538 -0
  271. package/dist/ui/utils/latex-to-unicode.js.map +1 -0
  272. package/dist/ui/utils/markdown-renderer.d.ts +20 -0
  273. package/dist/ui/utils/markdown-renderer.d.ts.map +1 -0
  274. package/dist/ui/utils/markdown-renderer.js +327 -0
  275. package/dist/ui/utils/markdown-renderer.js.map +1 -0
  276. package/dist/ui/utils/markdown-table.d.ts +9 -0
  277. package/dist/ui/utils/markdown-table.d.ts.map +1 -0
  278. package/dist/ui/utils/markdown-table.js +95 -0
  279. package/dist/ui/utils/markdown-table.js.map +1 -0
  280. package/dist/ui/utils/text-utils.d.ts +8 -0
  281. package/dist/ui/utils/text-utils.d.ts.map +1 -0
  282. package/dist/ui/utils/text-utils.js +16 -0
  283. package/dist/ui/utils/text-utils.js.map +1 -0
  284. package/dist/ui/utils/token-to-ansi.js +19 -9
  285. package/dist/ui/utils/token-to-ansi.js.map +1 -1
  286. package/dist/ui/utils/user-message-display.d.ts +7 -0
  287. package/dist/ui/utils/user-message-display.d.ts.map +1 -0
  288. package/dist/ui/utils/user-message-display.js +26 -0
  289. package/dist/ui/utils/user-message-display.js.map +1 -0
  290. package/dist/utils/format.js +0 -9
  291. package/dist/utils/format.js.map +1 -1
  292. package/package.json +9 -4
  293. package/dist/tools/enter-plan.d.ts +0 -8
  294. package/dist/tools/enter-plan.d.ts.map +0 -1
  295. package/dist/tools/enter-plan.js +0 -30
  296. package/dist/tools/enter-plan.js.map +0 -1
  297. package/dist/tools/exit-plan.d.ts +0 -8
  298. package/dist/tools/exit-plan.d.ts.map +0 -1
  299. package/dist/tools/exit-plan.js +0 -36
  300. package/dist/tools/exit-plan.js.map +0 -1
  301. package/dist/tools/tasks.d.ts +0 -16
  302. package/dist/tools/tasks.d.ts.map +0 -1
  303. package/dist/tools/tasks.js +0 -133
  304. package/dist/tools/tasks.js.map +0 -1
  305. package/dist/ui/components/EyesOverlay.d.ts +0 -10
  306. package/dist/ui/components/EyesOverlay.d.ts.map +0 -1
  307. package/dist/ui/components/EyesOverlay.js +0 -220
  308. package/dist/ui/components/EyesOverlay.js.map +0 -1
  309. package/dist/ui/components/TaskOverlay.d.ts +0 -10
  310. package/dist/ui/components/TaskOverlay.d.ts.map +0 -1
  311. package/dist/ui/components/TaskOverlay.js +0 -267
  312. 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,9 @@ 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";
64
63
  import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
65
64
  import { runGoalVerifierCommand } from "../core/goal-verifier.js";
66
65
  import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
@@ -117,6 +116,56 @@ export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS,
117
116
  fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
118
117
  };
119
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
+ }
120
169
  export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
121
170
  if (inputImages.length === 0)
122
171
  return text;
@@ -156,14 +205,13 @@ export function buildUserContentWithAttachments(text, inputImages, modelSupports
156
205
  // If only text parts remain after stripping images, simplify to plain string
157
206
  return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
158
207
  }
159
- /** Tools that get aggregated into a single compact group when concurrent. */
208
+ /** Tools that get aggregated into a single compact group when possible. */
160
209
  const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
161
210
  const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
162
211
  /**
163
- * Cap memory by replacing old items with tiny tombstones. Ink's <Static>
164
- * tracks rendered items by array length the array must never shrink, but
165
- * we can swap out heavy objects for lightweight `{ kind: "tombstone", id }`
166
- * 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.
167
215
  */
168
216
  const MAX_LIVE_HISTORY = 200;
169
217
  function compactHistory(items) {
@@ -190,66 +238,41 @@ function summarizeGoalCompletion(summary) {
190
238
  const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
191
239
  return statusLine ?? changedLine ?? verificationLine ?? lines[0];
192
240
  }
193
- function formatGoalWorkerFinishedTitle(taskTitle, status) {
194
- return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
195
- }
196
- function countGoalTasksByStatus(tasks, status) {
197
- return tasks.filter((task) => task.status === status).length;
198
- }
199
- function firstText(values) {
200
- return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
201
- }
202
- function truncateGoalSummary(value, maxLength = 90) {
203
- const normalized = value.replace(/\s+/g, " ").trim();
204
- if (normalized.length <= maxLength)
241
+ const GOAL_PROGRESS_TEXT_LIMIT = 72;
242
+ export function truncateGoalProgressText(text) {
243
+ const normalized = text.replace(/\s+/g, " ").trim();
244
+ if (normalized.length <= GOAL_PROGRESS_TEXT_LIMIT)
205
245
  return normalized;
206
- return `${normalized.slice(0, maxLength - 1)}…`;
246
+ return `${normalized.slice(0, GOAL_PROGRESS_TEXT_LIMIT - 1).trimEnd()}…`;
207
247
  }
208
- export function buildGoalSummaryRows(run) {
209
- const rows = [];
210
- const doneTasks = countGoalTasksByStatus(run.tasks, "done");
211
- const failedTasks = countGoalTasksByStatus(run.tasks, "failed");
212
- const blockedTasks = countGoalTasksByStatus(run.tasks, "blocked");
213
- const taskSuffix = [
214
- failedTasks > 0 ? `${failedTasks} failed` : undefined,
215
- blockedTasks > 0 ? `${blockedTasks} blocked` : undefined,
216
- ].filter((item) => item !== undefined);
217
- rows.push({
218
- label: "Tasks",
219
- value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
220
- ...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
221
- });
222
- const verifierResult = run.verifier?.lastResult;
223
- const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
224
- rows.push({
225
- label: "Verifier",
226
- value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
227
- ...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
228
- });
229
- const latestEvidence = run.evidence.at(-1);
230
- rows.push({
231
- label: "Evidence",
232
- value: `${run.evidence.length} recorded`,
233
- ...(latestEvidence
234
- ? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
235
- : {}),
236
- });
237
- if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
238
- rows.push({
239
- label: run.status === "paused" ? "Paused on" : "Blocked on",
240
- value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
241
- ? formatGoalBlockingPrerequisites(run)
242
- : (run.blockers[0] ?? "manual review"), 110),
243
- });
248
+ function formatGoalWorkerFinishedTitle(taskTitle, status) {
249
+ const prefix = status === "done" ? "Done" : "Failed";
250
+ return truncateGoalProgressText(`${prefix}: ${taskTitle}`);
251
+ }
252
+ function goalProgressLoaderStatus(item) {
253
+ if (item.status === "failed" || item.status === "fail" || item.status === "blocked")
254
+ return "error";
255
+ if (item.phase === "worker_finished" ||
256
+ item.phase === "verifier_finished" ||
257
+ item.phase === "terminal") {
258
+ return "done";
244
259
  }
245
- else if (run.successCriteria.length > 0) {
246
- rows.push({
247
- label: "Criteria",
248
- value: `${run.successCriteria.length} checked`,
249
- detail: truncateGoalSummary(run.successCriteria[0] ?? "", 80),
250
- });
260
+ return "running";
261
+ }
262
+ function goalProgressColor(item, theme) {
263
+ const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
264
+ if (isError)
265
+ return theme.error;
266
+ if (item.phase === "worker_finished" || item.phase === "terminal")
267
+ return theme.success;
268
+ if (item.phase === "verifier_finished" || item.phase === "verifier_started")
269
+ return theme.accent;
270
+ if (item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working") {
271
+ return theme.secondary;
251
272
  }
252
- return rows.slice(0, 4);
273
+ if (item.phase === "continuing")
274
+ return theme.warning;
275
+ return theme.primary;
253
276
  }
254
277
  function goalTerminalProgressId(run) {
255
278
  return `goal-terminal-${run.id}`;
@@ -262,10 +285,20 @@ function goalTerminalRunIdFromItem(item) {
262
285
  return item.id.slice("goal-terminal-".length);
263
286
  }
264
287
  function goalProgressMatchesDraft(item, draft) {
265
- return (item.title === draft.title &&
288
+ return (item.phase === draft.phase &&
289
+ item.title === draft.title &&
266
290
  item.detail === draft.detail &&
291
+ item.workerId === draft.workerId &&
267
292
  item.status === draft.status &&
268
- JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []));
293
+ JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
294
+ JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
295
+ }
296
+ export function appendGoalProgressDraft(items, draft, makeId) {
297
+ const previous = items.at(-1);
298
+ if (previous?.kind === "goal_progress" && goalProgressMatchesDraft(previous, draft)) {
299
+ return items;
300
+ }
301
+ return [...items, { ...draft, id: makeId() }];
269
302
  }
270
303
  /**
271
304
  * Reconcile terminal Goal cards that are already visible in this UI session.
@@ -277,6 +310,16 @@ function goalProgressMatchesDraft(item, draft) {
277
310
  * event append that card first, then use this helper to tombstone stale older
278
311
  * cards for the same run.
279
312
  */
313
+ export function getNextGeneratedItemId(items) {
314
+ let max = -1;
315
+ for (const item of items) {
316
+ const raw = item.id.startsWith("ui-") ? item.id.slice(3) : item.id;
317
+ const n = Number(raw);
318
+ if (Number.isInteger(n) && n >= 0 && n > max)
319
+ max = n;
320
+ }
321
+ return max + 1;
322
+ }
280
323
  export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
281
324
  const runIds = new Set(runs.map((run) => run.id));
282
325
  const terminalByRun = new Map(runs
@@ -304,8 +347,9 @@ export function formatGoalTerminalProgress(run) {
304
347
  kind: "goal_progress",
305
348
  phase: "terminal",
306
349
  title: `Goal passed: ${run.title}`,
307
- detail: "Verifier evidence is recorded; auto-continuation stopped.",
350
+ detail: goalPassedDetail(run),
308
351
  summaryRows: buildGoalSummaryRows(run),
352
+ summarySections: buildGoalFinalSummarySections(run),
309
353
  status: run.status,
310
354
  };
311
355
  case "failed":
@@ -345,29 +389,186 @@ export function formatGoalTerminalProgress(run) {
345
389
  }
346
390
  }
347
391
  export function shouldHideHistoryForOverlayView(isOverlayView, _isAgentRunning) {
348
- // Overlay panes are standalone full-screen states. Do not render chat Static
349
- // history behind them, otherwise panes appear below the previous transcript.
392
+ // Overlay panes are standalone full-screen states. Finalized chat rows are
393
+ // printed outside Ink, so overlays should never replay transcript UI behind them.
350
394
  return isOverlayView;
351
395
  }
352
396
  export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
353
- return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
397
+ return isAgentRunning && overlayPane === "goal";
354
398
  }
355
399
  export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender: _stabilizeOverlayPaneRerender, }) {
356
400
  return shouldHideHistoryForOverlay;
357
401
  }
358
- export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
359
- const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
402
+ export function getDoneFlushDecision({ planOverlayPending, goalMode, goalAutoExpand, }) {
403
+ return {
404
+ showDoneStatus: !(planOverlayPending ||
405
+ goalMode === "planner" ||
406
+ goalMode === "setup" ||
407
+ goalAutoExpand),
408
+ flushLiveItems: true,
409
+ };
410
+ }
411
+ export function getGoalSetupFinishedPaneTransition() {
412
+ return {
413
+ overlay: "goal",
414
+ goalAutoExpand: true,
415
+ planAutoExpand: false,
416
+ suppressDoneStatus: true,
417
+ };
418
+ }
419
+ export function getGoalSetupPaneTransitionAfterRun({ isGoalSetupCommand, setupPanePending, }) {
420
+ return isGoalSetupCommand && setupPanePending ? getGoalSetupFinishedPaneTransition() : null;
421
+ }
422
+ export function shouldResetUIForSetupPaneTransition({ hasResetUI, hasSessionStore, }) {
423
+ // Opening a review pane is a full-screen state transition. A bare React state
424
+ // flip hides history in the virtual tree, but it does not reset Ink/log-update's
425
+ // already-written terminal frame, so the pane can render below prior chat.
426
+ return hasResetUI && hasSessionStore;
427
+ }
428
+ export const shouldResetUIForGoalSetupPaneTransition = shouldResetUIForSetupPaneTransition;
429
+ export function getGoalActivationPaneTransition() {
430
+ return { overlay: null, goalAutoExpand: false, planAutoExpand: false, resetReviewScreen: true };
431
+ }
432
+ export function getGoalContinuationChoiceKey({ runId, decision, }) {
433
+ switch (decision.kind) {
434
+ case "create_task":
435
+ return `${runId}:create_task:${decision.title}:${decision.prompt}`;
436
+ case "start_worker":
437
+ case "pause":
438
+ return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
439
+ case "run_verifier":
440
+ return `${runId}:run_verifier:${decision.command}`;
441
+ case "blocked":
442
+ case "complete":
443
+ case "terminal":
444
+ case "wait":
445
+ return `${runId}:${decision.kind}:${decision.reason}`;
446
+ }
447
+ }
448
+ export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, hasParagraphBreakLiveUserMessage = false, }) {
449
+ const shouldPreserveStatic = isUserScrolled || hasTallLiveUserMessage || hasParagraphBreakLiveUserMessage;
450
+ const shouldAutoFollow = !(isUserScrolled || hasTallLiveUserMessage);
360
451
  return {
361
- preserveStatic: shouldStabilize && hasNewOutput,
362
- autoFollow: !shouldStabilize,
452
+ preserveStatic: shouldPreserveStatic && hasNewOutput,
453
+ autoFollow: shouldAutoFollow,
363
454
  };
364
455
  }
456
+ export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
457
+ if (wasGoalSetupTurn)
458
+ return "off";
459
+ if (currentMode === "planner" || currentMode === "setup")
460
+ return currentMode;
461
+ if (queuedSyntheticEvents > 0)
462
+ return "coordinator";
463
+ if (activeContinuationFlights > 0)
464
+ return "coordinator";
465
+ if (currentMode === "coordinator" && runningGoalIds > 0)
466
+ return "coordinator";
467
+ return "off";
468
+ }
469
+ export function hasParagraphBreakLiveUserMessage(text) {
470
+ return /\n[ \t]*\n/.test(text);
471
+ }
365
472
  export function isTallLiveUserMessage(text, rows) {
366
473
  return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
367
474
  }
368
475
  export function getStaticHistoryKey({ resizeKey }) {
369
476
  return `${resizeKey}`;
370
477
  }
478
+ const MIN_LIVE_AREA_ROWS = 3;
479
+ const INPUT_AREA_ROWS = 3;
480
+ const STATUS_SLOT_ROWS = 2;
481
+ const FOOTER_ONE_LINE_ROWS = 1;
482
+ const FOOTER_TWO_LINE_ROWS = 2;
483
+ const GOAL_STATUS_ROWS = 1;
484
+ const COLLAPSED_FOOTER_STATUS_ROWS = 1;
485
+ const MAX_EXPANDED_BACKGROUND_TASK_ROWS = 7;
486
+ function isAgentSpacingKind(kind) {
487
+ return [
488
+ "assistant",
489
+ "queued",
490
+ "goal_progress",
491
+ "tool_start",
492
+ "tool_done",
493
+ "tool_group",
494
+ "server_tool_start",
495
+ "server_tool_done",
496
+ "subagent_group",
497
+ ].includes(kind);
498
+ }
499
+ function isToolBoundaryKind(kind) {
500
+ return [
501
+ "goal_progress",
502
+ "tool_start",
503
+ "tool_done",
504
+ "tool_group",
505
+ "server_tool_start",
506
+ "server_tool_done",
507
+ "subagent_group",
508
+ ].includes(kind);
509
+ }
510
+ function isAgentSpacingItem(item) {
511
+ return isAgentSpacingKind(item.kind);
512
+ }
513
+ export function shouldTopSpaceAfterPrintedAgentBoundary({ currentKind, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
514
+ const needsExternalSpacing = [
515
+ "goal_progress",
516
+ "tool_start",
517
+ "tool_group",
518
+ "assistant",
519
+ "queued",
520
+ ].includes(currentKind);
521
+ if (!needsExternalSpacing)
522
+ return false;
523
+ if (previousLiveItem !== undefined)
524
+ return false;
525
+ const previousKind = lastPendingHistoryItem?.kind ?? lastHistoryItem?.kind;
526
+ return previousKind !== undefined && isAgentSpacingKind(previousKind);
527
+ }
528
+ export function shouldTopSpaceAssistantAfterToolBoundary({ text, previousLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
529
+ if (text.trim().length === 0)
530
+ return false;
531
+ if (shouldTopSpaceAfterPrintedAgentBoundary({
532
+ currentKind: "assistant",
533
+ previousLiveItem,
534
+ lastPendingHistoryItem,
535
+ lastHistoryItem,
536
+ })) {
537
+ return true;
538
+ }
539
+ const previousKind = previousLiveItem?.kind;
540
+ return previousKind !== undefined && isToolBoundaryKind(previousKind);
541
+ }
542
+ export function shouldTopSpaceStreamingAssistant({ visibleStreamingText, lastLiveItem, lastPendingHistoryItem, lastHistoryItem, }) {
543
+ return shouldTopSpaceAssistantAfterToolBoundary({
544
+ text: visibleStreamingText,
545
+ previousLiveItem: lastLiveItem,
546
+ lastPendingHistoryItem,
547
+ lastHistoryItem,
548
+ });
549
+ }
550
+ export function getChatControlsLayoutDecision({ rows, agentRunning, activityVisible, doneStatusVisible, stallStatusVisible, exitPending, footerStatusLayout, taskBarExpanded, goalStatusEntryCount, footerFitsOnOneLine, }) {
551
+ const statusRows = activityVisible || stallStatusVisible || doneStatusVisible || agentRunning
552
+ ? STATUS_SLOT_ROWS
553
+ : 0;
554
+ const footerRows = exitPending || footerFitsOnOneLine ? FOOTER_ONE_LINE_ROWS : FOOTER_TWO_LINE_ROWS;
555
+ const goalRows = !exitPending && goalStatusEntryCount > 0 ? GOAL_STATUS_ROWS : 0;
556
+ const footerStatusRows = footerStatusLayout.stack
557
+ ? Number(footerStatusLayout.hasBackgroundTasks) + Number(footerStatusLayout.hasUpdateNotice)
558
+ : footerStatusLayout.hasBackgroundTasks || footerStatusLayout.hasUpdateNotice
559
+ ? COLLAPSED_FOOTER_STATUS_ROWS
560
+ : 0;
561
+ const expandedTaskRows = taskBarExpanded && footerStatusLayout.hasBackgroundTasks
562
+ ? MAX_EXPANDED_BACKGROUND_TASK_ROWS - COLLAPSED_FOOTER_STATUS_ROWS
563
+ : 0;
564
+ const controlsRows = statusRows + INPUT_AREA_ROWS + footerRows + goalRows + footerStatusRows + expandedTaskRows;
565
+ const maxControlsRows = Math.max(1, rows - MIN_LIVE_AREA_ROWS);
566
+ const boundedControlsRows = Math.min(controlsRows, maxControlsRows);
567
+ return {
568
+ controlsRows: boundedControlsRows,
569
+ liveAreaRows: Math.max(MIN_LIVE_AREA_ROWS, rows - boundedControlsRows),
570
+ };
571
+ }
371
572
  // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
372
573
  /** Check whether an item is still active (running spinner, pending result). */
373
574
  export function isActiveItem(item) {
@@ -386,15 +587,17 @@ export function isActiveItem(item) {
386
587
  }
387
588
  }
388
589
  /**
389
- * Partition live items into completed (flushable to Static) and still-active.
590
+ * Partition live items into completed (flushable to finalized history) and still-active.
390
591
  * Completed items precede active ones — we flush the longest contiguous prefix
391
592
  * of completed items to keep ordering stable.
392
593
  */
393
- function partitionCompleted(items) {
394
- // Find the first active item — everything before it is safe to flush
594
+ export function partitionCompleted(items) {
595
+ // Find the first active item — everything before it is safe to flush as a
596
+ // single chronological prefix. Splitting assistant text out of that prefix
597
+ // lets later tool rows print to scrollback above the message that introduced
598
+ // them, so keep the prefix intact.
395
599
  const firstActiveIdx = items.findIndex(isActiveItem);
396
600
  if (firstActiveIdx === -1) {
397
- // All items are completed
398
601
  return { flushed: items, remaining: [] };
399
602
  }
400
603
  if (firstActiveIdx === 0) {
@@ -405,6 +608,29 @@ function partitionCompleted(items) {
405
608
  remaining: items.slice(firstActiveIdx),
406
609
  };
407
610
  }
611
+ function normalizeAssistantText(text) {
612
+ return stripDoneMarkers(text).trim();
613
+ }
614
+ function isSameAssistantText(item, text) {
615
+ return item.kind === "assistant" && normalizeAssistantText(item.text) === text;
616
+ }
617
+ export function pinStreamingTextBeforeToolBoundary({ items, visibleStreamingText, thinking, thinkingMs, makeId, }) {
618
+ const text = normalizeAssistantText(visibleStreamingText);
619
+ if (text.length === 0)
620
+ return items;
621
+ if (items.some((item) => item.kind === "assistant"))
622
+ return items;
623
+ return [
624
+ ...items,
625
+ {
626
+ kind: "assistant",
627
+ text,
628
+ thinking: thinking.length > 0 ? thinking : undefined,
629
+ thinkingMs: thinking.length > 0 ? thinkingMs : undefined,
630
+ id: makeId(),
631
+ },
632
+ ];
633
+ }
408
634
  // ── Duration summary ─────────────────────────────────────
409
635
  function formatDuration(ms) {
410
636
  const totalSec = Math.round(ms / 1000);
@@ -452,8 +678,8 @@ function pickDurationVerb(toolsUsed) {
452
678
  return "Ran & investigated for";
453
679
  if (has("bash"))
454
680
  return "Executed commands for";
455
- if (hasAny("tasks", "task-output", "task-stop"))
456
- return "Managed tasks for";
681
+ if (hasAny("task-output", "task-stop"))
682
+ return "Managed background processes for";
457
683
  if (has("grep") && has("read"))
458
684
  return "Investigated for";
459
685
  if (has("grep") && has("find"))
@@ -481,65 +707,16 @@ function pickDurationVerb(toolsUsed) {
481
707
  ];
482
708
  return phrases[Math.floor(Math.random() * phrases.length)];
483
709
  }
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
710
  // ── App Component ──────────────────────────────────────────
530
711
  export function App(props) {
531
712
  const theme = useTheme();
532
713
  const switchTheme = useSetTheme();
533
- const { columns, resizeKey } = useTerminalSize();
714
+ const { write: writeStdout } = useStdout();
715
+ const { columns, rows } = useTerminalSize();
534
716
  // Hoisted before terminal title hook so it can reference them
535
717
  const [lastUserMessage, setLastUserMessage] = useState("");
536
718
  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;
719
+ const [goalMode, setGoalMode] = useState(props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off");
543
720
  // Terminal title — updated later after agentLoop is created
544
721
  // (hoisted here so the hook is always called in the same order)
545
722
  const [titleRunning, setTitleRunning] = useState(false);
@@ -549,14 +726,11 @@ export function App(props) {
549
726
  isRunning: titleRunning,
550
727
  sessionTitle,
551
728
  });
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.
729
+ // Completed transcript rows are kept as durable session data but are no longer
730
+ // rendered through Ink history. They are serialized once into real terminal
731
+ // scrollback via terminalHistoryPrinter, while Ink owns only live rows and
732
+ // controls. This avoids Static/log-update replay drift on resize/remount.
557
733
  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
734
  const stored = props.sessionStore?.history;
561
735
  if (stored && stored.length > 0)
562
736
  return stored;
@@ -567,24 +741,19 @@ export function App(props) {
567
741
  });
568
742
  // Items from the current/last turn — rendered in the live area so they stay visible.
569
743
  // Seed from sessionStore so Goal progress/completion rows and other live output
570
- // survive pane/overlay/resize remounts before they are flushed to <Static>.
744
+ // survive pane/overlay/resize remounts before they are finalized.
571
745
  const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
572
746
  // overlay seeded from sessionStore (lives across remount). Falls back to
573
747
  // props.initialOverlay (CLI launched with one), then null.
574
748
  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
749
  const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
578
- const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
579
750
  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
751
  const agentRunningRef = useRef(false);
586
752
  const runningGoalIdsRef = useRef(new Set());
587
753
  const activeVerifierRunIdsRef = useRef(new Set());
754
+ const queuedGoalSyntheticEventsRef = useRef(0);
755
+ const goalContinuationFlightsRef = useRef(new Set());
756
+ const goalContinuationRecentChoicesRef = useRef(new Map());
588
757
  const startGoalRunRef = useRef(() => { });
589
758
  const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
590
759
  const currentPixelFixRef = useRef(null);
@@ -594,12 +763,14 @@ export function App(props) {
594
763
  const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
595
764
  // Suppress "done" status when a plan overlay is about to open
596
765
  const planOverlayPendingRef = useRef(false);
766
+ const goalSetupPanePendingRef = useRef(false);
597
767
  const [gitBranch, setGitBranch] = useState(null);
598
768
  const [currentModel, setCurrentModel] = useState(props.model);
599
769
  const [currentProvider, setCurrentProvider] = useState(props.provider);
600
770
  const [currentTools, setCurrentTools] = useState(props.tools);
601
771
  const currentToolsRef = useRef(props.tools);
602
772
  const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
773
+ const [renderMarkdown, setRenderMarkdown] = useState(true);
603
774
  const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
604
775
  const repoMapInjectionEnabledRef = useRef(true);
605
776
  const repoMapDirtyRef = useRef(true);
@@ -608,10 +779,12 @@ export function App(props) {
608
779
  const repoMapChangedCountRef = useRef(0);
609
780
  const repoMapCacheRef = useRef(createRepoMapCache());
610
781
  const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
782
+ const [goalAutoExpand, setGoalAutoExpand] = useState(props.sessionStore?.goalAutoExpand ?? false);
783
+ const goalAutoExpandRef = useRef(props.sessionStore?.goalAutoExpand ?? false);
611
784
  const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
612
785
  const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
613
786
  const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
614
- const planModeStateRef = useRef(planMode);
787
+ const goalModeStateRef = useRef(goalMode);
615
788
  // Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
616
789
  // times we've nudged the agent to continue the same step. Reset whenever a
617
790
  // new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
@@ -619,23 +792,14 @@ export function App(props) {
619
792
  const followUpNudgesRef = useRef({ step: 0, count: 0 });
620
793
  // Seed the per-item ID counter so it doesn't collide with IDs already in
621
794
  // 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
- })());
795
+ // (resize, overlay toggle, goal pane open, etc.) starts the counter at 0
796
+ // and new items generate ids "ui-0", "ui-1", "ui-2"… that collide with
797
+ // the same ids from the previous mount, triggering React's duplicate-key
798
+ // warning and causing duplicate/omitted renders.
799
+ const nextIdRef = useRef(getNextGeneratedItemId([
800
+ ...(props.sessionStore?.history ?? props.initialHistory ?? []),
801
+ ...(props.sessionStore?.liveItems ?? []),
802
+ ]));
639
803
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
640
804
  const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
641
805
  const persistedIndexRef = useRef(messagesRef.current.length);
@@ -666,8 +830,11 @@ export function App(props) {
666
830
  */
667
831
  const triggerAutoSetupRef = useRef(async () => { });
668
832
  const getId = () => `ui-${nextIdRef.current++}`;
833
+ const appendGoalAgentTransition = useCallback((text) => {
834
+ setLiveItems((prev) => [...prev, { kind: "goal_agent_transition", text, id: getId() }]);
835
+ }, []);
669
836
  const appendGoalProgress = useCallback((item) => {
670
- setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
837
+ setLiveItems((prev) => appendGoalProgressDraft(prev, item, getId));
671
838
  }, []);
672
839
  const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
673
840
  const clearGoalStatusEntry = useCallback((runId) => {
@@ -686,21 +853,59 @@ export function App(props) {
686
853
  return next;
687
854
  });
688
855
  }, [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. */
856
+ const sessionStore = props.sessionStore;
857
+ const terminalHistoryContextRef = useRef({
858
+ theme,
859
+ columns,
860
+ version: props.version,
861
+ model: currentModel,
862
+ provider: currentProvider,
863
+ cwd: displayedCwd,
864
+ });
865
+ useEffect(() => {
866
+ terminalHistoryContextRef.current = {
867
+ theme,
868
+ columns,
869
+ version: props.version,
870
+ model: currentModel,
871
+ provider: currentProvider,
872
+ cwd: displayedCwd,
873
+ };
874
+ }, [theme, columns, props.version, currentModel, currentProvider, displayedCwd]);
875
+ const printHistoryItems = useCallback((items, options) => {
876
+ if (!props.terminalHistoryPrinter || items.length === 0)
877
+ return;
878
+ props.terminalHistoryPrinter.print(items, terminalHistoryContextRef.current, {
879
+ ...options,
880
+ write: writeStdout,
881
+ });
882
+ }, [props.terminalHistoryPrinter, writeStdout]);
883
+ const pendingHistoryFlushRef = useRef([]);
884
+ const streamedAssistantFlushRef = useRef({
885
+ flushedChars: 0,
886
+ text: "",
887
+ });
888
+ const [historyFlushGeneration, setHistoryFlushGeneration] = useState(0);
694
889
  const queueFlush = useCallback((items) => {
695
- if (items.length === 0)
890
+ const flushed = trimFlushedItems(items);
891
+ if (flushed.length === 0)
696
892
  return;
697
- pendingFlushRef.current = [...pendingFlushRef.current, ...items];
698
- if (props.sessionStore) {
893
+ pendingHistoryFlushRef.current = [...pendingHistoryFlushRef.current, ...flushed];
894
+ if (sessionStore) {
699
895
  const queuedIds = new Set(items.map((item) => item.id));
700
- props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
896
+ sessionStore.liveItems = (sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
701
897
  }
702
- setFlushGeneration((g) => g + 1);
703
- }, [props.sessionStore]);
898
+ setHistoryFlushGeneration((generation) => generation + 1);
899
+ }, [sessionStore]);
900
+ const finalizeSubmittedUserItem = useCallback((item) => {
901
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
902
+ setLiveItems((prev) => {
903
+ if (prev.length > 0)
904
+ queueFlush(prev);
905
+ queueFlush([item]);
906
+ return [];
907
+ });
908
+ }, [queueFlush]);
704
909
  // Mirror runtime state choices (model/provider/thinking) into renderApp's
705
910
  // closure so unmount/remount preserves them.
706
911
  const onRuntimeStateChange = props.onRuntimeStateChange;
@@ -715,12 +920,35 @@ export function App(props) {
715
920
  thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
716
921
  });
717
922
  }, [thinkingEnabled, currentModel, onRuntimeStateChange]);
923
+ useEffect(() => {
924
+ printHistoryItems(history);
925
+ }, [history, printHistoryItems]);
926
+ useEffect(() => {
927
+ const flushed = pendingHistoryFlushRef.current;
928
+ if (flushed.length === 0)
929
+ return;
930
+ pendingHistoryFlushRef.current = [];
931
+ printHistoryItems(flushed);
932
+ const flushedIds = new Set(flushed.map((item) => item.id));
933
+ setLiveItems((prev) => prev.filter((item) => !flushedIds.has(item.id)));
934
+ setHistory((prev) => {
935
+ const existingIds = new Set(prev.map((item) => item.id));
936
+ const nextItems = flushed.filter((item) => !existingIds.has(item.id));
937
+ if (nextItems.length === 0)
938
+ return prev;
939
+ const next = compactHistory([...prev, ...nextItems]);
940
+ if (sessionStore)
941
+ sessionStore.history = next;
942
+ return next;
943
+ });
944
+ }, [historyFlushGeneration, printHistoryItems, sessionStore]);
718
945
  // Mirror session state into renderApp's closure so resetUI() can re-seed
719
946
  // the conversation on remount. Each panel that previously did a bare ANSI
720
- // screen clear (overlay open/close, plan accept/reject, /clear, startTask)
947
+ // screen clear (overlay open/close, plan accept/reject, /clear)
721
948
  // now goes through resetUI; without these mirrors, the chat would vanish.
722
- const sessionStore = props.sessionStore;
949
+ const historyRef = useRef(history);
723
950
  useEffect(() => {
951
+ historyRef.current = history;
724
952
  if (sessionStore)
725
953
  sessionStore.history = history;
726
954
  }, [history, sessionStore]);
@@ -744,10 +972,19 @@ export function App(props) {
744
972
  if (sessionStore)
745
973
  sessionStore.overlay = overlay;
746
974
  }, [overlay, sessionStore]);
975
+ useEffect(() => {
976
+ goalAutoExpandRef.current = goalAutoExpand;
977
+ if (sessionStore)
978
+ sessionStore.goalAutoExpand = goalAutoExpand;
979
+ }, [goalAutoExpand, sessionStore]);
747
980
  useEffect(() => {
748
981
  if (sessionStore)
749
982
  sessionStore.goalStatusEntries = goalStatusEntries;
750
983
  }, [goalStatusEntries, sessionStore]);
984
+ useEffect(() => {
985
+ if (sessionStore)
986
+ sessionStore.goalMode = goalMode;
987
+ }, [goalMode, sessionStore]);
751
988
  // pendingAction is consumed via a useEffect AFTER agentLoop is created
752
989
  // — see below where useAgentLoop is set up.
753
990
  const pendingActionConsumedRef = useRef(false);
@@ -769,10 +1006,8 @@ export function App(props) {
769
1006
  void reconcileActiveGoalRuns(props.cwd, {
770
1007
  isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
771
1008
  }).then(({ runs }) => {
772
- const counts = summarizeGoalCountsFromRuns(runs);
773
1009
  if (cancelled)
774
1010
  return;
775
- setGoalCount(counts.active);
776
1011
  setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
777
1012
  setGoalStatusEntries((prev) => {
778
1013
  const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
@@ -813,19 +1048,23 @@ export function App(props) {
813
1048
  useEffect(() => {
814
1049
  currentToolsRef.current = currentTools;
815
1050
  }, [currentTools]);
816
- // ── Plan mode wiring ─────────────────────────────────────
817
- // Sync planModeRef with React state
1051
+ // ── Runtime mode wiring ──────────────────────────────────
1052
+ // Sync runtime mode refs with React state.
818
1053
  useEffect(() => {
819
- planModeStateRef.current = planMode;
820
- if (props.planModeRef) {
821
- props.planModeRef.current = planMode;
822
- }
823
- }, [planMode, props.planModeRef]);
1054
+ goalModeStateRef.current = goalMode;
1055
+ if (props.goalModeRef) {
1056
+ props.goalModeRef.current = goalMode;
1057
+ }
1058
+ }, [goalMode, props.goalModeRef]);
1059
+ const setActiveGoalReferences = useCallback((references) => {
1060
+ if (props.goalReferencesRef)
1061
+ props.goalReferencesRef.current = references;
1062
+ }, [props.goalReferencesRef]);
824
1063
  const rebuildSystemPrompt = useCallback(async (options) => {
825
1064
  const approvedPlanPath = options?.clearApprovedPlan
826
1065
  ? undefined
827
1066
  : (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);
1067
+ 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
1068
  }, [props.skills]);
830
1069
  const replaceSystemPrompt = useCallback(async (options) => {
831
1070
  const newPrompt = await rebuildSystemPrompt(options);
@@ -834,6 +1073,28 @@ export function App(props) {
834
1073
  }
835
1074
  return newPrompt;
836
1075
  }, [rebuildSystemPrompt]);
1076
+ const setGoalModeAndPrompt = useCallback(async (nextMode, options) => {
1077
+ goalModeStateRef.current = nextMode;
1078
+ if (props.goalModeRef)
1079
+ props.goalModeRef.current = nextMode;
1080
+ if (props.sessionStore)
1081
+ props.sessionStore.goalMode = nextMode;
1082
+ setGoalMode(nextMode);
1083
+ await replaceSystemPrompt({ ...options, goalMode: nextMode });
1084
+ }, [props.goalModeRef, props.sessionStore, replaceSystemPrompt]);
1085
+ const clearGoalModeIfIdle = useCallback(() => {
1086
+ setTimeout(() => {
1087
+ if (goalModeStateRef.current === "off")
1088
+ return;
1089
+ if (runningGoalIdsRef.current.size > 0)
1090
+ return;
1091
+ if (activeVerifierRunIdsRef.current.size > 0)
1092
+ return;
1093
+ if (queuedGoalSyntheticEventsRef.current > 0)
1094
+ return;
1095
+ void setGoalModeAndPrompt("off");
1096
+ }, 0);
1097
+ }, [setGoalModeAndPrompt]);
837
1098
  /**
838
1099
  * Unified "apply detection result" pipeline. Called from three sites:
839
1100
  * 1. Initial mount (existing project at startup).
@@ -918,53 +1179,6 @@ export function App(props) {
918
1179
  useEffect(() => {
919
1180
  void applyLanguageDetectionRef.current("initial");
920
1181
  }, []);
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
1182
  const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
969
1183
  const sm = sessionManagerRef.current;
970
1184
  if (!sm)
@@ -1011,7 +1225,7 @@ export function App(props) {
1011
1225
  * Other tool kinds skip detection entirely to avoid wasted filesystem stats.
1012
1226
  *
1013
1227
  * No restart required: the system prompt is mutated in place, same mechanism
1014
- * already used for plan mode + pixel-fix chdir.
1228
+ * used for pixel-fix chdir.
1015
1229
  *
1016
1230
  * Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
1017
1231
  * empty to keep agent-loop options stable) can call the freshest version.
@@ -1312,7 +1526,6 @@ export function App(props) {
1312
1526
  }, [
1313
1527
  persistNewMessages,
1314
1528
  stripRepoMapMessages,
1315
- planMode,
1316
1529
  props.cwd,
1317
1530
  props.skills,
1318
1531
  currentProvider,
@@ -1322,6 +1535,11 @@ export function App(props) {
1322
1535
  resolveCredentials,
1323
1536
  ]),
1324
1537
  onTurnText: useCallback((text, thinking, thinkingMs) => {
1538
+ if (goalModeStateRef.current === "planner") {
1539
+ return;
1540
+ }
1541
+ const hadStreamedAssistantFlush = streamedAssistantFlushRef.current.flushedChars > 0;
1542
+ const unflushedAssistantText = text.slice(streamedAssistantFlushRef.current.flushedChars);
1325
1543
  // Track [DONE:n] markers for plan step progress
1326
1544
  if (planStepsRef.current.length > 0) {
1327
1545
  const completed = findCompletedMarkers(text);
@@ -1336,15 +1554,9 @@ export function App(props) {
1336
1554
  followUpNudgesRef.current = { step: 0, count: 0 };
1337
1555
  }
1338
1556
  }
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.
1557
+ // Flush completed rows from the previous turn to finalized terminal
1558
+ // history. Ink keeps only the active turn, preventing live-area growth
1559
+ // and avoiding Static/log-update replay during resize/remount churn.
1348
1560
  setLiveItems((prev) => {
1349
1561
  const flushed = flushOnTurnText(prev);
1350
1562
  if (flushed.length > 0) {
@@ -1353,10 +1565,7 @@ export function App(props) {
1353
1565
  // Split text on [DONE:N] markers so each marker renders inline as
1354
1566
  // a styled "✓ Step N: <description>" item at the position the
1355
1567
  // 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);
1568
+ const segments = segmentDisplayText(unflushedAssistantText, planStepsRef.current);
1360
1569
  const items = [];
1361
1570
  let thinkingAttached = false;
1362
1571
  for (const seg of segments) {
@@ -1369,7 +1578,7 @@ export function App(props) {
1369
1578
  // contains multiple text chunks split by markers.
1370
1579
  thinking: thinkingAttached ? undefined : thinking,
1371
1580
  thinkingMs: thinkingAttached ? undefined : thinkingMs,
1372
- planMode: planModeLocalRef.current,
1581
+ continuation: hadStreamedAssistantFlush,
1373
1582
  id: getId(),
1374
1583
  });
1375
1584
  thinkingAttached = true;
@@ -1384,36 +1593,51 @@ export function App(props) {
1384
1593
  }
1385
1594
  }
1386
1595
  // 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.
1596
+ // Still persist an assistant item so a thinking block renders in
1597
+ // terminal history if there was thinking content for this turn.
1389
1598
  if (items.length === 0) {
1390
1599
  items.push({
1391
1600
  kind: "assistant",
1392
1601
  text: "",
1393
1602
  thinking,
1394
1603
  thinkingMs,
1395
- planMode: planModeLocalRef.current,
1396
1604
  id: getId(),
1397
1605
  });
1398
1606
  }
1399
- return items;
1607
+ const assistantItems = prev.filter((item) => item.kind === "assistant");
1608
+ const newAssistantText = normalizeAssistantText(unflushedAssistantText);
1609
+ const duplicatePinnedText = newAssistantText.length > 0 &&
1610
+ [...assistantItems, ...pendingHistoryFlushRef.current, ...historyRef.current].some((item) => isSameAssistantText(item, newAssistantText));
1611
+ const nextItems = duplicatePinnedText
1612
+ ? items.filter((item) => !isSameAssistantText(item, newAssistantText))
1613
+ : items;
1614
+ const flushablePrev = prev.filter((item) => item.kind !== "assistant");
1615
+ if (flushablePrev.length > 0)
1616
+ queueFlush(flushablePrev);
1617
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
1618
+ return [...assistantItems, ...nextItems];
1400
1619
  });
1401
- }, []),
1402
- onToolStart: useCallback((toolCallId, name, args) => {
1620
+ }, [queueFlush]),
1621
+ onToolStart: useCallback((toolCallId, name, args, stream) => {
1403
1622
  log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
1404
1623
  const startedAt = Date.now();
1405
1624
  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);
1625
+ const appendToolStart = (prev) => {
1626
+ const visible = pinStreamingTextBeforeToolBoundary({
1627
+ items: prev,
1628
+ visibleStreamingText: stream.text,
1629
+ thinking: stream.thinking,
1630
+ thinkingMs: stream.thinkingMs,
1631
+ makeId: getId,
1632
+ });
1633
+ const { flushed, remaining } = partitionCompleted(visible);
1411
1634
  if (flushed.length > 0) {
1412
1635
  queueFlush(flushed);
1413
1636
  }
1414
1637
  return remaining;
1415
- });
1638
+ };
1416
1639
  if (name === "subagent") {
1640
+ setLiveItems(appendToolStart);
1417
1641
  // Create or update the sub-agent group item
1418
1642
  const newAgent = {
1419
1643
  toolCallId,
@@ -1438,25 +1662,32 @@ export function App(props) {
1438
1662
  });
1439
1663
  }
1440
1664
  else if (AGGREGATABLE_TOOLS.has(name)) {
1441
- // Group concurrent read-only tools into a single compact item
1442
1665
  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;
1666
+ const reusableGroupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1667
+ item.tools.every((tool) => tool.name === name && !tool.isError));
1668
+ const prior = reusableGroupIdx === -1 ? [] : prev.slice(0, reusableGroupIdx);
1669
+ if (reusableGroupIdx !== -1 && prior.every((item) => !isActiveItem(item))) {
1670
+ const flushablePrior = prior.filter((item) => item.kind !== "assistant");
1671
+ if (flushablePrior.length > 0)
1672
+ queueFlush(flushablePrior);
1673
+ const pinnedPrior = prior.filter((item) => item.kind === "assistant");
1674
+ const candidates = prev.slice(reusableGroupIdx);
1675
+ const group = candidates[0];
1676
+ return [
1677
+ ...pinnedPrior,
1678
+ {
1679
+ ...group,
1680
+ tools: [
1681
+ ...group.tools,
1682
+ { toolCallId, name, args, status: "running", animateUntil },
1683
+ ],
1684
+ },
1685
+ ...candidates.slice(1),
1686
+ ];
1457
1687
  }
1688
+ const remaining = appendToolStart(prev);
1458
1689
  return [
1459
- ...prev,
1690
+ ...remaining,
1460
1691
  {
1461
1692
  kind: "tool_group",
1462
1693
  tools: [{ toolCallId, name, args, status: "running", animateUntil }],
@@ -1467,11 +1698,11 @@ export function App(props) {
1467
1698
  }
1468
1699
  else {
1469
1700
  setLiveItems((prev) => [
1470
- ...prev,
1701
+ ...appendToolStart(prev),
1471
1702
  { kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
1472
1703
  ]);
1473
1704
  }
1474
- }, []),
1705
+ }, [queueFlush]),
1475
1706
  onToolUpdate: useCallback((toolCallId, update) => {
1476
1707
  const u = update;
1477
1708
  // Bash progress streaming — append output to tool_start item
@@ -1544,7 +1775,7 @@ export function App(props) {
1544
1775
  };
1545
1776
  const next = [...prev];
1546
1777
  next[groupIdx] = { ...group, agents: updatedAgents };
1547
- // Flush completed items to Static to keep the live area small
1778
+ // Flush completed items to finalized history to keep the live area small
1548
1779
  const { flushed, remaining } = partitionCompleted(next);
1549
1780
  if (flushed.length > 0) {
1550
1781
  queueFlush(flushed);
@@ -1603,7 +1834,7 @@ export function App(props) {
1603
1834
  ];
1604
1835
  }
1605
1836
  }
1606
- // Flush completed items to Static to keep the live area small
1837
+ // Flush completed items to finalized history to keep the live area small
1607
1838
  const { flushed, remaining } = partitionCompleted(updated);
1608
1839
  if (flushed.length > 0) {
1609
1840
  queueFlush(flushed);
@@ -1619,14 +1850,21 @@ export function App(props) {
1619
1850
  });
1620
1851
  }
1621
1852
  }, []),
1622
- onServerToolCall: useCallback((id, name, input) => {
1853
+ onServerToolCall: useCallback((id, name, input, stream) => {
1623
1854
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
1624
1855
  const startedAt = Date.now();
1625
1856
  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.
1857
+ // Flush completed items (including assistant text) before adding server
1858
+ // tool UI — same rationale as onToolStart.
1628
1859
  setLiveItems((prev) => {
1629
- const { flushed, remaining } = partitionCompleted(prev);
1860
+ const visible = pinStreamingTextBeforeToolBoundary({
1861
+ items: prev,
1862
+ visibleStreamingText: stream.text,
1863
+ thinking: stream.thinking,
1864
+ thinkingMs: stream.thinkingMs,
1865
+ makeId: getId,
1866
+ });
1867
+ const { flushed, remaining } = partitionCompleted(visible);
1630
1868
  if (flushed.length > 0) {
1631
1869
  queueFlush(flushed);
1632
1870
  }
@@ -1643,7 +1881,7 @@ export function App(props) {
1643
1881
  },
1644
1882
  ];
1645
1883
  });
1646
- }, []),
1884
+ }, [queueFlush]),
1647
1885
  onServerToolResult: useCallback((toolUseId, resultType, data) => {
1648
1886
  log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
1649
1887
  setLiveItems((prev) => {
@@ -1677,14 +1915,14 @@ export function App(props) {
1677
1915
  },
1678
1916
  ];
1679
1917
  }
1680
- // Flush completed items to Static
1918
+ // Flush completed items to finalized history
1681
1919
  const { flushed, remaining } = partitionCompleted(updated);
1682
1920
  if (flushed.length > 0) {
1683
1921
  queueFlush(flushed);
1684
1922
  }
1685
1923
  return remaining;
1686
1924
  });
1687
- }, []),
1925
+ }, [queueFlush]),
1688
1926
  onTurnEnd: useCallback((turn, stopReason, usage) => {
1689
1927
  log("INFO", "turn", `Turn ${turn} ended`, {
1690
1928
  stopReason,
@@ -1700,8 +1938,8 @@ export function App(props) {
1700
1938
  lastActualTokensRef.current =
1701
1939
  currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
1702
1940
  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.
1941
+ // For tool-only turns (no text), flush completed items to finalized
1942
+ // history so liveItems doesn't grow unbounded across consecutive turns.
1705
1943
  setLiveItems((prev) => {
1706
1944
  const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
1707
1945
  if (flushed.length > 0) {
@@ -1709,44 +1947,42 @@ export function App(props) {
1709
1947
  }
1710
1948
  return remaining;
1711
1949
  });
1712
- }, []),
1950
+ }, [queueFlush]),
1713
1951
  onDone: useCallback((durationMs, toolsUsed) => {
1714
1952
  log("INFO", "agent", `Agent done`, {
1715
1953
  duration: `${durationMs}ms`,
1716
1954
  toolsUsed: toolsUsed.join(",") || "none",
1717
1955
  });
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 [];
1956
+ const doneDecision = getDoneFlushDecision({
1957
+ planOverlayPending: planOverlayPendingRef.current,
1958
+ goalMode: goalModeStateRef.current,
1959
+ goalAutoExpand: goalAutoExpandRef.current,
1734
1960
  });
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);
1961
+ // Don't show "done" status when plan/goal review panes are about to open —
1962
+ // the agent loop finished but we're waiting for user approval/review.
1963
+ // Still flush live transcript rows before the pane remounts; otherwise
1964
+ // setup output remains in ephemeral liveItems and appears to vanish.
1965
+ if (doneDecision.showDoneStatus) {
1966
+ setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1967
+ playNotificationSound();
1968
+ }
1969
+ // Finalize rows now; the sink writes them outside Ink and then the
1970
+ // live area is cleared, so there is no Static/live repaint race.
1971
+ if (doneDecision.flushLiveItems) {
1972
+ setLiveItems((prev) => {
1973
+ if (prev.length > 0)
1974
+ queueFlush(prev);
1975
+ return [];
1976
+ });
1977
+ }
1978
+ const nextGoalMode = nextGoalModeAfterAgentDone({
1979
+ currentMode: goalModeStateRef.current,
1980
+ runningGoalIds: runningGoalIdsRef.current.size,
1981
+ queuedSyntheticEvents: queuedGoalSyntheticEventsRef.current,
1982
+ activeContinuationFlights: goalContinuationFlightsRef.current.size,
1983
+ });
1984
+ if (nextGoalMode !== goalModeStateRef.current) {
1985
+ void setGoalModeAndPrompt(nextGoalMode);
1750
1986
  }
1751
1987
  // Goal loop: after the orchestrator handles a worker/verifier event,
1752
1988
  // continue the same Goal automatically until it reaches a terminal state.
@@ -1788,12 +2024,16 @@ export function App(props) {
1788
2024
  }
1789
2025
  })();
1790
2026
  }
1791
- }, []),
2027
+ }, [setGoalModeAndPrompt]),
1792
2028
  onAborted: useCallback(() => {
1793
2029
  log("WARN", "agent", "Agent run aborted by user");
1794
- setRunAllTasks(false);
1795
2030
  setRunAllPixel(false);
1796
2031
  currentPixelFixRef.current = null;
2032
+ queuedGoalSyntheticEventsRef.current = 0;
2033
+ goalSetupPanePendingRef.current = false;
2034
+ setActiveGoalReferences(undefined);
2035
+ if (goalModeStateRef.current !== "off")
2036
+ void setGoalModeAndPrompt("off");
1797
2037
  setDoneStatus(null);
1798
2038
  setLiveItems((prev) => {
1799
2039
  const next = prev.map((item) => {
@@ -1836,7 +2076,7 @@ export function App(props) {
1836
2076
  });
1837
2077
  return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
1838
2078
  });
1839
- }, []),
2079
+ }, [setActiveGoalReferences, setGoalModeAndPrompt]),
1840
2080
  onQueuedStart: useCallback((content) => {
1841
2081
  // When a queued message starts processing, show it as a UserItem
1842
2082
  // and flush prior items to history. Synthetic system events are hidden
@@ -1848,6 +2088,8 @@ export function App(props) {
1848
2088
  .map((c) => c.text)
1849
2089
  .join("\n");
1850
2090
  if (isGoalSyntheticEvent(displayText)) {
2091
+ queuedGoalSyntheticEventsRef.current = Math.max(0, queuedGoalSyntheticEventsRef.current - 1);
2092
+ void setGoalModeAndPrompt("coordinator");
1851
2093
  const eventInfo = parseGoalSyntheticEvent(displayText);
1852
2094
  setLiveItems((prev) => {
1853
2095
  if (prev.length > 0)
@@ -1870,11 +2112,6 @@ export function App(props) {
1870
2112
  const imageCount = typeof content === "string"
1871
2113
  ? undefined
1872
2114
  : content.filter((c) => c.type === "image").length || undefined;
1873
- setLiveItems((prev) => {
1874
- if (prev.length > 0)
1875
- queueFlush(prev);
1876
- return [];
1877
- });
1878
2115
  const userItem = {
1879
2116
  kind: "user",
1880
2117
  text: displayText,
@@ -1883,8 +2120,8 @@ export function App(props) {
1883
2120
  };
1884
2121
  setLastUserMessage(displayText);
1885
2122
  setDoneStatus(null);
1886
- setLiveItems([userItem]);
1887
- }, []),
2123
+ finalizeSubmittedUserItem(userItem);
2124
+ }, [appendGoalProgress, finalizeSubmittedUserItem, setGoalModeAndPrompt]),
1888
2125
  // Inject a "continue with the next step" follow-up when the agent
1889
2126
  // would otherwise stop mid-plan. The prompt-only instruction wasn't
1890
2127
  // enough — some models (notably Opus) treat each [DONE:n] as a
@@ -1954,27 +2191,6 @@ export function App(props) {
1954
2191
  ]);
1955
2192
  }
1956
2193
  };
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
2194
  // Sync terminal title with agent loop state
1979
2195
  useEffect(() => {
1980
2196
  setTitleRunning(agentLoop.isRunning);
@@ -2002,19 +2218,29 @@ export function App(props) {
2002
2218
  return () => clearTimeout(timer);
2003
2219
  }
2004
2220
  }, [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.
2221
+ // Consume pending post-remount work once on mount. Set by resetUI options
2222
+ // for paths that remount AND immediately drive work (plan accept/reject,
2223
+ // pixel fix, Goal approval). The work survives the unmount because
2224
+ // it lives in renderApp's closure (sessionStore), not React state.
2009
2225
  useEffect(() => {
2010
2226
  if (pendingActionConsumedRef.current)
2011
2227
  return;
2012
2228
  const action = sessionStore?.pendingAction;
2013
- if (!action)
2229
+ const pendingGoalRun = sessionStore?.pendingGoalRun;
2230
+ if (!action && !pendingGoalRun)
2014
2231
  return;
2015
2232
  pendingActionConsumedRef.current = true;
2016
- if (sessionStore)
2233
+ if (sessionStore) {
2017
2234
  sessionStore.pendingAction = undefined;
2235
+ sessionStore.pendingGoalRun = undefined;
2236
+ }
2237
+ setDoneStatus(null);
2238
+ if (pendingGoalRun) {
2239
+ startGoalRunRef.current(pendingGoalRun);
2240
+ return;
2241
+ }
2242
+ if (!action)
2243
+ return;
2018
2244
  if (action.planEvent) {
2019
2245
  const ev = action.planEvent;
2020
2246
  setLiveItems((prev) => [
@@ -2028,7 +2254,6 @@ export function App(props) {
2028
2254
  { kind: "info", text: action.infoText, id: getId() },
2029
2255
  ]);
2030
2256
  }
2031
- setDoneStatus(null);
2032
2257
  void agentLoop.run(action.prompt).catch((err) => {
2033
2258
  const errMsg = err instanceof Error ? err.message : String(err);
2034
2259
  log("ERROR", "error", errMsg);
@@ -2036,14 +2261,6 @@ export function App(props) {
2036
2261
  });
2037
2262
  // Intentional one-shot: run once on mount, never re-fire on re-render.
2038
2263
  }, []);
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
2264
  const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
2048
2265
  const trimmed = input.trim();
2049
2266
  if (trimmed.startsWith("/")) {
@@ -2098,7 +2315,8 @@ export function App(props) {
2098
2315
  }
2099
2316
  // Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
2100
2317
  // React state in place without touching terminal scrollback.
2101
- pendingFlushRef.current = [];
2318
+ pendingHistoryFlushRef.current = [];
2319
+ props.terminalHistoryPrinter?.clear();
2102
2320
  setHistory([{ kind: "banner", id: "banner" }]);
2103
2321
  setLiveItems([]);
2104
2322
  setDoneStatus(null);
@@ -2121,44 +2339,19 @@ export function App(props) {
2121
2339
  setOverlay("theme");
2122
2340
  return;
2123
2341
  }
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,
2342
+ // Handle /markdownGemini-style rendered/raw markdown toggle
2343
+ if (trimmed === "/markdown" || trimmed === "/md") {
2344
+ setRenderMarkdown((prev) => {
2345
+ const next = !prev;
2346
+ setLiveItems([
2131
2347
  {
2132
2348
  kind: "info",
2133
- text: "Eyes not set up in this project. Run /setup-eyes to get started.",
2349
+ text: next ? "Rendered markdown mode." : "Raw markdown mode.",
2134
2350
  id: getId(),
2135
2351
  },
2136
2352
  ]);
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
- ]);
2353
+ return next;
2354
+ });
2162
2355
  return;
2163
2356
  }
2164
2357
  // Handle /clearplan — dismiss the approved plan
@@ -2217,55 +2410,45 @@ export function App(props) {
2217
2410
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2218
2411
  props.sessionStore.overlay = "goal";
2219
2412
  props.sessionStore.planAutoExpand = false;
2413
+ props.sessionStore.goalAutoExpand = false;
2220
2414
  props.resetUI();
2221
2415
  }
2222
2416
  else {
2223
2417
  if (props.sessionStore) {
2224
2418
  props.sessionStore.overlay = "goal";
2225
2419
  props.sessionStore.planAutoExpand = false;
2420
+ props.sessionStore.goalAutoExpand = false;
2226
2421
  if (agentLoop.isRunning)
2227
2422
  props.sessionStore.pendingResetUI = true;
2228
2423
  }
2229
2424
  setPlanAutoExpand(false);
2425
+ setGoalAutoExpand(false);
2230
2426
  setOverlay("goal");
2231
2427
  }
2232
2428
  return;
2233
2429
  }
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
2430
  // Handle prompt-template commands (built-in + custom from .gg/commands/)
2254
2431
  const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
2255
2432
  if (promptCommandRoute) {
2256
2433
  const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
2257
2434
  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
2435
  const hasImages = inputImages.length > 0;
2436
+ const isGoalSetupCommand = isGoalPromptCommandName(cmdName);
2437
+ let promptForAgent = fullPrompt;
2438
+ if (isGoalSetupCommand) {
2439
+ const referenceContext = await buildGoalReferenceContext({
2440
+ cwd: props.cwd,
2441
+ originalGoalPrompt: fullPrompt,
2442
+ attachments: inputImages,
2443
+ });
2444
+ setActiveGoalReferences(referenceContext.references);
2445
+ promptForAgent = referenceContext.promptSection
2446
+ ? `${fullPrompt}\n\n${referenceContext.promptSection}`
2447
+ : fullPrompt;
2448
+ }
2266
2449
  const modelInfo = getModel(currentModel);
2267
2450
  const modelSupportsImages = modelInfo?.supportsImages ?? true;
2268
- const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
2451
+ const userContent = buildUserContentWithAttachments(promptForAgent, inputImages, modelSupportsImages);
2269
2452
  // Show the typed command as the user message
2270
2453
  const userItem = {
2271
2454
  kind: "user",
@@ -2275,15 +2458,30 @@ export function App(props) {
2275
2458
  };
2276
2459
  setLastUserMessage(trimmed);
2277
2460
  setDoneStatus(null);
2278
- setLiveItems([userItem]);
2461
+ finalizeSubmittedUserItem(userItem);
2279
2462
  // Send the full prompt to the agent, with user args appended if provided
2280
2463
  try {
2281
- await agentLoop.run(userContent);
2464
+ if (isGoalSetupCommand) {
2465
+ goalSetupPanePendingRef.current = true;
2466
+ await runGoalPromptSetupSequence({
2467
+ userContent,
2468
+ fullPrompt: promptForAgent,
2469
+ messagesRef,
2470
+ setGoalModeAndPrompt,
2471
+ runAgent: (content) => agentLoop.run(content),
2472
+ onStage: appendGoalAgentTransition,
2473
+ });
2474
+ }
2475
+ else {
2476
+ await agentLoop.run(userContent);
2477
+ }
2282
2478
  }
2283
2479
  catch (err) {
2284
2480
  const msg = err instanceof Error ? err.message : String(err);
2285
2481
  log("ERROR", "error", msg);
2286
2482
  const isAbort = msg.includes("aborted") || msg.includes("abort");
2483
+ if (isGoalSetupCommand)
2484
+ goalSetupPanePendingRef.current = false;
2287
2485
  setLiveItems((prev) => [
2288
2486
  ...prev,
2289
2487
  isAbort
@@ -2291,6 +2489,41 @@ export function App(props) {
2291
2489
  : toErrorItem(err, getId()),
2292
2490
  ]);
2293
2491
  }
2492
+ finally {
2493
+ if (isGoalSetupCommand) {
2494
+ setActiveGoalReferences(undefined);
2495
+ const paneTransition = getGoalSetupPaneTransitionAfterRun({
2496
+ isGoalSetupCommand,
2497
+ setupPanePending: goalSetupPanePendingRef.current,
2498
+ });
2499
+ goalSetupPanePendingRef.current = false;
2500
+ if (goalModeStateRef.current !== "off") {
2501
+ await setGoalModeAndPrompt("off");
2502
+ }
2503
+ if (paneTransition) {
2504
+ goalAutoExpandRef.current = paneTransition.goalAutoExpand;
2505
+ setTimeout(() => {
2506
+ const resetUI = props.resetUI;
2507
+ const sessionStore = props.sessionStore;
2508
+ if (shouldResetUIForGoalSetupPaneTransition({
2509
+ hasResetUI: resetUI !== undefined,
2510
+ hasSessionStore: sessionStore !== undefined,
2511
+ }) &&
2512
+ resetUI &&
2513
+ sessionStore) {
2514
+ sessionStore.overlay = paneTransition.overlay;
2515
+ sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
2516
+ sessionStore.planAutoExpand = paneTransition.planAutoExpand;
2517
+ resetUI();
2518
+ return;
2519
+ }
2520
+ setGoalAutoExpand(paneTransition.goalAutoExpand);
2521
+ setPlanAutoExpand(paneTransition.planAutoExpand);
2522
+ setOverlay(paneTransition.overlay);
2523
+ }, 300);
2524
+ }
2525
+ }
2526
+ }
2294
2527
  // Reload custom commands in case a setup command created new ones
2295
2528
  reloadCustomCommands();
2296
2529
  return;
@@ -2326,17 +2559,6 @@ export function App(props) {
2326
2559
  setLiveItems((prev) => [...prev, queuedItem]);
2327
2560
  return;
2328
2561
  }
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
2562
  // Build display text — strip image paths, show badges instead
2341
2563
  let displayText = input;
2342
2564
  if (hasImages) {
@@ -2358,7 +2580,7 @@ export function App(props) {
2358
2580
  planStepsRef.current = [];
2359
2581
  setPlanSteps([]);
2360
2582
  }
2361
- setLiveItems([userItem]);
2583
+ finalizeSubmittedUserItem(userItem);
2362
2584
  // Run agent
2363
2585
  try {
2364
2586
  await agentLoop.run(userContent);
@@ -2376,11 +2598,20 @@ export function App(props) {
2376
2598
  }
2377
2599
  }, [
2378
2600
  agentLoop,
2379
- props.onSlashCommand,
2601
+ appendGoalAgentTransition,
2380
2602
  compactConversation,
2603
+ currentModel,
2604
+ finalizeSubmittedUserItem,
2605
+ props.cwd,
2606
+ props.onSlashCommand,
2607
+ props.resetUI,
2608
+ props.sessionStore,
2381
2609
  rebuildSystemPrompt,
2382
- replaceSystemPrompt,
2383
2610
  refreshRepoMap,
2611
+ reloadCustomCommands,
2612
+ replaceSystemPrompt,
2613
+ setActiveGoalReferences,
2614
+ setGoalModeAndPrompt,
2384
2615
  stripRepoMapMessages,
2385
2616
  ]);
2386
2617
  const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
@@ -2518,90 +2749,96 @@ export function App(props) {
2518
2749
  const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
2519
2750
  const fromPrompt = (name) => {
2520
2751
  const c = promptByName.get(name);
2521
- return c ? { name: c.name, aliases: c.aliases, description: c.description } : null;
2752
+ return c
2753
+ ? {
2754
+ name: c.name,
2755
+ aliases: c.aliases,
2756
+ description: c.description,
2757
+ sectionTitle: "workflows",
2758
+ }
2759
+ : null;
2522
2760
  };
2523
2761
  const promptOrder = [
2524
2762
  // Project audits / one-shot analysis
2525
2763
  "goal",
2526
2764
  "init",
2527
- "research",
2528
- "scan",
2529
- "verify",
2530
2765
  "expand",
2531
2766
  "bullet-proof",
2532
- "simplify",
2533
2767
  "compare",
2534
- "batch",
2535
2768
  // Setup / installers
2536
- "setup-lint",
2537
- "setup-tests",
2538
2769
  "setup-commit",
2539
- "setup-update",
2540
- "setup-eyes",
2541
- "eyes-improve",
2542
2770
  "setup-skills",
2543
2771
  ];
2544
2772
  const orderedPromptCommands = promptOrder
2545
2773
  .map(fromPrompt)
2546
2774
  .filter((c) => c !== null);
2547
2775
  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 }));
2776
+ const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
2777
+ name: c.name,
2778
+ aliases: c.aliases,
2779
+ description: c.description,
2780
+ sectionTitle: "workflows",
2781
+ }));
2549
2782
  return [
2550
2783
  // 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" },
2784
+ { name: "model", aliases: ["m"], description: "Switch model", sectionTitle: "built-in" },
2785
+ { name: "compact", aliases: ["c"], description: "Compact context", sectionTitle: "built-in" },
2786
+ { name: "clear", aliases: [], description: "Clear session", sectionTitle: "built-in" },
2787
+ { name: "theme", aliases: ["t"], description: "Switch theme", sectionTitle: "built-in" },
2556
2788
  ...orderedPromptCommands,
2557
2789
  ...remainingPromptCommands,
2558
2790
  ...customCommands.map((cmd) => ({
2559
2791
  name: cmd.name,
2560
2792
  aliases: [],
2561
2793
  description: cmd.description,
2794
+ sectionTitle: "custom",
2562
2795
  })),
2563
- { name: "quit", aliases: ["q", "exit"], description: "Exit the agent" },
2796
+ {
2797
+ name: "quit",
2798
+ aliases: ["q", "exit"],
2799
+ description: "Exit ggcoder",
2800
+ sectionTitle: "built-in",
2801
+ },
2564
2802
  ];
2565
2803
  }, [customCommands]);
2566
- const renderItem = (item) => {
2804
+ const normalizeStatusText = (text) => text.replace(/\\n/g, "\n").replace(/^\n+|\n+$/g, "");
2805
+ 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));
2806
+ const renderItem = (item, index, items) => {
2807
+ const previousLiveItem = index > 0 ? items[index - 1] : undefined;
2808
+ const shouldTopSpacePrintedBoundary = shouldTopSpaceAfterPrintedAgentBoundary({
2809
+ currentKind: item.kind,
2810
+ previousLiveItem,
2811
+ lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
2812
+ lastHistoryItem: history.at(-1),
2813
+ });
2814
+ const assistantMarginTop = item.kind === "assistant" &&
2815
+ (shouldTopSpacePrintedBoundary ||
2816
+ shouldTopSpaceAssistantAfterToolBoundary({
2817
+ text: item.text,
2818
+ previousLiveItem,
2819
+ lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
2820
+ lastHistoryItem: history.at(-1),
2821
+ }))
2822
+ ? 1
2823
+ : 0;
2824
+ const withPrintedBoundarySpacing = (node) => shouldTopSpacePrintedBoundary ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: node }, `${item.id}-printed-boundary`)) : (node);
2567
2825
  switch (item.kind) {
2568
2826
  case "tombstone":
2569
2827
  return null;
2570
2828
  case "banner":
2571
- return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
2829
+ return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd }, item.id));
2572
2830
  case "user":
2573
2831
  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
2832
  case "goal":
2577
- 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));
2833
+ 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: truncateGoalProgressText(item.title) }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }) }, item.id));
2578
2834
  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));
2835
+ const color = goalProgressColor(item, theme);
2836
+ const loaderStatus = goalProgressLoaderStatus(item);
2837
+ const hasBody = !!item.detail ||
2838
+ (item.summaryRows !== undefined && item.summaryRows.length > 0) ||
2839
+ (item.summarySections !== undefined && item.summarySections.length > 0);
2840
+ const headerContentWidth = Math.max(10, columns - 3);
2841
+ 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: truncateGoalProgressText(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: truncateGoalProgressText(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: truncateGoalProgressText(row.value) }), row.detail ? (_jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", truncateGoalProgressText(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: `• ${truncateGoalProgressText(line)}` }, `${section.title}-${sectionLineIndex}`)))] }, section.title)))] }) })) : null] }, item.id));
2605
2842
  }
2606
2843
  case "style_pack": {
2607
2844
  const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
@@ -2611,61 +2848,60 @@ export function App(props) {
2611
2848
  case "setup_hint":
2612
2849
  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
2850
  case "assistant":
2614
- return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
2851
+ return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, renderMarkdown: renderMarkdown, availableTerminalHeight: measuredLiveAreaRows, marginTop: assistantMarginTop }, item.id));
2615
2852
  case "tool_start":
2616
- return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
2853
+ return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
2617
2854
  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));
2855
+ return withPrintedBoundarySpacing(_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
2619
2856
  case "tool_group":
2620
- return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
2857
+ return withPrintedBoundarySpacing(_jsx(ToolGroupExecution, { tools: item.tools }, item.id));
2621
2858
  case "server_tool_start":
2622
- return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
2859
+ return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
2623
2860
  case "server_tool_done":
2624
- return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
2861
+ return withPrintedBoundarySpacing(_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
2625
2862
  case "error": {
2626
2863
  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));
2864
+ 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
2865
  }
2629
2866
  case "info":
2630
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
2867
+ return renderStatusMessage(item.id, "○ ", item.text, theme.commandColor, { muted: true });
2631
2868
  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));
2869
+ 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
2870
  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));
2871
+ return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2872
+ case "goal_agent_transition":
2873
+ return renderStatusMessage(item.id, "● ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2635
2874
  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));
2875
+ const glyphColor = item.active ? theme.commandColor : theme.textDim;
2876
+ return renderStatusMessage(item.id, "✻ ", item.active ? "Thinking ON" : "Thinking OFF", glyphColor, { bold: true, muted: !item.active });
2646
2877
  }
2878
+ case "model_transition":
2879
+ 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 });
2880
+ case "theme_transition":
2881
+ 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
2882
  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.
2883
+ // Plan-domain status changes (approve / reject / dismiss). Use the
2884
+ // command accent so transient TUI status rows share one purple voice.
2651
2885
  const label = item.event === "approved"
2652
2886
  ? "Plan approved"
2653
2887
  : item.event === "rejected"
2654
2888
  ? "Plan rejected"
2655
2889
  : "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));
2890
+ 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
2891
  }
2658
2892
  case "stopped":
2659
2893
  // Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
2660
2894
  // Muted dim treatment — this is an ack, not a state change worth a
2661
2895
  // 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));
2896
+ return renderStatusMessage(item.id, "⊘ ", normalizeStatusText(item.text), theme.commandColor, { bold: true });
2663
2897
  case "step_done":
2664
2898
  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));
2899
+ case "queued": {
2900
+ const suffix = item.imageCount
2901
+ ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
2902
+ : "";
2903
+ 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));
2904
+ }
2669
2905
  case "compacting":
2670
2906
  return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
2671
2907
  case "compacted":
@@ -2673,87 +2909,16 @@ export function App(props) {
2673
2909
  case "duration":
2674
2910
  return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
2675
2911
  case "subagent_group":
2676
- return _jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id);
2912
+ return withPrintedBoundarySpacing(_jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id));
2677
2913
  }
2678
2914
  };
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
2915
  const openOverlay = useCallback((kind) => {
2753
2916
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2754
2917
  props.sessionStore.overlay = kind;
2755
2918
  if (kind !== "plan")
2756
2919
  props.sessionStore.planAutoExpand = false;
2920
+ if (kind !== "goal")
2921
+ props.sessionStore.goalAutoExpand = false;
2757
2922
  props.resetUI();
2758
2923
  }
2759
2924
  else {
@@ -2761,12 +2926,16 @@ export function App(props) {
2761
2926
  props.sessionStore.overlay = kind;
2762
2927
  if (kind !== "plan")
2763
2928
  props.sessionStore.planAutoExpand = false;
2929
+ if (kind !== "goal")
2930
+ props.sessionStore.goalAutoExpand = false;
2764
2931
  if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
2765
2932
  props.sessionStore.pendingResetUI = true;
2766
2933
  }
2767
2934
  }
2768
2935
  if (kind !== "plan")
2769
2936
  setPlanAutoExpand(false);
2937
+ if (kind !== "goal")
2938
+ setGoalAutoExpand(false);
2770
2939
  setOverlay(kind);
2771
2940
  }
2772
2941
  }, [agentLoop.isRunning, props]);
@@ -2788,6 +2957,8 @@ export function App(props) {
2788
2957
  ? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
2789
2958
  : `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
2790
2959
  if (agentRunningRef.current) {
2960
+ queuedGoalSyntheticEventsRef.current += 1;
2961
+ void setGoalModeAndPrompt("coordinator");
2791
2962
  appendGoalProgress({
2792
2963
  kind: "goal_progress",
2793
2964
  phase: "orchestrator_reviewing",
@@ -2809,12 +2980,19 @@ export function App(props) {
2809
2980
  });
2810
2981
  setLastUserMessage("");
2811
2982
  setDoneStatus(null);
2812
- void agentLoop.run(eventText).catch((err) => {
2983
+ void (async () => {
2984
+ await setGoalModeAndPrompt("coordinator");
2985
+ await agentLoop.run(eventText);
2986
+ })().catch((err) => {
2813
2987
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2814
2988
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2989
+ clearGoalModeIfIdle();
2815
2990
  });
2816
- }, [agentLoop, appendGoalProgress]);
2991
+ }, [agentLoop, appendGoalProgress, clearGoalModeIfIdle, setGoalModeAndPrompt]);
2817
2992
  const continueGoalRun = useCallback((runId) => {
2993
+ if (goalContinuationFlightsRef.current.has(runId))
2994
+ return;
2995
+ goalContinuationFlightsRef.current.add(runId);
2818
2996
  void (async () => {
2819
2997
  const latestRun = await reconcileActiveGoalRuns(props.cwd, {
2820
2998
  isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
@@ -2822,11 +3000,24 @@ export function App(props) {
2822
3000
  if (!latestRun) {
2823
3001
  runningGoalIdsRef.current.delete(runId);
2824
3002
  clearGoalStatusEntry(runId);
3003
+ clearGoalModeIfIdle();
2825
3004
  return;
2826
3005
  }
2827
3006
  const decision = decideGoalNextAction(latestRun);
2828
3007
  if (decision.kind === "wait")
2829
3008
  return;
3009
+ const choiceKey = getGoalContinuationChoiceKey({ runId: latestRun.id, decision });
3010
+ const now = Date.now();
3011
+ const recentChoiceAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
3012
+ if (recentChoiceAt !== undefined && now - recentChoiceAt < 5000)
3013
+ return;
3014
+ goalContinuationRecentChoicesRef.current.set(choiceKey, now);
3015
+ if (goalContinuationRecentChoicesRef.current.size > 100) {
3016
+ for (const [key, startedAt] of goalContinuationRecentChoicesRef.current) {
3017
+ if (now - startedAt > 60_000)
3018
+ goalContinuationRecentChoicesRef.current.delete(key);
3019
+ }
3020
+ }
2830
3021
  if (decision.kind === "terminal" ||
2831
3022
  decision.kind === "blocked" ||
2832
3023
  decision.kind === "pause") {
@@ -2856,6 +3047,7 @@ export function App(props) {
2856
3047
  }
2857
3048
  runningGoalIdsRef.current.delete(runId);
2858
3049
  clearGoalStatusEntry(runId);
3050
+ clearGoalModeIfIdle();
2859
3051
  return;
2860
3052
  }
2861
3053
  let runForNextAction = latestRun;
@@ -2886,18 +3078,28 @@ export function App(props) {
2886
3078
  detail: "choosing next step",
2887
3079
  });
2888
3080
  startGoalRunRef.current(runForNextAction);
2889
- })().catch((err) => {
3081
+ })()
3082
+ .catch((err) => {
2890
3083
  runningGoalIdsRef.current.delete(runId);
2891
3084
  clearGoalStatusEntry(runId);
2892
3085
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2893
3086
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3087
+ })
3088
+ .finally(() => {
3089
+ goalContinuationFlightsRef.current.delete(runId);
3090
+ clearGoalModeIfIdle();
2894
3091
  });
2895
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
3092
+ }, [
3093
+ appendGoalProgress,
3094
+ clearGoalModeIfIdle,
3095
+ clearGoalStatusEntry,
3096
+ props.cwd,
3097
+ upsertGoalStatusEntry,
3098
+ ]);
2896
3099
  const handleGoalWorkerComplete = useCallback((run, completion) => {
2897
3100
  const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
2898
3101
  completion.worker.goalTaskId;
2899
3102
  const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
2900
- void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
2901
3103
  appendGoalProgress({
2902
3104
  kind: "goal_progress",
2903
3105
  phase: "worker_finished",
@@ -2951,7 +3153,16 @@ export function App(props) {
2951
3153
  }, [handleGoalWorkerComplete, props.cwd]);
2952
3154
  const startGoalRun = useCallback((run) => {
2953
3155
  runningGoalIdsRef.current.add(run.id);
3156
+ upsertGoalStatusEntry({
3157
+ runId: run.id,
3158
+ label: run.title,
3159
+ phase: "orchestrating",
3160
+ startedAt: Date.now(),
3161
+ detail: "choosing next step",
3162
+ goalNumber: goalNumberForRun(run.id),
3163
+ });
2954
3164
  void (async () => {
3165
+ await setGoalModeAndPrompt("coordinator");
2955
3166
  const currentRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
2956
3167
  const prereqCheck = await runGoalPrerequisiteChecks(props.cwd, currentRun);
2957
3168
  const checkedRun = prereqCheck.checkedCount > 0
@@ -2961,14 +3172,12 @@ export function App(props) {
2961
3172
  })
2962
3173
  : currentRun;
2963
3174
  if (goalHasBlockingPrerequisites(checkedRun)) {
2964
- setOverlay(null);
2965
3175
  const detail = formatGoalBlockingPrerequisites(checkedRun);
2966
3176
  await upsertGoalRun(props.cwd, {
2967
3177
  ...checkedRun,
2968
3178
  status: "blocked",
2969
3179
  blockers: Array.from(new Set([...checkedRun.blockers, detail])),
2970
3180
  });
2971
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2972
3181
  appendGoalProgress({
2973
3182
  kind: "goal_progress",
2974
3183
  phase: "terminal",
@@ -2978,6 +3187,7 @@ export function App(props) {
2978
3187
  });
2979
3188
  runningGoalIdsRef.current.delete(checkedRun.id);
2980
3189
  clearGoalStatusEntry(checkedRun.id);
3190
+ clearGoalModeIfIdle();
2981
3191
  return;
2982
3192
  }
2983
3193
  const decision = decideGoalNextAction(checkedRun);
@@ -2990,6 +3200,7 @@ export function App(props) {
2990
3200
  }
2991
3201
  runningGoalIdsRef.current.delete(checkedRun.id);
2992
3202
  clearGoalStatusEntry(checkedRun.id);
3203
+ clearGoalModeIfIdle();
2993
3204
  return;
2994
3205
  }
2995
3206
  if (decision.kind === "wait") {
@@ -3015,7 +3226,6 @@ export function App(props) {
3015
3226
  }
3016
3227
  if (decision.kind === "complete") {
3017
3228
  await upsertGoalRun(props.cwd, { ...checkedRun, status: "passed" });
3018
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3019
3229
  appendGoalProgress({
3020
3230
  kind: "goal_progress",
3021
3231
  phase: "terminal",
@@ -3025,6 +3235,7 @@ export function App(props) {
3025
3235
  });
3026
3236
  runningGoalIdsRef.current.delete(checkedRun.id);
3027
3237
  clearGoalStatusEntry(checkedRun.id);
3238
+ clearGoalModeIfIdle();
3028
3239
  return;
3029
3240
  }
3030
3241
  if (decision.kind === "run_verifier") {
@@ -3048,7 +3259,6 @@ export function App(props) {
3048
3259
  status: "blocked",
3049
3260
  blockers: [...checkedRun.blockers, decision.reason],
3050
3261
  });
3051
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3052
3262
  appendGoalProgress({
3053
3263
  kind: "goal_progress",
3054
3264
  phase: "terminal",
@@ -3058,6 +3268,7 @@ export function App(props) {
3058
3268
  });
3059
3269
  runningGoalIdsRef.current.delete(checkedRun.id);
3060
3270
  clearGoalStatusEntry(checkedRun.id);
3271
+ clearGoalModeIfIdle();
3061
3272
  return;
3062
3273
  }
3063
3274
  if (decision.kind === "pause") {
@@ -3077,7 +3288,6 @@ export function App(props) {
3077
3288
  continueRequestedAt: undefined,
3078
3289
  blockers: Array.from(new Set([...runWithPauseEvidence.blockers, decision.reason])),
3079
3290
  });
3080
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3081
3291
  appendGoalProgress({
3082
3292
  kind: "goal_progress",
3083
3293
  phase: "terminal",
@@ -3087,6 +3297,7 @@ export function App(props) {
3087
3297
  });
3088
3298
  runningGoalIdsRef.current.delete(checkedRun.id);
3089
3299
  clearGoalStatusEntry(checkedRun.id);
3300
+ clearGoalModeIfIdle();
3090
3301
  return;
3091
3302
  }
3092
3303
  const runWithAttempt = (await updateGoalTask(props.cwd, checkedRun.id, decision.task.id, {
@@ -3099,7 +3310,7 @@ export function App(props) {
3099
3310
  goalRunId: checkedRun.id,
3100
3311
  goalTaskId: decision.task.id,
3101
3312
  taskTitle: decision.task.title,
3102
- prompt: decision.task.prompt,
3313
+ prompt: buildGoalTaskPromptWithReferences(checkedRun, decision.task.prompt),
3103
3314
  });
3104
3315
  const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === checkedRun.id) ??
3105
3316
  runWithAttempt;
@@ -3112,8 +3323,6 @@ export function App(props) {
3112
3323
  ? { ...item, status: "running", workerId: worker.id, attempts: decision.attempts }
3113
3324
  : item),
3114
3325
  });
3115
- setOverlay(null);
3116
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3117
3326
  appendGoalProgress({
3118
3327
  kind: "goal_progress",
3119
3328
  phase: "worker_started",
@@ -3133,6 +3342,7 @@ export function App(props) {
3133
3342
  });
3134
3343
  })().catch((err) => {
3135
3344
  clearGoalStatusEntry(run.id);
3345
+ clearGoalModeIfIdle();
3136
3346
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3137
3347
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3138
3348
  });
@@ -3141,11 +3351,14 @@ export function App(props) {
3141
3351
  currentProvider,
3142
3352
  currentModel,
3143
3353
  appendGoalProgress,
3354
+ clearGoalModeIfIdle,
3144
3355
  clearGoalStatusEntry,
3145
3356
  goalNumberForRun,
3357
+ setGoalModeAndPrompt,
3146
3358
  upsertGoalStatusEntry,
3147
3359
  ]);
3148
3360
  const verifyGoalRun = useCallback(async (run) => {
3361
+ await setGoalModeAndPrompt("coordinator");
3149
3362
  if (!run.verifier?.command) {
3150
3363
  await appendGoalEvidence(props.cwd, run.id, {
3151
3364
  kind: "summary",
@@ -3166,6 +3379,7 @@ export function App(props) {
3166
3379
  });
3167
3380
  runningGoalIdsRef.current.delete(run.id);
3168
3381
  clearGoalStatusEntry(run.id);
3382
+ clearGoalModeIfIdle();
3169
3383
  return;
3170
3384
  }
3171
3385
  activeVerifierRunIdsRef.current.add(run.id);
@@ -3212,11 +3426,22 @@ export function App(props) {
3212
3426
  command: run.verifier?.command,
3213
3427
  lastResult: verification,
3214
3428
  },
3429
+ ...(status === "pass"
3430
+ ? {
3431
+ completionAudit: {
3432
+ status: "unknown",
3433
+ summary: "Final completion audit pending for latest verifier result.",
3434
+ checkedAt: verification.checkedAt,
3435
+ verifierCheckedAt: verification.checkedAt,
3436
+ ...(verification.outputPath ? { outputPath: verification.outputPath } : {}),
3437
+ },
3438
+ }
3439
+ : {}),
3215
3440
  };
3216
3441
  const completionCheck = canCompleteGoalRun(runWithVerifier);
3217
3442
  const verifiedRun = await upsertGoalRun(props.cwd, {
3218
3443
  ...runWithVerifier,
3219
- continueRequestedAt: status === "pass" && completionCheck.ok ? undefined : latestRun.continueRequestedAt,
3444
+ continueRequestedAt: latestRun.continueRequestedAt,
3220
3445
  status: status === "pass" && completionCheck.ok ? "passed" : "ready",
3221
3446
  });
3222
3447
  await appendGoalEvidence(props.cwd, run.id, {
@@ -3230,7 +3455,6 @@ export function App(props) {
3230
3455
  reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
3231
3456
  content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
3232
3457
  });
3233
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3234
3458
  appendGoalProgress({
3235
3459
  kind: "goal_progress",
3236
3460
  phase: "verifier_finished",
@@ -3249,22 +3473,25 @@ export function App(props) {
3249
3473
  const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
3250
3474
  runGoalSyntheticEvent(eventText);
3251
3475
  const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3252
- if (continuationRun?.continueRequestedAt || status === "fail") {
3476
+ if (continuationRun?.continueRequestedAt || status === "fail" || status === "pass") {
3253
3477
  setTimeout(() => continueGoalRun(run.id), 500);
3254
3478
  }
3255
3479
  })
3256
3480
  .catch((err) => {
3257
3481
  activeVerifierRunIdsRef.current.delete(run.id);
3258
3482
  clearGoalStatusEntry(run.id);
3483
+ clearGoalModeIfIdle();
3259
3484
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3260
3485
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3261
3486
  });
3262
3487
  }, [
3263
3488
  props.cwd,
3264
3489
  appendGoalProgress,
3490
+ clearGoalModeIfIdle,
3265
3491
  clearGoalStatusEntry,
3266
3492
  goalNumberForRun,
3267
3493
  runGoalSyntheticEvent,
3494
+ setGoalModeAndPrompt,
3268
3495
  upsertGoalStatusEntry,
3269
3496
  ]);
3270
3497
  const pauseGoalRun = useCallback((run) => {
@@ -3278,7 +3505,6 @@ export function App(props) {
3278
3505
  status: "paused",
3279
3506
  activeWorkerId: undefined,
3280
3507
  });
3281
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3282
3508
  appendGoalProgress({
3283
3509
  kind: "goal_progress",
3284
3510
  phase: "terminal",
@@ -3287,19 +3513,14 @@ export function App(props) {
3287
3513
  status: "paused",
3288
3514
  });
3289
3515
  clearGoalStatusEntry(run.id);
3516
+ clearGoalModeIfIdle();
3290
3517
  })().catch((err) => {
3291
3518
  log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3292
3519
  setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3293
3520
  });
3294
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
3521
+ }, [appendGoalProgress, clearGoalModeIfIdle, clearGoalStatusEntry, props.cwd]);
3295
3522
  // Keep refs in sync for access from stale closures (onDone)
3296
- startTaskRef.current = startTask;
3297
3523
  startGoalRunRef.current = startGoalRun;
3298
- useEffect(() => {
3299
- runAllTasksRef.current = runAllTasks;
3300
- if (props.sessionStore)
3301
- props.sessionStore.runAllTasks = runAllTasks;
3302
- }, [runAllTasks, props.sessionStore]);
3303
3524
  useEffect(() => {
3304
3525
  agentRunningRef.current = agentLoop.isRunning;
3305
3526
  }, [agentLoop.isRunning]);
@@ -3348,13 +3569,14 @@ export function App(props) {
3348
3569
  injectedLanguagesRef.current = detectedForPixelFix;
3349
3570
  const newSystemPrompt = await rebuildSystemPrompt({
3350
3571
  cwd: prep.projectPath,
3351
- planMode: false,
3352
3572
  clearApprovedPlan: true,
3353
3573
  activeLanguages: detectedForPixelFix,
3354
3574
  tools: toolsForPixelFix,
3355
3575
  });
3356
3576
  // Now that the cwd swap is committed, reset chat. Do not clear the
3357
3577
  // terminal here; terminal clear sequences can erase saved scrollback.
3578
+ pendingHistoryFlushRef.current = [];
3579
+ props.terminalHistoryPrinter?.clear();
3358
3580
  setHistory([{ kind: "banner", id: "banner" }]);
3359
3581
  setLiveItems([]);
3360
3582
  messagesRef.current = messagesRef.current.slice(0, 1);
@@ -3374,10 +3596,10 @@ export function App(props) {
3374
3596
  messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
3375
3597
  }
3376
3598
  const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
3377
- const taskItem = { kind: "task", title, id: getId() };
3599
+ const goalItem = { kind: "goal", title, id: getId() };
3378
3600
  setLastUserMessage(title);
3379
3601
  setDoneStatus(null);
3380
- setLiveItems([taskItem]);
3602
+ setLiveItems([goalItem]);
3381
3603
  await agentLoop.run(prep.prompt);
3382
3604
  }
3383
3605
  catch (err) {
@@ -3398,266 +3620,333 @@ export function App(props) {
3398
3620
  if (props.sessionStore)
3399
3621
  props.sessionStore.runAllPixel = runAllPixel;
3400
3622
  }, [runAllPixel, props.sessionStore]);
3401
- const isTaskView = overlay === "tasks";
3402
3623
  const isGoalView = overlay === "goal";
3403
3624
  const isSkillsView = overlay === "skills";
3404
3625
  const isPlanView = overlay === "plan";
3405
- const isEyesView = overlay === "eyes";
3406
3626
  const footerStatusLayout = getFooterStatusLayoutDecision({
3407
3627
  columns,
3408
3628
  backgroundTaskCount: bgTasks.length,
3409
- eyesCount,
3410
3629
  updatePending,
3411
3630
  });
3631
+ const activityVisible = agentLoop.isRunning && agentLoop.activityPhase !== "idle";
3632
+ const stallStatusVisible = !activityVisible && !!agentLoop.stallError;
3633
+ const doneStatusVisible = !activityVisible && !stallStatusVisible && !!doneStatus && !agentLoop.isRunning;
3634
+ const statusSlotVisible = activityVisible || stallStatusVisible || doneStatusVisible;
3635
+ const [controlsHeight, setControlsHeight] = useState(0);
3636
+ const controlsObserverRef = useRef(null);
3637
+ const mainControlsRef = useCallback((node) => {
3638
+ if (controlsObserverRef.current) {
3639
+ controlsObserverRef.current.disconnect();
3640
+ controlsObserverRef.current = null;
3641
+ }
3642
+ if (!node || typeof ResizeObserver === "undefined")
3643
+ return;
3644
+ const observer = new ResizeObserver((entries) => {
3645
+ const entry = entries[0];
3646
+ if (!entry)
3647
+ return;
3648
+ const roundedHeight = Math.round(entry.contentRect.height);
3649
+ setControlsHeight((prev) => (roundedHeight !== prev ? roundedHeight : prev));
3650
+ });
3651
+ observer.observe(node);
3652
+ controlsObserverRef.current = observer;
3653
+ }, []);
3654
+ useEffect(() => () => controlsObserverRef.current?.disconnect(), []);
3655
+ const footerFitsOnOneLine = doesFooterFitOnOneLine({
3656
+ columns,
3657
+ model: currentModel,
3658
+ tokensIn: agentLoop.contextUsed,
3659
+ contextWindowOptions,
3660
+ cwd: displayedCwd,
3661
+ gitBranch,
3662
+ thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
3663
+ goalMode,
3664
+ });
3665
+ const chatControlsLayout = getChatControlsLayoutDecision({
3666
+ rows,
3667
+ columns,
3668
+ agentRunning: agentLoop.isRunning,
3669
+ activityVisible,
3670
+ doneStatusVisible,
3671
+ stallStatusVisible,
3672
+ exitPending,
3673
+ footerStatusLayout,
3674
+ taskBarExpanded,
3675
+ goalStatusEntryCount: goalStatusEntries.length,
3676
+ footerFitsOnOneLine,
3677
+ });
3678
+ const stableControlsRows = controlsHeight > 0 ? controlsHeight : chatControlsLayout.controlsRows;
3679
+ const measuredLiveAreaRows = Math.max(MIN_LIVE_AREA_ROWS, rows - stableControlsRows - 1);
3412
3680
  const isPixelView = overlay === "pixel";
3413
- const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
3414
- const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
3415
- const stabilizeOverlayPaneRerender = shouldStabilizeOverlayPaneRerender({
3416
- overlayPane: overlay,
3417
- isAgentRunning: agentLoop.isRunning,
3681
+ const hasLiveAssistantItem = liveItems.some((item) => item.kind === "assistant");
3682
+ const rawVisibleStreamingText = goalModeStateRef.current === "planner" || hasLiveAssistantItem ? "" : agentLoop.streamingText;
3683
+ useEffect(() => {
3684
+ if (!rawVisibleStreamingText) {
3685
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
3686
+ return;
3687
+ }
3688
+ if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
3689
+ return;
3690
+ const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
3691
+ const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
3692
+ const split = splitAssistantStreamingText(unflushedText);
3693
+ if (split.flushedText.length > 0) {
3694
+ queueFlush([
3695
+ {
3696
+ kind: "assistant",
3697
+ text: split.flushedText,
3698
+ continuation: streamedAssistantFlushRef.current.flushedChars > 0,
3699
+ id: getId(),
3700
+ },
3701
+ ]);
3702
+ streamedAssistantFlushRef.current = {
3703
+ flushedChars: alreadyFlushed + split.flushedText.length,
3704
+ text: rawVisibleStreamingText,
3705
+ };
3706
+ return;
3707
+ }
3708
+ streamedAssistantFlushRef.current = {
3709
+ ...streamedAssistantFlushRef.current,
3710
+ text: rawVisibleStreamingText,
3711
+ };
3712
+ }, [rawVisibleStreamingText, queueFlush]);
3713
+ const visibleStreamingText = rawVisibleStreamingText.slice(streamedAssistantFlushRef.current.flushedChars);
3714
+ const shouldReserveStreamingSpacing = agentLoop.isRunning &&
3715
+ !hasLiveAssistantItem &&
3716
+ (visibleStreamingText.trim().length > 0 || liveItems.some(isAgentSpacingItem));
3717
+ const shouldTopSpaceStreamingText = shouldTopSpaceStreamingAssistant({
3718
+ visibleStreamingText,
3719
+ lastLiveItem: liveItems.at(-1),
3720
+ lastPendingHistoryItem: pendingHistoryFlushRef.current.at(-1),
3721
+ lastHistoryItem: history.at(-1),
3418
3722
  });
3419
- const staticItems = shouldHideStaticItemsForOverlayView({
3420
- shouldHideHistoryForOverlay,
3421
- stabilizeOverlayPaneRerender,
3422
- })
3423
- ? []
3424
- : history;
3425
- 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: () => {
3426
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3427
- props.sessionStore.overlay = null;
3428
- props.resetUI();
3723
+ return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, autoExpandNewest: goalAutoExpand, onClose: () => {
3724
+ goalAutoExpandRef.current = false;
3725
+ setGoalAutoExpand(false);
3726
+ if (props.sessionStore)
3727
+ props.sessionStore.goalAutoExpand = false;
3728
+ closeOverlay();
3729
+ }, onRunGoal: (run) => {
3730
+ const paneTransition = getGoalActivationPaneTransition();
3731
+ goalAutoExpandRef.current = paneTransition.goalAutoExpand;
3732
+ setGoalAutoExpand(paneTransition.goalAutoExpand);
3733
+ setPlanAutoExpand(paneTransition.planAutoExpand);
3734
+ if (props.sessionStore) {
3735
+ props.sessionStore.overlay = paneTransition.overlay;
3736
+ props.sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
3737
+ props.sessionStore.planAutoExpand = paneTransition.planAutoExpand;
3738
+ }
3739
+ if (paneTransition.resetReviewScreen && props.resetUI && props.sessionStore) {
3740
+ props.sessionStore.pendingGoalRun = run;
3741
+ props.resetUI();
3742
+ return;
3743
+ }
3744
+ setOverlay(paneTransition.overlay);
3745
+ startGoalRun(run);
3746
+ }, onVerifyGoal: (run) => {
3747
+ void verifyGoalRun(run);
3748
+ }, onPauseGoal: (run) => {
3749
+ pauseGoalRun(run);
3750
+ }, onRefineGoal: (run, feedback) => {
3751
+ goalAutoExpandRef.current = true;
3752
+ setGoalAutoExpand(true);
3753
+ void (async () => {
3754
+ try {
3755
+ await setGoalModeAndPrompt("setup");
3756
+ 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}`);
3429
3757
  }
3430
- else {
3431
- if (props.sessionStore) {
3432
- props.sessionStore.overlay = null;
3433
- if (agentLoop.isRunning)
3434
- props.sessionStore.pendingResetUI = true;
3435
- }
3436
- setTaskCount(getTaskCount(props.cwd));
3437
- setOverlay(null);
3758
+ catch (err) {
3759
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3760
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3438
3761
  }
3439
- }, onWorkOnTask: (title, prompt, taskId) => {
3440
- setOverlay(null);
3441
- startTask(title, prompt, taskId);
3442
- }, onRunAllTasks: () => {
3443
- setOverlay(null);
3444
- setRunAllTasks(true);
3445
- const next = getNextPendingTask(props.cwd);
3446
- if (next) {
3447
- markTaskInProgress(props.cwd, next.id);
3448
- startTask(next.title, next.prompt, next.id);
3762
+ finally {
3763
+ await setGoalModeAndPrompt("off");
3764
+ const paneTransition = getGoalSetupFinishedPaneTransition();
3765
+ goalAutoExpandRef.current = paneTransition.goalAutoExpand;
3766
+ setTimeout(() => {
3767
+ const resetUI = props.resetUI;
3768
+ const sessionStore = props.sessionStore;
3769
+ if (shouldResetUIForGoalSetupPaneTransition({
3770
+ hasResetUI: resetUI !== undefined,
3771
+ hasSessionStore: sessionStore !== undefined,
3772
+ }) &&
3773
+ resetUI &&
3774
+ sessionStore) {
3775
+ sessionStore.overlay = paneTransition.overlay;
3776
+ sessionStore.goalAutoExpand = paneTransition.goalAutoExpand;
3777
+ sessionStore.planAutoExpand = paneTransition.planAutoExpand;
3778
+ resetUI();
3779
+ return;
3780
+ }
3781
+ setGoalAutoExpand(paneTransition.goalAutoExpand);
3782
+ setPlanAutoExpand(paneTransition.planAutoExpand);
3783
+ setOverlay(paneTransition.overlay);
3784
+ }, 300);
3449
3785
  }
3450
- } })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3451
- void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
3452
- closeOverlay();
3453
- }, onRunGoal: (run) => {
3454
- setOverlay(null);
3455
- startGoalRun(run);
3456
- }, onVerifyGoal: (run) => {
3457
- void verifyGoalRun(run);
3458
- }, onPauseGoal: (run) => {
3459
- pauseGoalRun(run);
3460
- } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
3461
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3786
+ })();
3787
+ } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
3788
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3789
+ props.sessionStore.overlay = null;
3790
+ props.resetUI();
3791
+ }
3792
+ else {
3793
+ if (props.sessionStore) {
3462
3794
  props.sessionStore.overlay = null;
3463
- props.resetUI();
3464
- }
3465
- else {
3466
- if (props.sessionStore) {
3467
- props.sessionStore.overlay = null;
3468
- if (agentLoop.isRunning)
3469
- props.sessionStore.pendingResetUI = true;
3470
- }
3471
- setOverlay(null);
3795
+ if (agentLoop.isRunning)
3796
+ props.sessionStore.pendingResetUI = true;
3472
3797
  }
3473
- }, onFixOne: (entry) => {
3474
3798
  setOverlay(null);
3475
- startPixelFix(entry.errorId);
3476
- }, onFixAll: (entries) => {
3477
- const first = entries.find((e) => e.status === "open") ?? entries[0];
3478
- if (!first)
3479
- return;
3480
- setOverlay(null);
3481
- setRunAllPixel(true);
3482
- startPixelFix(first.errorId);
3483
- } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
3484
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3485
- props.sessionStore.overlay = null;
3486
- props.resetUI();
3487
- }
3488
- else {
3489
- if (props.sessionStore) {
3490
- props.sessionStore.overlay = null;
3491
- if (agentLoop.isRunning)
3492
- props.sessionStore.pendingResetUI = true;
3493
- }
3494
- setOverlay(null);
3495
- }
3496
- } })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
3497
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3799
+ }
3800
+ }, onFixOne: (entry) => {
3801
+ setOverlay(null);
3802
+ startPixelFix(entry.errorId);
3803
+ }, onFixAll: (entries) => {
3804
+ const first = entries.find((e) => e.status === "open") ?? entries[0];
3805
+ if (!first)
3806
+ return;
3807
+ setOverlay(null);
3808
+ setRunAllPixel(true);
3809
+ startPixelFix(first.errorId);
3810
+ } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
3811
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3812
+ props.sessionStore.overlay = null;
3813
+ props.resetUI();
3814
+ }
3815
+ else {
3816
+ if (props.sessionStore) {
3498
3817
  props.sessionStore.overlay = null;
3499
- props.resetUI();
3818
+ if (agentLoop.isRunning)
3819
+ props.sessionStore.pendingResetUI = true;
3500
3820
  }
3501
- else {
3502
- if (props.sessionStore) {
3503
- props.sessionStore.overlay = null;
3504
- if (agentLoop.isRunning)
3505
- props.sessionStore.pendingResetUI = true;
3506
- }
3507
- setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
3508
- setOverlay(null);
3509
- }
3510
- }, onQueueMessage: (msg) => {
3511
- agentLoop.queueMessage(msg);
3512
- } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
3513
- planOverlayPendingRef.current = false;
3514
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3821
+ setOverlay(null);
3822
+ }
3823
+ } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
3824
+ planOverlayPendingRef.current = false;
3825
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3826
+ props.sessionStore.overlay = null;
3827
+ props.sessionStore.planAutoExpand = false;
3828
+ props.resetUI();
3829
+ }
3830
+ else {
3831
+ if (props.sessionStore) {
3515
3832
  props.sessionStore.overlay = null;
3516
3833
  props.sessionStore.planAutoExpand = false;
3517
- props.resetUI();
3834
+ if (agentLoop.isRunning)
3835
+ props.sessionStore.pendingResetUI = true;
3518
3836
  }
3519
- else {
3520
- if (props.sessionStore) {
3837
+ setPlanAutoExpand(false);
3838
+ setOverlay(null);
3839
+ }
3840
+ }, onApprove: (planPath) => {
3841
+ log("INFO", "plan", "Plan approved — transitioning to implementation", {
3842
+ planPath,
3843
+ });
3844
+ planOverlayPendingRef.current = false;
3845
+ void (async () => {
3846
+ try {
3847
+ // Read plan steps for progress tracking — handed to the new
3848
+ // mount via sessionStore.planSteps below.
3849
+ const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
3850
+ const steps = extractPlanSteps(planContent);
3851
+ // Build the new system prompt with the approved plan baked in.
3852
+ const newPrompt = await rebuildSystemPrompt({
3853
+ approvedPlanPath: planPath,
3854
+ });
3855
+ // Create a new session file BEFORE remount so the new tree
3856
+ // picks it up via sessionStore.sessionPath.
3857
+ let newSessionPath;
3858
+ const sm = sessionManagerRef.current;
3859
+ if (sm) {
3860
+ const s = await sm.create(props.cwd, currentProvider, currentModel);
3861
+ newSessionPath = s.path;
3862
+ }
3863
+ if (props.resetUI && props.sessionStore) {
3864
+ // Clear the overlay so the new mount lands on the chat,
3865
+ // not back inside the plan pane.
3521
3866
  props.sessionStore.overlay = null;
3522
3867
  props.sessionStore.planAutoExpand = false;
3523
- if (agentLoop.isRunning)
3524
- props.sessionStore.pendingResetUI = true;
3525
- }
3526
- setPlanAutoExpand(false);
3527
- setOverlay(null);
3528
- }
3529
- }, onApprove: (planPath) => {
3530
- log("INFO", "plan", "Plan approved — transitioning to implementation", {
3531
- planPath,
3532
- });
3533
- planOverlayPendingRef.current = false;
3534
- void (async () => {
3535
- try {
3536
- // Read plan steps for progress tracking — handed to the new
3537
- // mount via sessionStore.planSteps below.
3538
- const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
3539
- const steps = extractPlanSteps(planContent);
3540
- // Build the new system prompt with the approved plan baked in.
3541
- const newPrompt = await rebuildSystemPrompt({
3542
- planMode: false,
3868
+ props.resetUI({
3869
+ wipeSession: true,
3870
+ messages: [{ role: "system", content: newPrompt }],
3543
3871
  approvedPlanPath: planPath,
3544
- });
3545
- // Create a new session file BEFORE remount so the new tree
3546
- // picks it up via sessionStore.sessionPath.
3547
- let newSessionPath;
3548
- const sm = sessionManagerRef.current;
3549
- if (sm) {
3550
- const s = await sm.create(props.cwd, currentProvider, currentModel);
3551
- newSessionPath = s.path;
3552
- }
3553
- if (props.resetUI && props.sessionStore) {
3554
- // Clear the overlay so the new mount lands on the chat,
3555
- // not back inside the plan pane.
3556
- props.sessionStore.overlay = null;
3557
- props.sessionStore.planAutoExpand = false;
3558
- props.resetUI({
3559
- wipeSession: true,
3560
- messages: [{ role: "system", content: newPrompt }],
3561
- approvedPlanPath: planPath,
3562
- planSteps: steps,
3563
- sessionPath: newSessionPath,
3564
- pendingAction: {
3565
- prompt: "The plan has been approved. Implement it now, following each step in order.",
3566
- planEvent: { event: "approved" },
3567
- },
3568
- });
3569
- return;
3570
- }
3571
- // Fallback path (resetUI not wired — tests). Mutate in place.
3572
- approvedPlanPathRef.current = planPath;
3573
- planStepsRef.current = steps;
3574
- setPlanSteps(steps);
3575
- setHistory([{ kind: "banner", id: "banner" }]);
3576
- setLiveItems([]);
3577
- setPlanAutoExpand(false);
3578
- setOverlay(null);
3579
- messagesRef.current = [{ role: "system", content: newPrompt }];
3580
- agentLoop.reset();
3581
- persistedIndexRef.current = messagesRef.current.length;
3582
- if (newSessionPath)
3583
- sessionPathRef.current = newSessionPath;
3584
- setLiveItems([
3585
- {
3586
- kind: "info",
3587
- text: "Plan approved — starting fresh session for implementation",
3588
- id: getId(),
3872
+ planSteps: steps,
3873
+ sessionPath: newSessionPath,
3874
+ pendingAction: {
3875
+ prompt: "The plan has been approved. Implement it now, following each step in order.",
3876
+ planEvent: { event: "approved" },
3589
3877
  },
3590
- ]);
3591
- setDoneStatus(null);
3592
- await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3593
- }
3594
- catch (err) {
3595
- const errMsg = err instanceof Error ? err.message : String(err);
3596
- log("ERROR", "error", errMsg);
3597
- setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3878
+ });
3879
+ return;
3598
3880
  }
3599
- })();
3600
- }, onReject: (planPath, feedback) => {
3601
- planOverlayPendingRef.current = false;
3602
- const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
3603
- `Please revise the plan based on this feedback.`;
3604
- if (props.resetUI && props.sessionStore) {
3605
- props.sessionStore.overlay = null;
3606
- props.sessionStore.planAutoExpand = false;
3607
- // No wipeSession — keep history, messages, plan mode etc. The
3608
- // agent picks up the rejection mid-conversation.
3609
- props.resetUI({
3610
- pendingAction: {
3611
- prompt: rejectionMsg,
3612
- planEvent: { event: "rejected", detail: feedback },
3881
+ // Fallback path (resetUI not wired — tests). Mutate in place.
3882
+ approvedPlanPathRef.current = planPath;
3883
+ planStepsRef.current = steps;
3884
+ setPlanSteps(steps);
3885
+ pendingHistoryFlushRef.current = [];
3886
+ props.terminalHistoryPrinter?.clear();
3887
+ setHistory([{ kind: "banner", id: "banner" }]);
3888
+ setLiveItems([]);
3889
+ setPlanAutoExpand(false);
3890
+ setOverlay(null);
3891
+ messagesRef.current = [{ role: "system", content: newPrompt }];
3892
+ agentLoop.reset();
3893
+ persistedIndexRef.current = messagesRef.current.length;
3894
+ if (newSessionPath)
3895
+ sessionPathRef.current = newSessionPath;
3896
+ setLiveItems([
3897
+ {
3898
+ kind: "info",
3899
+ text: "Plan approved — starting fresh session for implementation",
3900
+ id: getId(),
3613
3901
  },
3614
- });
3615
- return;
3902
+ ]);
3903
+ setDoneStatus(null);
3904
+ await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3616
3905
  }
3617
- setPlanAutoExpand(false);
3618
- setOverlay(null);
3619
- setDoneStatus(null);
3620
- setLiveItems((prev) => [
3621
- ...prev,
3622
- { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3623
- ]);
3624
- void agentLoop.run(rejectionMsg).catch((err) => {
3906
+ catch (err) {
3625
3907
  const errMsg = err instanceof Error ? err.message : String(err);
3626
3908
  log("ERROR", "error", errMsg);
3627
3909
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3910
+ }
3911
+ })();
3912
+ }, onReject: (planPath, feedback) => {
3913
+ planOverlayPendingRef.current = false;
3914
+ const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
3915
+ `Please revise the plan based on this feedback.`;
3916
+ if (props.resetUI && props.sessionStore) {
3917
+ props.sessionStore.overlay = null;
3918
+ props.sessionStore.planAutoExpand = false;
3919
+ // No wipeSession — keep history and messages so the agent picks
3920
+ // up the rejection mid-conversation.
3921
+ props.resetUI({
3922
+ pendingAction: {
3923
+ prompt: rejectionMsg,
3924
+ planEvent: { event: "rejected", detail: feedback },
3925
+ },
3628
3926
  });
3629
- } })) : (_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 &&
3630
- !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: () => {
3631
- // Just flip the overlay state — Ink's log-update handles the
3632
- // live-area transition (chat input → TaskOverlay) natively, and
3633
- // the chat history above stays in scrollback. When the overlay
3634
- // closes, the history is still there (banner included).
3635
- openOverlay("tasks");
3636
- }, onToggleGoal: () => {
3637
- openOverlay("goal");
3638
- }, onToggleSkills: () => {
3639
- openOverlay("skills");
3640
- }, onTogglePixel: () => {
3641
- openOverlay("pixel");
3642
- }, onTogglePlanMode: () => {
3643
- const next = !planMode;
3644
- setPlanMode(next);
3645
- log("INFO", "plan", `Plan mode ${next ? "enabled" : "disabled"}`);
3646
- setLiveItems((items) => [
3647
- ...items,
3648
- {
3649
- kind: "plan_transition",
3650
- text: next ? "Plan Mode Activated" : "Plan Mode Deactivated",
3651
- active: next,
3652
- id: getId(),
3653
- },
3654
- ]);
3655
- }, 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 ||
3656
- footerStatusLayout.hasEyesSignals ||
3657
- 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 ||
3658
- (!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
3659
- ? 1
3660
- : 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
3927
+ return;
3928
+ }
3929
+ setPlanAutoExpand(false);
3930
+ setOverlay(null);
3931
+ setDoneStatus(null);
3932
+ setLiveItems((prev) => [
3933
+ ...prev,
3934
+ { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3935
+ ]);
3936
+ void agentLoop.run(rejectionMsg).catch((err) => {
3937
+ const errMsg = err instanceof Error ? err.message : String(err);
3938
+ log("ERROR", "error", errMsg);
3939
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3940
+ });
3941
+ } })) : (_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: () => {
3942
+ openOverlay("goal");
3943
+ }, onToggleSkills: () => {
3944
+ openOverlay("skills");
3945
+ }, onTogglePixel: () => {
3946
+ openOverlay("pixel");
3947
+ }, onToggleMarkdown: () => {
3948
+ setRenderMarkdown((prev) => !prev);
3949
+ }, 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" }) }))] }))] })] })) }));
3661
3950
  }
3662
3951
  function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
3663
3952
  const status = enabled ? "on" : "off";