@prestyj/cli 4.3.161 → 4.3.200

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 (299) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.js +112 -27
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/agent-session.d.ts +50 -1
  5. package/dist/core/agent-session.d.ts.map +1 -1
  6. package/dist/core/agent-session.js +175 -20
  7. package/dist/core/agent-session.js.map +1 -1
  8. package/dist/core/agents.d.ts +6 -1
  9. package/dist/core/agents.d.ts.map +1 -1
  10. package/dist/core/agents.js +112 -3
  11. package/dist/core/agents.js.map +1 -1
  12. package/dist/core/auth-storage.d.ts.map +1 -1
  13. package/dist/core/auth-storage.js +20 -1
  14. package/dist/core/auth-storage.js.map +1 -1
  15. package/dist/core/claude-code-version.d.ts +10 -0
  16. package/dist/core/claude-code-version.d.ts.map +1 -0
  17. package/dist/core/claude-code-version.js +106 -0
  18. package/dist/core/claude-code-version.js.map +1 -0
  19. package/dist/core/compaction/compactor.d.ts +13 -0
  20. package/dist/core/compaction/compactor.d.ts.map +1 -1
  21. package/dist/core/compaction/compactor.js +20 -1
  22. package/dist/core/compaction/compactor.js.map +1 -1
  23. package/dist/core/compaction/compactor.test.js +12 -1
  24. package/dist/core/compaction/compactor.test.js.map +1 -1
  25. package/dist/core/index.d.ts +1 -1
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/language-detector.d.ts +32 -0
  30. package/dist/core/language-detector.d.ts.map +1 -0
  31. package/dist/core/language-detector.js +191 -0
  32. package/dist/core/language-detector.js.map +1 -0
  33. package/dist/core/language-detector.test.d.ts +2 -0
  34. package/dist/core/language-detector.test.d.ts.map +1 -0
  35. package/dist/core/language-detector.test.js +109 -0
  36. package/dist/core/language-detector.test.js.map +1 -0
  37. package/dist/core/model-registry.d.ts +30 -2
  38. package/dist/core/model-registry.d.ts.map +1 -1
  39. package/dist/core/model-registry.js +41 -26
  40. package/dist/core/model-registry.js.map +1 -1
  41. package/dist/core/model-registry.test.d.ts +2 -0
  42. package/dist/core/model-registry.test.d.ts.map +1 -0
  43. package/dist/core/model-registry.test.js +19 -0
  44. package/dist/core/model-registry.test.js.map +1 -0
  45. package/dist/core/oauth/anthropic.d.ts.map +1 -1
  46. package/dist/core/oauth/anthropic.js +64 -42
  47. package/dist/core/oauth/anthropic.js.map +1 -1
  48. package/dist/core/oauth/openai.js +1 -1
  49. package/dist/core/oauth/openai.js.map +1 -1
  50. package/dist/core/prompt-commands.d.ts.map +1 -1
  51. package/dist/core/prompt-commands.js +569 -96
  52. package/dist/core/prompt-commands.js.map +1 -1
  53. package/dist/core/prompt-commands.test.d.ts +2 -0
  54. package/dist/core/prompt-commands.test.d.ts.map +1 -0
  55. package/dist/core/prompt-commands.test.js +33 -0
  56. package/dist/core/prompt-commands.test.js.map +1 -0
  57. package/dist/core/repomap-context.d.ts +11 -0
  58. package/dist/core/repomap-context.d.ts.map +1 -0
  59. package/dist/core/repomap-context.js +68 -0
  60. package/dist/core/repomap-context.js.map +1 -0
  61. package/dist/core/repomap-context.test.d.ts +2 -0
  62. package/dist/core/repomap-context.test.d.ts.map +1 -0
  63. package/dist/core/repomap-context.test.js +47 -0
  64. package/dist/core/repomap-context.test.js.map +1 -0
  65. package/dist/core/repomap.d.ts +74 -0
  66. package/dist/core/repomap.d.ts.map +1 -0
  67. package/dist/core/repomap.js +897 -0
  68. package/dist/core/repomap.js.map +1 -0
  69. package/dist/core/repomap.test.d.ts +2 -0
  70. package/dist/core/repomap.test.d.ts.map +1 -0
  71. package/dist/core/repomap.test.js +444 -0
  72. package/dist/core/repomap.test.js.map +1 -0
  73. package/dist/core/session-compaction.d.ts +15 -0
  74. package/dist/core/session-compaction.d.ts.map +1 -0
  75. package/dist/core/session-compaction.js +31 -0
  76. package/dist/core/session-compaction.js.map +1 -0
  77. package/dist/core/session-compaction.test.d.ts +2 -0
  78. package/dist/core/session-compaction.test.d.ts.map +1 -0
  79. package/dist/core/session-compaction.test.js +70 -0
  80. package/dist/core/session-compaction.test.js.map +1 -0
  81. package/dist/core/settings-manager.d.ts +3 -5
  82. package/dist/core/settings-manager.d.ts.map +1 -1
  83. package/dist/core/settings-manager.js +6 -5
  84. package/dist/core/settings-manager.js.map +1 -1
  85. package/dist/core/setup-history.d.ts +13 -0
  86. package/dist/core/setup-history.d.ts.map +1 -0
  87. package/dist/core/setup-history.js +71 -0
  88. package/dist/core/setup-history.js.map +1 -0
  89. package/dist/core/setup-history.test.d.ts +2 -0
  90. package/dist/core/setup-history.test.d.ts.map +1 -0
  91. package/dist/core/setup-history.test.js +74 -0
  92. package/dist/core/setup-history.test.js.map +1 -0
  93. package/dist/core/slash-commands.d.ts +2 -0
  94. package/dist/core/slash-commands.d.ts.map +1 -1
  95. package/dist/core/slash-commands.js +15 -14
  96. package/dist/core/slash-commands.js.map +1 -1
  97. package/dist/core/style-packs/index.d.ts +19 -0
  98. package/dist/core/style-packs/index.d.ts.map +1 -0
  99. package/dist/core/style-packs/index.js +69 -0
  100. package/dist/core/style-packs/index.js.map +1 -0
  101. package/dist/core/style-packs/packs.d.ts +18 -0
  102. package/dist/core/style-packs/packs.d.ts.map +1 -0
  103. package/dist/core/style-packs/packs.js +229 -0
  104. package/dist/core/style-packs/packs.js.map +1 -0
  105. package/dist/core/verify-commands.d.ts +35 -0
  106. package/dist/core/verify-commands.d.ts.map +1 -0
  107. package/dist/core/verify-commands.js +209 -0
  108. package/dist/core/verify-commands.js.map +1 -0
  109. package/dist/core/verify-commands.test.d.ts +2 -0
  110. package/dist/core/verify-commands.test.d.ts.map +1 -0
  111. package/dist/core/verify-commands.test.js +85 -0
  112. package/dist/core/verify-commands.test.js.map +1 -0
  113. package/dist/index.d.ts +2 -2
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +1 -1
  116. package/dist/index.js.map +1 -1
  117. package/dist/interactive.d.ts.map +1 -1
  118. package/dist/interactive.js +12 -6
  119. package/dist/interactive.js.map +1 -1
  120. package/dist/modes/json-mode.d.ts +7 -0
  121. package/dist/modes/json-mode.d.ts.map +1 -1
  122. package/dist/modes/json-mode.js +9 -1
  123. package/dist/modes/json-mode.js.map +1 -1
  124. package/dist/system-prompt.d.ts +2 -1
  125. package/dist/system-prompt.d.ts.map +1 -1
  126. package/dist/system-prompt.js +116 -92
  127. package/dist/system-prompt.js.map +1 -1
  128. package/dist/system-prompt.test.d.ts +2 -0
  129. package/dist/system-prompt.test.d.ts.map +1 -0
  130. package/dist/system-prompt.test.js +107 -0
  131. package/dist/system-prompt.test.js.map +1 -0
  132. package/dist/tools/bash.d.ts.map +1 -1
  133. package/dist/tools/bash.js +1 -0
  134. package/dist/tools/bash.js.map +1 -1
  135. package/dist/tools/edit-diff.d.ts +66 -1
  136. package/dist/tools/edit-diff.d.ts.map +1 -1
  137. package/dist/tools/edit-diff.js +208 -10
  138. package/dist/tools/edit-diff.js.map +1 -1
  139. package/dist/tools/edit-diff.test.js +175 -15
  140. package/dist/tools/edit-diff.test.js.map +1 -1
  141. package/dist/tools/edit.d.ts +3 -1
  142. package/dist/tools/edit.d.ts.map +1 -1
  143. package/dist/tools/edit.js +190 -44
  144. package/dist/tools/edit.js.map +1 -1
  145. package/dist/tools/edit.test.js +392 -3
  146. package/dist/tools/edit.test.js.map +1 -1
  147. package/dist/tools/enter-plan.d.ts.map +1 -1
  148. package/dist/tools/enter-plan.js +1 -0
  149. package/dist/tools/enter-plan.js.map +1 -1
  150. package/dist/tools/exit-plan.d.ts.map +1 -1
  151. package/dist/tools/exit-plan.js +1 -0
  152. package/dist/tools/exit-plan.js.map +1 -1
  153. package/dist/tools/index.d.ts +13 -0
  154. package/dist/tools/index.d.ts.map +1 -1
  155. package/dist/tools/index.js +4 -4
  156. package/dist/tools/index.js.map +1 -1
  157. package/dist/tools/prompt-hints.d.ts.map +1 -1
  158. package/dist/tools/prompt-hints.js +6 -4
  159. package/dist/tools/prompt-hints.js.map +1 -1
  160. package/dist/tools/read.d.ts +1 -1
  161. package/dist/tools/read.d.ts.map +1 -1
  162. package/dist/tools/read.js +2 -1
  163. package/dist/tools/read.js.map +1 -1
  164. package/dist/tools/subagent.d.ts +1 -1
  165. package/dist/tools/subagent.d.ts.map +1 -1
  166. package/dist/tools/subagent.js +55 -2
  167. package/dist/tools/subagent.js.map +1 -1
  168. package/dist/tools/tasks.d.ts.map +1 -1
  169. package/dist/tools/tasks.js +1 -0
  170. package/dist/tools/tasks.js.map +1 -1
  171. package/dist/tools/web-fetch.d.ts +1 -0
  172. package/dist/tools/web-fetch.d.ts.map +1 -1
  173. package/dist/tools/web-fetch.js +96 -6
  174. package/dist/tools/web-fetch.js.map +1 -1
  175. package/dist/tools/web-fetch.test.d.ts +2 -0
  176. package/dist/tools/web-fetch.test.d.ts.map +1 -0
  177. package/dist/tools/web-fetch.test.js +90 -0
  178. package/dist/tools/web-fetch.test.js.map +1 -0
  179. package/dist/tools/web-search.d.ts +1 -0
  180. package/dist/tools/web-search.d.ts.map +1 -1
  181. package/dist/tools/web-search.js +167 -27
  182. package/dist/tools/web-search.js.map +1 -1
  183. package/dist/tools/web-search.test.d.ts +2 -0
  184. package/dist/tools/web-search.test.d.ts.map +1 -0
  185. package/dist/tools/web-search.test.js +119 -0
  186. package/dist/tools/web-search.test.js.map +1 -0
  187. package/dist/tools/write.d.ts +1 -1
  188. package/dist/tools/write.d.ts.map +1 -1
  189. package/dist/tools/write.js +3 -1
  190. package/dist/tools/write.js.map +1 -1
  191. package/dist/tools/write.test.js +23 -0
  192. package/dist/tools/write.test.js.map +1 -1
  193. package/dist/ui/App.d.ts +74 -3
  194. package/dist/ui/App.d.ts.map +1 -1
  195. package/dist/ui/App.js +727 -223
  196. package/dist/ui/App.js.map +1 -1
  197. package/dist/ui/activity-phrases.d.ts +2 -1
  198. package/dist/ui/activity-phrases.d.ts.map +1 -1
  199. package/dist/ui/activity-phrases.js +11 -1
  200. package/dist/ui/activity-phrases.js.map +1 -1
  201. package/dist/ui/components/ActivityIndicator.d.ts +15 -1
  202. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  203. package/dist/ui/components/ActivityIndicator.js +143 -44
  204. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  205. package/dist/ui/components/ActivityIndicator.test.d.ts +2 -0
  206. package/dist/ui/components/ActivityIndicator.test.d.ts.map +1 -0
  207. package/dist/ui/components/ActivityIndicator.test.js +40 -0
  208. package/dist/ui/components/ActivityIndicator.test.js.map +1 -0
  209. package/dist/ui/components/AnimationContext.d.ts +8 -2
  210. package/dist/ui/components/AnimationContext.d.ts.map +1 -1
  211. package/dist/ui/components/AnimationContext.js +94 -16
  212. package/dist/ui/components/AnimationContext.js.map +1 -1
  213. package/dist/ui/components/AssistantMessage.js +1 -1
  214. package/dist/ui/components/AssistantMessage.js.map +1 -1
  215. package/dist/ui/components/CompactionNotice.d.ts +5 -1
  216. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  217. package/dist/ui/components/CompactionNotice.js +4 -6
  218. package/dist/ui/components/CompactionNotice.js.map +1 -1
  219. package/dist/ui/components/Footer.d.ts +10 -2
  220. package/dist/ui/components/Footer.d.ts.map +1 -1
  221. package/dist/ui/components/Footer.js +45 -8
  222. package/dist/ui/components/Footer.js.map +1 -1
  223. package/dist/ui/components/InputArea.d.ts.map +1 -1
  224. package/dist/ui/components/InputArea.js +76 -28
  225. package/dist/ui/components/InputArea.js.map +1 -1
  226. package/dist/ui/components/ServerToolExecution.d.ts +1 -0
  227. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  228. package/dist/ui/components/ServerToolExecution.js +21 -2
  229. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  230. package/dist/ui/components/Spinner.d.ts +3 -1
  231. package/dist/ui/components/Spinner.d.ts.map +1 -1
  232. package/dist/ui/components/Spinner.js +10 -4
  233. package/dist/ui/components/Spinner.js.map +1 -1
  234. package/dist/ui/components/StreamingArea.js +1 -1
  235. package/dist/ui/components/StreamingArea.js.map +1 -1
  236. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  237. package/dist/ui/components/SubAgentPanel.js +3 -4
  238. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  239. package/dist/ui/components/ToolExecution.d.ts +2 -0
  240. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  241. package/dist/ui/components/ToolExecution.js +24 -5
  242. package/dist/ui/components/ToolExecution.js.map +1 -1
  243. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  244. package/dist/ui/components/ToolGroupExecution.js +23 -2
  245. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  246. package/dist/ui/components/ToolUseLoader.d.ts +4 -2
  247. package/dist/ui/components/ToolUseLoader.d.ts.map +1 -1
  248. package/dist/ui/components/ToolUseLoader.js +3 -3
  249. package/dist/ui/components/ToolUseLoader.js.map +1 -1
  250. package/dist/ui/components/index.d.ts +1 -2
  251. package/dist/ui/components/index.d.ts.map +1 -1
  252. package/dist/ui/components/index.js +1 -2
  253. package/dist/ui/components/index.js.map +1 -1
  254. package/dist/ui/hooks/useAgentLoop.d.ts +4 -1
  255. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  256. package/dist/ui/hooks/useAgentLoop.js +110 -13
  257. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  258. package/dist/ui/hooks/useBlink.d.ts.map +1 -1
  259. package/dist/ui/hooks/useBlink.js +3 -4
  260. package/dist/ui/hooks/useBlink.js.map +1 -1
  261. package/dist/ui/hooks/useTerminalSize.d.ts +12 -3
  262. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  263. package/dist/ui/hooks/useTerminalSize.js +16 -1
  264. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  265. package/dist/ui/pixel.js +1 -1
  266. package/dist/ui/pixel.test.js +1 -1
  267. package/dist/ui/pixel.test.js.map +1 -1
  268. package/dist/ui/render.d.ts +48 -1
  269. package/dist/ui/render.d.ts.map +1 -1
  270. package/dist/ui/render.js +56 -6
  271. package/dist/ui/render.js.map +1 -1
  272. package/dist/ui/theme/dark-ansi.json +2 -1
  273. package/dist/ui/theme/dark-daltonized.json +2 -1
  274. package/dist/ui/theme/dark.json +2 -1
  275. package/dist/ui/theme/light-ansi.json +2 -1
  276. package/dist/ui/theme/light-daltonized.json +2 -1
  277. package/dist/ui/theme/light.json +2 -1
  278. package/dist/ui/theme/theme.d.ts +1 -0
  279. package/dist/ui/theme/theme.d.ts.map +1 -1
  280. package/dist/utils/error-handler.d.ts.map +1 -1
  281. package/dist/utils/error-handler.js +2 -0
  282. package/dist/utils/error-handler.js.map +1 -1
  283. package/package.json +9 -6
  284. package/dist/ui/buddy/Buddy.d.ts +0 -15
  285. package/dist/ui/buddy/Buddy.d.ts.map +0 -1
  286. package/dist/ui/buddy/Buddy.js +0 -118
  287. package/dist/ui/buddy/Buddy.js.map +0 -1
  288. package/dist/ui/buddy/gacha.d.ts +0 -8
  289. package/dist/ui/buddy/gacha.d.ts.map +0 -1
  290. package/dist/ui/buddy/gacha.js +0 -94
  291. package/dist/ui/buddy/gacha.js.map +0 -1
  292. package/dist/ui/buddy/species.d.ts +0 -52
  293. package/dist/ui/buddy/species.d.ts.map +0 -1
  294. package/dist/ui/buddy/species.js +0 -257
  295. package/dist/ui/buddy/species.js.map +0 -1
  296. package/dist/ui/components/ThinkingIndicator.d.ts +0 -6
  297. package/dist/ui/components/ThinkingIndicator.d.ts.map +0 -1
  298. package/dist/ui/components/ThinkingIndicator.js +0 -145
  299. package/dist/ui/components/ThinkingIndicator.js.map +0 -1
