@prestyj/cli 4.3.164 → 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 (258) hide show
  1. package/dist/cli.js +106 -27
  2. package/dist/cli.js.map +1 -1
  3. package/dist/core/agent-session.d.ts +50 -1
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +175 -20
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/agents.d.ts +6 -1
  8. package/dist/core/agents.d.ts.map +1 -1
  9. package/dist/core/agents.js +112 -3
  10. package/dist/core/agents.js.map +1 -1
  11. package/dist/core/auth-storage.d.ts.map +1 -1
  12. package/dist/core/auth-storage.js +20 -1
  13. package/dist/core/auth-storage.js.map +1 -1
  14. package/dist/core/claude-code-version.d.ts +10 -0
  15. package/dist/core/claude-code-version.d.ts.map +1 -0
  16. package/dist/core/claude-code-version.js +106 -0
  17. package/dist/core/claude-code-version.js.map +1 -0
  18. package/dist/core/compaction/compactor.d.ts +13 -0
  19. package/dist/core/compaction/compactor.d.ts.map +1 -1
  20. package/dist/core/compaction/compactor.js +20 -1
  21. package/dist/core/compaction/compactor.js.map +1 -1
  22. package/dist/core/compaction/compactor.test.js +12 -1
  23. package/dist/core/compaction/compactor.test.js.map +1 -1
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/index.js +1 -1
  27. package/dist/core/index.js.map +1 -1
  28. package/dist/core/model-registry.d.ts +30 -2
  29. package/dist/core/model-registry.d.ts.map +1 -1
  30. package/dist/core/model-registry.js +41 -26
  31. package/dist/core/model-registry.js.map +1 -1
  32. package/dist/core/model-registry.test.d.ts +2 -0
  33. package/dist/core/model-registry.test.d.ts.map +1 -0
  34. package/dist/core/model-registry.test.js +19 -0
  35. package/dist/core/model-registry.test.js.map +1 -0
  36. package/dist/core/oauth/anthropic.d.ts.map +1 -1
  37. package/dist/core/oauth/anthropic.js +64 -42
  38. package/dist/core/oauth/anthropic.js.map +1 -1
  39. package/dist/core/oauth/openai.js +1 -1
  40. package/dist/core/oauth/openai.js.map +1 -1
  41. package/dist/core/prompt-commands.d.ts.map +1 -1
  42. package/dist/core/prompt-commands.js +480 -100
  43. package/dist/core/prompt-commands.js.map +1 -1
  44. package/dist/core/prompt-commands.test.d.ts +2 -0
  45. package/dist/core/prompt-commands.test.d.ts.map +1 -0
  46. package/dist/core/prompt-commands.test.js +33 -0
  47. package/dist/core/prompt-commands.test.js.map +1 -0
  48. package/dist/core/repomap-context.d.ts +11 -0
  49. package/dist/core/repomap-context.d.ts.map +1 -0
  50. package/dist/core/repomap-context.js +68 -0
  51. package/dist/core/repomap-context.js.map +1 -0
  52. package/dist/core/repomap-context.test.d.ts +2 -0
  53. package/dist/core/repomap-context.test.d.ts.map +1 -0
  54. package/dist/core/repomap-context.test.js +47 -0
  55. package/dist/core/repomap-context.test.js.map +1 -0
  56. package/dist/core/repomap.d.ts +74 -0
  57. package/dist/core/repomap.d.ts.map +1 -0
  58. package/dist/core/repomap.js +897 -0
  59. package/dist/core/repomap.js.map +1 -0
  60. package/dist/core/repomap.test.d.ts +2 -0
  61. package/dist/core/repomap.test.d.ts.map +1 -0
  62. package/dist/core/repomap.test.js +444 -0
  63. package/dist/core/repomap.test.js.map +1 -0
  64. package/dist/core/session-compaction.d.ts +15 -0
  65. package/dist/core/session-compaction.d.ts.map +1 -0
  66. package/dist/core/session-compaction.js +31 -0
  67. package/dist/core/session-compaction.js.map +1 -0
  68. package/dist/core/session-compaction.test.d.ts +2 -0
  69. package/dist/core/session-compaction.test.d.ts.map +1 -0
  70. package/dist/core/session-compaction.test.js +70 -0
  71. package/dist/core/session-compaction.test.js.map +1 -0
  72. package/dist/core/settings-manager.d.ts +3 -5
  73. package/dist/core/settings-manager.d.ts.map +1 -1
  74. package/dist/core/settings-manager.js +6 -5
  75. package/dist/core/settings-manager.js.map +1 -1
  76. package/dist/core/slash-commands.d.ts +2 -0
  77. package/dist/core/slash-commands.d.ts.map +1 -1
  78. package/dist/core/slash-commands.js +15 -14
  79. package/dist/core/slash-commands.js.map +1 -1
  80. package/dist/core/style-packs/index.d.ts.map +1 -1
  81. package/dist/core/style-packs/index.js +8 -9
  82. package/dist/core/style-packs/index.js.map +1 -1
  83. package/dist/index.d.ts +2 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +1 -1
  86. package/dist/index.js.map +1 -1
  87. package/dist/interactive.d.ts.map +1 -1
  88. package/dist/interactive.js +12 -6
  89. package/dist/interactive.js.map +1 -1
  90. package/dist/modes/json-mode.d.ts +7 -0
  91. package/dist/modes/json-mode.d.ts.map +1 -1
  92. package/dist/modes/json-mode.js +9 -1
  93. package/dist/modes/json-mode.js.map +1 -1
  94. package/dist/system-prompt.d.ts.map +1 -1
  95. package/dist/system-prompt.js +105 -108
  96. package/dist/system-prompt.js.map +1 -1
  97. package/dist/system-prompt.test.d.ts +2 -0
  98. package/dist/system-prompt.test.d.ts.map +1 -0
  99. package/dist/system-prompt.test.js +107 -0
  100. package/dist/system-prompt.test.js.map +1 -0
  101. package/dist/tools/bash.d.ts.map +1 -1
  102. package/dist/tools/bash.js +1 -0
  103. package/dist/tools/bash.js.map +1 -1
  104. package/dist/tools/edit-diff.d.ts +66 -1
  105. package/dist/tools/edit-diff.d.ts.map +1 -1
  106. package/dist/tools/edit-diff.js +208 -10
  107. package/dist/tools/edit-diff.js.map +1 -1
  108. package/dist/tools/edit-diff.test.js +175 -15
  109. package/dist/tools/edit-diff.test.js.map +1 -1
  110. package/dist/tools/edit.d.ts +3 -1
  111. package/dist/tools/edit.d.ts.map +1 -1
  112. package/dist/tools/edit.js +190 -44
  113. package/dist/tools/edit.js.map +1 -1
  114. package/dist/tools/edit.test.js +392 -3
  115. package/dist/tools/edit.test.js.map +1 -1
  116. package/dist/tools/enter-plan.d.ts.map +1 -1
  117. package/dist/tools/enter-plan.js +1 -0
  118. package/dist/tools/enter-plan.js.map +1 -1
  119. package/dist/tools/exit-plan.d.ts.map +1 -1
  120. package/dist/tools/exit-plan.js +1 -0
  121. package/dist/tools/exit-plan.js.map +1 -1
  122. package/dist/tools/index.d.ts +13 -0
  123. package/dist/tools/index.d.ts.map +1 -1
  124. package/dist/tools/index.js +4 -4
  125. package/dist/tools/index.js.map +1 -1
  126. package/dist/tools/prompt-hints.d.ts.map +1 -1
  127. package/dist/tools/prompt-hints.js +6 -4
  128. package/dist/tools/prompt-hints.js.map +1 -1
  129. package/dist/tools/read.d.ts +1 -1
  130. package/dist/tools/read.d.ts.map +1 -1
  131. package/dist/tools/read.js +2 -1
  132. package/dist/tools/read.js.map +1 -1
  133. package/dist/tools/subagent.d.ts +1 -1
  134. package/dist/tools/subagent.d.ts.map +1 -1
  135. package/dist/tools/subagent.js +55 -2
  136. package/dist/tools/subagent.js.map +1 -1
  137. package/dist/tools/tasks.d.ts.map +1 -1
  138. package/dist/tools/tasks.js +1 -0
  139. package/dist/tools/tasks.js.map +1 -1
  140. package/dist/tools/web-fetch.d.ts +1 -0
  141. package/dist/tools/web-fetch.d.ts.map +1 -1
  142. package/dist/tools/web-fetch.js +96 -6
  143. package/dist/tools/web-fetch.js.map +1 -1
  144. package/dist/tools/web-fetch.test.d.ts +2 -0
  145. package/dist/tools/web-fetch.test.d.ts.map +1 -0
  146. package/dist/tools/web-fetch.test.js +90 -0
  147. package/dist/tools/web-fetch.test.js.map +1 -0
  148. package/dist/tools/web-search.d.ts +1 -0
  149. package/dist/tools/web-search.d.ts.map +1 -1
  150. package/dist/tools/web-search.js +167 -27
  151. package/dist/tools/web-search.js.map +1 -1
  152. package/dist/tools/web-search.test.d.ts +2 -0
  153. package/dist/tools/web-search.test.d.ts.map +1 -0
  154. package/dist/tools/web-search.test.js +119 -0
  155. package/dist/tools/web-search.test.js.map +1 -0
  156. package/dist/tools/write.d.ts +1 -1
  157. package/dist/tools/write.d.ts.map +1 -1
  158. package/dist/tools/write.js +3 -1
  159. package/dist/tools/write.js.map +1 -1
  160. package/dist/tools/write.test.js +23 -0
  161. package/dist/tools/write.test.js.map +1 -1
  162. package/dist/ui/App.d.ts +54 -5
  163. package/dist/ui/App.d.ts.map +1 -1
  164. package/dist/ui/App.js +533 -241
  165. package/dist/ui/App.js.map +1 -1
  166. package/dist/ui/activity-phrases.d.ts +2 -1
  167. package/dist/ui/activity-phrases.d.ts.map +1 -1
  168. package/dist/ui/activity-phrases.js +11 -1
  169. package/dist/ui/activity-phrases.js.map +1 -1
  170. package/dist/ui/components/ActivityIndicator.d.ts +15 -1
  171. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  172. package/dist/ui/components/ActivityIndicator.js +143 -44
  173. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  174. package/dist/ui/components/ActivityIndicator.test.d.ts +2 -0
  175. package/dist/ui/components/ActivityIndicator.test.d.ts.map +1 -0
  176. package/dist/ui/components/ActivityIndicator.test.js +40 -0
  177. package/dist/ui/components/ActivityIndicator.test.js.map +1 -0
  178. package/dist/ui/components/AnimationContext.d.ts +8 -2
  179. package/dist/ui/components/AnimationContext.d.ts.map +1 -1
  180. package/dist/ui/components/AnimationContext.js +94 -16
  181. package/dist/ui/components/AnimationContext.js.map +1 -1
  182. package/dist/ui/components/AssistantMessage.js +1 -1
  183. package/dist/ui/components/AssistantMessage.js.map +1 -1
  184. package/dist/ui/components/CompactionNotice.d.ts +5 -1
  185. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  186. package/dist/ui/components/CompactionNotice.js +4 -6
  187. package/dist/ui/components/CompactionNotice.js.map +1 -1
  188. package/dist/ui/components/Footer.d.ts +10 -2
  189. package/dist/ui/components/Footer.d.ts.map +1 -1
  190. package/dist/ui/components/Footer.js +45 -8
  191. package/dist/ui/components/Footer.js.map +1 -1
  192. package/dist/ui/components/InputArea.d.ts.map +1 -1
  193. package/dist/ui/components/InputArea.js +76 -28
  194. package/dist/ui/components/InputArea.js.map +1 -1
  195. package/dist/ui/components/ServerToolExecution.d.ts +1 -0
  196. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  197. package/dist/ui/components/ServerToolExecution.js +21 -2
  198. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  199. package/dist/ui/components/Spinner.d.ts +3 -1
  200. package/dist/ui/components/Spinner.d.ts.map +1 -1
  201. package/dist/ui/components/Spinner.js +10 -4
  202. package/dist/ui/components/Spinner.js.map +1 -1
  203. package/dist/ui/components/StreamingArea.js +1 -1
  204. package/dist/ui/components/StreamingArea.js.map +1 -1
  205. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  206. package/dist/ui/components/SubAgentPanel.js +3 -4
  207. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  208. package/dist/ui/components/ToolExecution.d.ts +2 -0
  209. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  210. package/dist/ui/components/ToolExecution.js +24 -5
  211. package/dist/ui/components/ToolExecution.js.map +1 -1
  212. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  213. package/dist/ui/components/ToolGroupExecution.js +23 -2
  214. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  215. package/dist/ui/components/ToolUseLoader.d.ts +4 -2
  216. package/dist/ui/components/ToolUseLoader.d.ts.map +1 -1
  217. package/dist/ui/components/ToolUseLoader.js +3 -3
  218. package/dist/ui/components/ToolUseLoader.js.map +1 -1
  219. package/dist/ui/components/index.d.ts +1 -2
  220. package/dist/ui/components/index.d.ts.map +1 -1
  221. package/dist/ui/components/index.js +1 -2
  222. package/dist/ui/components/index.js.map +1 -1
  223. package/dist/ui/hooks/useAgentLoop.d.ts +4 -1
  224. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  225. package/dist/ui/hooks/useAgentLoop.js +110 -13
  226. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  227. package/dist/ui/hooks/useBlink.d.ts.map +1 -1
  228. package/dist/ui/hooks/useBlink.js +3 -4
  229. package/dist/ui/hooks/useBlink.js.map +1 -1
  230. package/dist/ui/hooks/useTerminalSize.d.ts +12 -3
  231. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  232. package/dist/ui/hooks/useTerminalSize.js +16 -1
  233. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  234. package/dist/ui/pixel.js +1 -1
  235. package/dist/ui/render.d.ts +44 -5
  236. package/dist/ui/render.d.ts.map +1 -1
  237. package/dist/ui/render.js +56 -8
  238. package/dist/ui/render.js.map +1 -1
  239. package/dist/utils/error-handler.d.ts.map +1 -1
  240. package/dist/utils/error-handler.js +2 -0
  241. package/dist/utils/error-handler.js.map +1 -1
  242. package/package.json +10 -7
  243. package/dist/ui/buddy/Buddy.d.ts +0 -15
  244. package/dist/ui/buddy/Buddy.d.ts.map +0 -1
  245. package/dist/ui/buddy/Buddy.js +0 -118
  246. package/dist/ui/buddy/Buddy.js.map +0 -1
  247. package/dist/ui/buddy/gacha.d.ts +0 -8
  248. package/dist/ui/buddy/gacha.d.ts.map +0 -1
  249. package/dist/ui/buddy/gacha.js +0 -94
  250. package/dist/ui/buddy/gacha.js.map +0 -1
  251. package/dist/ui/buddy/species.d.ts +0 -52
  252. package/dist/ui/buddy/species.d.ts.map +0 -1
  253. package/dist/ui/buddy/species.js +0 -257
  254. package/dist/ui/buddy/species.js.map +0 -1
  255. package/dist/ui/components/ThinkingIndicator.d.ts +0 -6
  256. package/dist/ui/components/ThinkingIndicator.d.ts.map +0 -1
  257. package/dist/ui/components/ThinkingIndicator.js +0 -145
  258. 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,16 +35,16 @@ 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";
