@prestyj/cli 4.2.16 → 4.2.45

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 (286) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -7
  3. package/dist/cli.js +458 -238
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +1 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +66 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +22 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +184 -23
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/agents.d.ts +0 -2
  14. package/dist/core/agents.d.ts.map +1 -1
  15. package/dist/core/agents.js +1 -5
  16. package/dist/core/agents.js.map +1 -1
  17. package/dist/core/auth-storage.d.ts +2 -0
  18. package/dist/core/auth-storage.d.ts.map +1 -1
  19. package/dist/core/auth-storage.js +66 -17
  20. package/dist/core/auth-storage.js.map +1 -1
  21. package/dist/core/auto-update.js +1 -1
  22. package/dist/core/auto-update.js.map +1 -1
  23. package/dist/core/compaction/compactor.d.ts +36 -5
  24. package/dist/core/compaction/compactor.d.ts.map +1 -1
  25. package/dist/core/compaction/compactor.js +222 -63
  26. package/dist/core/compaction/compactor.js.map +1 -1
  27. package/dist/core/compaction/compactor.test.js +377 -32
  28. package/dist/core/compaction/compactor.test.js.map +1 -1
  29. package/dist/core/compaction/token-estimator.d.ts +5 -0
  30. package/dist/core/compaction/token-estimator.d.ts.map +1 -1
  31. package/dist/core/compaction/token-estimator.js +41 -2
  32. package/dist/core/compaction/token-estimator.js.map +1 -1
  33. package/dist/core/compaction/token-estimator.test.js +43 -21
  34. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  35. package/dist/core/event-bus.d.ts +6 -0
  36. package/dist/core/event-bus.d.ts.map +1 -1
  37. package/dist/core/event-bus.js +7 -0
  38. package/dist/core/event-bus.js.map +1 -1
  39. package/dist/core/file-lock.d.ts +6 -0
  40. package/dist/core/file-lock.d.ts.map +1 -0
  41. package/dist/core/file-lock.js +76 -0
  42. package/dist/core/file-lock.js.map +1 -0
  43. package/dist/core/index.d.ts +2 -2
  44. package/dist/core/index.d.ts.map +1 -1
  45. package/dist/core/index.js +1 -1
  46. package/dist/core/index.js.map +1 -1
  47. package/dist/core/mcp/client.d.ts +0 -26
  48. package/dist/core/mcp/client.d.ts.map +1 -1
  49. package/dist/core/mcp/client.js +34 -141
  50. package/dist/core/mcp/client.js.map +1 -1
  51. package/dist/core/mcp/defaults.d.ts.map +1 -1
  52. package/dist/core/mcp/defaults.js +4 -8
  53. package/dist/core/mcp/defaults.js.map +1 -1
  54. package/dist/core/mcp/types.d.ts +0 -2
  55. package/dist/core/mcp/types.d.ts.map +1 -1
  56. package/dist/core/model-registry.d.ts +2 -1
  57. package/dist/core/model-registry.d.ts.map +1 -1
  58. package/dist/core/model-registry.js +16 -5
  59. package/dist/core/model-registry.js.map +1 -1
  60. package/dist/core/oauth/anthropic.d.ts.map +1 -1
  61. package/dist/core/oauth/anthropic.js +3 -3
  62. package/dist/core/oauth/anthropic.js.map +1 -1
  63. package/dist/core/prompt-commands.d.ts.map +1 -1
  64. package/dist/core/prompt-commands.js +21 -0
  65. package/dist/core/prompt-commands.js.map +1 -1
  66. package/dist/core/session-manager.d.ts +34 -2
  67. package/dist/core/session-manager.d.ts.map +1 -1
  68. package/dist/core/session-manager.js +160 -54
  69. package/dist/core/session-manager.js.map +1 -1
  70. package/dist/core/settings-manager.d.ts +2 -2
  71. package/dist/core/skills.d.ts +2 -1
  72. package/dist/core/skills.d.ts.map +1 -1
  73. package/dist/core/skills.js +8 -8
  74. package/dist/core/skills.js.map +1 -1
  75. package/dist/core/slash-commands.d.ts +4 -0
  76. package/dist/core/slash-commands.d.ts.map +1 -1
  77. package/dist/core/slash-commands.js +22 -0
  78. package/dist/core/slash-commands.js.map +1 -1
  79. package/dist/core/telegram.d.ts +94 -0
  80. package/dist/core/telegram.d.ts.map +1 -0
  81. package/dist/core/telegram.js +227 -0
  82. package/dist/core/telegram.js.map +1 -0
  83. package/dist/interactive.d.ts.map +1 -1
  84. package/dist/interactive.js +36 -5
  85. package/dist/interactive.js.map +1 -1
  86. package/dist/modes/index.d.ts +1 -0
  87. package/dist/modes/index.d.ts.map +1 -1
  88. package/dist/modes/index.js +1 -0
  89. package/dist/modes/index.js.map +1 -1
  90. package/dist/modes/json-mode.d.ts.map +1 -1
  91. package/dist/modes/json-mode.js +2 -1
  92. package/dist/modes/json-mode.js.map +1 -1
  93. package/dist/modes/print-mode.d.ts.map +1 -1
  94. package/dist/modes/print-mode.js +2 -1
  95. package/dist/modes/print-mode.js.map +1 -1
  96. package/dist/modes/rpc-mode.d.ts +28 -0
  97. package/dist/modes/rpc-mode.d.ts.map +1 -0
  98. package/dist/modes/rpc-mode.js +146 -0
  99. package/dist/modes/rpc-mode.js.map +1 -0
  100. package/dist/modes/serve-mode.d.ts +21 -0
  101. package/dist/modes/serve-mode.d.ts.map +1 -0
  102. package/dist/modes/serve-mode.js +650 -0
  103. package/dist/modes/serve-mode.js.map +1 -0
  104. package/dist/system-prompt.d.ts +1 -1
  105. package/dist/system-prompt.d.ts.map +1 -1
  106. package/dist/system-prompt.js +45 -10
  107. package/dist/system-prompt.js.map +1 -1
  108. package/dist/tools/bash.d.ts +4 -1
  109. package/dist/tools/bash.d.ts.map +1 -1
  110. package/dist/tools/bash.js +58 -5
  111. package/dist/tools/bash.js.map +1 -1
  112. package/dist/tools/edit.d.ts +4 -1
  113. package/dist/tools/edit.d.ts.map +1 -1
  114. package/dist/tools/edit.js +9 -5
  115. package/dist/tools/edit.js.map +1 -1
  116. package/dist/tools/enter-plan.d.ts +8 -0
  117. package/dist/tools/enter-plan.d.ts.map +1 -0
  118. package/dist/tools/enter-plan.js +28 -0
  119. package/dist/tools/enter-plan.js.map +1 -0
  120. package/dist/tools/exit-plan.d.ts +8 -0
  121. package/dist/tools/exit-plan.d.ts.map +1 -0
  122. package/dist/tools/exit-plan.js +35 -0
  123. package/dist/tools/exit-plan.js.map +1 -0
  124. package/dist/tools/grep.d.ts +2 -1
  125. package/dist/tools/grep.d.ts.map +1 -1
  126. package/dist/tools/grep.js +7 -8
  127. package/dist/tools/grep.js.map +1 -1
  128. package/dist/tools/index.d.ts +17 -0
  129. package/dist/tools/index.d.ts.map +1 -1
  130. package/dist/tools/index.js +26 -7
  131. package/dist/tools/index.js.map +1 -1
  132. package/dist/tools/ls.d.ts +2 -1
  133. package/dist/tools/ls.d.ts.map +1 -1
  134. package/dist/tools/ls.js +4 -4
  135. package/dist/tools/ls.js.map +1 -1
  136. package/dist/tools/operations.d.ts +39 -0
  137. package/dist/tools/operations.d.ts.map +1 -0
  138. package/dist/tools/operations.js +27 -0
  139. package/dist/tools/operations.js.map +1 -0
  140. package/dist/tools/path-utils.d.ts +5 -0
  141. package/dist/tools/path-utils.d.ts.map +1 -1
  142. package/dist/tools/path-utils.js +18 -0
  143. package/dist/tools/path-utils.js.map +1 -1
  144. package/dist/tools/plan-mode.test.d.ts +2 -0
  145. package/dist/tools/plan-mode.test.d.ts.map +1 -0
  146. package/dist/tools/plan-mode.test.js +214 -0
  147. package/dist/tools/plan-mode.test.js.map +1 -0
  148. package/dist/tools/read.d.ts +2 -1
  149. package/dist/tools/read.d.ts.map +1 -1
  150. package/dist/tools/read.js +6 -5
  151. package/dist/tools/read.js.map +1 -1
  152. package/dist/tools/skill.d.ts +10 -0
  153. package/dist/tools/skill.d.ts.map +1 -0
  154. package/dist/tools/skill.js +36 -0
  155. package/dist/tools/skill.js.map +1 -0
  156. package/dist/tools/subagent.d.ts +3 -1
  157. package/dist/tools/subagent.d.ts.map +1 -1
  158. package/dist/tools/subagent.js +46 -4
  159. package/dist/tools/subagent.js.map +1 -1
  160. package/dist/tools/tasks.d.ts +2 -2
  161. package/dist/tools/web-fetch.d.ts.map +1 -1
  162. package/dist/tools/web-fetch.js +47 -0
  163. package/dist/tools/web-fetch.js.map +1 -1
  164. package/dist/tools/write.d.ts +4 -1
  165. package/dist/tools/write.d.ts.map +1 -1
  166. package/dist/tools/write.js +17 -6
  167. package/dist/tools/write.js.map +1 -1
  168. package/dist/ui/App.d.ts +35 -1
  169. package/dist/ui/App.d.ts.map +1 -1
  170. package/dist/ui/App.js +367 -91
  171. package/dist/ui/App.js.map +1 -1
  172. package/dist/ui/components/ActivityIndicator.d.ts +2 -1
  173. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  174. package/dist/ui/components/ActivityIndicator.js +25 -4
  175. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  176. package/dist/ui/components/AnimationContext.d.ts +22 -0
  177. package/dist/ui/components/AnimationContext.d.ts.map +1 -0
  178. package/dist/ui/components/AnimationContext.js +35 -0
  179. package/dist/ui/components/AnimationContext.js.map +1 -0
  180. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  181. package/dist/ui/components/AssistantMessage.js +6 -1
  182. package/dist/ui/components/AssistantMessage.js.map +1 -1
  183. package/dist/ui/components/Banner.d.ts.map +1 -1
  184. package/dist/ui/components/Banner.js +4 -7
  185. package/dist/ui/components/Banner.js.map +1 -1
  186. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  187. package/dist/ui/components/CompactionNotice.js +4 -8
  188. package/dist/ui/components/CompactionNotice.js.map +1 -1
  189. package/dist/ui/components/Footer.d.ts +2 -1
  190. package/dist/ui/components/Footer.d.ts.map +1 -1
  191. package/dist/ui/components/Footer.js +8 -15
  192. package/dist/ui/components/Footer.js.map +1 -1
  193. package/dist/ui/components/InputArea.d.ts +3 -1
  194. package/dist/ui/components/InputArea.d.ts.map +1 -1
  195. package/dist/ui/components/InputArea.js +23 -25
  196. package/dist/ui/components/InputArea.js.map +1 -1
  197. package/dist/ui/components/PlanApproval.d.ts +8 -0
  198. package/dist/ui/components/PlanApproval.d.ts.map +1 -0
  199. package/dist/ui/components/PlanApproval.js +44 -0
  200. package/dist/ui/components/PlanApproval.js.map +1 -0
  201. package/dist/ui/components/PlanBanner.d.ts +6 -0
  202. package/dist/ui/components/PlanBanner.d.ts.map +1 -0
  203. package/dist/ui/components/PlanBanner.js +14 -0
  204. package/dist/ui/components/PlanBanner.js.map +1 -0
  205. package/dist/ui/components/PlanOverlay.d.ts +11 -0
  206. package/dist/ui/components/PlanOverlay.d.ts.map +1 -0
  207. package/dist/ui/components/PlanOverlay.js +263 -0
  208. package/dist/ui/components/PlanOverlay.js.map +1 -0
  209. package/dist/ui/components/ServerToolExecution.d.ts +1 -0
  210. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  211. package/dist/ui/components/ServerToolExecution.js +12 -3
  212. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  213. package/dist/ui/components/SkillsOverlay.d.ts +7 -0
  214. package/dist/ui/components/SkillsOverlay.d.ts.map +1 -0
  215. package/dist/ui/components/SkillsOverlay.js +158 -0
  216. package/dist/ui/components/SkillsOverlay.js.map +1 -0
  217. package/dist/ui/components/Spinner.d.ts.map +1 -1
  218. package/dist/ui/components/Spinner.js +5 -9
  219. package/dist/ui/components/Spinner.js.map +1 -1
  220. package/dist/ui/components/StreamingArea.d.ts +2 -1
  221. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  222. package/dist/ui/components/StreamingArea.js +11 -4
  223. package/dist/ui/components/StreamingArea.js.map +1 -1
  224. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  225. package/dist/ui/components/SubAgentPanel.js +27 -29
  226. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  227. package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
  228. package/dist/ui/components/TaskOverlay.js +7 -3
  229. package/dist/ui/components/TaskOverlay.js.map +1 -1
  230. package/dist/ui/components/ThinkingBlock.d.ts.map +1 -1
  231. package/dist/ui/components/ThinkingBlock.js +4 -1
  232. package/dist/ui/components/ThinkingBlock.js.map +1 -1
  233. package/dist/ui/components/ThinkingIndicator.d.ts.map +1 -1
  234. package/dist/ui/components/ThinkingIndicator.js +8 -33
  235. package/dist/ui/components/ThinkingIndicator.js.map +1 -1
  236. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  237. package/dist/ui/components/ToolExecution.js +95 -41
  238. package/dist/ui/components/ToolExecution.js.map +1 -1
  239. package/dist/ui/components/ToolGroupExecution.d.ts +7 -0
  240. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -0
  241. package/dist/ui/components/ToolGroupExecution.js +115 -0
  242. package/dist/ui/components/ToolGroupExecution.js.map +1 -0
  243. package/dist/ui/components/UserMessage.js +1 -1
  244. package/dist/ui/components/UserMessage.js.map +1 -1
  245. package/dist/ui/components/index.d.ts +1 -0
  246. package/dist/ui/components/index.d.ts.map +1 -1
  247. package/dist/ui/components/index.js +1 -0
  248. package/dist/ui/components/index.js.map +1 -1
  249. package/dist/ui/hooks/useAgentLoop.d.ts +8 -0
  250. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  251. package/dist/ui/hooks/useAgentLoop.js +320 -220
  252. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  253. package/dist/ui/hooks/useTerminalSize.d.ts +21 -9
  254. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  255. package/dist/ui/hooks/useTerminalSize.js +33 -16
  256. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  257. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  258. package/dist/ui/hooks/useTerminalTitle.js +8 -16
  259. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  260. package/dist/ui/live-item-flush.d.ts +11 -1
  261. package/dist/ui/live-item-flush.d.ts.map +1 -1
  262. package/dist/ui/live-item-flush.js +81 -2
  263. package/dist/ui/live-item-flush.js.map +1 -1
  264. package/dist/ui/login.d.ts +1 -1
  265. package/dist/ui/login.d.ts.map +1 -1
  266. package/dist/ui/login.js +30 -6
  267. package/dist/ui/login.js.map +1 -1
  268. package/dist/ui/render.d.ts +11 -0
  269. package/dist/ui/render.d.ts.map +1 -1
  270. package/dist/ui/render.js +24 -10
  271. package/dist/ui/render.js.map +1 -1
  272. package/dist/ui/sessions.d.ts +2 -0
  273. package/dist/ui/sessions.d.ts.map +1 -0
  274. package/dist/ui/sessions.js +208 -0
  275. package/dist/ui/sessions.js.map +1 -0
  276. package/dist/ui/theme/dark.json +3 -1
  277. package/dist/ui/theme/light.json +3 -1
  278. package/dist/ui/theme/theme.d.ts +2 -0
  279. package/dist/ui/theme/theme.d.ts.map +1 -1
  280. package/dist/utils/error-handler.d.ts.map +1 -1
  281. package/dist/utils/error-handler.js +17 -5
  282. package/dist/utils/error-handler.js.map +1 -1
  283. package/dist/utils/image.d.ts.map +1 -1
  284. package/dist/utils/image.js +60 -5
  285. package/dist/utils/image.js.map +1 -1
  286. package/package.json +8 -8