package/dist/ui/App.js CHANGED
@@ -4,11 +4,12 @@ import { Box, Text, Static, 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 crypto, { createHash } from "node:crypto";
7
+ import { createHash } from "node:crypto";
8
8
  import { readFileSync, writeFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { playNotificationSound } from "../utils/sound.js";
12
+ import { formatError, } from "@prestyj/ai";
12
13
  import { extractImagePaths } from "../utils/image.js";
13
14
  import { useAgentLoop } from "./hooks/useAgentLoop.js";
14
15
  import { isEyesActive, journalCount } from "@prestyj/eyes";
@@ -34,63 +35,64 @@ import { EyesOverlay } from "./components/EyesOverlay.js";
34
35
  import { ThemeSelector } from "./components/ThemeSelector.js";
35
36
  import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
36
37
  import { useTheme, useSetTheme } from "./theme/theme.js";
37
- import { useAnimationTick, useAnimationActive, deriveFrame, } from "./components/AnimationContext.js";
38
38
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
39
39
  import { getGitBranch } from "../utils/git.js";
40
- import { getModel, getContextWindow } from "../core/model-registry.js";
40
+ import { getModel, getContextWindow, getMaxThinkingLevel } from "../core/model-registry.js";
41
41
  import { SessionManager } from "../core/session-manager.js";
42
+ import { appendMessagesToSession as appendSessionMessages, createCompactedSessionCheckpoint, } from "../core/session-compaction.js";
42
43
  import { log } from "../core/logger.js";
43
44
  import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
44
45
  import { generateSessionTitle } from "../utils/session-title.js";
45
46
  import { SettingsManager } from "../core/settings-manager.js";
46
- import { shouldCompact, compact } from "../core/compaction/compactor.js";
47
+ import { shouldCompact, compact, getCompactionReserveTokens, } from "../core/compaction/compactor.js";
47
48
  import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
48
49
  import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
50
+ import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
49
51
  import { loadCustomCommands } from "../core/custom-commands.js";
50
52
  import { buildSystemPrompt } from "../system-prompt.js";
53
+ import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
54
+ import { detectVerifyCommands } from "../core/verify-commands.js";
55
+ import { FOCUSED_REPO_MAP_MAX_CHARS, FIRST_TURN_REPO_MAP_MAX_CHARS, buildRepoMap, createRepoMapCache, } from "../core/repomap.js";
56
+ import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMessages, } from "../core/repomap-context.js";
51
57
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
52
58
  import { getMCPServers } from "../core/mcp/index.js";
53
59
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
54
- import { Buddy } from "./buddy/Buddy.js";
55
- // ── Provider Error Hints ──────────────────────────────────
56
- /** Detect provider-side errors and return a user-facing hint. */
57
- function getProviderErrorHint(message) {
58
- const lower = message.toLowerCase();
59
- if (lower.includes("overloaded") || lower.includes("engine_overloaded")) {
60
- return "This is a provider-side issue — their servers are under heavy load. Try again in a moment.";
61
- }
62
- if (lower.includes("insufficient balance") ||
63
- lower.includes("no resource package") ||
64
- lower.includes("quota exceeded") ||
65
- lower.includes("recharge")) {
66
- return "The provider reports a billing or quota issue. Check your account balance or resource package.";
67
- }
68
- if (lower.includes("rate limit") ||
69
- lower.includes("too many requests") ||
70
- lower.includes("429")) {
71
- return "You've hit the provider's rate limit. Wait a moment before retrying.";
72
- }
73
- if (lower.includes("502") || lower.includes("bad gateway")) {
74
- return "The provider returned a server error. This is not a ezcoder issue — try again shortly.";
75
- }
76
- if (lower.includes("503") || lower.includes("service unavailable")) {
77
- return "The provider's service is temporarily unavailable. Try again in a moment.";
78
- }
79
- if (lower.includes("timeout") || lower.includes("timed out")) {
80
- return "The request to the provider timed out. Their servers may be slow — try again.";
81
- }
82
- if (lower.includes("500") && lower.includes("internal server error")) {
83
- return "The provider experienced an internal error. This is not a ezcoder issue.";
84
- }
85
- if (lower.includes("does not recognize the requested model") ||
86
- (lower.includes("model") &&
87
- (lower.includes("not exist") || lower.includes("not found") || lower.includes("no access")))) {
88
- return "Use /model to switch to a different model, or check that your account has access to the requested model.";
89
- }
90
- return null;
60
+ /** Where ezcoder bugs should be reported. Surfaced in the guidance line. */
61
+ const GGCODER_BUG_REPORT_URL = "github.com/Gahroot/ezcoder/issues";
62
+ /**
63
+ * Build an ErrorItem from any thrown value. Centralises headline / message /
64
+ * guidance extraction so every error answers the same question for the user:
65
+ * "Should I retry, or is this a ezcoder bug to report?"
66
+ */
67
+ function toErrorItem(err, id, contextPrefix) {
68
+ const f = formatError(err);
69
+ const headline = contextPrefix ? `${contextPrefix} — ${f.headline}` : f.headline;
70
+ // For ezcoder bugs, swap the generic "see /help" guidance for an actual URL
71
+ // so users have a clear place to send the report.
72
+ const guidance = f.source === "ezcoder"
73
+ ? `This looks like a ezcoder bug — please send it to the dev at ${GGCODER_BUG_REPORT_URL}.`
74
+ : f.guidance;
75
+ // Mirror every user-visible error into ~/.ezcoder/debug.log so reports can be
76
+ // diagnosed even after the terminal scrollback is gone.
77
+ log("ERROR", "ui-error", headline, {
78
+ source: f.source,
79
+ message: f.message,
80
+ ...(f.provider ? { provider: f.provider } : {}),
81
+ ...(f.statusCode != null ? { statusCode: String(f.statusCode) } : {}),
82
+ ...(f.requestId ? { requestId: f.requestId } : {}),
83
+ ...(err instanceof Error && err.stack ? { stack: err.stack } : {}),
84
+ });
85
+ return {
86
+ kind: "error",
87
+ headline,
88
+ message: f.message,
89
+ guidance,
90
+ id,
91
+ };
91
92
  }
92
93
  /** Tools that get aggregated into a single compact group when concurrent. */
93
94
  const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
95
+ const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
94
96
  /**
95
97
  * Cap memory by replacing old items with tiny tombstones. Ink's <Static>
96
98
  * tracks rendered items by array length — the array must never shrink, but
@@ -312,15 +314,22 @@ export function App(props) {
312
314
  // Items from the current/last turn — rendered in the live area so they stay visible
313
315
  const [liveItems, setLiveItems] = useState([]);
314
316
  // overlay seeded from sessionStore (lives across remount). Falls back to
315
- // props.initialOverlay (CLI launched with one), then null.
316
- const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
317
+ // props.initialOverlay (CLI launched with one) ONLY when there's no
318
+ // sessionStore otherwise sessionStore is authoritative (its initial
319
+ // value already incorporated initialOverlay at session creation). Without
320
+ // this distinction, closing an overlay that was opened via initialOverlay
321
+ // (e.g. `ezcoder pixel`) re-asserts the overlay on resetUI remount because
322
+ // `null ?? "pixel"` is `"pixel"`.
323
+ const [overlay, setOverlay] = useState(props.sessionStore ? (props.sessionStore.overlay ?? null) : (props.initialOverlay ?? null));
317
324
  const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
318
325
  const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
319
326
  const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
320
- const [runAllTasks, setRunAllTasks] = useState(false);
321
- const runAllTasksRef = useRef(false);
327
+ // Seed from sessionStore so "Run All" chaining survives the resetUI()
328
+ // remount that startTask() triggers between tasks.
329
+ const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
330
+ const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
322
331
  const startTaskRef = useRef(() => { });
323
- const runAllPixelRef = useRef(false);
332
+ const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
324
333
  const currentPixelFixRef = useRef(null);
325
334
  const startPixelFixRef = useRef(() => { });
326
335
  const cwdRef = useRef(props.cwd);
@@ -333,13 +342,41 @@ export function App(props) {
333
342
  const [currentModel, setCurrentModel] = useState(props.model);
334
343
  const [currentProvider, setCurrentProvider] = useState(props.provider);
335
344
  const [currentTools, setCurrentTools] = useState(props.tools);
345
+ const currentToolsRef = useRef(props.tools);
336
346
  const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
337
347
  const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
348
+ const repoMapInjectionEnabledRef = useRef(true);
349
+ const repoMapDirtyRef = useRef(true);
350
+ const repoMapMarkdownRef = useRef("");
351
+ const repoMapSnapshotRef = useRef(undefined);
352
+ const repoMapChangedCountRef = useRef(0);
353
+ const repoMapCacheRef = useRef(createRepoMapCache());
338
354
  const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
339
355
  const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
340
356
  const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
341
357
  const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
342
- const nextIdRef = useRef(0);
358
+ const planModeStateRef = useRef(planMode);
359
+ // Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
360
+ // times we've nudged the agent to continue the same step. Reset whenever a
361
+ // new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
362
+ // so a genuinely stuck agent surfaces instead of looping forever.
363
+ const followUpNudgesRef = useRef({ step: 0, count: 0 });
364
+ // Seed the per-item ID counter so it doesn't collide with IDs already in
365
+ // sessionStore.history (which survives remount). Without this, a remount
366
+ // (resize, overlay toggle, etc.) starts the counter at 0 and new items
367
+ // generate ids "0", "1", "2"… that collide with the same ids from the
368
+ // previous mount, triggering React's duplicate-key warning and causing
369
+ // duplicate/omitted renders.
370
+ const nextIdRef = useRef((() => {
371
+ const hist = props.sessionStore?.history ?? props.initialHistory ?? [];
372
+ let max = -1;
373
+ for (const item of hist) {
374
+ const n = Number(item.id);
375
+ if (Number.isFinite(n) && n > max)
376
+ max = n;
377
+ }
378
+ return max + 1;
379
+ })());
343
380
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
344
381
  const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
345
382
  const persistedIndexRef = useRef(messagesRef.current.length);
@@ -349,6 +386,26 @@ export function App(props) {
349
386
  const lastActualTokensTimestampRef = useRef(0);
350
387
  /** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
351
388
  const lastCompactionTimeRef = useRef(0);
389
+ /**
390
+ * Languages whose style packs are currently injected into the system prompt.
391
+ * Grown by `maybeInjectLanguagePacks` after `write`/`bash` tool results when
392
+ * the language detector sees new marker files. Reset on `chdir` (pixel-fix).
393
+ * Only grows within a session; we never strip packs once injected (cheaper
394
+ * than invalidating prompt caching, and stale guidance is harmless).
395
+ */
396
+ const injectedLanguagesRef = useRef(new Set());
397
+ /**
398
+ * True until the first style-pack badge is pushed. Used to gate the
399
+ * one-time "/setup" hint so users learn the slash command without being
400
+ * spammed on every subsequent pack swap.
401
+ */
402
+ const setupHintShownRef = useRef(false);
403
+ /**
404
+ * Callback that fires `/setup` programmatically. Assigned later in the
405
+ * component once `agentLoop` is in scope. Called from the initial
406
+ * language-detection path when this cwd has never been audited before.
407
+ */
408
+ const triggerAutoSetupRef = useRef(async () => { });
352
409
  const getId = () => String(nextIdRef.current++);
353
410
  // Two-phase flush: items waiting to be moved to Static history after the
354
411
  // live area has been cleared and Ink has committed the smaller output.
@@ -372,9 +429,9 @@ export function App(props) {
372
429
  }, [currentProvider, onRuntimeStateChange]);
373
430
  useEffect(() => {
374
431
  onRuntimeStateChange?.({
375
- thinking: thinkingEnabled ? (props.thinking ?? "medium") : undefined,
432
+ thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
376
433
  });
377
- }, [thinkingEnabled, props.thinking, onRuntimeStateChange]);
434
+ }, [thinkingEnabled, currentModel, onRuntimeStateChange]);
378
435
  // Mirror session state into renderApp's closure so resetUI() can re-seed