49
50
  import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
@@ -51,49 +52,47 @@ import { loadCustomCommands } from "../core/custom-commands.js";
51
52
  import { buildSystemPrompt } from "../system-prompt.js";
52
53
  import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
53
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";
54
57
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
55
58
  import { getMCPServers } from "../core/mcp/index.js";
56
59
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
57
- import { Buddy } from "./buddy/Buddy.js";
58
- // ── Provider Error Hints ──────────────────────────────────
59
- /** Detect provider-side errors and return a user-facing hint. */
60
- function getProviderErrorHint(message) {
61
- const lower = message.toLowerCase();
62
- if (lower.includes("overloaded") || lower.includes("engine_overloaded")) {
63
- return "This is a provider-side issue — their servers are under heavy load. Try again in a moment.";
64
- }
65
- if (lower.includes("insufficient balance") ||
66
- lower.includes("no resource package") ||
67
- lower.includes("quota exceeded") ||
68
- lower.includes("recharge")) {
69
- return "The provider reports a billing or quota issue. Check your account balance or resource package.";
70
- }
71
- if (lower.includes("rate limit") ||
72
- lower.includes("too many requests") ||
73
- lower.includes("429")) {
74
- return "You've hit the provider's rate limit. Wait a moment before retrying.";
75
- }
76
- if (lower.includes("502") || lower.includes("bad gateway")) {
77
- return "The provider returned a server error. This is not a ezcoder issue — try again shortly.";
78
- }
79
- if (lower.includes("503") || lower.includes("service unavailable")) {
80
- return "The provider's service is temporarily unavailable. Try again in a moment.";
81
- }
82
- if (lower.includes("timeout") || lower.includes("timed out")) {
83
- return "The request to the provider timed out. Their servers may be slow — try again.";
84
- }
85
- if (lower.includes("500") && lower.includes("internal server error")) {
86
- return "The provider experienced an internal error. This is not a ezcoder issue.";
87
- }
88
- if (lower.includes("does not recognize the requested model") ||
89
- (lower.includes("model") &&
90
- (lower.includes("not exist") || lower.includes("not found") || lower.includes("no access")))) {
91
- return "Use /model to switch to a different model, or check that your account has access to the requested model.";
92
- }
93
- 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
+ };
94
92
  }