package/dist/ui/App.js CHANGED
@@ -12,6 +12,7 @@ import { useAgentLoop } from "./hooks/useAgentLoop.js";
12
12
  import { UserMessage } from "./components/UserMessage.js";
13
13
  import { AssistantMessage } from "./components/AssistantMessage.js";
14
14
  import { ToolExecution } from "./components/ToolExecution.js";
15
+ import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
15
16
  import { ServerToolExecution } from "./components/ServerToolExecution.js";
16
17
  import { SubAgentPanel } from "./components/SubAgentPanel.js";
17
18
  import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
@@ -20,10 +21,13 @@ import { ActivityIndicator } from "./components/ActivityIndicator.js";
20
21
  import { InputArea } from "./components/InputArea.js";
21
22
  import { Footer } from "./components/Footer.js";
22
23
  import { Banner } from "./components/Banner.js";
24
+ import { PlanOverlay } from "./components/PlanOverlay.js";
23
25
  import { ModelSelector } from "./components/ModelSelector.js";
24
26
  import { TaskOverlay } from "./components/TaskOverlay.js";
27
+ import { SkillsOverlay } from "./components/SkillsOverlay.js";
25
28
  import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
26
29
  import { useTheme } from "./theme/theme.js";
30
+ import { useAnimationTick, deriveFrame } from "./components/AnimationContext.js";
27
31
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
28
32
  import { getGitBranch } from "../utils/git.js";