379
436
  // the conversation on remount. Each panel that previously did a bare ANSI
380
437
  // screen clear (overlay open/close, plan accept/reject, /clear, startTask)
@@ -404,6 +461,7 @@ export function App(props) {
404
461
  const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
405
462
  const activeAccountId = currentCreds?.accountId ?? props.accountId;
406
463
  const activeBaseUrl = currentCreds?.baseUrl ?? props.baseUrl;
464
+ const contextWindowOptions = useMemo(() => ({ provider: currentProvider, accountId: activeAccountId }), [currentProvider, activeAccountId]);
407
465
  // Load git branch — re-runs whenever the displayed cwd changes (e.g. when
408
466
  // a pixel fix moves the agent into a different project root).
409
467
  useEffect(() => {
@@ -425,25 +483,118 @@ export function App(props) {
425
483
  useEffect(() => {
426
484
  reloadCustomCommands();
427
485
  }, [reloadCustomCommands]);
486
+ useEffect(() => {
487
+ currentToolsRef.current = currentTools;
488
+ }, [currentTools]);
428
489
  // ── Plan mode wiring ─────────────────────────────────────
429
490
  // Sync planModeRef with React state
430
491
  useEffect(() => {
492
+ planModeStateRef.current = planMode;
431
493
  if (props.planModeRef) {
432
494
  props.planModeRef.current = planMode;
433
495
  }
434
496
  }, [planMode, props.planModeRef]);
497
+ const rebuildSystemPrompt = useCallback(async (options) => {
498
+ const approvedPlanPath = options?.clearApprovedPlan
499
+ ? undefined
500
+ : (options?.approvedPlanPath ?? approvedPlanPathRef.current);
501
+ 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);
502
+ }, [props.skills]);
503
+ const replaceSystemPrompt = useCallback(async (options) => {
504
+ const newPrompt = await rebuildSystemPrompt(options);
505
+ if (messagesRef.current[0]?.role === "system") {
506
+ messagesRef.current[0] = { role: "system", content: newPrompt };
507
+ }
508
+ return newPrompt;
509
+ }, [rebuildSystemPrompt]);
510
+ /**
511
+ * Unified "apply detection result" pipeline. Called from three sites:
512
+ * 1. Initial mount (existing project at startup).
513
+ * 2. After every `write`/`bash` tool result (reactive to new manifests).
514
+ * 3. Before every user submit (catches external changes between turns,
515
+ * and ensures non-writing prompts still surface the badge).
516
+ *
517
+ * No-op when no new languages were added vs `injectedLanguagesRef.current`.
518
+ * The set-growth gate keeps this safe to call from every hot path.
519
+ */
520
+ const applyLanguageDetectionRef = useRef(async () => { });
521
+ applyLanguageDetectionRef.current = async (source) => {
522
+ const cwd = cwdRef.current;
523
+ const detected = detectLanguages(cwd);
524
+ const added = [];
525
+ for (const id of detected) {
526
+ if (!injectedLanguagesRef.current.has(id))
527
+ added.push(id);
528
+ }
529
+ if (added.length === 0) {
530
+ // No new packs to inject. The empty-detection hint + auto-run are
531
+ // first-time-per-cwd only — once the user has been shown the box and
532
+ // /setup has had a chance to run, re-showing on every session is noise.
533
+ // The with-packs path below is gated the same way via
534
+ // getAnnouncedLanguages / markLanguagesAnnounced: badge fires once per
535
+ // (cwd, language) and stays silent on subsequent sessions / /clear.
536
+ if (source === "initial" &&
537
+ !setupHintShownRef.current &&
538
+ injectedLanguagesRef.current.size === 0 &&
539
+ isFirstTimeSetup(cwd)) {
540
+ setupHintShownRef.current = true;
541
+ markSetupAudited(cwd);
542
+ log("INFO", "language", `No style packs detected for ${cwd}`, { source });
543
+ setLiveItems((prev) => [...prev, { kind: "setup_hint", id: getId() }]);
544
+ // /setup handles the empty / parent-folder / scratch-dir case via
545
+ // its brand-new-empty-project branch in the prompt template.
546
+ void triggerAutoSetupRef.current();
547
+ }
548
+ return;
549
+ }
550
+ injectedLanguagesRef.current = detected;
551
+ try {
552
+ await replaceSystemPrompt({ cwd, activeLanguages: detected });
553
+ const verifyCmds = detectVerifyCommands(cwd, detected);
554
+ const tag = source === "initial" ? "Initial style packs" : "Style pack(s) loaded";
555
+ log("INFO", "language", `${tag}: ${added.join(", ")}`, {
556
+ source,
557
+ active: [...detected].join(","),
558
+ verify_count: String(verifyCmds.length),
559
+ verify: verifyCmds.map((c) => `${c.language}:${c.label}=${c.command}`).join(" | "),
560
+ });
561
+ // The badge is purely user-facing notification ("hey, this pack just
562
+ // turned on"). The system prompt is already updated above — that's the
563
+ // load-bearing part. We persist the announced set per-cwd so /clear,
564
+ // restart, and new sessions stay quiet for packs the user has seen.
565
+ const alreadyAnnounced = new Set(getAnnouncedLanguages(cwd));
566
+ const toAnnounce = added.filter((id) => !alreadyAnnounced.has(id));
567
+ if (toAnnounce.length > 0) {
568
+ markLanguagesAnnounced(cwd, toAnnounce);
569
+ const showSetupHint = !setupHintShownRef.current;
570
+ setupHintShownRef.current = true;
571
+ setLiveItems((prev) => [
572
+ ...prev,
573
+ { kind: "style_pack", added: toAnnounce, showSetupHint, id: getId() },
574
+ ]);
575
+ }
576
+ // First-time-per-project auto-run. Fires only on the initial mount
577
+ // detection path — not on tool/input triggers — so we don't surprise
578
+ // users mid-session. Persisted across sessions via setup-history.json.
579
+ if (source === "initial" && isFirstTimeSetup(cwd)) {
580
+ markSetupAudited(cwd);
581
+ void triggerAutoSetupRef.current();
582
+ }
583
+ }
584
+ catch (err) {
585
+ log("WARN", "language", `Detection apply failed (${source}): ${err.message}`);
586
+ }
587
+ };
588
+ // Initial language detection — runs once on mount so existing projects with
589
+ // marker files (package.json, Cargo.toml, etc.) get their style packs from
590
+ // turn 1, with a visible badge.
591
+ useEffect(() => {
592
+ void applyLanguageDetectionRef.current("initial");
593
+ }, []);
435
594
  // Rebuild system prompt when plan mode changes
436
595
  useEffect(() => {
437
- void (async () => {
438
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, approvedPlanPathRef.current);
439
- if (messagesRef.current[0]?.role === "system") {
440
- messagesRef.current[0] = {
441
- role: "system",
442
- content: newPrompt,
443
- };
444
- }
445
- })();
446
- }, [planMode, props.cwd, props.skills]);
596
+ void replaceSystemPrompt({ planMode });
597
+ }, [planMode, replaceSystemPrompt]);
447
598
  // Wire onEnterPlan callback ref