95
93
  /** Tools that get aggregated into a single compact group when concurrent. */
96
94
  const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
95
+ const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
97
96
  /**
98
97
  * Cap memory by replacing old items with tiny tombstones. Ink's <Static>
99
98
  * tracks rendered items by array length — the array must never shrink, but
@@ -315,20 +314,22 @@ export function App(props) {
315
314
  // Items from the current/last turn — rendered in the live area so they stay visible
316
315
  const [liveItems, setLiveItems] = useState([]);
317
316
  // overlay seeded from sessionStore (lives across remount). Falls back to
318
- // props.initialOverlay (CLI launched with one), then null.
319
- 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));
320
324
  const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
321
325
  const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
322
326
  const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
323
- // Seed run-all flags from sessionStore so the chain survives the
324
- // unmount/remount that startTask / startPixelFix trigger via resetUI.
325
- // Without this, the remounted App has runAllTasks=false and onDone
326
- // never picks up the next task. (Regression from 0246c6d — unmount/
327
- // remount across all panes.)
328
- const [runAllTasks, setRunAllTasks] = useState(() => props.sessionStore?.runAllTasks ?? false);
329
- const runAllTasksRef = useRef(runAllTasks);
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);
330
331
  const startTaskRef = useRef(() => { });
331
- const runAllPixelRef = useRef(false);
332
+ const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
332
333
  const currentPixelFixRef = useRef(null);
333
334
  const startPixelFixRef = useRef(() => { });
334
335
  const cwdRef = useRef(props.cwd);
@@ -341,13 +342,41 @@ export function App(props) {
341
342
  const [currentModel, setCurrentModel] = useState(props.model);
342
343
  const [currentProvider, setCurrentProvider] = useState(props.provider);
343
344
  const [currentTools, setCurrentTools] = useState(props.tools);
345
+ const currentToolsRef = useRef(props.tools);
344
346
  const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
345
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());
346
354
  const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
347
355
  const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
348
356
  const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
349
357
  const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
350
- 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
+ })());
351
380
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
352
381
  const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
353
382
  const persistedIndexRef = useRef(messagesRef.current.length);
@@ -400,9 +429,9 @@ export function App(props) {
400
429
  }, [currentProvider, onRuntimeStateChange]);
401
430
  useEffect(() => {
402
431
  onRuntimeStateChange?.({
403
- thinking: thinkingEnabled ? (props.thinking ?? "medium") : undefined,
432
+ thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
404
433
  });
405
- }, [thinkingEnabled, props.thinking, onRuntimeStateChange]);
434
+ }, [thinkingEnabled, currentModel, onRuntimeStateChange]);
406
435
  // Mirror session state into renderApp's closure so resetUI() can re-seed
407
436
  // the conversation on remount. Each panel that previously did a bare ANSI
408
437
  // screen clear (overlay open/close, plan accept/reject, /clear, startTask)
@@ -432,6 +461,7 @@ export function App(props) {
432
461
  const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
433
462
  const activeAccountId = currentCreds?.accountId ?? props.accountId;
434
463
  const activeBaseUrl = currentCreds?.baseUrl ?? props.baseUrl;
464
+ const contextWindowOptions = useMemo(() => ({ provider: currentProvider, accountId: activeAccountId }), [currentProvider, activeAccountId]);
435
465
  // Load git branch — re-runs whenever the displayed cwd changes (e.g. when
436
466
  // a pixel fix moves the agent into a different project root).
437
467
  useEffect(() => {
@@ -453,13 +483,30 @@ export function App(props) {
453
483
  useEffect(() => {
454
484
  reloadCustomCommands();
455
485
  }, [reloadCustomCommands]);
486
+ useEffect(() => {
487
+ currentToolsRef.current = currentTools;
488
+ }, [currentTools]);
456
489
  // ── Plan mode wiring ─────────────────────────────────────
457
490
  // Sync planModeRef with React state
458
491
  useEffect(() => {
492
+ planModeStateRef.current = planMode;
459
493
  if (props.planModeRef) {
460
494
  props.planModeRef.current = planMode;
461
495
  }
462
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]);
463
510
  /**
464
511
  * Unified "apply detection result" pipeline. Called from three sites:
465
512
  * 1. Initial mount (existing project at startup).
@@ -502,10 +549,7 @@ export function App(props) {
502
549
  }
503
550
  injectedLanguagesRef.current = detected;
504
551
  try {
505
- const newPrompt = await buildSystemPrompt(cwd, props.skills, planMode, approvedPlanPathRef.current, undefined, detected);
506
- if (messagesRef.current[0]?.role === "system") {
507
- messagesRef.current[0] = { role: "system", content: newPrompt };
508
- }
552
+ await replaceSystemPrompt({ cwd, activeLanguages: detected });
509
553
  const verifyCmds = detectVerifyCommands(cwd, detected);
510
554
  const tag = source === "initial" ? "Initial style packs" : "Style pack(s) loaded";
511
555
  log("INFO", "language", `${tag}: ${added.join(", ")}`, {
@@ -549,16 +593,8 @@ export function App(props) {
549
593
  }, []);
550
594
  // Rebuild system prompt when plan mode changes
551
595
  useEffect(() => {
552
- void (async () => {
553
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, approvedPlanPathRef.current, undefined, injectedLanguagesRef.current);
554
- if (messagesRef.current[0]?.role === "system") {
555
- messagesRef.current[0] = {
556
- role: "system",
557
- content: newPrompt,
558
- };
559
- }
560
- })();
561
- }, [planMode, props.cwd, props.skills]);
596
+ void replaceSystemPrompt({ planMode });
597
+ }, [planMode, replaceSystemPrompt]);
562
598
  // Wire onEnterPlan callback ref
563
599
  useEffect(() => {
564
600
  if (props.onEnterPlanRef) {
@@ -577,8 +613,10 @@ export function App(props) {
577
613
  if (props.onExitPlanRef) {
578
614
  props.onExitPlanRef.current = async (planPath) => {
579
615
  // Deactivate plan mode, store approved plan path, open pane
616
+ planModeStateRef.current = false;
580
617
  setPlanMode(false);
581
618
  approvedPlanPathRef.current = planPath;
619
+ await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
582
620
  // Use setTimeout to open pane after the current tool execution completes,
583
621
  // so the turn can finish and the UI transitions cleanly
584
622
  // Flag that the plan overlay is about to open — suppresses the
@@ -607,28 +645,43 @@ export function App(props) {
607
645
  planPath);
608
646
  };
609
647
  }
610
- }, [props.onExitPlanRef, stdout]);
611
- const persistNewMessages = useCallback(async () => {
648
+ }, [props.onExitPlanRef, replaceSystemPrompt, stdout]);
649
+ const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
612
650
  const sm = sessionManagerRef.current;
651
+ if (!sm)
652
+ return;
653
+ await appendSessionMessages(sm, sessionPath, messages, startIndex);
654
+ }, []);
655
+ const persistCompactedSession = useCallback(async (compactedMessages) => {
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 () => {
613
674
  const sp = sessionPathRef.current;
614
- if (!sm || !sp)
675
+ if (!sp)
615
676
  return;
616
677
  const allMsgs = messagesRef.current;
617
- for (let i = persistedIndexRef.current; i < allMsgs.length; i++) {
618
- const msg = allMsgs[i];
619
- if (msg.role === "system")
620
- continue;
621
- const entry = {
622
- type: "message",
623
- id: crypto.randomUUID(),
624
- parentId: null,
625
- timestamp: new Date().toISOString(),
626
- message: msg,
627
- };
628
- await sm.appendEntry(sp, entry);
629
- }
678
+ await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
630
679
  persistedIndexRef.current = allMsgs.length;
631
- }, []);
680
+ if (sessionStore) {
681
+ sessionStore.messages = [...allMsgs];
682
+ sessionStore.sessionPath = sp;
683
+ }
684
+ }, [appendMessagesToSession, sessionStore]);
632
685
  /**
633
686
  * Run the language detector against the current cwd. If the detected set is a
634
687
  * strict superset of what's already injected, rebuild the system prompt with
@@ -653,20 +706,18 @@ export function App(props) {
653
706
  await applyLanguageDetectionRef.current("tool");
654
707
  };
655
708
  // ── Compaction ─────────────────────────────────────────
656
- // Load settings for auto-compaction + buddy
709
+ // Load settings for auto-compaction
657
710
  const settingsRef = useRef(null);
658
- const [buddyEnabled, setBuddyEnabled] = useState(false);
659
711
  useEffect(() => {
660
712
  if (props.settingsFile) {
661
713
  const sm = new SettingsManager(props.settingsFile);
662
714
  sm.load().then(() => {
663
715
  settingsRef.current = sm;
664
- setBuddyEnabled(sm.get("buddyEnabled") ?? false);
665
716
  });
666
717
  }
667
718
  }, [props.settingsFile]);
668
719
  const compactConversation = useCallback(async (messages) => {
669
- const contextWindow = getContextWindow(currentModel);
720
+ const contextWindow = getContextWindow(currentModel, contextWindowOptions);
670
721
  const tokensBefore = estimateConversationTokens(messages);
671
722
  const spinId = getId();
672
723
  log("INFO", "compaction", `Running compaction`, {
@@ -679,14 +730,20 @@ export function App(props) {
679
730
  try {
680
731
  // Resolve fresh credentials for compaction too
681
732
  let compactApiKey = activeApiKey;
733
+ let compactAccountId = activeAccountId;
734
+ let compactBaseUrl = activeBaseUrl;
682
735
  if (props.authStorage) {
683
736
  const creds = await props.authStorage.resolveCredentials(currentProvider);
684
737
  compactApiKey = creds.accessToken;
738
+ compactAccountId = creds.accountId;
739
+ compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
685
740
  }
686
741
  const result = await compact(messages, {
687
742
  provider: currentProvider,
688
743
  model: currentModel,
689
744
  apiKey: compactApiKey,
745
+ accountId: compactAccountId,
746
+ baseUrl: compactBaseUrl,
690
747
  contextWindow,
691
748
  signal: undefined,
692
749
  approvedPlanPath: approvedPlanPathRef.current,
@@ -715,65 +772,118 @@ export function App(props) {
715
772
  const msg = err instanceof Error ? err.message : String(err);
716
773
  log("ERROR", "compaction", `Compaction failed: ${msg}`);
717
774
  // Replace spinner with error
718
- setLiveItems((prev) => prev.map((item) => item.id === spinId
719
- ? { kind: "error", message: `Compaction failed: ${msg}`, id: spinId }
720
- : item));
775
+ setLiveItems((prev) => prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
721
776
  return messages; // Return unchanged on failure
722
777
  }
723
- }, [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]);
724
838
  /**
725
839
  * transformContext callback for the agent loop.
726
840
  * Called before each LLM call and on context overflow.
727
- * Checks if auto-compaction is needed and runs it.
728
- *
729
- * Uses actual API-reported token counts (from previous turn_end) when
730
- * available, falling back to the character-based estimate. A 30-second
731
- * cooldown prevents repeated compaction — matching the pattern used by
732
- * Mysti, openclaw, and other real-world agent frameworks.
841
+ * Compacts persistent chat only, then injects the dynamic repo map transiently.
733
842
  */