29
33
  import { getModel, getContextWindow } from "../core/model-registry.js";
@@ -34,28 +38,29 @@ import { shouldCompact, compact } from "../core/compaction/compactor.js";
34
38
  import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
35
39
  import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
36
40
  import { loadCustomCommands } from "../core/custom-commands.js";
41
+ import { buildSystemPrompt } from "../system-prompt.js";
37
42
  import { getMCPServers } from "../core/mcp/index.js";
38
- import { pruneHistory, flushOnTurnText, flushOnTurnEnd } from "./live-item-flush.js";
43
+ import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd } from "./live-item-flush.js";
39
44
  // ── Provider Error Hints ──────────────────────────────────
40
45
  /** Detect provider-side errors and return a user-facing hint. */
41
46
  function getProviderErrorHint(message) {
42
47
  const lower = message.toLowerCase();
43
- // Billing / insufficient balance (some providers use 429 for this)
44
- if (lower.includes("insufficient balance") ||
45
- lower.includes("quota exceeded") ||
46
- lower.includes("billing")) {
47
- return "Your account has insufficient balance or no active resource package. Top up your account at the provider's dashboard.";
48
- }
49
48
  if (lower.includes("overloaded") || lower.includes("engine_overloaded")) {
50
49
  return "This is a provider-side issue — their servers are under heavy load. Try again in a moment.";
51
50
  }
51
+ if (lower.includes("insufficient balance") ||
52
+ lower.includes("no resource package") ||
53
+ lower.includes("quota exceeded") ||
54
+ lower.includes("recharge")) {
55
+ return "The provider reports a billing or quota issue. Check your account balance or resource package.";
56
+ }
52
57
  if (lower.includes("rate limit") ||
53
58
  lower.includes("too many requests") ||
54
59
  lower.includes("429")) {
55
60
  return "You've hit the provider's rate limit. Wait a moment before retrying.";
56
61
  }
57
62
  if (lower.includes("502") || lower.includes("bad gateway")) {
58
- return "The provider returned a server error. This is not an ezcoder issue — try again shortly.";
63
+ return "The provider returned a server error. This is not a ezcoder issue — try again shortly.";
59
64
  }
60
65
  if (lower.includes("503") || lower.includes("service unavailable")) {
61
66
  return "The provider's service is temporarily unavailable. Try again in a moment.";
@@ -64,12 +69,39 @@ function getProviderErrorHint(message) {
64
69
  return "The request to the provider timed out. Their servers may be slow — try again.";
65
70
  }
66
71
  if (lower.includes("500") && lower.includes("internal server error")) {
67
- return "The provider experienced an internal error. This is not an ezcoder issue.";
72
+ return "The provider experienced an internal error. This is not a ezcoder issue.";
73
+ }
74
+ if (lower.includes("does not recognize the requested model") ||
75
+ (lower.includes("model") &&
76
+ (lower.includes("not exist") || lower.includes("not found") || lower.includes("no access")))) {
77
+ return "Use /model to switch to a different model, or check that your account has access to the requested model.";
68
78
  }
69
79
  return null;
70
80
  }
71
- // pruneHistory, flushOnTurnText, flushOnTurnEnd, MAX_HISTORY_ITEMS
72
- // are imported from ./live-item-flush.ts
81
+ /** Tools that get aggregated into a single compact group when concurrent. */
82
+ const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
83
+ /**
84
+ * Cap memory by replacing old items with tiny tombstones. Ink's <Static>
85
+ * tracks rendered items by array length — the array must never shrink, but
86
+ * we can swap out heavy objects for lightweight `{ kind: "tombstone", id }`
87
+ * entries so GC can reclaim the original data.
88
+ */
89
+ const MAX_LIVE_HISTORY = 200;
90
+ function compactHistory(items) {
91
+ if (items.length <= MAX_LIVE_HISTORY)
92
+ return items;
93
+ const cutoff = items.length - MAX_LIVE_HISTORY;
94
+ const compacted = new Array(items.length);
95
+ for (let i = 0; i < cutoff; i++) {
96
+ const it = items[i];
97
+ compacted[i] = it.kind === "tombstone" ? it : { kind: "tombstone", id: it.id };
98
+ }
99
+ for (let i = cutoff; i < items.length; i++) {
100
+ compacted[i] = items[i];
101
+ }
102
+ return compacted;
103
+ }
104
+ // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
73
105
  // ── Duration summary ─────────────────────────────────────
74
106
  function formatDuration(ms) {
75
107
  const totalSec = Math.round(ms / 1000);
@@ -211,7 +243,7 @@ export function App(props) {
211
243
  useEffect(() => {
212
244
  if (isRestoredSession && !restoredRef.current) {
213
245
  restoredRef.current = true;
214
- setHistory((prev) => pruneHistory([...prev, ...props.initialHistory]));
246
+ setHistory((prev) => compactHistory([...prev, ...trimFlushedItems(props.initialHistory)]));
215
247
  }
216
248
  }, [isRestoredSession, props.initialHistory]);
217
249
  // Items from the current/last turn — rendered in the live area so they stay visible
@@ -231,6 +263,9 @@ export function App(props) {
231
263
  const [currentTools, setCurrentTools] = useState(props.tools);
232
264
  const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
233
265
  const messagesRef = useRef(props.messages);
266
+ const [planMode, setPlanMode] = useState(false);
267
+ const [planAutoExpand, setPlanAutoExpand] = useState(false);
268
+ const approvedPlanPathRef = useRef(undefined);
234
269
  const nextIdRef = useRef(0);
235
270
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
236
271
  const sessionPathRef = useRef(props.sessionPath);
@@ -255,6 +290,56 @@ export function App(props) {
255
290
  useEffect(() => {
256
291
  reloadCustomCommands();
257
292
  }, [reloadCustomCommands]);
293
+ // ── Plan mode wiring ─────────────────────────────────────
294
+ // Sync planModeRef with React state
295
+ useEffect(() => {
296
+ if (props.planModeRef) {
297
+ props.planModeRef.current = planMode;
298
+ }
299
+ }, [planMode, props.planModeRef]);
300
+ // Rebuild system prompt when plan mode changes
301
+ useEffect(() => {
302
+ void (async () => {
303
+ const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, approvedPlanPathRef.current);
304
+ if (messagesRef.current[0]?.role === "system") {
305
+ messagesRef.current[0] = {
306
+ role: "system",
307
+ content: newPrompt,
308
+ };
309
+ }
310
+ })();
311
+ }, [planMode, props.cwd, props.skills]);
312
+ // Wire onEnterPlan callback ref
313
+ useEffect(() => {
314
+ if (props.onEnterPlanRef) {
315
+ props.onEnterPlanRef.current = (reason) => {
316
+ setPlanMode(true);
317
+ const msg = reason ? `Plan mode activated: ${reason}` : "Plan mode activated";
318
+ setLiveItems((prev) => [...prev, { kind: "info", text: msg, id: getId() }]);
319
+ };
320
+ }
321
+ }, [props.onEnterPlanRef]);
322
+ // Wire onExitPlan callback ref
323
+ useEffect(() => {
324
+ if (props.onExitPlanRef) {
325
+ props.onExitPlanRef.current = async (planPath) => {
326
+ // Deactivate plan mode, store approved plan path, open pane
327
+ setPlanMode(false);
328
+ approvedPlanPathRef.current = planPath;
329
+ // Use setTimeout to open pane after the current tool execution completes,
330
+ // so the turn can finish and the UI transitions cleanly
331
+ setTimeout(() => {
332
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
333
+ setPlanAutoExpand(true);
334
+ setOverlay("plan");
335
+ }, 300);
336
+ return ("Plan submitted. Exiting plan mode.\n" +
337
+ "The plan pane is opening for user review.\n" +
338
+ "Plan saved at: " +
339
+ planPath);
340
+ };
341
+ }
342
+ }, [props.onExitPlanRef, stdout]);
258
343
  const persistNewMessages = useCallback(async () => {
259
344
  const sm = sessionManagerRef.current;
260
345
  const sp = sessionPathRef.current;
@@ -311,6 +396,7 @@ export function App(props) {
311
396
  apiKey: compactApiKey,
312
397
  contextWindow,
313
398
  signal: undefined,
399
+ approvedPlanPath: approvedPlanPathRef.current,
314
400
  });
315
401
  // Replace spinner with completed notice
316
402
  setLiveItems((prev) => prev.map((item) => item.id === spinId
@@ -440,7 +526,7 @@ export function App(props) {
440
526
  setLiveItems((prev) => {
441
527
  const flushed = flushOnTurnText(prev);
442
528
  if (flushed.length > 0) {
443
- setHistory((h) => pruneHistory([...h, ...flushed]));
529
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
444
530
  }
445
531
  return [{ kind: "assistant", text, thinking, thinkingMs, id: getId() }];
446
532
  });
@@ -471,6 +557,31 @@ export function App(props) {
471
557
  return [...prev, { kind: "subagent_group", agents: [newAgent], id: getId() }];
472
558
  });
473
559
  }
560
+ else if (AGGREGATABLE_TOOLS.has(name)) {
561
+ // Group concurrent read-only tools into a single compact item
562
+ setLiveItems((prev) => {
563
+ // Find an active tool group (has at least one running tool)
564
+ const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
565
+ item.tools.some((t) => t.status === "running"));
566
+ if (groupIdx !== -1) {
567
+ const group = prev[groupIdx];
568
+ const next = [...prev];
569
+ next[groupIdx] = {
570
+ ...group,
571
+ tools: [...group.tools, { toolCallId, name, args, status: "running" }],
572
+ };
573
+ return next;
574
+ }
575
+ return [
576
+ ...prev,
577
+ {
578
+ kind: "tool_group",
579
+ tools: [{ toolCallId, name, args, status: "running" }],
580
+ id: getId(),
581
+ },
582
+ ];
583
+ });
584
+ }
474
585
  else {
475
586
  setLiveItems((prev) => [
476
587
  ...prev,
@@ -533,6 +644,20 @@ export function App(props) {
533
644
  }
534
645
  else {
535
646
  setLiveItems((prev) => {
647
+ // Check if this tool is in a tool_group
648
+ const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
649
+ item.tools.some((t) => t.toolCallId === toolCallId));
650
+ if (groupIdx !== -1) {
651
+ const group = prev[groupIdx];
652
+ const next = [...prev];
653
+ next[groupIdx] = {
654
+ ...group,
655
+ tools: group.tools.map((t) => t.toolCallId === toolCallId
656
+ ? { ...t, status: "done", result, isError }
657
+ : t),
658
+ };
659
+ return next;
660
+ }
536
661
  // Find the matching tool_start and replace it with tool_done
537
662
  const startIdx = prev.findIndex((item) => item.kind === "tool_start" && item.toolCallId === toolCallId);
538
663
  if (startIdx !== -1) {
@@ -618,7 +743,7 @@ export function App(props) {
618
743
  setLiveItems((prev) => {
619
744
  const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
620
745
  if (flushed.length > 0) {
621
- setHistory((h) => pruneHistory([...h, ...flushed]));
746
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
622
747
  }
623
748
  return remaining;
624
749
  });
@@ -646,23 +771,6 @@ export function App(props) {
646
771
  // (allow the two-phase flush to complete first)
647
772
  if (runAllTasksRef.current) {
648
773
  setTimeout(() => {
649
- // Memory watchdog: log and GC if RSS is high between tasks
650
- const memUsage = process.memoryUsage();
651
- const rssMB = Math.round(memUsage.rss / 1024 / 1024);
652
- const RSS_THRESHOLD_MB = 2048;
653
- if (rssMB > RSS_THRESHOLD_MB) {
654
- log("WARN", "memory", `High RSS between tasks: ${rssMB} MB`, {
655
- rss: String(memUsage.rss),
656
- heapUsed: String(memUsage.heapUsed),
657
- external: String(memUsage.external),
658
- });
659
- globalThis.gc?.();
660
- const afterMB = Math.round(process.memoryUsage().rss / 1024 / 1024);
661
- log("WARN", "memory", `Post-GC RSS: ${afterMB} MB (was ${rssMB} MB)`);
662
- if (afterMB > RSS_THRESHOLD_MB) {
663
- log("ERROR", "memory", `RSS still above threshold after GC (${afterMB} MB). Consider restarting.`);
664
- }
665
- }
666
774
  const cwd = cwdRef.current;
667
775
  const next = getNextPendingTask(cwd);
668
776
  if (next) {
@@ -679,11 +787,77 @@ export function App(props) {
679
787
  onAborted: useCallback(() => {
680
788
  log("WARN", "agent", "Agent run aborted by user");
681
789
  setRunAllTasks(false);
790
+ setDoneStatus(null);
682
791
  setLiveItems((prev) => {
683
- const next = prev.map((item) => item.kind === "subagent_group" ? { ...item, aborted: true } : item);
792
+ const next = prev.map((item) => {
793
+ if (item.kind === "subagent_group")
794
+ return { ...item, aborted: true };
795
+ // Convert running tools to stopped state so spinners stop
796
+ if (item.kind === "tool_start") {
797
+ return {
798
+ kind: "tool_done",
799
+ name: item.name,
800
+ args: item.args,
801
+ result: "Stopped.",
802
+ isError: true,
803
+ durationMs: 0,
804
+ id: item.id,
805
+ };
806
+ }
807
+ if (item.kind === "server_tool_start") {
808
+ return {
809
+ kind: "server_tool_done",
810
+ name: item.name,
811
+ input: item.input,
812
+ resultType: "aborted",
813
+ data: null,
814
+ durationMs: 0,
815
+ id: item.id,
816
+ };
817
+ }
818
+ if (item.kind === "tool_group") {
819
+ const tools = item.tools.map((t) => t.status === "running"
820
+ ? { ...t, status: "done", result: "Stopped.", isError: true }
821
+ : t);
822
+ return { ...item, tools };
823
+ }
824
+ // Remove compaction spinner (compaction can't complete after abort)
825
+ if (item.kind === "compacting") {
826
+ return { kind: "tombstone", id: item.id };
827
+ }
828
+ return item;
829
+ });
684
830
  return [...next, { kind: "info", text: "Request was stopped.", id: getId() }];
685
831
  });
686
832
  }, []),
833
+ onQueuedStart: useCallback((content) => {
834
+ // When a queued message starts processing, show it as a UserItem
835
+ // and flush prior items to history
836
+ const displayText = typeof content === "string"
837
+ ? content
838
+ : content
839
+ .filter((c) => c.type === "text")
840
+ .map((c) => c.text)
841
+ .join("\n");
842
+ const imageCount = typeof content === "string"
843
+ ? undefined
844
+ : content.filter((c) => c.type === "image").length || undefined;
845
+ setLiveItems((prev) => {
846
+ if (prev.length > 0) {
847
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(prev)]));
848
+ }
849
+ return [];
850
+ });
851
+ const userItem = {
852
+ kind: "user",
853
+ text: displayText,
854
+ imageCount,
855
+ id: getId(),
856
+ };
857
+ setLastUserMessage(displayText);
858
+ setDoneStatus(null);
859
+ setLiveItems([userItem]);
860
+ }, []),
687
861
  });
688
862
  // Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
689
863
  // and Ink renders the smaller live area (updating its internal line
@@ -696,7 +870,7 @@ export function App(props) {
696
870
  if (pendingFlushRef.current.length > 0) {
697
871
  const items = pendingFlushRef.current;
698
872
  pendingFlushRef.current = [];
699
- setHistory((h) => pruneHistory([...h, ...items]));
873
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(items)]));
700
874
  }
701
875
  });
702
876
  // Sync terminal title with agent loop state
@@ -704,25 +878,11 @@ export function App(props) {
704
878
  setTitlePhase(agentLoop.activityPhase);
705
879
  setTitleRunning(agentLoop.isRunning);
706
880
  }, [agentLoop.activityPhase, agentLoop.isRunning]);
707
- // Animated thinking border
708
- const [thinkingBorderFrame, setThinkingBorderFrame] = useState(0);
709
- useEffect(() => {
710
- if (agentLoop.activityPhase !== "thinking")
711
- return;
712
- const timer = setInterval(() => {
713
- setThinkingBorderFrame((f) => (f + 1) % THINKING_BORDER_COLORS.length);
714
- }, 1000);
715
- return () => clearInterval(timer);
716
- }, [agentLoop.activityPhase]);
717
- // Success flash on turn completion
718
- const [doneFlash, setDoneFlash] = useState(false);
719
- useEffect(() => {
720
- if (doneStatus) {
721
- setDoneFlash(true);
722
- const timer = setTimeout(() => setDoneFlash(false), 600);
723
- return () => clearTimeout(timer);
724
- }
725
- }, [doneStatus]);
881
+ // Animated thinking border — derived from global animation tick
882
+ const animTick = useAnimationTick();
883
+ const thinkingBorderFrame = agentLoop.activityPhase === "thinking"
884
+ ? deriveFrame(animTick, 1000, THINKING_BORDER_COLORS.length)
885
+ : 0;
726
886
  const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
727
887
  const trimmed = input.trim();
728
888
  if (trimmed.startsWith("/")) {
@@ -763,6 +923,30 @@ export function App(props) {
763
923
  setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
764
924
  return;
765
925
  }
926
+ // Handle /plan — toggle plan mode
927
+ if (trimmed === "/plan" || trimmed === "/plan on") {
928
+ setPlanMode(true);
929
+ setLiveItems((prev) => [
930
+ ...prev,
931
+ { kind: "info", text: "Plan mode activated", id: getId() },
932
+ ]);
933
+ return;
934
+ }
935
+ if (trimmed === "/plan off") {
936
+ setPlanMode(false);
937
+ setLiveItems((prev) => [
938
+ ...prev,
939
+ { kind: "info", text: "Plan mode deactivated", id: getId() },
940
+ ]);
941
+ return;
942
+ }
943
+ // Handle /plans — open plan pane
944
+ if (trimmed === "/plans") {
945
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
946
+ setPlanAutoExpand(false);
947
+ setOverlay("plan");
948
+ return;
949
+ }
766
950
  // Handle prompt-template commands (built-in + custom from .ezcoder/commands/)
767
951
  if (trimmed.startsWith("/")) {
768
952
  const parts = trimmed.slice(1).split(" ");
@@ -776,7 +960,7 @@ export function App(props) {
776
960
  // Move live items into history before starting
777
961
  setLiveItems((prev) => {
778
962
  if (prev.length > 0) {
779
- setHistory((h) => pruneHistory([...h, ...prev]));
963
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(prev)]));
780
964
  }
781
965
  return [];
782
966
  });
@@ -816,31 +1000,8 @@ export function App(props) {
816
1000
  return;
817
1001
  }
818
1002
  }
819
- // Move any remaining live items into history (Static) before starting new turn
820
- setLiveItems((prev) => {
821
- if (prev.length > 0) {
822
- setHistory((h) => pruneHistory([...h, ...prev]));
823
- }
824
- return [];
825
- });
826
- // Build display text — strip image paths, show badges instead
1003
+ // ── Build user content (shared by normal + queued paths) ──
827
1004
  const hasImages = inputImages.length > 0;
828
- let displayText = input;
829
- if (hasImages) {
830
- const { cleanText } = await extractImagePaths(input, props.cwd);
831
- displayText = cleanText;
832
- }
833
- const userItem = {
834
- kind: "user",
835
- text: displayText,
836
- imageCount: hasImages ? inputImages.length : undefined,
837
- pasteInfo,
838
- id: getId(),
839
- };
840
- setLastUserMessage(input);
841
- setDoneStatus(null);
842
- setLiveItems([userItem]);
843
- // Build user content — plain string or content array with images
844
1005
  const modelInfo = getModel(currentModel);
845
1006
  const modelSupportsImages = modelInfo?.supportsImages ?? true;
846
1007
  let userContent;
@@ -884,6 +1045,47 @@ export function App(props) {
884
1045
  else {
885
1046
  userContent = input;
886
1047
  }
1048
+ // ── Queue message if agent is already running ──
1049
+ if (agentLoop.isRunning) {
1050
+ log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
1051
+ agentLoop.queueMessage(userContent);
1052
+ let displayText = input;
1053
+ if (hasImages) {
1054
+ const { cleanText } = await extractImagePaths(input, props.cwd);
1055
+ displayText = cleanText;
1056
+ }
1057
+ const queuedItem = {
1058
+ kind: "queued",
1059
+ text: displayText,
1060
+ imageCount: hasImages ? inputImages.length : undefined,
1061
+ id: getId(),
1062
+ };
1063
+ setLiveItems((prev) => [...prev, queuedItem]);
1064
+ return;
1065
+ }
1066
+ // Move any remaining live items into history (Static) before starting new turn
1067
+ setLiveItems((prev) => {
1068
+ if (prev.length > 0) {
1069
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(prev)]));
1070
+ }
1071
+ return [];
1072
+ });
1073
+ // Build display text — strip image paths, show badges instead
1074
+ let displayText = input;
1075
+ if (hasImages) {
1076
+ const { cleanText } = await extractImagePaths(input, props.cwd);
1077
+ displayText = cleanText;
1078
+ }
1079
+ const userItem = {
1080
+ kind: "user",
1081
+ text: displayText,
1082
+ imageCount: hasImages ? inputImages.length : undefined,
1083
+ pasteInfo,
1084
+ id: getId(),
1085
+ };
1086
+ setLastUserMessage(input);
1087
+ setDoneStatus(null);
1088
+ setLiveItems([userItem]);
887
1089
  // Run agent
888
1090
  try {
889
1091
  await agentLoop.run(userContent);
@@ -902,6 +1104,7 @@ export function App(props) {
902
1104
  }, [agentLoop, props.onSlashCommand, compactConversation]);
903
1105
  const handleAbort = useCallback(() => {
904
1106
  if (agentLoop.isRunning) {
1107
+ agentLoop.clearQueue();
905
1108
  agentLoop.abort();
906
1109
  }
907
1110
  else {
@@ -952,15 +1155,10 @@ export function App(props) {
952
1155
  apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
953
1156
  }
954
1157
  try {
955
- const allConfigs = getMCPServers(newProvider, apiKey);
956
- const eagerConfigs = allConfigs.filter((c) => !c.lazy);
957
- const lazyConfigs = allConfigs.filter((c) => c.lazy);
958
- const mcpTools = await props.mcpManager.connectAll(eagerConfigs);
959
- const lazyTools = props.mcpManager.registerLazy(lazyConfigs);
1158
+ const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
960
1159
  setCurrentTools((prev) => [
961
1160
  ...prev.filter((t) => !t.name.startsWith("mcp__")),
962
1161
  ...mcpTools,
963
- ...lazyTools,
964
1162
  ]);
965
1163
  log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
966
1164
  }
@@ -995,6 +1193,8 @@ export function App(props) {
995
1193
  { name: "compact", aliases: ["c"], description: "Compact conversation" },
996
1194
  { name: "clear", aliases: [], description: "Clear session and terminal" },
997
1195
  { name: "quit", aliases: ["q", "exit"], description: "Exit the agent" },
1196
+ { name: "plan", aliases: [], description: "Toggle plan mode (on/off)" },
1197
+ { name: "plans", aliases: [], description: "Open plans pane" },
998
1198
  ...PROMPT_COMMANDS.map((cmd) => ({
999
1199
  name: cmd.name,
1000
1200
  aliases: cmd.aliases,
@@ -1008,6 +1208,8 @@ export function App(props) {
1008
1208
  ], [customCommands]);
1009
1209
  const renderItem = (item) => {
1010
1210
  switch (item.kind) {
1211
+ case "tombstone":
1212
+ return null;
1011
1213
  case "banner":
1012
1214
  return (_jsx(Banner, { version: props.version, model: props.model, provider: props.provider, cwd: props.cwd, taskCount: taskCount }, item.id));
1013
1215
  case "user":
@@ -1020,16 +1222,22 @@ export function App(props) {
1020
1222
  return _jsx(ToolExecution, { status: "running", name: item.name, args: item.args }, item.id);
1021
1223
  case "tool_done":
1022
1224
  return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError }, item.id));
1225
+ case "tool_group":
1226
+ return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
1023
1227
  case "server_tool_start":
1024
1228
  return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt }, item.id));
1025
1229
  case "server_tool_done":
1026
- return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs }, item.id));
1230
+ return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
1027
1231
  case "error": {
1028
1232
  const providerHint = getProviderErrorHint(item.message);
1029
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.error, children: ["✗ ", item.message] }), providerHint && (_jsxs(Text, { color: theme.textDim, children: [" Hint: ", providerHint] }))] }, item.id));
1233
+ 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));
1030
1234
  }