448
599
  useEffect(() => {
449
600
  if (props.onEnterPlanRef) {
@@ -462,8 +613,10 @@ export function App(props) {
462
613
  if (props.onExitPlanRef) {
463
614
  props.onExitPlanRef.current = async (planPath) => {
464
615
  // Deactivate plan mode, store approved plan path, open pane
616
+ planModeStateRef.current = false;
465
617
  setPlanMode(false);
466
618
  approvedPlanPathRef.current = planPath;
619
+ await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
467
620
  // Use setTimeout to open pane after the current tool execution completes,
468
621
  // so the turn can finish and the UI transitions cleanly
469
622
  // Flag that the plan overlay is about to open — suppresses the
@@ -492,43 +645,79 @@ export function App(props) {
492
645
  planPath);
493
646
  };
494
647
  }
495
- }, [props.onExitPlanRef, stdout]);
496
- const persistNewMessages = useCallback(async () => {
648
+ }, [props.onExitPlanRef, replaceSystemPrompt, stdout]);
649
+ const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
650
+ const sm = sessionManagerRef.current;
651
+ if (!sm)
652
+ return;
653
+ await appendSessionMessages(sm, sessionPath, messages, startIndex);
654
+ }, []);
655
+ const persistCompactedSession = useCallback(async (compactedMessages) => {
497
656
  const sm = sessionManagerRef.current;
657
+ if (!sm)
658
+ return;
659
+ const session = await createCompactedSessionCheckpoint(sm, {
660
+ cwd: cwdRef.current,
661
+ provider: currentProvider,
662
+ model: currentModel,
663
+ messages: compactedMessages,
664
+ });
665
+ sessionPathRef.current = session.path;
666
+ persistedIndexRef.current = compactedMessages.length;
667
+ if (sessionStore) {
668
+ sessionStore.sessionPath = session.path;
669
+ sessionStore.messages = [...compactedMessages];
670
+ }
671
+ log("INFO", "compaction", "Persisted compacted session checkpoint", { path: session.path });
672
+ }, [currentModel, currentProvider, sessionStore]);
673
+ const persistNewMessages = useCallback(async () => {
498
674
  const sp = sessionPathRef.current;
499
- if (!sm || !sp)
675
+ if (!sp)
500
676
  return;
501
677
  const allMsgs = messagesRef.current;
502
- for (let i = persistedIndexRef.current; i < allMsgs.length; i++) {
503
- const msg = allMsgs[i];
504
- if (msg.role === "system")
505
- continue;
506
- const entry = {
507
- type: "message",
508
- id: crypto.randomUUID(),
509
- parentId: null,
510
- timestamp: new Date().toISOString(),
511
- message: msg,
512
- };
513
- await sm.appendEntry(sp, entry);
514
- }
678
+ await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
515
679
  persistedIndexRef.current = allMsgs.length;
516
- }, []);
680
+ if (sessionStore) {
681
+ sessionStore.messages = [...allMsgs];
682
+ sessionStore.sessionPath = sp;
683
+ }
684
+ }, [appendMessagesToSession, sessionStore]);
685
+ /**
686
+ * Run the language detector against the current cwd. If the detected set is a
687
+ * strict superset of what's already injected, rebuild the system prompt with
688
+ * the expanded set and swap `messagesRef.current[0]`.
689
+ *
690
+ * Called from `onToolEnd` after `write`/`bash` succeeds — these are the only
691
+ * tools that can introduce new marker files (package.json, Cargo.toml, etc.).
692
+ * Other tool kinds skip detection entirely to avoid wasted filesystem stats.
693
+ *
694
+ * No restart required: the system prompt is mutated in place, same mechanism
695
+ * already used for plan mode + pixel-fix chdir.
696
+ *
697
+ * Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
698
+ * empty to keep agent-loop options stable) can call the freshest version.
699
+ */
700
+ const maybeInjectLanguagePacksRef = useRef(async () => { });
701
+ maybeInjectLanguagePacksRef.current = async (toolName, isError) => {
702
+ if (isError)
703
+ return;
704
+ if (toolName !== "write" && toolName !== "bash")
705
+ return;
706
+ await applyLanguageDetectionRef.current("tool");
707
+ };
517
708
  // ── Compaction ─────────────────────────────────────────
518
- // Load settings for auto-compaction + buddy
709
+ // Load settings for auto-compaction
519
710
  const settingsRef = useRef(null);
520
- const [buddyEnabled, setBuddyEnabled] = useState(false);
521
711
  useEffect(() => {
522
712
  if (props.settingsFile) {
523
713
  const sm = new SettingsManager(props.settingsFile);
524
714
  sm.load().then(() => {
525
715
  settingsRef.current = sm;
526
- setBuddyEnabled(sm.get("buddyEnabled") ?? false);
527
716
  });
528
717
  }
529
718
  }, [props.settingsFile]);