734
843
  const transformContext = useCallback(async (messages, options) => {
844
+ const stripped = stripRepoMapMessages(messages);
735
845
  const settings = settingsRef.current;
736
846
  const autoCompact = settings?.get("autoCompact") ?? true;
737
847
  const threshold = settings?.get("compactThreshold") ?? 0.8;
738
848
  // Force-compact on context overflow regardless of settings
739
849
  if (options?.force) {
740
- const result = await compactConversation(messages);
850
+ const result = await compactConversation(stripped);
851
+ if (result !== stripped) {
852
+ messagesRef.current = result;
853
+ await persistCompactedSession(result);
854
+ }
741
855
  lastCompactionTimeRef.current = Date.now();
742
- return result;
856
+ return injectRepoMapContext(result);
743
857
  }
744
858
  if (!autoCompact)
745
- return messages;
859
+ return injectRepoMapContext(stripped);
746
860
  // Time-based cooldown: skip if compaction ran within the last 30 seconds
747
861
  if (Date.now() - lastCompactionTimeRef.current < 30_000) {
748
862
  log("INFO", "compaction", `Skipping compaction — cooldown active`);
749
- return messages;
863
+ return injectRepoMapContext(stripped);
750
864
  }
751
- const contextWindow = getContextWindow(currentModel);
752
- // Reserve = max output budget + ~5K headroom for system prompt + tool
753
- // schemas. Otherwise the API rejects requests where the prompt fits the
754
- // window but leaves no room for the response (e.g. Codex Mini at 200K
755
- // ctx / 100K out — pre-turn estimate may say 160K but a 100K reasoning
756
- // response then overflows).
757
- const modelInfo = getModel(currentModel);
758
- const reserveTokens = (modelInfo?.maxOutputTokens ?? 0) + 5_000;
759
- // Prefer actual API-reported tokens over char-based estimate, but only
760
- // when the token count was recorded AFTER the most recent compaction.
761
- // A count from before compaction is stale — it reflects the old context
762
- // size and would trigger compaction again immediately for no reason.
865
+ const contextWindow = getContextWindow(currentModel, contextWindowOptions);
866
+ const reserveTokens = getCompactionReserveTokens(props.maxTokens);
763
867
  const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
764
868
  const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
765
- if (shouldCompact(messages, contextWindow, threshold, actualTokens, reserveTokens)) {
766
- const before = messages.length;
767
- 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
+ }
768
875
  lastCompactionTimeRef.current = Date.now();
769
- // If compaction was a no-op (e.g. too few middle messages to summarize),
770
- // return the original reference so the agent loop doesn't replace messages.
771
- if (result.length === before)
772
- return messages;
773
- return result;
876
+ return injectRepoMapContext(result);
774
877
  }
775
- return messages;
776
- }, [currentModel, compactConversation]);
878
+ return injectRepoMapContext(stripped);
879
+ }, [
880
+ currentModel,
881
+ compactConversation,
882
+ contextWindowOptions,
883
+ injectRepoMapContext,
884
+ persistCompactedSession,
885
+ stripRepoMapMessages,
886
+ ]);
777
887
  // ── Background task bar state (external store) ──────────