1031
1235
  case "info":
1032
- return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.textDim, children: item.text }) }, item.id));
1236
+ return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
1237
+ case "queued":
1238
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "⏳ Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
1239
+ ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
1240
+ : ""] })] }, item.id));
1033
1241
  case "compacting":
1034
1242
  return _jsx(CompactionSpinner, {}, item.id);
1035
1243
  case "compacted":
@@ -1049,7 +1257,6 @@ export function App(props) {
1049
1257
  setLiveItems([]);
1050
1258
  messagesRef.current = messagesRef.current.slice(0, 1);
1051
1259
  agentLoop.reset();
1052
- globalThis.gc?.();
1053
1260
  persistedIndexRef.current = messagesRef.current.length;
1054
1261
  const sm = sessionManagerRef.current;
1055
1262
  if (sm) {
@@ -1093,7 +1300,10 @@ export function App(props) {
1093
1300
  runAllTasksRef.current = runAllTasks;
1094
1301
  }, [runAllTasks]);
1095
1302
  const isTaskView = overlay === "tasks";
1096
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: isTaskView ? [] : 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: () => {
1303
+ const isSkillsView = overlay === "skills";
1304
+ const isPlanView = overlay === "plan";
1305
+ const isOverlayView = isTaskView || isSkillsView || isPlanView;
1306
+ return (_jsxs(Box, { flexDirection: "column", 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: () => {
1097
1307
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1098
1308
  setTaskCount(getTaskCount(props.cwd));
1099
1309
  setStaticKey((k) => k + 1);
@@ -1109,11 +1319,77 @@ export function App(props) {
1109
1319
  markTaskInProgress(props.cwd, next.id);
1110
1320
  startTask(next.title, next.prompt, next.id);
1111
1321
  }
1112
- } })) : (_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 })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking"
1322
+ } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
1323
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1324
+ setStaticKey((k) => k + 1);
1325
+ setOverlay(null);
1326
+ } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
1327
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1328
+ setStaticKey((k) => k + 1);
1329
+ setPlanAutoExpand(false);
1330
+ setOverlay(null);
1331
+ }, onApprove: (planPath) => {
1332
+ // Store approved plan path — will be injected into the new system prompt
1333
+ approvedPlanPathRef.current = planPath;
1334
+ // Clear session for a fresh context focused on the plan
1335
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1336
+ setHistory([{ kind: "banner", id: "banner" }]);
1337
+ setLiveItems([]);
1338
+ setStaticKey((k) => k + 1);
1339
+ setPlanAutoExpand(false);
1340
+ setOverlay(null);
1341
+ // Rebuild system prompt with the approved plan, then reset the session
1342
+ void (async () => {
1343
+ const newPrompt = await buildSystemPrompt(props.cwd, props.skills, false, planPath);
1344
+ messagesRef.current = [{ role: "system", content: newPrompt }];
1345
+ agentLoop.reset();
1346
+ persistedIndexRef.current = messagesRef.current.length;
1347
+ // Create a new session file
1348
+ const sm = sessionManagerRef.current;
1349
+ if (sm) {
1350
+ const s = await sm.create(props.cwd, currentProvider, currentModel);
1351
+ sessionPathRef.current = s.path;
1352
+ }
1353
+ // Start implementation with a clean context
1354
+ setLiveItems([
1355
+ {
1356
+ kind: "info",
1357
+ text: "Plan approved — starting fresh session for implementation",
1358
+ id: getId(),
1359
+ },
1360
+ ]);
1361
+ setDoneStatus(null);
1362
+ await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
1363
+ })();
1364
+ }, onReject: (planPath, feedback) => {
1365
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1366
+ setStaticKey((k) => k + 1);
1367
+ setPlanAutoExpand(false);
1368
+ setOverlay(null);
1369
+ // Send rejection + feedback to the agent
1370
+ const msg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
1371
+ `Please revise the plan based on this feedback.`;
1372
+ setLiveItems((prev) => [
1373
+ ...prev,
1374
+ { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
1375
+ ]);
1376
+ void agentLoop.run(msg);
1377
+ } })) : (_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"
1113
1378
  ? THINKING_BORDER_COLORS[thinkingBorderFrame]
1114
- : "transparent", paddingLeft: 1, paddingRight: 1, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, tokenEstimate: agentLoop.streamedTokenEstimate, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name) }) })) : (doneStatus && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: doneFlash ? theme.success : theme.textDim, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
1379
+ : "transparent", paddingLeft: 1, paddingRight: 1, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, tokenEstimate: agentLoop.streamedTokenEstimate, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode }) })) : (doneStatus && (_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: () => {
1115
1380
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1116
1381
  setOverlay("tasks");
1117
- }, cwd: props.cwd, commands: allCommands }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, cwd: props.cwd, gitBranch: gitBranch, thinkingEnabled: thinkingEnabled })), bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate }))] }))] }));
1382
+ }, onToggleSkills: () => {
1383
+ stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1384
+ setOverlay("skills");
1385
+ }, onTogglePlanMode: () => {
1386
+ const next = !planMode;
1387
+ setPlanMode(next);
1388
+ log("INFO", "plan", `Plan mode ${next ? "enabled" : "disabled"}`);
1389
+ setLiveItems((items) => [
1390
+ ...items,
1391
+ { kind: "info", text: `Plan mode ${next ? "on" : "off"}`, id: getId() },
1392
+ ]);
1393
+ }, cwd: props.cwd, commands: allCommands }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, cwd: props.cwd, gitBranch: gitBranch, thinkingEnabled: thinkingEnabled, planMode: planMode })), bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate }))] }))] }));
1118
1394
  }
1119
1395
  //# sourceMappingURL=App.js.map