530
719
  const compactConversation = useCallback(async (messages) => {
531
- const contextWindow = getContextWindow(currentModel);
720
+ const contextWindow = getContextWindow(currentModel, contextWindowOptions);
532
721
  const tokensBefore = estimateConversationTokens(messages);
533
722
  const spinId = getId();
534
723
  log("INFO", "compaction", `Running compaction`, {
@@ -541,14 +730,20 @@ export function App(props) {
541
730
  try {
542
731
  // Resolve fresh credentials for compaction too
543
732
  let compactApiKey = activeApiKey;
733
+ let compactAccountId = activeAccountId;
734
+ let compactBaseUrl = activeBaseUrl;
544
735
  if (props.authStorage) {
545
736
  const creds = await props.authStorage.resolveCredentials(currentProvider);
546
737
  compactApiKey = creds.accessToken;
738
+ compactAccountId = creds.accountId;
739
+ compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
547
740
  }
548
741
  const result = await compact(messages, {
549
742
  provider: currentProvider,
550
743
  model: currentModel,
551
744
  apiKey: compactApiKey,
745
+ accountId: compactAccountId,
746
+ baseUrl: compactBaseUrl,
552
747
  contextWindow,
553
748
  signal: undefined,
554
749
  approvedPlanPath: approvedPlanPathRef.current,
@@ -577,65 +772,118 @@ export function App(props) {
577
772
  const msg = err instanceof Error ? err.message : String(err);
578
773
  log("ERROR", "compaction", `Compaction failed: ${msg}`);
579
774
  // Replace spinner with error
580
- setLiveItems((prev) => prev.map((item) => item.id === spinId
581
- ? { kind: "error", message: `Compaction failed: ${msg}`, id: spinId }
582
- : item));
775
+ setLiveItems((prev) => prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
583
776
  return messages; // Return unchanged on failure
584
777
  }
585
- }, [currentModel, currentProvider, activeApiKey]);
778
+ }, [
779
+ currentModel,
780
+ currentProvider,
781
+ activeApiKey,
782
+ activeAccountId,
783
+ activeBaseUrl,
784
+ contextWindowOptions,
785
+ props.authStorage,
786
+ ]);
787
+ const getRepoMapSignalCount = useCallback(() => {
788
+ return ((props.repoMapChangedFilesRef?.current.size ?? 0) +
789
+ (props.repoMapReadFilesRef?.current.size ?? 0));
790
+ }, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef]);
791
+ const getRepoMapBudget = useCallback(() => {
792
+ const userTurns = messagesRef.current.filter((message) => message.role === "user").length;
793
+ const readCount = props.repoMapReadFilesRef?.current.size ?? 0;
794
+ if (userTurns <= 1 && readCount === 0)
795
+ return FIRST_TURN_REPO_MAP_MAX_CHARS;
796
+ if (readCount > 0)
797
+ return FOCUSED_REPO_MAP_MAX_CHARS;
798
+ return FOCUSED_REPO_MAP_MAX_CHARS + 1000;
799
+ }, [props.repoMapReadFilesRef]);
800
+ const refreshRepoMap = useCallback(async (latestUserPrompt) => {
801
+ const rendered = await buildRepoMap({
802
+ cwd: cwdRef.current,
803
+ maxChars: getRepoMapBudget(),
804
+ changedFiles: [...(props.repoMapChangedFilesRef?.current ?? new Set())],
805
+ readFiles: [...(props.repoMapReadFilesRef?.current ?? new Set())],
806
+ focusTerms: latestUserPrompt ? [latestUserPrompt] : [],
807
+ cache: repoMapCacheRef.current,
808
+ });
809
+ repoMapMarkdownRef.current = rendered.markdown;
810
+ repoMapSnapshotRef.current = rendered.snapshot;
811
+ repoMapChangedCountRef.current = getRepoMapSignalCount();
812
+ repoMapDirtyRef.current = false;
813
+ return rendered.markdown;
814
+ }, [
815
+ getRepoMapBudget,
816
+ getRepoMapSignalCount,
817
+ props.repoMapChangedFilesRef,
818
+ props.repoMapReadFilesRef,
819
+ ]);
820
+ const stripRepoMapMessages = useCallback((messages) => {
821
+ return stripRepoMapContextMessages(messages);
822
+ }, []);
823
+ const injectRepoMapContext = useCallback(async (messages) => {
824
+ if (!repoMapInjectionEnabledRef.current)
825
+ return stripRepoMapMessages(messages);
826
+ const stripped = stripRepoMapMessages(messages);
827
+ const latestUserPrompt = getLatestUserText(stripped);
828
+ const signalCount = getRepoMapSignalCount();
829
+ if (signalCount !== repoMapChangedCountRef.current)
830
+ repoMapDirtyRef.current = true;
831
+ if (repoMapDirtyRef.current || !repoMapMarkdownRef.current) {
832
+ await refreshRepoMap(latestUserPrompt);
833
+ }
834
+ if (!repoMapMarkdownRef.current)
835
+ return stripped;
836
+ return injectRepoMapContextMessages(stripped, repoMapMarkdownRef.current);
837
+ }, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef, refreshRepoMap, stripRepoMapMessages]);
586
838
  /**
587
839
  * transformContext callback for the agent loop.
588
840
  * Called before each LLM call and on context overflow.
589
- * Checks if auto-compaction is needed and runs it.
590
- *
591
- * Uses actual API-reported token counts (from previous turn_end) when
592
- * available, falling back to the character-based estimate. A 30-second
593
- * cooldown prevents repeated compaction — matching the pattern used by
594
- * Mysti, openclaw, and other real-world agent frameworks.
841
+ * Compacts persistent chat only, then injects the dynamic repo map transiently.
595
842
  */
596
843
  const transformContext = useCallback(async (messages, options) => {
844
+ const stripped = stripRepoMapMessages(messages);
597
845
  const settings = settingsRef.current;
598
846
  const autoCompact = settings?.get("autoCompact") ?? true;
599
847
  const threshold = settings?.get("compactThreshold") ?? 0.8;
600
848
  // Force-compact on context overflow regardless of settings
601
849
  if (options?.force) {
602
- const result = await compactConversation(messages);
850
+ const result = await compactConversation(stripped);
851
+ if (result !== stripped) {
852
+ messagesRef.current = result;
853
+ await persistCompactedSession(result);
854
+ }
603
855
  lastCompactionTimeRef.current = Date.now();
604
- return result;
856
+ return injectRepoMapContext(result);
605
857
  }
606
858
  if (!autoCompact)
607
- return messages;
859
+ return injectRepoMapContext(stripped);
608
860
  // Time-based cooldown: skip if compaction ran within the last 30 seconds
609
861
  if (Date.now() - lastCompactionTimeRef.current < 30_000) {
610
862
  log("INFO", "compaction", `Skipping compaction — cooldown active`);
611
- return messages;
863
+ return injectRepoMapContext(stripped);
612
864
  }
613
- const contextWindow = getContextWindow(currentModel);
614
- // Reserve = max output budget + ~5K headroom for system prompt + tool
615
- // schemas. Otherwise the API rejects requests where the prompt fits the
616
- // window but leaves no room for the response (e.g. Codex Mini at 200K
617
- // ctx / 100K out — pre-turn estimate may say 160K but a 100K reasoning
618
- // response then overflows).
619
- const modelInfo = getModel(currentModel);
620
- const reserveTokens = (modelInfo?.maxOutputTokens ?? 0) + 5_000;
621
- // Prefer actual API-reported tokens over char-based estimate, but only
622
- // when the token count was recorded AFTER the most recent compaction.
623
- // A count from before compaction is stale — it reflects the old context
624
- // size and would trigger compaction again immediately for no reason.
865
+ const contextWindow = getContextWindow(currentModel, contextWindowOptions);
866
+ const reserveTokens = getCompactionReserveTokens(props.maxTokens);
625
867
  const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
626
868
  const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
627
- if (shouldCompact(messages, contextWindow, threshold, actualTokens, reserveTokens)) {
628
- const before = messages.length;
629
- const result = await compactConversation(messages);
869
+ if (shouldCompact(stripped, contextWindow, threshold, actualTokens, reserveTokens)) {
870
+ const result = await compactConversation(stripped);
871
+ if (result !== stripped) {
872
+ messagesRef.current = result;
873
+ await persistCompactedSession(result);
874
+ }
630
875
  lastCompactionTimeRef.current = Date.now();
631
- // If compaction was a no-op (e.g. too few middle messages to summarize),
632
- // return the original reference so the agent loop doesn't replace messages.
633
- if (result.length === before)
634
- return messages;
635
- return result;
876
+ return injectRepoMapContext(result);
636
877
  }
637
- return messages;
638
- }, [currentModel, compactConversation]);
878
+ return injectRepoMapContext(stripped);
879
+ }, [
880
+ currentModel,
881
+ compactConversation,
882
+ contextWindowOptions,
883
+ injectRepoMapContext,
884
+ persistCompactedSession,
885
+ stripRepoMapMessages,
886
+ ]);
639
887
  // ── Background task bar state (external store) ──────────
640
888
  const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
641
889
  useTaskBarPolling(props.processManager);
@@ -663,7 +911,7 @@ export function App(props) {
663
911
  tools: currentTools,
664
912
  webSearch: props.webSearch,
665
913
  maxTokens: props.maxTokens,
666
- thinking: thinkingEnabled ? (props.thinking ?? "medium") : undefined,
914
+ thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
667
915
  apiKey: activeApiKey,
668
916
  baseUrl: activeBaseUrl,
669
917
  accountId: activeAccountId,
@@ -671,6 +919,7 @@ export function App(props) {
671
919
  transformContext,
672
920
  }, {
673
921
  onComplete: useCallback(() => {
922
+ messagesRef.current = stripRepoMapMessages(messagesRef.current);
674
923
  persistNewMessages();
675
924
  // Auto-clear plan progress and approved plan when all steps are completed
676
925
  const steps = planStepsRef.current;
@@ -679,12 +928,7 @@ export function App(props) {
679
928
  setPlanSteps([]);
680
929
  approvedPlanPathRef.current = undefined;
681
930
  // Rebuild system prompt to remove the completed plan from context
682
- void (async () => {
683
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
684
- if (messagesRef.current[0]?.role === "system") {
685
- messagesRef.current[0] = { role: "system", content: newPrompt };
686
- }
687
- })();
931
+ void replaceSystemPrompt({ clearApprovedPlan: true });
688
932
  }
689
933
  // Generate session title after the first turn (background, best-effort)
690
934
  if (!sessionTitleGeneratedRef.current) {
@@ -728,6 +972,7 @@ export function App(props) {
728
972
  }
729
973
  }, [
730
974
  persistNewMessages,
975
+ stripRepoMapMessages,
731
976
  planMode,
732
977
  props.cwd,
733
978
  props.skills,
@@ -747,6 +992,9 @@ export function App(props) {
747
992
  planStepsRef.current = updated;
748
993
  setPlanSteps(updated);
749
994
  }
995
+ // Real progress happened — reset the stuck-guard so the next
996
+ // step gets its own fresh nudge budget.
997
+ followUpNudgesRef.current = { step: 0, count: 0 };
750
998
  }
751
999
  }
752
1000
  // Flush all completed items from the previous turn to Static history.
@@ -814,6 +1062,8 @@ export function App(props) {
814
1062
  }, []),
815
1063
  onToolStart: useCallback((toolCallId, name, args) => {
816
1064
  log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
1065
+ const startedAt = Date.now();
1066
+ const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
817
1067
  // Flush completed items (assistant text, finished tools) to Static
818
1068
  // before adding tool UI. Keeping both in the live area makes it tall
819
1069
  // and causes Ink's cursor math to clip the top.
@@ -859,7 +1109,10 @@ export function App(props) {
859
1109
  const next = [...prev];
860
1110
  next[groupIdx] = {
861
1111
  ...group,
862
- tools: [...group.tools, { toolCallId, name, args, status: "running" }],
1112
+ tools: [
1113
+ ...group.tools,
1114
+ { toolCallId, name, args, status: "running", animateUntil },
1115
+ ],
863
1116
  };
864
1117
  return next;
865
1118
  }
@@ -867,7 +1120,7 @@ export function App(props) {
867
1120
  ...prev,
868
1121
  {
869
1122
  kind: "tool_group",
870
- tools: [{ toolCallId, name, args, status: "running" }],
1123
+ tools: [{ toolCallId, name, args, status: "running", animateUntil }],
871
1124
  id: getId(),
872
1125
  },
873
1126
  ];
@@ -876,7 +1129,7 @@ export function App(props) {
876
1129
  else {
877
1130
  setLiveItems((prev) => [
878
1131
  ...prev,
879
- { kind: "tool_start", toolCallId, name, args, id: getId() },
1132
+ { kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
880
1133
  ]);
881
1134
  }
882
1135
  }, []),
@@ -921,6 +1174,10 @@ export function App(props) {
921
1174
  });
922
1175
  }, []),
923
1176
  onToolEnd: useCallback((toolCallId, name, result, isError, durationMs, details) => {
1177
+ // Language-pack detection — gated on `write`/`bash` inside the
1178
+ // helper; cheap to call unconditionally. Fire-and-forget; the next
1179
+ // LLM turn picks up the swapped system prompt automatically.
1180
+ void maybeInjectLanguagePacksRef.current(name, isError);
924
1181
  const level = isError ? "ERROR" : "INFO";
925
1182
  log(level, "tool", `Tool call ended: ${name}`, {
926
1183
  id: toolCallId,
@@ -1025,6 +1282,8 @@ export function App(props) {
1025
1282
  }, []),
1026
1283
  onServerToolCall: useCallback((id, name, input) => {
1027
1284
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
1285
+ const startedAt = Date.now();
1286
+ const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1028
1287
  // Flush completed items (including assistant text) to Static before
1029
1288
  // adding server tool UI — same rationale as onToolStart.
1030
1289
  setLiveItems((prev) => {
@@ -1039,7 +1298,8 @@ export function App(props) {
1039
1298
  serverToolCallId: id,
1040
1299
  name,
1041
1300
  input,
1042
- startedAt: Date.now(),
1301
+ startedAt,
1302
+ animateUntil,
1043
1303
  id: getId(),
1044
1304
  },
1045
1305
  ];
@@ -1230,7 +1490,7 @@ export function App(props) {
1230
1490
  }
1231
1491
  return item;
1232
1492
  });
1233
- return [...next, { kind: "info", text: "Request was stopped.", id: getId() }];
1493
+ return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
1234
1494
  });
1235
1495
  }, []),
1236
1496
  onQueuedStart: useCallback((content) => {
@@ -1260,7 +1520,75 @@ export function App(props) {
1260
1520
  setDoneStatus(null);
1261
1521
  setLiveItems([userItem]);
1262
1522
  }, []),
1523
+ // Inject a "continue with the next step" follow-up when the agent
1524
+ // would otherwise stop mid-plan. The prompt-only instruction wasn't
1525
+ // enough — some models (notably Opus) treat each [DONE:n] as a
1526
+ // natural completion boundary regardless. The stuck-guard caps
1527
+ // nudges per step so a genuinely blocked agent surfaces.
1528
+ getFollowUpMessages: useCallback(() => {
1529
+ const steps = planStepsRef.current;
1530
+ if (steps.length === 0 || !approvedPlanPathRef.current)
1531
+ return null;
1532
+ const next = steps.find((s) => !s.completed);
1533
+ if (!next)
1534
+ return null;
1535
+ const r = followUpNudgesRef.current;
1536
+ if (r.step !== next.step) {
1537
+ r.step = next.step;
1538
+ r.count = 0;
1539
+ }
1540
+ if (r.count >= 2)
1541
+ return null;
1542
+ r.count++;
1543
+ return [
1544
+ {
1545
+ role: "user",
1546
+ content: `Continue with step ${next.step}: ${next.text}. ` +
1547
+ `Emit [DONE:${next.step}] when done, then proceed to step ${next.step + 1} ` +
1548
+ `in the same turn. Only stop when every step in \`## Steps\` is complete ` +
1549
+ `or you genuinely need user input.`,
1550
+ },
1551
+ ];
1552
+ }, []),
1263
1553
  });
1554
+ // First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
1555
+ // scope so the ref closure can dispatch to it. Called from the initial
1556
+ // language-detection path when `isFirstTimeSetup(cwd)` is true. Pushes a
1557
+ // notice item explaining what's happening, then runs the audit prompt.
1558
+ triggerAutoSetupRef.current = async () => {
1559
+ const setupCmd = getPromptCommand("setup");
1560
+ if (!setupCmd) {
1561
+ log("WARN", "setup", "Auto-setup skipped — /setup command not found in registry.");
1562
+ return;
1563
+ }
1564
+ log("INFO", "setup", `Auto-running /setup (first session for ${cwdRef.current})`);
1565
+ setLiveItems((prev) => [
1566
+ ...prev,
1567
+ {
1568
+ kind: "info",
1569
+ text: "First time in this project — auto-running /setup to audit hygiene, tooling, and style-pack alignment. " +
1570
+ "Press Esc to cancel.",
1571
+ id: getId(),
1572
+ },
1573
+ { kind: "user", text: "/setup", id: getId() },
1574
+ ]);
1575
+ setLastUserMessage("/setup");
1576
+ setDoneStatus(null);
1577
+ try {
1578
+ await agentLoop.run(setupCmd.prompt);
1579
+ }
1580
+ catch (err) {
1581
+ const msg = err instanceof Error ? err.message : String(err);
1582
+ const isAbort = msg.includes("aborted") || msg.includes("abort");
1583
+ log(isAbort ? "INFO" : "ERROR", "setup", `Auto-setup ended: ${msg}`);
1584
+ setLiveItems((prev) => [
1585
+ ...prev,
1586
+ isAbort
1587
+ ? { kind: "stopped", text: "Auto-setup cancelled.", id: getId() }
1588
+ : toErrorItem(err, getId()),
1589
+ ]);
1590
+ }
1591
+ };
1264
1592
  // Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
1265
1593
  // and Ink renders the smaller live area (updating its internal line
1266
1594
  // counter), this effect pushes the stashed items into Static history.
@@ -1279,6 +1607,29 @@ export function App(props) {
1279
1607
  useEffect(() => {
1280
1608
  setTitleRunning(agentLoop.isRunning);
1281
1609
  }, [agentLoop.isRunning]);
1610
+ // Mirror agent running state into sessionStore so renderApp's resize
1611
+ // handler and overlay toggles can skip their unmount/remount while the
1612
+ // agent is in flight (unmounting fires useAgentLoop's cleanup which
1613
+ // aborts the in-flight request). On the running→idle transition,
1614
+ // consume any pendingResetUI flag set during the run by scheduling a
1615
+ // deferred resetUI to clean up accumulated log-update drift. The 100ms
1616
+ // setTimeout lets onDone's two-phase flush commit to sessionStore.history
1617
+ // first, so the chat isn't lost. The cleanup also bails if the user
1618
+ // started a new run before the timer fires, to avoid aborting it.
1619
+ useEffect(() => {
1620
+ if (!sessionStore)
1621
+ return;
1622
+ sessionStore.isAgentRunning = agentLoop.isRunning;
1623
+ if (!agentLoop.isRunning && sessionStore.pendingResetUI) {
1624
+ sessionStore.pendingResetUI = false;
1625
+ const timer = setTimeout(() => {
1626
+ if (sessionStore.isAgentRunning)
1627
+ return;
1628
+ props.resetUI?.();
1629
+ }, 100);
1630
+ return () => clearTimeout(timer);
1631
+ }
1632
+ }, [agentLoop.isRunning, sessionStore, props.resetUI]);
1282
1633
  // Consume sessionStore.pendingAction once on mount. Set by resetUI options
1283
1634
  // for paths that remount AND immediately drive the agent (plan accept,
1284
1635
  // plan reject, startTask, pixel fix). The action survives the unmount
@@ -1292,7 +1643,14 @@ export function App(props) {
1292
1643
  pendingActionConsumedRef.current = true;
1293
1644
  if (sessionStore)
1294
1645
  sessionStore.pendingAction = undefined;
1295
- if (action.infoText) {
1646
+ if (action.planEvent) {
1647
+ const ev = action.planEvent;
1648
+ setLiveItems((prev) => [
1649
+ ...prev,
1650
+ { kind: "plan_event", event: ev.event, detail: ev.detail, id: getId() },
1651
+ ]);
1652
+ }
1653
+ else if (action.infoText) {
1296
1654
  setLiveItems((prev) => [
1297
1655
  ...prev,
1298
1656
  { kind: "info", text: action.infoText, id: getId() },
@@ -1302,7 +1660,7 @@ export function App(props) {
1302
1660
  void agentLoop.run(action.prompt).catch((err) => {
1303
1661
  const errMsg = err instanceof Error ? err.message : String(err);
1304
1662
  log("ERROR", "error", errMsg);
1305
- setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
1663
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
1306
1664
  });
1307
1665
  // Intentional one-shot: run once on mount, never re-fire on re-render.
1308
1666
  }, []);
@@ -1314,12 +1672,6 @@ export function App(props) {
1314
1672
  setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
1315
1673
  }
1316
1674
  }, [agentLoop.isRunning, props.cwd]);
1317
- // Animated thinking border — derived from global animation tick
1318
- useAnimationActive();
1319
- const animTick = useAnimationTick();
1320
- const thinkingBorderFrame = agentLoop.activityPhase === "thinking"
1321
- ? deriveFrame(animTick, 1000, THINKING_BORDER_COLORS.length)
1322
- : 0;
1323
1675
  const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
1324
1676
  const trimmed = input.trim();
1325
1677
  if (trimmed.startsWith("/")) {
@@ -1328,6 +1680,11 @@ export function App(props) {
1328
1680
  else {
1329
1681
  const truncated = trimmed.length > 100 ? trimmed.slice(0, 100) + "..." : trimmed;
1330
1682
  log("INFO", "input", `User input: ${truncated}${inputImages.length > 0 ? ` (+${inputImages.length} image${inputImages.length > 1 ? "s" : ""})` : ""}`);
1683
+ // Re-detect on every user submit — cheap (fs stats only). Catches
1684
+ // external changes between turns and ensures non-writing prompts still
1685
+ // surface the badge when packs are newly applicable. No-op if the set
1686
+ // has not grown.
1687
+ await applyLanguageDetectionRef.current("input");
1331
1688
  }
1332
1689
  // Handle /model directly — open inline selector
1333
1690
  if (trimmed === "/model" || trimmed === "/m") {
@@ -1339,7 +1696,7 @@ export function App(props) {
1339
1696
  const compacted = await compactConversation(messagesRef.current);
1340
1697
  if (compacted !== messagesRef.current) {
1341
1698
  messagesRef.current = compacted;
1342
- persistedIndexRef.current = 0; // Re-persist after compaction
1699
+ await persistCompactedSession(compacted);
1343
1700
  }
1344
1701
  return;
1345
1702
  }
@@ -1359,7 +1716,7 @@ export function App(props) {
1359
1716
  if (trimmed === "/clear") {
1360
1717
  if (props.resetUI) {
1361
1718
  void (async () => {
1362
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
1719
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1363
1720
  props.resetUI?.({
1364
1721
  wipeSession: true,
1365
1722
  messages: [{ role: "system", content: newPrompt }],
@@ -1378,7 +1735,7 @@ export function App(props) {
1378
1735
  planStepsRef.current = [];
1379
1736
  setPlanSteps([]);
1380
1737
  void (async () => {
1381
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
1738
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1382
1739
  messagesRef.current = [{ role: "system", content: newPrompt }];
1383
1740
  persistedIndexRef.current = messagesRef.current.length;
1384
1741
  })();
@@ -1440,29 +1797,46 @@ export function App(props) {
1440
1797
  planStepsRef.current = [];
1441
1798
  setPlanSteps([]);
1442
1799
  // Rebuild system prompt without the plan
1443
- void (async () => {
1444
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
1445
- if (messagesRef.current[0]?.role === "system") {
1446
- messagesRef.current[0] = { role: "system", content: newPrompt };
1447
- }
1448
- })();
1449
- setLiveItems([{ kind: "info", text: "Approved plan dismissed.", id: getId() }]);
1800
+ void replaceSystemPrompt({ clearApprovedPlan: true });
1801
+ setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
1450
1802
  return;
1451
1803
  }
1452
- // Handle /buddy — toggle companion
1453
- if (trimmed === "/buddy") {
1454
- const next = !buddyEnabled;
1455
- setBuddyEnabled(next);
1456
- if (settingsRef.current) {
1457
- settingsRef.current.set("buddyEnabled", next);
1804
+ // Handle /mapshow, refresh, or toggle dynamic repo map injection
1805
+ if (trimmed === "/map" ||
1806
+ trimmed === "/map refresh" ||
1807
+ trimmed === "/map on" ||
1808
+ trimmed === "/map off") {
1809
+ const action = trimmed.slice("/map".length).trim();
1810
+ if (action === "on") {
1811
+ repoMapInjectionEnabledRef.current = true;
1812
+ repoMapDirtyRef.current = true;
1813
+ setLiveItems((prev) => [
1814
+ ...prev,
1815
+ { kind: "info", text: "Dynamic repo map injection is on.", id: getId() },
1816
+ ]);
1817
+ return;
1458
1818
  }
1459
- setLiveItems((items) => [
1460
- ...items,
1819
+ if (action === "off") {
1820
+ repoMapInjectionEnabledRef.current = false;
1821
+ messagesRef.current = stripRepoMapMessages(messagesRef.current);
1822
+ setLiveItems((prev) => [
1823
+ ...prev,
1824
+ {
1825
+ kind: "info",
1826
+ text: "Dynamic repo map injection is off for this session.",
1827
+ id: getId(),
1828
+ },
1829
+ ]);
1830
+ return;
1831
+ }
1832
+ if (action === "refresh")
1833
+ repoMapDirtyRef.current = true;
1834
+ const markdown = await refreshRepoMap(getLatestUserText(messagesRef.current));
1835
+ setLiveItems((prev) => [
1836
+ ...prev,
1461
1837
  {
1462
1838
  kind: "info",
1463
- text: next
1464
- ? "Buddy enabled! Your companion will appear near the prompt."
1465
- : "Buddy disabled.",
1839
+ text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
1466
1840
  id: getId(),
1467
1841
  },
1468
1842
  ]);
@@ -1470,13 +1844,18 @@ export function App(props) {
1470
1844
  }
1471
1845
  // Handle /plans — open plan pane
1472
1846
  if (trimmed === "/plans") {
1473
- if (props.resetUI && props.sessionStore) {
1847
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
1474
1848
  props.sessionStore.overlay = "plan";
1475
1849
  props.sessionStore.planAutoExpand = false;
1476
1850
  props.resetUI();
1477
1851
  }
1478
1852
  else {
1479
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1853
+ if (props.sessionStore) {
1854
+ props.sessionStore.overlay = "plan";
1855
+ props.sessionStore.planAutoExpand = false;
1856
+ if (agentLoop.isRunning)
1857
+ props.sessionStore.pendingResetUI = true;
1858
+ }
1480
1859
  setPlanAutoExpand(false);
1481
1860
  setOverlay("plan");
1482
1861
  }
@@ -1518,8 +1897,8 @@ export function App(props) {
1518
1897
  setLiveItems((prev) => [
1519
1898
  ...prev,
1520
1899
  isAbort
1521
- ? { kind: "info", text: "Request was stopped.", id: getId() }
1522
- : { kind: "error", message: msg, id: getId() },
1900
+ ? { kind: "stopped", text: "Request was stopped.", id: getId() }
1901
+ : toErrorItem(err, getId()),
1523
1902
  ]);
1524
1903
  }
1525
1904
  // Reload custom commands in case a setup command created new ones
@@ -1642,11 +2021,19 @@ export function App(props) {
1642
2021
  setLiveItems((prev) => [
1643
2022
  ...prev,
1644
2023
  isAbort
1645
- ? { kind: "info", text: "Request was stopped.", id: getId() }
1646
- : { kind: "error", message: msg, id: getId() },
2024
+ ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2025
+ : toErrorItem(err, getId()),
1647
2026
  ]);
1648
2027
  }
1649
- }, [agentLoop, props.onSlashCommand, compactConversation]);
2028
+ }, [
2029
+ agentLoop,
2030
+ props.onSlashCommand,
2031
+ compactConversation,
2032
+ rebuildSystemPrompt,
2033
+ replaceSystemPrompt,
2034
+ refreshRepoMap,
2035
+ stripRepoMapMessages,
2036
+ ]);
1650
2037
  const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
1651
2038
  const handleAbort = useCallback(() => {
1652
2039
  if (agentLoop.isRunning) {
@@ -1663,7 +2050,7 @@ export function App(props) {
1663
2050
  log("INFO", "thinking", `Thinking ${next ? "enabled" : "disabled"}`);
1664
2051
  setLiveItems((items) => [
1665
2052
  ...items,
1666
- { kind: "info", text: `Thinking ${next ? "on" : "off"}`, id: getId() },
2053
+ { kind: "thinking_transition", active: next, id: getId() },
1667
2054
  ]);
1668
2055
  if (props.settingsFile) {
1669
2056
  const sm = new SettingsManager(props.settingsFile);
@@ -1680,6 +2067,10 @@ export function App(props) {
1680
2067
  const newProvider = value.slice(0, colonIdx);
1681
2068
  const newModelId = value.slice(colonIdx + 1);
1682
2069
  log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
2070
+ const rebuildPromptWithTools = (tools) => {
2071
+ currentToolsRef.current = tools;
2072
+ void replaceSystemPrompt({ tools });
2073
+ };
1683
2074
  // Handle provider-specific tool changes when provider changes
1684
2075
  setCurrentProvider((prevProvider) => {
1685
2076
  if (newProvider !== prevProvider) {
@@ -1687,15 +2078,17 @@ export function App(props) {
1687
2078
  // Anthropic has native server-side web search; all other providers need the client tool.
1688
2079
  setCurrentTools((prev) => {
1689
2080
  const hasWebSearch = prev.some((t) => t.name === "web_search");
2081
+ let next = prev;
1690
2082
  if (newProvider === "anthropic" && hasWebSearch) {
1691
2083
  // Switching TO anthropic — remove client-side web_search (server-side handles it)
1692
- return prev.filter((t) => t.name !== "web_search");
2084
+ next = prev.filter((t) => t.name !== "web_search");
1693
2085
  }
1694
2086
  else if (newProvider !== "anthropic" && !hasWebSearch) {
1695
2087
  // Switching FROM anthropic — add client-side web_search
1696
- return [...prev, createWebSearchTool()];
2088
+ next = [...prev, createWebSearchTool()];
1697
2089
  }
1698
- return prev;
2090
+ rebuildPromptWithTools(next);
2091
+ return next;
1699
2092
  });
1700
2093
  // Reconnect MCP servers
1701
2094
  if (props.mcpManager) {
@@ -1718,16 +2111,21 @@ export function App(props) {
1718
2111
  }
1719
2112
  try {
1720
2113
  const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
1721
- setCurrentTools((prev) => [
1722
- ...prev.filter((t) => !t.name.startsWith("mcp__")),
1723
- ...mcpTools,
1724
- ]);
2114
+ setCurrentTools((prev) => {
2115
+ const next = [...prev.filter((t) => !t.name.startsWith("mcp__")), ...mcpTools];
2116
+ rebuildPromptWithTools(next);
2117
+ return next;
2118
+ });
1725
2119
  log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
1726
2120
  }
1727
2121
  catch (err) {
1728
2122
  log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
1729
2123
  // Still remove old MCP tools even if reconnection fails
1730
- setCurrentTools((prev) => prev.filter((t) => !t.name.startsWith("mcp__")));
2124
+ setCurrentTools((prev) => {
2125
+ const next = prev.filter((t) => !t.name.startsWith("mcp__"));
2126
+ rebuildPromptWithTools(next);
2127
+ return next;
2128
+ });
1731
2129
  }
1732
2130
  })();
1733
2131
  }
@@ -1739,7 +2137,7 @@ export function App(props) {
1739
2137
  const displayName = modelInfo?.name ?? newModelId;
1740
2138
  setLiveItems((prev) => [
1741
2139
  ...prev,
1742
- { kind: "info", text: `Switched to ${displayName}`, id: getId() },
2140
+ { kind: "model_transition", modelName: displayName, id: getId() },
1743
2141
  ]);
1744
2142
  // Persist model selection for next CLI launch
1745
2143
  if (props.settingsFile) {
@@ -1760,10 +2158,7 @@ export function App(props) {
1760
2158
  const sm = new SettingsManager(props.settingsFile);
1761
2159
  sm.load().then(() => sm.set("theme", name));
1762
2160
  }
1763
- setLiveItems((prev) => [
1764
- ...prev,
1765
- { kind: "info", text: `Theme switched to: ${name}`, id: getId() },
1766
- ]);
2161
+ setLiveItems((prev) => [...prev, { kind: "theme_transition", themeName: name, id: getId() }]);
1767
2162
  }, [switchTheme, props.settingsFile]);
1768
2163
  // All available slash commands for the command palette — ordered by how
1769
2164
  // commonly they're used and grouped by purpose; /quit stays dead last.
@@ -1779,6 +2174,8 @@ export function App(props) {
1779
2174
  "research",
1780
2175
  "scan",
1781
2176
  "verify",
2177
+ "expand",
2178
+ "bullet-proof",
1782
2179
  "simplify",
1783
2180
  "compare",
1784
2181
  "batch",
@@ -1823,21 +2220,28 @@ export function App(props) {
1823
2220
  return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
1824
2221
  case "task":
1825
2222
  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));
2223
+ case "style_pack": {
2224
+ const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
2225
+ const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
2226
+ return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: headerLabel })] }), _jsx(Text, { color: theme.text, bold: true, wrap: "wrap", children: names.join(", ") }), item.showSetupHint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit this project against the active pack(s)" })] }) }))] }, item.id));
2227
+ }
2228
+ case "setup_hint":
2229
+ 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));
1826
2230
  case "assistant":
1827
- return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, showThinking: props.showThinking, planMode: item.planMode }, item.id));
2231
+ return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
1828
2232
  case "tool_start":
1829
- return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput }, item.id));
2233
+ return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
1830
2234
  case "tool_done":
1831
2235
  return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
1832
2236
  case "tool_group":
1833
2237
  return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
1834
2238
  case "server_tool_start":
1835
- return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt }, item.id));
2239
+ return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
1836
2240
  case "server_tool_done":
1837
2241
  return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
1838
2242
  case "error": {
1839
- const providerHint = getProviderErrorHint(item.message);
1840
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { color: theme.error, wrap: "wrap", children: ["✗ ", item.message] }), providerHint && (_jsxs(Text, { color: theme.textDim, wrap: "wrap", children: [" Hint: ", providerHint] }))] }, item.id));
2243
+ const showMessage = item.message && item.message !== item.headline;
2244
+ 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));
1841
2245
  }
1842
2246
  case "info":
1843
2247
  return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
@@ -1845,6 +2249,34 @@ export function App(props) {
1845
2249
  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));
1846
2250
  case "plan_transition":
1847
2251
  return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
2252
+ case "thinking_transition": {
2253
+ const glyphColor = item.active ? THINKING_BORDER_COLORS[0] : theme.textDim;
2254
+ 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));
2255
+ }
2256
+ case "model_transition": {
2257
+ const glyphColor = THINKING_BORDER_COLORS[0];
2258
+ 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));
2259
+ }
2260
+ case "theme_transition": {
2261
+ const glyphColor = THINKING_BORDER_COLORS[0];
2262
+ 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));
2263
+ }
2264
+ case "plan_event": {
2265
+ // Plan-domain status changes (approve / reject / dismiss). Uses
2266
+ // theme.planPrimary to match the existing plan_transition family,
2267
+ // distinct from the model/thinking gradient.
2268
+ const label = item.event === "approved"
2269
+ ? "Plan approved"
2270
+ : item.event === "rejected"
2271
+ ? "Plan rejected"
2272
+ : "Plan dismissed";
2273
+ 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));
2274
+ }
2275
+ case "stopped":
2276
+ // Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
2277
+ // Muted dim treatment — this is an ack, not a state change worth a
2278
+ // gradient. Glyph `⊘` reads as "stop" without being alarming.
2279
+ return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.textDim, bold: true, children: ["⊘ ", item.text] }) }, item.id));
1848
2280
  case "step_done":