778
888
  const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
779
889
  useTaskBarPolling(props.processManager);
@@ -801,7 +911,7 @@ export function App(props) {
801
911
  tools: currentTools,
802
912
  webSearch: props.webSearch,
803
913
  maxTokens: props.maxTokens,
804
- thinking: thinkingEnabled ? (props.thinking ?? "medium") : undefined,
914
+ thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
805
915
  apiKey: activeApiKey,
806
916
  baseUrl: activeBaseUrl,
807
917
  accountId: activeAccountId,
@@ -809,6 +919,7 @@ export function App(props) {
809
919
  transformContext,
810
920
  }, {
811
921
  onComplete: useCallback(() => {
922
+ messagesRef.current = stripRepoMapMessages(messagesRef.current);
812
923
  persistNewMessages();
813
924
  // Auto-clear plan progress and approved plan when all steps are completed
814
925
  const steps = planStepsRef.current;
@@ -817,12 +928,7 @@ export function App(props) {
817
928
  setPlanSteps([]);
818
929
  approvedPlanPathRef.current = undefined;
819
930
  // Rebuild system prompt to remove the completed plan from context
820
- void (async () => {
821
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined, undefined, injectedLanguagesRef.current);
822
- if (messagesRef.current[0]?.role === "system") {
823
- messagesRef.current[0] = { role: "system", content: newPrompt };
824
- }
825
- })();
931
+ void replaceSystemPrompt({ clearApprovedPlan: true });
826
932
  }
827
933
  // Generate session title after the first turn (background, best-effort)
828
934
  if (!sessionTitleGeneratedRef.current) {
@@ -866,6 +972,7 @@ export function App(props) {
866
972
  }
867
973
  }, [
868
974
  persistNewMessages,
975
+ stripRepoMapMessages,
869
976
  planMode,
870
977
  props.cwd,
871
978
  props.skills,
@@ -885,6 +992,9 @@ export function App(props) {
885
992
  planStepsRef.current = updated;
886
993
  setPlanSteps(updated);
887
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 };
888
998
  }
889
999
  }
890
1000
  // Flush all completed items from the previous turn to Static history.
@@ -952,6 +1062,8 @@ export function App(props) {
952
1062
  }, []),
953
1063
  onToolStart: useCallback((toolCallId, name, args) => {
954
1064
  log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
1065
+ const startedAt = Date.now();
1066
+ const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
955
1067
  // Flush completed items (assistant text, finished tools) to Static
956
1068
  // before adding tool UI. Keeping both in the live area makes it tall
957
1069
  // and causes Ink's cursor math to clip the top.
@@ -997,7 +1109,10 @@ export function App(props) {
997
1109
  const next = [...prev];
998
1110
  next[groupIdx] = {
999
1111
  ...group,
1000
- tools: [...group.tools, { toolCallId, name, args, status: "running" }],
1112
+ tools: [
1113
+ ...group.tools,
1114
+ { toolCallId, name, args, status: "running", animateUntil },
1115
+ ],
1001
1116
  };
1002
1117
  return next;
1003
1118
  }
@@ -1005,7 +1120,7 @@ export function App(props) {
1005
1120
  ...prev,
1006
1121
  {
1007
1122
  kind: "tool_group",
1008
- tools: [{ toolCallId, name, args, status: "running" }],
1123
+ tools: [{ toolCallId, name, args, status: "running", animateUntil }],
1009
1124
  id: getId(),
1010
1125
  },
1011
1126
  ];
@@ -1014,7 +1129,7 @@ export function App(props) {
1014
1129
  else {
1015
1130
  setLiveItems((prev) => [
1016
1131
  ...prev,
1017
- { kind: "tool_start", toolCallId, name, args, id: getId() },
1132
+ { kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
1018
1133
  ]);
1019
1134
  }
1020
1135
  }, []),
@@ -1167,6 +1282,8 @@ export function App(props) {
1167
1282
  }, []),
1168
1283
  onServerToolCall: useCallback((id, name, input) => {
1169
1284
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
1285
+ const startedAt = Date.now();
1286
+ const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1170
1287
  // Flush completed items (including assistant text) to Static before
1171
1288
  // adding server tool UI — same rationale as onToolStart.
1172
1289
  setLiveItems((prev) => {
@@ -1181,7 +1298,8 @@ export function App(props) {
1181
1298
  serverToolCallId: id,
1182
1299
  name,
1183
1300
  input,
1184
- startedAt: Date.now(),
1301
+ startedAt,
1302
+ animateUntil,
1185
1303
  id: getId(),
1186
1304
  },
1187
1305
  ];
@@ -1331,12 +1449,6 @@ export function App(props) {
1331
1449
  log("WARN", "agent", "Agent run aborted by user");
1332
1450
  setRunAllTasks(false);
1333
1451
  setRunAllPixel(false);
1334
- // Mirror immediately — the useEffect that syncs sessionStore
1335
- // won't run if we unmount before the next render commit.
1336
- if (props.sessionStore) {
1337
- props.sessionStore.runAllTasks = false;
1338
- props.sessionStore.runAllPixel = false;
1339
- }
1340
1452
  currentPixelFixRef.current = null;
1341
1453
  setDoneStatus(null);
1342
1454
  setLiveItems((prev) => {
@@ -1378,7 +1490,7 @@ export function App(props) {
1378
1490
  }
1379
1491
  return item;
1380
1492
  });
1381
- return [...next, { kind: "info", text: "Request was stopped.", id: getId() }];
1493
+ return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
1382
1494
  });
1383
1495
  }, []),
1384
1496
  onQueuedStart: useCallback((content) => {
@@ -1408,6 +1520,36 @@ export function App(props) {
1408
1520
  setDoneStatus(null);
1409
1521
  setLiveItems([userItem]);
1410
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
+ }, []),
1411
1553
  });
1412
1554
  // First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
1413
1555
  // scope so the ref closure can dispatch to it. Called from the initial
@@ -1442,8 +1584,8 @@ export function App(props) {
1442
1584
  setLiveItems((prev) => [
1443
1585
  ...prev,
1444
1586
  isAbort
1445
- ? { kind: "info", text: "Auto-setup cancelled.", id: getId() }
1446
- : { kind: "error", message: msg, id: getId() },
1587
+ ? { kind: "stopped", text: "Auto-setup cancelled.", id: getId() }
1588
+ : toErrorItem(err, getId()),
1447
1589
  ]);
1448
1590
  }
1449
1591
  };