1849
2281
  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));
1850
2282
  case "queued":
@@ -1852,7 +2284,7 @@ export function App(props) {
1852
2284
  ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
1853
2285
  : ""] })] }, item.id));
1854
2286
  case "compacting":
1855
- return _jsx(CompactionSpinner, {}, item.id);
2287
+ return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
1856
2288
  case "compacted":
1857
2289
  return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
1858
2290
  case "duration":
@@ -1928,8 +2360,8 @@ export function App(props) {
1928
2360
  setLiveItems((prev) => [
1929
2361
  ...prev,
1930
2362
  isAbort
1931
- ? { kind: "info", text: "Request was stopped.", id: getId() }
1932
- : { kind: "error", message: msg, id: getId() },
2363
+ ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2364
+ : toErrorItem(err, getId()),
1933
2365
  ]);
1934
2366
  setRunAllTasks(false);
1935
2367
  }
@@ -1947,7 +2379,9 @@ export function App(props) {
1947
2379
  startTaskRef.current = startTask;
1948
2380
  useEffect(() => {
1949
2381
  runAllTasksRef.current = runAllTasks;
1950
- }, [runAllTasks]);
2382
+ if (props.sessionStore)
2383
+ props.sessionStore.runAllTasks = runAllTasks;
2384
+ }, [runAllTasks, props.sessionStore]);
1951
2385
  const startPixelFix = useCallback((errorId) => {
1952
2386
  void (async () => {
1953
2387
  try {
@@ -1969,11 +2403,35 @@ export function App(props) {
1969
2403
  log("WARN", "pixel", `chdir failed: ${err.message}`);
1970
2404
  }
1971
2405
  cwdRef.current = prep.projectPath;
2406
+ repoMapDirtyRef.current = true;
2407
+ repoMapMarkdownRef.current = "";
2408
+ repoMapSnapshotRef.current = undefined;
2409
+ repoMapChangedCountRef.current = 0;
2410
+ repoMapCacheRef.current = createRepoMapCache();
2411
+ props.repoMapChangedFilesRef?.current.clear();
2412
+ props.repoMapReadFilesRef?.current.clear();
1972
2413
  setDisplayedCwd(prep.projectPath);
2414
+ let toolsForPixelFix = currentToolsRef.current;
1973
2415
  if (props.rebuildToolsForCwd) {
1974
- setCurrentTools(props.rebuildToolsForCwd(prep.projectPath));
2416
+ toolsForPixelFix = props.rebuildToolsForCwd(prep.projectPath);
2417
+ currentToolsRef.current = toolsForPixelFix;
2418
+ setCurrentTools(toolsForPixelFix);
1975
2419
  }
1976
- const newSystemPrompt = await buildSystemPrompt(prep.projectPath, props.skills, false, undefined);
2420
+ // Pixel-fix swaps the project root reset injected packs so the
2421
+ // new project re-detects from scratch on the next tool call. Also
2422
+ // reset the setup-hint flag so the new project's first badge re-
2423
+ // surfaces the tip (different project, may need the reminder).
2424
+ injectedLanguagesRef.current = new Set();
2425
+ setupHintShownRef.current = false;
2426
+ const detectedForPixelFix = detectLanguages(prep.projectPath);
2427
+ injectedLanguagesRef.current = detectedForPixelFix;
2428
+ const newSystemPrompt = await rebuildSystemPrompt({
2429
+ cwd: prep.projectPath,
2430
+ planMode: false,
2431
+ clearApprovedPlan: true,
2432
+ activeLanguages: detectedForPixelFix,
2433
+ tools: toolsForPixelFix,
2434
+ });
1977
2435
  // Now that the cwd swap is committed, reset chat. Doing this BEFORE
1978
2436
  // the chdir would print a banner with the old cwd, then bumping
1979
2437
  // staticKey would print a second banner with the new cwd — leaving
@@ -2010,30 +2468,37 @@ export function App(props) {
2010
2468
  log("ERROR", "pixel", msg);
2011
2469
  currentPixelFixRef.current = null;
2012
2470
  setRunAllPixel(false);
2013
- setLiveItems((prev) => [...prev, { kind: "error", message: msg, id: getId() }]);
2471
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2014
2472
  }
2015
2473
  })();
2016
2474
  }, [props.cwd, stdout, agentLoop, currentProvider, currentModel]);
2017
2475
  startPixelFixRef.current = startPixelFix;
2018
- const [runAllPixel, setRunAllPixel] = useState(false);
2476
+ // Seed from sessionStore so "Fix All" chaining survives a deferred
2477
+ // resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
2478
+ const [runAllPixel, setRunAllPixel] = useState(props.sessionStore?.runAllPixel ?? false);
2019
2479
  useEffect(() => {
2020
2480
  runAllPixelRef.current = runAllPixel;
2021
- }, [runAllPixel]);
2481
+ if (props.sessionStore)
2482
+ props.sessionStore.runAllPixel = runAllPixel;
2483
+ }, [runAllPixel, props.sessionStore]);
2022
2484
  const isTaskView = overlay === "tasks";