@@ -1465,6 +1607,29 @@ export function App(props) {
1465
1607
  useEffect(() => {
1466
1608
  setTitleRunning(agentLoop.isRunning);
1467
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]);
1468
1633
  // Consume sessionStore.pendingAction once on mount. Set by resetUI options
1469
1634
  // for paths that remount AND immediately drive the agent (plan accept,
1470
1635
  // plan reject, startTask, pixel fix). The action survives the unmount
@@ -1478,7 +1643,14 @@ export function App(props) {
1478
1643
  pendingActionConsumedRef.current = true;
1479
1644
  if (sessionStore)
1480
1645
  sessionStore.pendingAction = undefined;
1481
- 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) {
1482
1654
  setLiveItems((prev) => [
1483
1655
  ...prev,
1484
1656
  { kind: "info", text: action.infoText, id: getId() },
@@ -1488,7 +1660,7 @@ export function App(props) {
1488
1660
  void agentLoop.run(action.prompt).catch((err) => {
1489
1661
  const errMsg = err instanceof Error ? err.message : String(err);
1490
1662
  log("ERROR", "error", errMsg);
1491
- setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
1663
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
1492
1664
  });
1493
1665
  // Intentional one-shot: run once on mount, never re-fire on re-render.
1494
1666
  }, []);
@@ -1500,12 +1672,6 @@ export function App(props) {
1500
1672
  setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
1501
1673
  }
1502
1674
  }, [agentLoop.isRunning, props.cwd]);
1503
- // Animated thinking border — derived from global animation tick
1504
- useAnimationActive();
1505
- const animTick = useAnimationTick();
1506
- const thinkingBorderFrame = agentLoop.activityPhase === "thinking"
1507
- ? deriveFrame(animTick, 1000, THINKING_BORDER_COLORS.length)
1508
- : 0;
1509
1675
  const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
1510
1676
  const trimmed = input.trim();
1511
1677
  if (trimmed.startsWith("/")) {
@@ -1518,7 +1684,7 @@ export function App(props) {
1518
1684
  // external changes between turns and ensures non-writing prompts still
1519
1685
  // surface the badge when packs are newly applicable. No-op if the set
1520
1686
  // has not grown.
1521
- void applyLanguageDetectionRef.current("input");
1687
+ await applyLanguageDetectionRef.current("input");
1522
1688
  }
1523
1689
  // Handle /model directly — open inline selector
1524
1690
  if (trimmed === "/model" || trimmed === "/m") {
@@ -1530,7 +1696,7 @@ export function App(props) {
1530
1696
  const compacted = await compactConversation(messagesRef.current);
1531
1697
  if (compacted !== messagesRef.current) {
1532
1698
  messagesRef.current = compacted;
1533
- persistedIndexRef.current = 0; // Re-persist after compaction
1699
+ await persistCompactedSession(compacted);
1534
1700
  }
1535
1701
  return;
1536
1702
  }
@@ -1550,7 +1716,7 @@ export function App(props) {
1550
1716
  if (trimmed === "/clear") {
1551
1717
  if (props.resetUI) {
1552
1718
  void (async () => {
1553
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined, undefined, injectedLanguagesRef.current);
1719
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1554
1720
  props.resetUI?.({
1555
1721
  wipeSession: true,
1556
1722
  messages: [{ role: "system", content: newPrompt }],
@@ -1569,7 +1735,7 @@ export function App(props) {
1569
1735
  planStepsRef.current = [];
1570
1736
  setPlanSteps([]);
1571
1737
  void (async () => {
1572
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined, undefined, injectedLanguagesRef.current);
1738
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1573
1739
  messagesRef.current = [{ role: "system", content: newPrompt }];
1574
1740
  persistedIndexRef.current = messagesRef.current.length;
1575
1741
  })();
@@ -1631,29 +1797,46 @@ export function App(props) {
1631
1797
  planStepsRef.current = [];
1632
1798
  setPlanSteps([]);
1633
1799
  // Rebuild system prompt without the plan
1634
- void (async () => {
1635
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined, undefined, injectedLanguagesRef.current);
1636
- if (messagesRef.current[0]?.role === "system") {
1637
- messagesRef.current[0] = { role: "system", content: newPrompt };
1638
- }
1639
- })();
1640
- setLiveItems([{ kind: "info", text: "Approved plan dismissed.", id: getId() }]);
1800
+ void replaceSystemPrompt({ clearApprovedPlan: true });
1801
+ setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
1641
1802
  return;
1642
1803
  }
1643
- // Handle /buddy — toggle companion
1644
- if (trimmed === "/buddy") {
1645
- const next = !buddyEnabled;
1646
- setBuddyEnabled(next);
1647
- if (settingsRef.current) {
1648
- 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;
1649
1818
  }
1650
- setLiveItems((items) => [
1651
- ...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,
1652
1837
  {
1653
1838
  kind: "info",
1654
- text: next
1655
- ? "Buddy enabled! Your companion will appear near the prompt."
1656
- : "Buddy disabled.",
1839
+ text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
1657
1840
  id: getId(),
1658
1841
  },
1659
1842
  ]);
@@ -1661,13 +1844,18 @@ export function App(props) {
1661
1844
  }
1662
1845
  // Handle /plans — open plan pane
1663
1846
  if (trimmed === "/plans") {
1664
- if (props.resetUI && props.sessionStore) {
1847
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
1665
1848
  props.sessionStore.overlay = "plan";
1666
1849
  props.sessionStore.planAutoExpand = false;
1667
1850
  props.resetUI();
1668
1851
  }
1669
1852
  else {
1670
- 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
+ }
1671
1859
  setPlanAutoExpand(false);
1672
1860
  setOverlay("plan");
1673
1861
  }
@@ -1709,8 +1897,8 @@ export function App(props) {
1709
1897
  setLiveItems((prev) => [
1710
1898
  ...prev,
1711
1899
  isAbort
1712
- ? { kind: "info", text: "Request was stopped.", id: getId() }
1713
- : { kind: "error", message: msg, id: getId() },
1900
+ ? { kind: "stopped", text: "Request was stopped.", id: getId() }
1901
+ : toErrorItem(err, getId()),
1714
1902
  ]);
1715
1903
  }
1716
1904
  // Reload custom commands in case a setup command created new ones
@@ -1833,11 +2021,19 @@ export function App(props) {
1833
2021
  setLiveItems((prev) => [
1834
2022
  ...prev,
1835
2023
  isAbort
1836
- ? { kind: "info", text: "Request was stopped.", id: getId() }
1837
- : { kind: "error", message: msg, id: getId() },
2024
+ ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2025
+ : toErrorItem(err, getId()),
1838
2026
  ]);
1839
2027
  }
1840
- }, [agentLoop, props.onSlashCommand, compactConversation]);
2028
+ }, [
2029
+ agentLoop,
2030
+ props.onSlashCommand,
2031
+ compactConversation,
2032
+ rebuildSystemPrompt,
2033
+ replaceSystemPrompt,
2034
+ refreshRepoMap,
2035
+ stripRepoMapMessages,
2036
+ ]);
1841
2037
  const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
1842
2038
  const handleAbort = useCallback(() => {
1843
2039
  if (agentLoop.isRunning) {
@@ -1854,7 +2050,7 @@ export function App(props) {
1854
2050
  log("INFO", "thinking", `Thinking ${next ? "enabled" : "disabled"}`);
1855
2051
  setLiveItems((items) => [
1856
2052
  ...items,
1857
- { kind: "info", text: `Thinking ${next ? "on" : "off"}`, id: getId() },
2053
+ { kind: "thinking_transition", active: next, id: getId() },
1858
2054
  ]);
1859
2055
  if (props.settingsFile) {
1860
2056
  const sm = new SettingsManager(props.settingsFile);
@@ -1871,6 +2067,10 @@ export function App(props) {
1871
2067
  const newProvider = value.slice(0, colonIdx);
1872
2068
  const newModelId = value.slice(colonIdx + 1);
1873
2069
  log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
2070
+ const rebuildPromptWithTools = (tools) => {
2071
+ currentToolsRef.current = tools;
2072
+ void replaceSystemPrompt({ tools });
2073
+ };
1874
2074
  // Handle provider-specific tool changes when provider changes
1875
2075
  setCurrentProvider((prevProvider) => {
1876
2076
  if (newProvider !== prevProvider) {
@@ -1878,15 +2078,17 @@ export function App(props) {
1878
2078
  // Anthropic has native server-side web search; all other providers need the client tool.
1879
2079
  setCurrentTools((prev) => {
1880
2080
  const hasWebSearch = prev.some((t) => t.name === "web_search");
2081
+ let next = prev;
1881
2082
  if (newProvider === "anthropic" && hasWebSearch) {
1882
2083
  // Switching TO anthropic — remove client-side web_search (server-side handles it)
1883
- return prev.filter((t) => t.name !== "web_search");
2084
+ next = prev.filter((t) => t.name !== "web_search");
1884
2085
  }
1885
2086
  else if (newProvider !== "anthropic" && !hasWebSearch) {
1886
2087
  // Switching FROM anthropic — add client-side web_search
1887
- return [...prev, createWebSearchTool()];
2088
+ next = [...prev, createWebSearchTool()];
1888
2089
  }
1889
- return prev;
2090
+ rebuildPromptWithTools(next);
2091
+ return next;
1890
2092
  });
1891
2093
  // Reconnect MCP servers
1892
2094
  if (props.mcpManager) {
@@ -1909,16 +2111,21 @@ export function App(props) {
1909
2111
  }
1910
2112
  try {
1911
2113
  const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
1912
- setCurrentTools((prev) => [
1913
- ...prev.filter((t) => !t.name.startsWith("mcp__")),
1914
- ...mcpTools,
1915
- ]);
2114
+ setCurrentTools((prev) => {
2115
+ const next = [...prev.filter((t) => !t.name.startsWith("mcp__")), ...mcpTools];
2116
+ rebuildPromptWithTools(next);
2117
+ return next;
2118
+ });
1916
2119
  log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
1917
2120
  }
1918
2121
  catch (err) {
1919
2122
  log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
1920
2123
  // Still remove old MCP tools even if reconnection fails
1921
- 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
+ });
1922
2129
  }
1923
2130
  })();
1924
2131
  }
@@ -1930,7 +2137,7 @@ export function App(props) {
1930
2137
  const displayName = modelInfo?.name ?? newModelId;
1931
2138
  setLiveItems((prev) => [
1932
2139
  ...prev,
1933
- { kind: "info", text: `Switched to ${displayName}`, id: getId() },
2140
+ { kind: "model_transition", modelName: displayName, id: getId() },
1934
2141
  ]);
1935
2142
  // Persist model selection for next CLI launch
1936
2143
  if (props.settingsFile) {
@@ -1951,10 +2158,7 @@ export function App(props) {
1951
2158
  const sm = new SettingsManager(props.settingsFile);
1952
2159
  sm.load().then(() => sm.set("theme", name));
1953
2160
  }
1954
- setLiveItems((prev) => [
1955
- ...prev,
1956
- { kind: "info", text: `Theme switched to: ${name}`, id: getId() },
1957
- ]);
2161
+ setLiveItems((prev) => [...prev, { kind: "theme_transition", themeName: name, id: getId() }]);
1958
2162
  }, [switchTheme, props.settingsFile]);
1959
2163
  // All available slash commands for the command palette — ordered by how
1960
2164
  // commonly they're used and grouped by purpose; /quit stays dead last.
@@ -1970,6 +2174,8 @@ export function App(props) {
1970
2174
  "research",
1971
2175
  "scan",
1972
2176
  "verify",
2177
+ "expand",
2178
+ "bullet-proof",
1973
2179
  "simplify",
1974
2180
  "compare",
1975
2181
  "batch",
@@ -2022,20 +2228,20 @@ export function App(props) {
2022
2228
  case "setup_hint":
2023
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));
2024
2230
  case "assistant":
2025
- 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));
2026
2232
  case "tool_start":
2027
- 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));
2028
2234
  case "tool_done":
2029
2235
  return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
2030
2236
  case "tool_group":
2031
2237
  return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
2032
2238
  case "server_tool_start":
2033
- 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));
2034
2240
  case "server_tool_done":
2035
2241
  return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
2036
2242
  case "error": {
2037
- const providerHint = getProviderErrorHint(item.message);
2038
- 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));
2039
2245
  }
2040
2246
  case "info":
2041
2247
  return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
@@ -2043,6 +2249,34 @@ export function App(props) {
2043
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));
2044
2250
  case "plan_transition":
2045
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));
2046
2280
  case "step_done":
2047
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));
2048
2282
  case "queued":
@@ -2050,7 +2284,7 @@ export function App(props) {
2050
2284
  ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
2051
2285
  : ""] })] }, item.id));
2052
2286
  case "compacting":
2053
- return _jsx(CompactionSpinner, {}, item.id);
2287
+ return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
2054
2288
  case "compacted":
2055
2289
  return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
2056
2290
  case "duration":
@@ -2126,8 +2360,8 @@ export function App(props) {
2126
2360
  setLiveItems((prev) => [
2127
2361
  ...prev,
2128
2362
  isAbort
2129
- ? { kind: "info", text: "Request was stopped.", id: getId() }
2130
- : { kind: "error", message: msg, id: getId() },
2363
+ ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2364
+ : toErrorItem(err, getId()),
2131
2365
  ]);
2132
2366
  setRunAllTasks(false);
2133
2367
  }
@@ -2145,8 +2379,6 @@ export function App(props) {
2145
2379
  startTaskRef.current = startTask;
2146
2380
  useEffect(() => {
2147
2381
  runAllTasksRef.current = runAllTasks;
2148
- // Mirror into sessionStore so the flag survives unmount/remount
2149
- // when startTask triggers resetUI for the NEXT task in the chain.
2150
2382
  if (props.sessionStore)
2151
2383
  props.sessionStore.runAllTasks = runAllTasks;
2152
2384
  }, [runAllTasks, props.sessionStore]);
@@ -2171,9 +2403,19 @@ export function App(props) {
2171
2403
  log("WARN", "pixel", `chdir failed: ${err.message}`);
2172
2404
  }
2173
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();
2174
2413
  setDisplayedCwd(prep.projectPath);
2414
+ let toolsForPixelFix = currentToolsRef.current;
2175
2415
  if (props.rebuildToolsForCwd) {
2176
- setCurrentTools(props.rebuildToolsForCwd(prep.projectPath));
2416
+ toolsForPixelFix = props.rebuildToolsForCwd(prep.projectPath);
2417
+ currentToolsRef.current = toolsForPixelFix;
2418
+ setCurrentTools(toolsForPixelFix);
2177
2419
  }
2178
2420
  // Pixel-fix swaps the project root — reset injected packs so the
2179
2421
  // new project re-detects from scratch on the next tool call. Also
@@ -2183,7 +2425,13 @@ export function App(props) {
2183
2425
  setupHintShownRef.current = false;
2184
2426
  const detectedForPixelFix = detectLanguages(prep.projectPath);
2185
2427
  injectedLanguagesRef.current = detectedForPixelFix;
2186
- const newSystemPrompt = await buildSystemPrompt(prep.projectPath, props.skills, false, undefined, undefined, detectedForPixelFix);
2428
+ const newSystemPrompt = await rebuildSystemPrompt({
2429
+ cwd: prep.projectPath,
2430
+ planMode: false,
2431
+ clearApprovedPlan: true,
2432
+ activeLanguages: detectedForPixelFix,
2433
+ tools: toolsForPixelFix,
2434
+ });
2187
2435
  // Now that the cwd swap is committed, reset chat. Doing this BEFORE
2188
2436
  // the chdir would print a banner with the old cwd, then bumping
2189
2437
  // staticKey would print a second banner with the new cwd — leaving
@@ -2220,12 +2468,14 @@ export function App(props) {
2220
2468
  log("ERROR", "pixel", msg);
2221
2469
  currentPixelFixRef.current = null;
2222
2470
  setRunAllPixel(false);
2223
- setLiveItems((prev) => [...prev, { kind: "error", message: msg, id: getId() }]);
2471
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2224
2472
  }
2225
2473
  })();