2023
2485
  const isSkillsView = overlay === "skills";
2024
2486
  const isPlanView = overlay === "plan";
2025
2487
  const isEyesView = overlay === "eyes";
2026
2488
  const isPixelView = overlay === "pixel";
2027
2489
  const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
2028
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: isOverlayView ? [] : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, `${resizeKey}-${staticKey}`), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
2029
- if (props.resetUI && props.sessionStore) {
2490
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: isOverlayView && !agentLoop.isRunning ? [] : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, `${resizeKey}-${staticKey}`), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
2491
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2030
2492
  props.sessionStore.overlay = null;
2031
2493
  props.resetUI();
2032
2494
  }
2033
2495
  else {
2034
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2496
+ if (props.sessionStore) {
2497
+ props.sessionStore.overlay = null;
2498
+ if (agentLoop.isRunning)
2499
+ props.sessionStore.pendingResetUI = true;
2500
+ }
2035
2501
  setTaskCount(getTaskCount(props.cwd));
2036
- setStaticKey((k) => k + 1);
2037
2502
  setOverlay(null);
2038
2503
  }
2039
2504
  }, onWorkOnTask: (title, prompt, taskId) => {
@@ -2048,13 +2513,16 @@ export function App(props) {
2048
2513
  startTask(next.title, next.prompt, next.id);
2049
2514
  }
2050
2515
  } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