2226
2474
  }, [props.cwd, stdout, agentLoop, currentProvider, currentModel]);
2227
2475
  startPixelFixRef.current = startPixelFix;
2228
- const [runAllPixel, setRunAllPixel] = useState(() => props.sessionStore?.runAllPixel ?? 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);
2229
2479
  useEffect(() => {
2230
2480
  runAllPixelRef.current = runAllPixel;
2231
2481
  if (props.sessionStore)
@@ -2237,15 +2487,18 @@ export function App(props) {
2237
2487
  const isEyesView = overlay === "eyes";
2238
2488
  const isPixelView = overlay === "pixel";
2239
2489
  const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
2240
- 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: () => {
2241
- 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) {
2242
2492
  props.sessionStore.overlay = null;
2243
2493
  props.resetUI();
2244
2494
  }
2245
2495
  else {
2246
- 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
+ }
2247
2501
  setTaskCount(getTaskCount(props.cwd));
2248
- setStaticKey((k) => k + 1);
2249
2502
  setOverlay(null);
2250
2503
  }
2251
2504
  }, onWorkOnTask: (title, prompt, taskId) => {
@@ -2260,13 +2513,16 @@ export function App(props) {
2260
2513
  startTask(next.title, next.prompt, next.id);
2261
2514
  }
2262
2515
  } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
2263
- if (props.resetUI && props.sessionStore) {
2516
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2264
2517
  props.sessionStore.overlay = null;
2265
2518
  props.resetUI();
2266
2519
  }
2267
2520
  else {
2268
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2269
- setStaticKey((k) => k + 1);
2521
+ if (props.sessionStore) {
2522
+ props.sessionStore.overlay = null;
2523
+ if (agentLoop.isRunning)
2524
+ props.sessionStore.pendingResetUI = true;
2525
+ }
2270
2526
  setOverlay(null);
2271
2527
  }
2272
2528
  }, onFixOne: (entry) => {
@@ -2280,38 +2536,48 @@ export function App(props) {
2280
2536
  setRunAllPixel(true);
2281
2537
  startPixelFix(first.errorId);
2282
2538
  } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
2283
- if (props.resetUI && props.sessionStore) {
2539
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2284
2540
  props.sessionStore.overlay = null;
2285
2541
  props.resetUI();
2286
2542
  }
2287
2543
  else {
2288
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2289
- setStaticKey((k) => k + 1);
2544
+ if (props.sessionStore) {
2545
+ props.sessionStore.overlay = null;
2546
+ if (agentLoop.isRunning)
2547
+ props.sessionStore.pendingResetUI = true;
2548
+ }
2290
2549
  setOverlay(null);
2291
2550
  }
2292
2551
  } })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
2293
- if (props.resetUI && props.sessionStore) {
2552
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2294
2553
  props.sessionStore.overlay = null;
2295
2554
  props.resetUI();
2296
2555
  }
2297
2556
  else {
2298
- 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
+ }
2299
2562
  setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
2300
- setStaticKey((k) => k + 1);
2301
2563
  setOverlay(null);
2302
2564
  }
2303
2565
  }, onQueueMessage: (msg) => {
2304
2566
  agentLoop.queueMessage(msg);
2305
2567
  } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
2306
2568
  planOverlayPendingRef.current = false;
2307
- if (props.resetUI && props.sessionStore) {
2569
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2308
2570
  props.sessionStore.overlay = null;
2309
2571
  props.sessionStore.planAutoExpand = false;
2310
2572
  props.resetUI();
2311
2573
  }
2312
2574
  else {
2313
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2314
- 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
+ }
2315
2581
  setPlanAutoExpand(false);
2316
2582
  setOverlay(null);
2317
2583
  }
@@ -2327,7 +2593,10 @@ export function App(props) {
2327
2593
  const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
2328
2594
  const steps = extractPlanSteps(planContent);
2329
2595
  // Build the new system prompt with the approved plan baked in.
2330
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, false, planPath, undefined, injectedLanguagesRef.current);
2596
+ const newPrompt = await rebuildSystemPrompt({
2597
+ planMode: false,
2598
+ approvedPlanPath: planPath,
2599
+ });
2331
2600
  // Create a new session file BEFORE remount so the new tree
2332
2601
  // picks it up via sessionStore.sessionPath.
2333
2602
  let newSessionPath;
@@ -2349,7 +2618,7 @@ export function App(props) {
2349
2618
  sessionPath: newSessionPath,
2350
2619
  pendingAction: {
2351
2620
  prompt: "The plan has been approved. Implement it now, following each step in order.",
2352
- infoText: "Plan approved — starting fresh session for implementation",
2621
+ planEvent: { event: "approved" },
2353
2622
  },
2354
2623
  });
2355
2624
  return;
@@ -2382,7 +2651,7 @@ export function App(props) {
2382
2651
  catch (err) {
2383
2652
  const errMsg = err instanceof Error ? err.message : String(err);
2384
2653
  log("ERROR", "error", errMsg);
2385
- setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
2654
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2386
2655
  }
2387
2656
  })();
2388
2657
  }, onReject: (planPath, feedback) => {
@@ -2397,7 +2666,7 @@ export function App(props) {
2397
2666
  props.resetUI({
2398
2667
  pendingAction: {
2399
2668
  prompt: rejectionMsg,
2400
- infoText: `Plan rejected "${feedback}"`,
2669
+ planEvent: { event: "rejected", detail: feedback },
2401
2670
  },
2402
2671
  });
2403
2672
  return;
@@ -2414,36 +2683,52 @@ export function App(props) {
2414
2683
  void agentLoop.run(rejectionMsg).catch((err) => {
2415
2684
  const errMsg = err instanceof Error ? err.message : String(err);
2416
2685
  log("ERROR", "error", errMsg);
2417
- setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
2686
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2418
2687
  });
2419
- } })) : (_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"
2420
- ? THINKING_BORDER_COLORS[thinkingBorderFrame]
2421
- : "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 &&
2422
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: () => {
2423
- 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) {
2424
2697
  props.sessionStore.overlay = "tasks";
2425
2698
  props.resetUI();
2426
2699
  }
2427
2700
  else {
2428
- 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
+ }
2429
2706
  setOverlay("tasks");
2430
2707
  }
2431
2708
  }, onToggleSkills: () => {
2432
- if (props.resetUI && props.sessionStore) {
2709
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2433
2710
  props.sessionStore.overlay = "skills";
2434
2711
  props.resetUI();
2435
2712
  }
2436
2713
  else {
2437
- 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
+ }
2438
2719
  setOverlay("skills");
2439
2720
  }
2440
2721
  }, onTogglePixel: () => {
2441
- if (props.resetUI && props.sessionStore) {
2722
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2442
2723
  props.sessionStore.overlay = "pixel";
2443
2724
  props.resetUI();
2444
2725
  }
2445
2726
  else {
2446
- 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
+ }
2447
2732
  setOverlay("pixel");
2448
2733
  }
2449
2734
  }, onTogglePlanMode: () => {
@@ -2459,6 +2744,13 @@ export function App(props) {
2459
2744
  id: getId(),
2460
2745
  },
2461
2746
  ]);
2462
- }, 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}`;
2463
2755
  }
2464
2756
  //# sourceMappingURL=App.js.map