2051
- if (props.resetUI && props.sessionStore) {
2516
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2052
2517
  props.sessionStore.overlay = null;
2053
2518
  props.resetUI();
2054
2519
  }
2055
2520
  else {
2056
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2057
- setStaticKey((k) => k + 1);
2521
+ if (props.sessionStore) {
2522
+ props.sessionStore.overlay = null;
2523
+ if (agentLoop.isRunning)
2524
+ props.sessionStore.pendingResetUI = true;
2525
+ }
2058
2526
  setOverlay(null);
2059
2527
  }
2060
2528
  }, onFixOne: (entry) => {
@@ -2068,38 +2536,48 @@ export function App(props) {
2068
2536
  setRunAllPixel(true);
2069
2537
  startPixelFix(first.errorId);
2070
2538
  } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
2071
- if (props.resetUI && props.sessionStore) {
2539
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2072
2540
  props.sessionStore.overlay = null;
2073
2541
  props.resetUI();
2074
2542
  }
2075
2543
  else {
2076
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2077
- setStaticKey((k) => k + 1);
2544
+ if (props.sessionStore) {
2545
+ props.sessionStore.overlay = null;
2546
+ if (agentLoop.isRunning)
2547
+ props.sessionStore.pendingResetUI = true;
2548
+ }
2078
2549
  setOverlay(null);
2079
2550
  }
2080
2551
  } })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
2081
- if (props.resetUI && props.sessionStore) {
2552
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2082
2553
  props.sessionStore.overlay = null;
2083
2554
  props.resetUI();
2084
2555
  }
2085
2556
  else {
2086
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2557
+ if (props.sessionStore) {
2558
+ props.sessionStore.overlay = null;
2559
+ if (agentLoop.isRunning)
2560
+ props.sessionStore.pendingResetUI = true;
2561
+ }
2087
2562
  setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
2088
- setStaticKey((k) => k + 1);
2089
2563
  setOverlay(null);
2090
2564
  }
2091
2565
  }, onQueueMessage: (msg) => {
2092
2566
  agentLoop.queueMessage(msg);
2093
2567
  } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
2094
2568
  planOverlayPendingRef.current = false;
2095
- if (props.resetUI && props.sessionStore) {
2569
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2096
2570
  props.sessionStore.overlay = null;
2097
2571
  props.sessionStore.planAutoExpand = false;
2098
2572
  props.resetUI();
2099
2573
  }
2100
2574
  else {
2101
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2102
- setStaticKey((k) => k + 1);
2575
+ if (props.sessionStore) {
2576
+ props.sessionStore.overlay = null;
2577
+ props.sessionStore.planAutoExpand = false;
2578
+ if (agentLoop.isRunning)
2579
+ props.sessionStore.pendingResetUI = true;
2580
+ }
2103
2581
  setPlanAutoExpand(false);
2104
2582
  setOverlay(null);
2105
2583
  }
@@ -2115,7 +2593,10 @@ export function App(props) {
2115
2593
  const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
2116
2594
  const steps = extractPlanSteps(planContent);
2117
2595
  // Build the new system prompt with the approved plan baked in.
2118
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, false, planPath);
2596
+ const newPrompt = await rebuildSystemPrompt({
2597
+ planMode: false,
2598
+ approvedPlanPath: planPath,
2599
+ });
2119
2600
  // Create a new session file BEFORE remount so the new tree
2120
2601
  // picks it up via sessionStore.sessionPath.
2121
2602
  let newSessionPath;
@@ -2137,7 +2618,7 @@ export function App(props) {
2137
2618
  sessionPath: newSessionPath,
2138
2619
  pendingAction: {
2139
2620
  prompt: "The plan has been approved. Implement it now, following each step in order.",
2140
- infoText: "Plan approved — starting fresh session for implementation",
2621
+ planEvent: { event: "approved" },
2141
2622
  },
2142
2623
  });
2143
2624
  return;
@@ -2170,7 +2651,7 @@ export function App(props) {
2170
2651
  catch (err) {
2171
2652
  const errMsg = err instanceof Error ? err.message : String(err);
2172
2653
  log("ERROR", "error", errMsg);
2173
- setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
2654
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2174
2655
  }
2175
2656
  })();
2176
2657
  }, onReject: (planPath, feedback) => {
@@ -2185,7 +2666,7 @@ export function App(props) {
2185
2666
  props.resetUI({
2186
2667
  pendingAction: {
2187
2668
  prompt: rejectionMsg,
2188
- infoText: `Plan rejected "${feedback}"`,
2669
+ planEvent: { event: "rejected", detail: feedback },
2189
2670
  },
2190
2671
  });
2191
2672
  return;
@@ -2202,36 +2683,52 @@ export function App(props) {
2202
2683
  void agentLoop.run(rejectionMsg).catch((err) => {
2203
2684
  const errMsg = err instanceof Error ? err.message : String(err);
2204
2685
  log("ERROR", "error", errMsg);
2205
- setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
2686
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2206
2687
  });
2207
- } })) : (_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, showThinking: props.showThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking"
2208
- ? THINKING_BORDER_COLORS[thinkingBorderFrame]
2209
- : "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, 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 }) })) : 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 &&
2688
+ } })) : (_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 &&
2210
2689
  !agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
2211
- if (props.resetUI && props.sessionStore) {
2690
+ // While the agent is running, skip the screen-clear + staticKey
2691
+ // bump that would otherwise wipe the chat history from scrollback.
2692
+ // Just flip the overlay state — Ink's log-update handles the
2693
+ // live-area transition (chat input → TaskOverlay) natively, and
2694
+ // the chat history above stays in scrollback. When the overlay
2695
+ // closes, the history is still there (banner included).
2696
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2212
2697
  props.sessionStore.overlay = "tasks";
2213
2698
  props.resetUI();
2214
2699
  }
2215
2700
  else {
2216
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2701
+ if (props.sessionStore) {
2702
+ props.sessionStore.overlay = "tasks";
2703
+ if (agentLoop.isRunning)
2704
+ props.sessionStore.pendingResetUI = true;
2705
+ }
2217
2706
  setOverlay("tasks");
2218
2707
  }
2219
2708
  }, onToggleSkills: () => {
2220
- if (props.resetUI && props.sessionStore) {
2709
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2221
2710
  props.sessionStore.overlay = "skills";
2222
2711
  props.resetUI();
2223
2712
  }
2224
2713
  else {
2225
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2714
+ if (props.sessionStore) {
2715
+ props.sessionStore.overlay = "skills";
2716
+ if (agentLoop.isRunning)
2717
+ props.sessionStore.pendingResetUI = true;
2718
+ }
2226
2719
  setOverlay("skills");
2227
2720
  }
2228
2721
  }, onTogglePixel: () => {
2229
- if (props.resetUI && props.sessionStore) {
2722
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2230
2723
  props.sessionStore.overlay = "pixel";
2231
2724
  props.resetUI();
2232
2725
  }
2233
2726
  else {
2234
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2727
+ if (props.sessionStore) {
2728
+ props.sessionStore.overlay = "pixel";
2729
+ if (agentLoop.isRunning)
2730
+ props.sessionStore.pendingResetUI = true;
2731
+ }
2235
2732
  setOverlay("pixel");
2236
2733
  }
2237
2734
  }, onTogglePlanMode: () => {
@@ -2247,6 +2744,13 @@ export function App(props) {
2247
2744
  id: getId(),
2248
2745
  },
2249
2746
  ]);
2250
- }, 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 })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, cwd: displayedCwd, gitBranch: gitBranch, thinkingEnabled: thinkingEnabled, planMode: planMode, exitPending: exitPending })), buddyEnabled && _jsx(Buddy, { phase: agentLoop.activityPhase }), (bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) || updatePending) && (_jsxs(Box, { children: [bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate })), eyesCount !== undefined && eyesCount > 0 && (_jsx(Box, { paddingLeft: bgTasks.length > 0 ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance EZ Coder` }) })), updatePending && (_jsx(Box, { paddingLeft: bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
2747
+ }, 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 })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending })), (bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) || updatePending) && (_jsxs(Box, { children: [bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate })), eyesCount !== undefined && eyesCount > 0 && (_jsx(Box, { paddingLeft: bgTasks.length > 0 ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance EZ Coder` }) })), updatePending && (_jsx(Box, { paddingLeft: bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
2748
+ }
2749
+ function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
2750
+ const status = enabled ? "on" : "off";
2751
+ const prefix = refreshed
2752
+ ? `Dynamic repo map refreshed · injection: ${status}`
2753
+ : `Dynamic repo map · injection: ${status}`;
2754
+ return `${prefix}\n\n${markdown}`;
2251
2755
  }
2252
2756
  //# sourceMappingURL=App.js.map