@prestyj/cli 4.2.77 → 4.3.34

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 (233) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +335 -85
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +10 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +33 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +42 -26
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/auth-storage.d.ts.map +1 -1
  12. package/dist/core/auth-storage.js +17 -2
  13. package/dist/core/auth-storage.js.map +1 -1
  14. package/dist/core/auto-update.d.ts +16 -3
  15. package/dist/core/auto-update.d.ts.map +1 -1
  16. package/dist/core/auto-update.js +126 -56
  17. package/dist/core/auto-update.js.map +1 -1
  18. package/dist/core/auto-update.test.d.ts +2 -0
  19. package/dist/core/auto-update.test.d.ts.map +1 -0
  20. package/dist/core/auto-update.test.js +205 -0
  21. package/dist/core/auto-update.test.js.map +1 -0
  22. package/dist/core/compaction/compactor.d.ts +18 -1
  23. package/dist/core/compaction/compactor.d.ts.map +1 -1
  24. package/dist/core/compaction/compactor.js +48 -7
  25. package/dist/core/compaction/compactor.js.map +1 -1
  26. package/dist/core/compaction/compactor.test.js +39 -6
  27. package/dist/core/compaction/compactor.test.js.map +1 -1
  28. package/dist/core/compaction/token-estimator.d.ts.map +1 -1
  29. package/dist/core/compaction/token-estimator.js +4 -0
  30. package/dist/core/compaction/token-estimator.js.map +1 -1
  31. package/dist/core/compaction/token-estimator.test.js +1 -1
  32. package/dist/core/index.d.ts +1 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/core/index.js +1 -1
  35. package/dist/core/index.js.map +1 -1
  36. package/dist/core/mcp/client.d.ts.map +1 -1
  37. package/dist/core/mcp/client.js +15 -2
  38. package/dist/core/mcp/client.js.map +1 -1
  39. package/dist/core/model-registry.d.ts.map +1 -1
  40. package/dist/core/model-registry.js +74 -5
  41. package/dist/core/model-registry.js.map +1 -1
  42. package/dist/core/oauth/types.d.ts +1 -0
  43. package/dist/core/oauth/types.d.ts.map +1 -1
  44. package/dist/core/prompt-commands.d.ts.map +1 -1
  45. package/dist/core/prompt-commands.js +134 -0
  46. package/dist/core/prompt-commands.js.map +1 -1
  47. package/dist/core/settings-manager.d.ts +5 -0
  48. package/dist/core/settings-manager.d.ts.map +1 -1
  49. package/dist/core/settings-manager.js +14 -2
  50. package/dist/core/settings-manager.js.map +1 -1
  51. package/dist/interactive.js +1 -1
  52. package/dist/interactive.js.map +1 -1
  53. package/dist/modes/agent-home-mode.d.ts +21 -0
  54. package/dist/modes/agent-home-mode.d.ts.map +1 -0
  55. package/dist/modes/agent-home-mode.js +553 -0
  56. package/dist/modes/agent-home-mode.js.map +1 -0
  57. package/dist/system-prompt.d.ts.map +1 -1
  58. package/dist/system-prompt.js +30 -14
  59. package/dist/system-prompt.js.map +1 -1
  60. package/dist/tools/bash.d.ts.map +1 -1
  61. package/dist/tools/bash.js +9 -2
  62. package/dist/tools/bash.js.map +1 -1
  63. package/dist/tools/edit.d.ts +4 -2
  64. package/dist/tools/edit.d.ts.map +1 -1
  65. package/dist/tools/edit.js +48 -33
  66. package/dist/tools/edit.js.map +1 -1
  67. package/dist/tools/edit.test.js +80 -20
  68. package/dist/tools/edit.test.js.map +1 -1
  69. package/dist/tools/grep.d.ts.map +1 -1
  70. package/dist/tools/grep.js +12 -0
  71. package/dist/tools/grep.js.map +1 -1
  72. package/dist/tools/index.d.ts +1 -0
  73. package/dist/tools/index.d.ts.map +1 -1
  74. package/dist/tools/index.js +6 -0
  75. package/dist/tools/index.js.map +1 -1
  76. package/dist/tools/overflow.js +1 -1
  77. package/dist/tools/overflow.js.map +1 -1
  78. package/dist/tools/plan-mode.test.js +2 -2
  79. package/dist/tools/plan-mode.test.js.map +1 -1
  80. package/dist/tools/read.d.ts.map +1 -1
  81. package/dist/tools/read.js +14 -1
  82. package/dist/tools/read.js.map +1 -1
  83. package/dist/tools/subagent.d.ts.map +1 -1
  84. package/dist/tools/subagent.js +20 -3
  85. package/dist/tools/subagent.js.map +1 -1
  86. package/dist/tools/web-search.d.ts +9 -0
  87. package/dist/tools/web-search.d.ts.map +1 -0
  88. package/dist/tools/web-search.js +241 -0
  89. package/dist/tools/web-search.js.map +1 -0
  90. package/dist/tools/write.js +2 -2
  91. package/dist/tools/write.js.map +1 -1
  92. package/dist/tools/write.test.js +7 -9
  93. package/dist/tools/write.test.js.map +1 -1
  94. package/dist/ui/App.d.ts +4 -0
  95. package/dist/ui/App.d.ts.map +1 -1
  96. package/dist/ui/App.js +324 -161
  97. package/dist/ui/App.js.map +1 -1
  98. package/dist/ui/activity-phrases.d.ts.map +1 -1
  99. package/dist/ui/activity-phrases.js +74 -129
  100. package/dist/ui/activity-phrases.js.map +1 -1
  101. package/dist/ui/components/ActivityIndicator.d.ts +3 -1
  102. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  103. package/dist/ui/components/ActivityIndicator.js +5 -2
  104. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  105. package/dist/ui/components/AssistantMessage.d.ts +1 -0
  106. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  107. package/dist/ui/components/AssistantMessage.js +2 -2
  108. package/dist/ui/components/AssistantMessage.js.map +1 -1
  109. package/dist/ui/components/DiffFrame.d.ts +10 -0
  110. package/dist/ui/components/DiffFrame.d.ts.map +1 -0
  111. package/dist/ui/components/DiffFrame.js +13 -0
  112. package/dist/ui/components/DiffFrame.js.map +1 -0
  113. package/dist/ui/components/Footer.d.ts +2 -3
  114. package/dist/ui/components/Footer.d.ts.map +1 -1
  115. package/dist/ui/components/Footer.js +6 -5
  116. package/dist/ui/components/Footer.js.map +1 -1
  117. package/dist/ui/components/InputArea.d.ts.map +1 -1
  118. package/dist/ui/components/InputArea.js +297 -8
  119. package/dist/ui/components/InputArea.js.map +1 -1
  120. package/dist/ui/components/Markdown.d.ts +1 -1
  121. package/dist/ui/components/Markdown.d.ts.map +1 -1
  122. package/dist/ui/components/Markdown.js +25 -213
  123. package/dist/ui/components/Markdown.js.map +1 -1
  124. package/dist/ui/components/ModelSelector.d.ts.map +1 -1
  125. package/dist/ui/components/ModelSelector.js +10 -2
  126. package/dist/ui/components/ModelSelector.js.map +1 -1
  127. package/dist/ui/components/Ratchet.d.ts.map +1 -1
  128. package/dist/ui/components/Ratchet.js +10 -3
  129. package/dist/ui/components/Ratchet.js.map +1 -1
  130. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  131. package/dist/ui/components/StreamingArea.js +2 -2
  132. package/dist/ui/components/StreamingArea.js.map +1 -1
  133. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  134. package/dist/ui/components/SubAgentPanel.js +5 -1
  135. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  136. package/dist/ui/components/ThemeSelector.d.ts +9 -0
  137. package/dist/ui/components/ThemeSelector.d.ts.map +1 -0
  138. package/dist/ui/components/ThemeSelector.js +25 -0
  139. package/dist/ui/components/ThemeSelector.js.map +1 -0
  140. package/dist/ui/components/ToolExecution.d.ts +2 -0
  141. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  142. package/dist/ui/components/ToolExecution.js +95 -30
  143. package/dist/ui/components/ToolExecution.js.map +1 -1
  144. package/dist/ui/components/ToolGroupExecution.d.ts +3 -2
  145. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  146. package/dist/ui/components/ToolGroupExecution.js +68 -51
  147. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  148. package/dist/ui/hooks/stall-retry-duplicate.test.d.ts +2 -0
  149. package/dist/ui/hooks/stall-retry-duplicate.test.d.ts.map +1 -0
  150. package/dist/ui/hooks/stall-retry-duplicate.test.js +94 -0
  151. package/dist/ui/hooks/stall-retry-duplicate.test.js.map +1 -0
  152. package/dist/ui/hooks/useAgentLoop.d.ts +10 -3
  153. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  154. package/dist/ui/hooks/useAgentLoop.js +131 -16
  155. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  156. package/dist/ui/hooks/useDoublePress.d.ts +8 -0
  157. package/dist/ui/hooks/useDoublePress.d.ts.map +1 -0
  158. package/dist/ui/hooks/useDoublePress.js +44 -0
  159. package/dist/ui/hooks/useDoublePress.js.map +1 -0
  160. package/dist/ui/hooks/useTerminalProgress.d.ts +1 -1
  161. package/dist/ui/hooks/useTerminalProgress.d.ts.map +1 -1
  162. package/dist/ui/hooks/useTerminalProgress.js +18 -15
  163. package/dist/ui/hooks/useTerminalProgress.js.map +1 -1
  164. package/dist/ui/hooks/useTerminalTitle.d.ts +3 -6
  165. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  166. package/dist/ui/hooks/useTerminalTitle.js +10 -19
  167. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  168. package/dist/ui/live-item-flush.d.ts +4 -0
  169. package/dist/ui/live-item-flush.d.ts.map +1 -1
  170. package/dist/ui/live-item-flush.js +36 -0
  171. package/dist/ui/live-item-flush.js.map +1 -1
  172. package/dist/ui/login.d.ts.map +1 -1
  173. package/dist/ui/login.js +3 -0
  174. package/dist/ui/login.js.map +1 -1
  175. package/dist/ui/render.d.ts +3 -1
  176. package/dist/ui/render.d.ts.map +1 -1
  177. package/dist/ui/render.js +9 -3
  178. package/dist/ui/render.js.map +1 -1
  179. package/dist/ui/stores/create-store.d.ts +15 -0
  180. package/dist/ui/stores/create-store.d.ts.map +1 -0
  181. package/dist/ui/stores/create-store.js +30 -0
  182. package/dist/ui/stores/create-store.js.map +1 -0
  183. package/dist/ui/stores/index.d.ts +3 -0
  184. package/dist/ui/stores/index.d.ts.map +1 -0
  185. package/dist/ui/stores/index.js +3 -0
  186. package/dist/ui/stores/index.js.map +1 -0
  187. package/dist/ui/stores/taskbar-store.d.ts +19 -0
  188. package/dist/ui/stores/taskbar-store.d.ts.map +1 -0
  189. package/dist/ui/stores/taskbar-store.js +59 -0
  190. package/dist/ui/stores/taskbar-store.js.map +1 -0
  191. package/dist/ui/theme/dark-ansi.json +48 -0
  192. package/dist/ui/theme/dark-daltonized.json +48 -0
  193. package/dist/ui/theme/dark.json +22 -1
  194. package/dist/ui/theme/detect-theme.d.ts +12 -8
  195. package/dist/ui/theme/detect-theme.d.ts.map +1 -1
  196. package/dist/ui/theme/detect-theme.js +58 -39
  197. package/dist/ui/theme/detect-theme.js.map +1 -1
  198. package/dist/ui/theme/light-ansi.json +48 -0
  199. package/dist/ui/theme/light-daltonized.json +48 -0
  200. package/dist/ui/theme/light.json +44 -23
  201. package/dist/ui/theme/theme.d.ts +27 -1
  202. package/dist/ui/theme/theme.d.ts.map +1 -1
  203. package/dist/ui/theme/theme.js +24 -1
  204. package/dist/ui/theme/theme.js.map +1 -1
  205. package/dist/ui/utils/highlight.d.ts +1 -1
  206. package/dist/ui/utils/highlight.d.ts.map +1 -1
  207. package/dist/ui/utils/highlight.js +24 -4
  208. package/dist/ui/utils/highlight.js.map +1 -1
  209. package/dist/ui/utils/hyperlink.d.ts +11 -0
  210. package/dist/ui/utils/hyperlink.d.ts.map +1 -0
  211. package/dist/ui/utils/hyperlink.js +27 -0
  212. package/dist/ui/utils/hyperlink.js.map +1 -0
  213. package/dist/ui/utils/markdown-cache.d.ts +5 -6
  214. package/dist/ui/utils/markdown-cache.d.ts.map +1 -1
  215. package/dist/ui/utils/markdown-cache.js +5 -5
  216. package/dist/ui/utils/markdown-cache.js.map +1 -1
  217. package/dist/ui/utils/signal.d.ts +21 -0
  218. package/dist/ui/utils/signal.d.ts.map +1 -0
  219. package/dist/ui/utils/signal.js +30 -0
  220. package/dist/ui/utils/signal.js.map +1 -0
  221. package/dist/ui/utils/supports-hyperlinks.d.ts +6 -0
  222. package/dist/ui/utils/supports-hyperlinks.d.ts.map +1 -0
  223. package/dist/ui/utils/supports-hyperlinks.js +52 -0
  224. package/dist/ui/utils/supports-hyperlinks.js.map +1 -0
  225. package/dist/ui/utils/token-to-ansi.d.ts +9 -0
  226. package/dist/ui/utils/token-to-ansi.d.ts.map +1 -0
  227. package/dist/ui/utils/token-to-ansi.js +305 -0
  228. package/dist/ui/utils/token-to-ansi.js.map +1 -0
  229. package/dist/utils/session-title.d.ts +19 -0
  230. package/dist/utils/session-title.d.ts.map +1 -0
  231. package/dist/utils/session-title.js +82 -0
  232. package/dist/utils/session-title.js.map +1 -0
  233. package/package.json +7 -5
package/dist/ui/App.js CHANGED
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
3
3
  import { Box, Text, Static, useStdout } from "ink";
4
4
  import { useTerminalSize } from "./hooks/useTerminalSize.js";
5
+ import { useDoublePress } from "./hooks/useDoublePress.js";
6
+ import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
5
7
  import crypto, { createHash } from "node:crypto";
6
8
  import { readFileSync, writeFileSync } from "node:fs";
7
9
  import { homedir } from "node:os";
@@ -16,6 +18,7 @@ import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
16
18
  import { ServerToolExecution } from "./components/ServerToolExecution.js";
17
19
  import { SubAgentPanel } from "./components/SubAgentPanel.js";
18
20
  import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
21
+ import { createWebSearchTool } from "../tools/web-search.js";
19
22
  import { StreamingArea } from "./components/StreamingArea.js";
20
23
  import { ActivityIndicator } from "./components/ActivityIndicator.js";
21
24
  import { InputArea } from "./components/InputArea.js";
@@ -25,8 +28,9 @@ import { PlanOverlay } from "./components/PlanOverlay.js";
25
28
  import { ModelSelector } from "./components/ModelSelector.js";
26
29
  import { TaskOverlay } from "./components/TaskOverlay.js";
27
30
  import { SkillsOverlay } from "./components/SkillsOverlay.js";
31
+ import { ThemeSelector } from "./components/ThemeSelector.js";
28
32
  import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
29
- import { useTheme } from "./theme/theme.js";
33
+ import { useTheme, useSetTheme } from "./theme/theme.js";
30
34
  import { useAnimationTick, useAnimationActive, deriveFrame, } from "./components/AnimationContext.js";
31
35
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
32
36
  import { useTerminalProgress } from "./hooks/useTerminalProgress.js";
@@ -34,6 +38,8 @@ import { getGitBranch } from "../utils/git.js";
34
38
  import { getModel, getContextWindow } from "../core/model-registry.js";
35
39
  import { SessionManager } from "../core/session-manager.js";
36
40
  import { log } from "../core/logger.js";
41
+ import { startPeriodicUpdateCheck, stopPeriodicUpdateCheck } from "../core/auto-update.js";
42
+ import { generateSessionTitle } from "../utils/session-title.js";
37
43
  import { SettingsManager } from "../core/settings-manager.js";
38
44
  import { shouldCompact, compact } from "../core/compaction/compactor.js";
39
45
  import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
@@ -42,7 +48,7 @@ import { loadCustomCommands } from "../core/custom-commands.js";
42
48
  import { buildSystemPrompt } from "../system-prompt.js";
43
49
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, stripDoneMarkers, } from "../utils/plan-steps.js";
44
50
  import { getMCPServers } from "../core/mcp/index.js";
45
- import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd } from "./live-item-flush.js";
51
+ import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
46
52
  import { Buddy } from "./buddy/Buddy.js";
47
53
  // ── Provider Error Hints ──────────────────────────────────
48
54
  /** Detect provider-side errors and return a user-facing hint. */
@@ -264,22 +270,23 @@ function markTaskInProgress(cwd, taskId) {
264
270
  // ── App Component ──────────────────────────────────────────
265
271
  export function App(props) {
266
272
  const theme = useTheme();
273
+ const switchTheme = useSetTheme();
267
274
  const { stdout } = useStdout();
268
275
  const { columns, resizeKey } = useTerminalSize();
269
276
  // Hoisted before terminal title hook so it can reference them
270
277
  const [lastUserMessage, setLastUserMessage] = useState("");
278
+ const [exitPending, setExitPending] = useState(false);
271
279
  const [planMode, setPlanMode] = useState(false);
280
+ const planModeLocalRef = useRef(false);
281
+ planModeLocalRef.current = planMode;
272
282
  // Terminal title — updated later after agentLoop is created
273
283
  // (hoisted here so the hook is always called in the same order)
274
- const [titlePhase, setTitlePhase] = useState("idle");
275
284
  const [titleRunning, setTitleRunning] = useState(false);
276
- const [titleToolNames, setTitleToolNames] = useState([]);
285
+ const [sessionTitle, setSessionTitle] = useState(undefined);
286
+ const sessionTitleGeneratedRef = useRef(false);
277
287
  useTerminalTitle({
278
- phase: titlePhase,
279
288
  isRunning: titleRunning,
280
- userMessage: lastUserMessage,
281
- activeToolNames: titleToolNames,
282
- planMode,
289
+ sessionTitle,
283
290
  });
284
291
  // Items scrolled into Static (history). For restored sessions, skip the
285
292
  // banner and add restored items via useEffect so Ink's <Static> treats them
@@ -322,20 +329,38 @@ export function App(props) {
322
329
  const persistedIndexRef = useRef(messagesRef.current.length);
323
330
  /** Last actual API-reported input token count (from turn_end). */
324
331
  const lastActualTokensRef = useRef(0);
325
- /** Timestamp of last compaction used for time-based cooldown. */
332
+ /** Timestamp (ms) when lastActualTokensRef was last updated by turn_end. */
333
+ const lastActualTokensTimestampRef = useRef(0);
334
+ /** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
326
335
  const lastCompactionTimeRef = useRef(0);
327
336
  const getId = () => String(nextIdRef.current++);
328
337
  // Two-phase flush: items waiting to be moved to Static history after the
329
338
  // live area has been cleared and Ink has committed the smaller output.
330
339
  const pendingFlushRef = useRef([]);
340
+ const [flushGeneration, setFlushGeneration] = useState(0);
341
+ /** Queue items for two-phase flush and signal the drain effect. */
342
+ const queueFlush = useCallback((items) => {
343
+ if (items.length === 0)
344
+ return;
345
+ pendingFlushRef.current = [...pendingFlushRef.current, ...items];
346
+ setFlushGeneration((g) => g + 1);
347
+ }, []);
331
348
  // Derive credentials for the current provider
332
349
  const currentCreds = props.credentialsByProvider?.[currentProvider];
333
350
  const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
334
351
  const activeAccountId = currentCreds?.accountId ?? props.accountId;
352
+ const activeBaseUrl = currentCreds?.baseUrl ?? props.baseUrl;
335
353
  // Load git branch
336
354
  useEffect(() => {
337
355
  getGitBranch(props.cwd).then(setGitBranch);
338
356
  }, [props.cwd]);
357
+ // Periodic update check during long sessions
358
+ useEffect(() => {
359
+ startPeriodicUpdateCheck(props.version, (msg) => {
360
+ setLiveItems((prev) => [...prev, { kind: "info", text: msg, id: getId() }]);
361
+ });
362
+ return () => stopPeriodicUpdateCheck();
363
+ }, [props.version]);
339
364
  // Load custom commands from .ezcoder/commands/
340
365
  const [customCommands, setCustomCommands] = useState([]);
341
366
  const reloadCustomCommands = useCallback(() => {
@@ -392,7 +417,11 @@ export function App(props) {
392
417
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
393
418
  setPlanAutoExpand(true);
394
419
  setOverlay("plan");
395
- planOverlayPendingRef.current = false;
420
+ // Don't clear planOverlayPendingRef here — keep it true until
421
+ // the user actually approves/rejects the plan. Clearing it on a
422
+ // timer causes a race where agent_done fires after the 300ms
423
+ // timeout but before the user interacts, triggering a premature
424
+ // completion sound.
396
425
  }, 300);
397
426
  return ("Plan submitted. Exiting plan mode.\n" +
398
427
  "The plan pane is opening for user review.\n" +
@@ -461,17 +490,24 @@ export function App(props) {
461
490
  signal: undefined,
462
491
  approvedPlanPath: approvedPlanPathRef.current,
463
492
  });
464
- // Replace spinner with completed notice
465
- setLiveItems((prev) => prev.map((item) => item.id === spinId
466
- ? {
467
- kind: "compacted",
468
- originalCount: result.result.originalCount,
469
- newCount: result.result.newCount,
470
- tokensBefore: result.result.tokensBeforeEstimate,
471
- tokensAfter: result.result.tokensAfterEstimate,
472
- id: spinId,
473
- }
474
- : item));
493
+ if (result.result.compacted) {
494
+ // Replace spinner with completed notice
495
+ setLiveItems((prev) => prev.map((item) => item.id === spinId
496
+ ? {
497
+ kind: "compacted",
498
+ originalCount: result.result.originalCount,
499
+ newCount: result.result.newCount,
500
+ tokensBefore: result.result.tokensBeforeEstimate,
501
+ tokensAfter: result.result.tokensAfterEstimate,
502
+ id: spinId,
503
+ }
504
+ : item));
505
+ }
506
+ else {
507
+ // Nothing was actually compacted — remove spinner silently
508
+ log("INFO", "compaction", `Compaction skipped: ${result.result.reason ?? "unknown"}`);
509
+ setLiveItems((prev) => prev.filter((item) => item.id !== spinId));
510
+ }
475
511
  return result.messages;
476
512
  }
477
513
  catch (err) {
@@ -502,7 +538,6 @@ export function App(props) {
502
538
  if (options?.force) {
503
539
  const result = await compactConversation(messages);
504
540
  lastCompactionTimeRef.current = Date.now();
505
- lastActualTokensRef.current = 0; // Reset stale pre-compaction count
506
541
  return result;
507
542
  }
508
543
  if (!autoCompact)
@@ -513,73 +548,41 @@ export function App(props) {
513
548
  return messages;
514
549
  }
515
550
  const contextWindow = getContextWindow(currentModel);
516
- // Prefer actual API-reported tokens over char-based estimate
517
- const actualTokens = lastActualTokensRef.current > 0 ? lastActualTokensRef.current : undefined;
551
+ // Prefer actual API-reported tokens over char-based estimate, but only
552
+ // when the token count was recorded AFTER the most recent compaction.
553
+ // A count from before compaction is stale — it reflects the old context
554
+ // size and would trigger compaction again immediately for no reason.
555
+ const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
556
+ const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
518
557
  if (shouldCompact(messages, contextWindow, threshold, actualTokens)) {
558
+ const before = messages.length;
519
559
  const result = await compactConversation(messages);
520
560
  lastCompactionTimeRef.current = Date.now();
521
- lastActualTokensRef.current = 0; // Reset stale pre-compaction count
561
+ // If compaction was a no-op (e.g. too few middle messages to summarize),
562
+ // return the original reference so the agent loop doesn't replace messages.
563
+ if (result.length === before)
564
+ return messages;
522
565
  return result;
523
566
  }
524
567
  return messages;
525
568
  }, [currentModel, compactConversation]);
526
- // ── Background task bar state ───────────────────────────
527
- const [bgTasks, setBgTasks] = useState([]);
528
- const [taskBarFocused, setTaskBarFocused] = useState(false);
529
- const [taskBarExpanded, setTaskBarExpanded] = useState(false);
530
- const [selectedTaskIndex, setSelectedTaskIndex] = useState(0);
531
- // Poll ProcessManager every 2s for running tasks
532
- useEffect(() => {
533
- if (!props.processManager)
534
- return;
535
- const pm = props.processManager;
536
- const poll = () => {
537
- const running = pm.list().filter((p) => p.exitCode === null);
538
- setBgTasks(running);
539
- };
540
- poll();
541
- const interval = setInterval(poll, 2000);
542
- return () => clearInterval(interval);
543
- }, [props.processManager]);
544
- // Auto-exit task panel when all tasks gone
545
- useEffect(() => {
546
- if (bgTasks.length === 0) {
547
- setTaskBarFocused(false);
548
- setTaskBarExpanded(false);
549
- }
550
- // Clamp selected index
551
- const maxIdx = Math.min(bgTasks.length, 5) - 1;
552
- if (selectedTaskIndex > maxIdx && maxIdx >= 0) {
553
- setSelectedTaskIndex(maxIdx);
554
- }
555
- }, [bgTasks.length, selectedTaskIndex]);
556
- const handleFocusTaskBar = useCallback(() => {
557
- if (bgTasks.length > 0) {
558
- setTaskBarFocused(true);
559
- }
560
- }, [bgTasks.length]);
561
- const handleTaskBarExit = useCallback(() => {
562
- setTaskBarFocused(false);
563
- setTaskBarExpanded(false);
564
- }, []);
565
- const handleTaskBarExpand = useCallback(() => {
566
- setTaskBarExpanded(true);
567
- setSelectedTaskIndex(0);
568
- }, []);
569
- const handleTaskBarCollapse = useCallback(() => {
570
- setTaskBarExpanded(false);
571
- }, []);
569
+ // ── Background task bar state (external store) ──────────
570
+ const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
571
+ useTaskBarPolling(props.processManager);
572
+ const handleFocusTaskBar = useCallback(() => focusTaskBar(), []);
573
+ const handleTaskBarExit = useCallback(() => exitTaskBar(), []);
574
+ const handleTaskBarExpand = useCallback(() => expandTaskBar(), []);
575
+ const handleTaskBarCollapse = useCallback(() => collapseTaskBar(), []);
572
576
  const handleTaskKill = useCallback((id) => {
573
- props.processManager?.stop(id);
577
+ if (props.processManager)
578
+ killTask(props.processManager, id);
574
579
  }, [props.processManager]);
575
- const handleTaskNavigate = useCallback((index) => {
576
- setSelectedTaskIndex(index);
577
- }, []);
580
+ const handleTaskNavigate = useCallback((index) => navigateTaskBar(index), []);
578
581
  // Resolve fresh OAuth credentials before each agent loop run.
579
582
  // Falls back to the static props when authStorage is not available.
580
- const resolveCredentials = useCallback(async () => {
583
+ const resolveCredentials = useCallback(async (opts) => {
581
584
  if (props.authStorage) {
582
- const creds = await props.authStorage.resolveCredentials(currentProvider);
585
+ const creds = await props.authStorage.resolveCredentials(currentProvider, opts);
583
586
  return { apiKey: creds.accessToken, accountId: creds.accountId };
584
587
  }
585
588
  return { apiKey: activeApiKey, accountId: activeAccountId };
@@ -592,7 +595,7 @@ export function App(props) {
592
595
  maxTokens: props.maxTokens,
593
596
  thinking: thinkingEnabled ? (props.thinking ?? "medium") : undefined,
594
597
  apiKey: activeApiKey,
595
- baseUrl: props.baseUrl,
598
+ baseUrl: activeBaseUrl,
596
599
  accountId: activeAccountId,
597
600
  resolveCredentials,
598
601
  transformContext,
@@ -613,7 +616,57 @@ export function App(props) {
613
616
  }
614
617
  })();
615
618
  }
616
- }, [persistNewMessages, planMode, props.cwd, props.skills]),
619
+ // Generate session title after the first turn (background, best-effort)
620
+ if (!sessionTitleGeneratedRef.current) {
621
+ sessionTitleGeneratedRef.current = true;
622
+ const msgs = messagesRef.current;
623
+ // Find the first user message and first assistant text
624
+ const userMsg = msgs.find((m) => m.role === "user");
625
+ const assistantMsg = msgs.find((m) => m.role === "assistant");
626
+ const userText = typeof userMsg?.content === "string"
627
+ ? userMsg.content
628
+ : Array.isArray(userMsg?.content)
629
+ ? userMsg.content
630
+ .filter((c) => c.type === "text")
631
+ .map((c) => c.text)
632
+ .join(" ")
633
+ : "";
634
+ const assistantText = typeof assistantMsg?.content === "string"
635
+ ? assistantMsg.content
636
+ : Array.isArray(assistantMsg?.content)
637
+ ? assistantMsg.content
638
+ .filter((c) => c.type === "text")
639
+ .map((c) => c.text)
640
+ .join(" ")
641
+ : "";
642
+ if (userText) {
643
+ generateSessionTitle({
644
+ provider: currentProvider,
645
+ userMessage: userText,
646
+ assistantPreview: assistantText.slice(0, 200),
647
+ apiKey: activeApiKey,
648
+ baseUrl: activeBaseUrl,
649
+ accountId: activeAccountId,
650
+ resolveCredentials,
651
+ }).then((title) => {
652
+ setSessionTitle(title);
653
+ log("INFO", "title", `Session title generated: ${title}`);
654
+ }, () => {
655
+ // Best-effort — silently ignore failures
656
+ });
657
+ }
658
+ }
659
+ }, [
660
+ persistNewMessages,
661
+ planMode,
662
+ props.cwd,
663
+ props.skills,
664
+ currentProvider,
665
+ activeApiKey,
666
+ activeAccountId,
667
+ activeBaseUrl,
668
+ resolveCredentials,
669
+ ]),
617
670
  onTurnText: useCallback((text, thinking, thinkingMs) => {
618
671
  // Track [DONE:n] markers for plan step progress
619
672
  if (planStepsRef.current.length > 0) {
@@ -629,13 +682,28 @@ export function App(props) {
629
682
  // Flush all completed items from the previous turn to Static history.
630
683
  // This keeps liveItems bounded per-turn, preventing Ink's live area from
631
684
  // growing unbounded, which makes Ink's live-area re-renders expensive.
685
+ //
686
+ // Items are queued in pendingFlushRef (not sent to setHistory directly)
687
+ // so the Static write happens in a SEPARATE render cycle from the
688
+ // live-area change — avoiding both Ink cursor-math clipping and the
689
+ // brief duplicate that occurred when setHistory was nested inside the
690
+ // setLiveItems updater.
632
691
  setLiveItems((prev) => {
633
692
  const flushed = flushOnTurnText(prev);
634
693
  if (flushed.length > 0) {
635
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
694
+ queueFlush(flushed);
636
695
  }
637
696
  const displayText = planStepsRef.current.length > 0 ? stripDoneMarkers(text) : text;
638
- return [{ kind: "assistant", text: displayText, thinking, thinkingMs, id: getId() }];
697
+ return [
698
+ {
699
+ kind: "assistant",
700
+ text: displayText,
701
+ thinking,
702
+ thinkingMs,
703
+ planMode: planModeLocalRef.current,
704
+ id: getId(),
705
+ },
706
+ ];
639
707
  });
640
708
  }, []),
641
709
  onToolStart: useCallback((toolCallId, name, args) => {
@@ -646,7 +714,7 @@ export function App(props) {
646
714
  setLiveItems((prev) => {
647
715
  const { flushed, remaining } = partitionCompleted(prev);
648
716
  if (flushed.length > 0) {
649
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
717
+ queueFlush(flushed);
650
718
  }
651
719
  return remaining;
652
720
  });
@@ -707,6 +775,24 @@ export function App(props) {
707
775
  }
708
776
  }, []),
709
777
  onToolUpdate: useCallback((toolCallId, update) => {
778
+ const u = update;
779
+ // Bash progress streaming — append output to tool_start item
780
+ if (u.type === "bash_progress") {
781
+ setLiveItems((prev) => {
782
+ const idx = prev.findIndex((item) => item.kind === "tool_start" && item.toolCallId === toolCallId);
783
+ if (idx === -1)
784
+ return prev;
785
+ const item = prev[idx];
786
+ const next = [...prev];
787
+ next[idx] = {
788
+ ...item,
789
+ progressOutput: (item.progressOutput ?? "") + String(u.output ?? ""),
790
+ };
791
+ return next;
792
+ });
793
+ return;
794
+ }
795
+ // Subagent updates
710
796
  setLiveItems((prev) => {
711
797
  const groupIdx = prev.findIndex((item) => item.kind === "subagent_group");
712
798
  if (groupIdx === -1)
@@ -759,7 +845,7 @@ export function App(props) {
759
845
  // Flush completed items to Static to keep the live area small
760
846
  const { flushed, remaining } = partitionCompleted(next);
761
847
  if (flushed.length > 0) {
762
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
848
+ queueFlush(flushed);
763
849
  }
764
850
  return remaining;
765
851
  });
@@ -818,7 +904,14 @@ export function App(props) {
818
904
  // Flush completed items to Static to keep the live area small
819
905
  const { flushed, remaining } = partitionCompleted(updated);
820
906
  if (flushed.length > 0) {
821
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
907
+ queueFlush(flushed);
908
+ return remaining;
909
+ }
910
+ // Overflow flush: if live area is still large, flush aggressively
911
+ const overflow = flushOverflow(updated);
912
+ if (overflow.flushed.length > 0) {
913
+ queueFlush(overflow.flushed);
914
+ return overflow.remaining;
822
915
  }
823
916
  return remaining;
824
917
  });
@@ -831,7 +924,7 @@ export function App(props) {
831
924
  setLiveItems((prev) => {
832
925
  const { flushed, remaining } = partitionCompleted(prev);
833
926
  if (flushed.length > 0) {
834
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
927
+ queueFlush(flushed);
835
928
  }
836
929
  return [
837
930
  ...remaining,
@@ -882,7 +975,7 @@ export function App(props) {
882
975
  // Flush completed items to Static
883
976
  const { flushed, remaining } = partitionCompleted(updated);
884
977
  if (flushed.length > 0) {
885
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
978
+ queueFlush(flushed);
886
979
  }
887
980
  return remaining;
888
981
  });
@@ -895,15 +988,19 @@ export function App(props) {
895
988
  ...(usage.cacheRead != null && { cacheRead: String(usage.cacheRead) }),
896
989
  ...(usage.cacheWrite != null && { cacheWrite: String(usage.cacheWrite) }),
897
990
  });
898
- // Track actual token count for compaction decisions
991
+ // Track actual token count for compaction decisions.
992
+ // Anthropic has separate input/output limits — only count input.
993
+ // All other providers share the context window — count both.
994
+ const inputContext = usage.inputTokens + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
899
995
  lastActualTokensRef.current =
900
- usage.inputTokens + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
996
+ currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
997
+ lastActualTokensTimestampRef.current = Date.now();
901
998
  // For tool-only turns (no text), flush completed items to Static so
902
999
  // liveItems doesn't grow unbounded across consecutive tool-only turns.
903
1000
  setLiveItems((prev) => {
904
1001
  const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
905
1002
  if (flushed.length > 0) {
906
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
1003
+ queueFlush(flushed);
907
1004
  }
908
1005
  return remaining;
909
1006
  });
@@ -926,9 +1023,8 @@ export function App(props) {
926
1023
  // separate render cycle so the Static write never coincides with
927
1024
  // a live-area height change in the same frame.
928
1025
  setLiveItems((prev) => {
929
- if (prev.length > 0) {
930
- pendingFlushRef.current = prev;
931
- }
1026
+ if (prev.length > 0)
1027
+ queueFlush(prev);
932
1028
  return [];
933
1029
  });
934
1030
  // Run-all: auto-start next pending task after a short delay
@@ -1007,9 +1103,8 @@ export function App(props) {
1007
1103
  ? undefined
1008
1104
  : content.filter((c) => c.type === "image").length || undefined;
1009
1105
  setLiveItems((prev) => {
1010
- if (prev.length > 0) {
1011
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(prev)]));
1012
- }
1106
+ if (prev.length > 0)
1107
+ queueFlush(prev);
1013
1108
  return [];
1014
1109
  });
1015
1110
  const userItem = {
@@ -1036,14 +1131,11 @@ export function App(props) {
1036
1131
  pendingFlushRef.current = [];
1037
1132
  setHistory((h) => compactHistory([...h, ...trimFlushedItems(items)]));
1038
1133
  }
1039
- });
1134
+ }, [flushGeneration]);
1040
1135
  // Sync terminal title with agent loop state
1041
- const activeToolNamesKey = agentLoop.activeToolCalls.map((tc) => tc.name).join(",");
1042
1136
  useEffect(() => {
1043
- setTitlePhase(agentLoop.activityPhase);
1044
1137
  setTitleRunning(agentLoop.isRunning);
1045
- setTitleToolNames(agentLoop.activeToolCalls.map((tc) => tc.name));
1046
- }, [agentLoop.activityPhase, agentLoop.isRunning, activeToolNamesKey]);
1138
+ }, [agentLoop.isRunning]);
1047
1139
  // Terminal progress bar (OSC 9;4) — pulsing bar in supported terminals
1048
1140
  useTerminalProgress(agentLoop.isRunning, agentLoop.activeToolCalls.length > 0);
1049
1141
  // Animated thinking border — derived from global animation tick
@@ -1084,6 +1176,9 @@ export function App(props) {
1084
1176
  // Clear terminal screen + scrollback — needed because Ink's <Static>
1085
1177
  // writes directly to stdout and can't be removed by clearing React state
1086
1178
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1179
+ // Discard any items queued for two-phase flush so they don't leak
1180
+ // into the new session after the Static remount.
1181
+ pendingFlushRef.current = [];
1087
1182
  setHistory([{ kind: "banner", id: "banner" }]);
1088
1183
  setLiveItems([]);
1089
1184
  setDoneStatus(null);
@@ -1094,11 +1189,22 @@ export function App(props) {
1094
1189
  void (async () => {
1095
1190
  const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
1096
1191
  messagesRef.current = [{ role: "system", content: newPrompt }];
1192
+ persistedIndexRef.current = messagesRef.current.length;
1097
1193
  })();
1098
1194
  agentLoop.reset();
1195
+ setSessionTitle(undefined);
1196
+ sessionTitleGeneratedRef.current = false;
1197
+ // Bump staticKey to force Ink's <Static> to remount, discarding its
1198
+ // internal record of previously rendered items so they don't reappear.
1199
+ setStaticKey((k) => k + 1);
1099
1200
  setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
1100
1201
  return;
1101
1202
  }
1203
+ // Handle /theme — open theme selector overlay
1204
+ if (trimmed === "/theme" || trimmed === "/t") {
1205
+ setOverlay("theme");
1206
+ return;
1207
+ }
1102
1208
  // Handle /plan — toggle plan mode
1103
1209
  if (trimmed === "/plan" || trimmed === "/plan on") {
1104
1210
  setPlanMode(true);
@@ -1175,7 +1281,7 @@ export function App(props) {
1175
1281
  // Move live items into history before starting
1176
1282
  setLiveItems((prev) => {
1177
1283
  if (prev.length > 0) {
1178
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(prev)]));
1284
+ pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
1179
1285
  }
1180
1286
  return [];
1181
1287
  });
@@ -1278,10 +1384,14 @@ export function App(props) {
1278
1384
  setLiveItems((prev) => [...prev, queuedItem]);
1279
1385
  return;
1280
1386
  }
1281
- // Move any remaining live items into history (Static) before starting new turn
1387
+ // Move any remaining live items into history (Static) before starting a
1388
+ // new turn. Must go through queueFlush so flushGeneration bumps and the
1389
+ // drain effect actually runs — mutating pendingFlushRef directly here
1390
+ // stashed items that nothing was signalled to pick up, so they sat in
1391
+ // limbo until some unrelated later code path happened to call queueFlush.
1282
1392
  setLiveItems((prev) => {
1283
1393
  if (prev.length > 0) {
1284
- setHistory((h) => compactHistory([...h, ...trimFlushedItems(prev)]));
1394
+ queueFlush(prev);
1285
1395
  }
1286
1396
  return [];
1287
1397
  });
@@ -1323,15 +1433,16 @@ export function App(props) {
1323
1433
  ]);
1324
1434
  }
1325
1435
  }, [agentLoop, props.onSlashCommand, compactConversation]);
1436
+ const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
1326
1437
  const handleAbort = useCallback(() => {
1327
1438
  if (agentLoop.isRunning) {
1328
1439
  agentLoop.clearQueue();
1329
1440
  agentLoop.abort();
1330
1441
  }
1331
1442
  else {
1332
- process.exit(0);
1443
+ handleDoubleExit();
1333
1444
  }
1334
- }, [agentLoop]);
1445
+ }, [agentLoop, handleDoubleExit]);
1335
1446
  const handleToggleThinking = useCallback(() => {
1336
1447
  setThinkingEnabled((prev) => {
1337
1448
  const next = !prev;
@@ -1355,40 +1466,57 @@ export function App(props) {
1355
1466
  const newProvider = value.slice(0, colonIdx);
1356
1467
  const newModelId = value.slice(colonIdx + 1);
1357
1468
  log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
1358
- // Reconnect MCP servers when provider changes
1469
+ // Handle provider-specific tool changes when provider changes
1359
1470
  setCurrentProvider((prevProvider) => {
1360
- if (newProvider !== prevProvider && props.mcpManager) {
1361
- void (async () => {
1362
- // Disconnect old MCP servers
1363
- await props.mcpManager.dispose();
1364
- // Remove old MCP tools, connect new ones
1365
- let apiKey;
1366
- if (newProvider === "glm" && props.authStorage) {
1471
+ if (newProvider !== prevProvider) {
1472
+ // Add/remove client-side web_search tool based on provider.
1473
+ // Anthropic has native server-side web search; all other providers need the client tool.
1474
+ setCurrentTools((prev) => {
1475
+ const hasWebSearch = prev.some((t) => t.name === "web_search");
1476
+ if (newProvider === "anthropic" && hasWebSearch) {
1477
+ // Switching TO anthropic remove client-side web_search (server-side handles it)
1478
+ return prev.filter((t) => t.name !== "web_search");
1479
+ }
1480
+ else if (newProvider !== "anthropic" && !hasWebSearch) {
1481
+ // Switching FROM anthropic — add client-side web_search
1482
+ return [...prev, createWebSearchTool()];
1483
+ }
1484
+ return prev;
1485
+ });
1486
+ // Reconnect MCP servers
1487
+ if (props.mcpManager) {
1488
+ void (async () => {
1489
+ // Disconnect old MCP servers
1490
+ await props.mcpManager.dispose();
1491
+ // Remove old MCP tools, connect new ones
1492
+ let apiKey;
1493
+ if (newProvider === "glm" && props.authStorage) {
1494
+ try {
1495
+ const glmCreds = await props.authStorage.resolveCredentials("glm");
1496
+ apiKey = glmCreds.accessToken;
1497
+ }
1498
+ catch {
1499
+ // GLM not configured — skip Z.AI MCP servers
1500
+ }
1501
+ }
1502
+ else if (newProvider === "glm") {
1503
+ apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
1504
+ }
1367
1505
  try {
1368
- const glmCreds = await props.authStorage.resolveCredentials("glm");
1369
- apiKey = glmCreds.accessToken;
1506
+ const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
1507
+ setCurrentTools((prev) => [
1508
+ ...prev.filter((t) => !t.name.startsWith("mcp__")),
1509
+ ...mcpTools,
1510
+ ]);
1511
+ log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
1370
1512
  }
1371
- catch {
1372
- // GLM not configured skip Z.AI MCP servers
1513
+ catch (err) {
1514
+ log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
1515
+ // Still remove old MCP tools even if reconnection fails
1516
+ setCurrentTools((prev) => prev.filter((t) => !t.name.startsWith("mcp__")));
1373
1517
  }
1374
- }
1375
- else if (newProvider === "glm") {
1376
- apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
1377
- }
1378
- try {
1379
- const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
1380
- setCurrentTools((prev) => [
1381
- ...prev.filter((t) => !t.name.startsWith("mcp__")),
1382
- ...mcpTools,
1383
- ]);
1384
- log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
1385
- }
1386
- catch (err) {
1387
- log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
1388
- // Still remove old MCP tools even if reconnection fails
1389
- setCurrentTools((prev) => prev.filter((t) => !t.name.startsWith("mcp__")));
1390
- }
1391
- })();
1518
+ })();
1519
+ }
1392
1520
  }
1393
1521
  return newProvider;
1394
1522
  });
@@ -1408,12 +1536,28 @@ export function App(props) {
1408
1536
  });
1409
1537
  }
1410
1538
  }, [props.settingsFile, props.mcpManager, props.credentialsByProvider, props.authStorage]);
1539
+ const handleThemeSelect = useCallback((name) => {
1540
+ setOverlay(null);
1541
+ if (switchTheme) {
1542
+ switchTheme(name);
1543
+ }
1544
+ // Persist to settings
1545
+ if (props.settingsFile) {
1546
+ const sm = new SettingsManager(props.settingsFile);
1547
+ sm.load().then(() => sm.set("theme", name));
1548
+ }
1549
+ setLiveItems((prev) => [
1550
+ ...prev,
1551
+ { kind: "info", text: `Theme switched to: ${name}`, id: getId() },
1552
+ ]);
1553
+ }, [switchTheme, props.settingsFile]);
1411
1554
  // All available slash commands for the command palette
1412
1555
  const allCommands = useMemo(() => [
1413
1556
  { name: "model", aliases: ["m"], description: "Switch model" },
1414
1557
  { name: "compact", aliases: ["c"], description: "Compact conversation" },
1415
1558
  { name: "clear", aliases: [], description: "Clear session and terminal" },
1416
1559
  { name: "quit", aliases: ["q", "exit"], description: "Exit the agent" },
1560
+ { name: "theme", aliases: ["t"], description: "Switch theme" },
1417
1561
  { name: "plan", aliases: [], description: "Toggle plan mode (on/off)" },
1418
1562
  { name: "plans", aliases: [], description: "Open plans pane" },
1419
1563
  ...PROMPT_COMMANDS.map((cmd) => ({
@@ -1432,15 +1576,15 @@ export function App(props) {
1432
1576
  case "tombstone":
1433
1577
  return null;
1434
1578
  case "banner":
1435
- return (_jsx(Banner, { version: props.version, model: props.model, provider: props.provider, cwd: props.cwd, taskCount: taskCount }, item.id));
1579
+ return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: props.cwd, taskCount: taskCount }, item.id));
1436
1580
  case "user":
1437
1581
  return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
1438
1582
  case "task":
1439
1583
  return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Task: " }), _jsx(Text, { color: theme.success, children: item.title })] }) }, item.id));
1440
1584
  case "assistant":
1441
- return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, showThinking: props.showThinking }, item.id));
1585
+ return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, showThinking: props.showThinking, planMode: item.planMode }, item.id));
1442
1586
  case "tool_start":
1443
- return _jsx(ToolExecution, { status: "running", name: item.name, args: item.args }, item.id);
1587
+ return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput }, item.id));
1444
1588
  case "tool_done":
1445
1589
  return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
1446
1590
  case "tool_group":
@@ -1456,7 +1600,7 @@ export function App(props) {
1456
1600
  case "info":
1457
1601
  return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
1458
1602
  case "plan_transition":
1459
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? " " : " ", item.text] }) }, item.id));
1603
+ return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? " " : " ", item.text] }) }, item.id));
1460
1604
  case "queued":
1461
1605
  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
1462
1606
  ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
@@ -1547,11 +1691,17 @@ export function App(props) {
1547
1691
  setStaticKey((k) => k + 1);
1548
1692
  setOverlay(null);
1549
1693
  } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
1694
+ planOverlayPendingRef.current = false;
1550
1695
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1551
1696
  setStaticKey((k) => k + 1);
1552
1697
  setPlanAutoExpand(false);
1553
1698
  setOverlay(null);
1554
1699
  }, onApprove: (planPath) => {
1700
+ log("INFO", "plan", "Plan approved — transitioning to implementation", {
1701
+ planPath,
1702
+ });
1703
+ // Plan overlay dismissed — allow future onDone to fire normally
1704
+ planOverlayPendingRef.current = false;
1555
1705
  // Store approved plan path — will be injected into the new system prompt
1556
1706
  approvedPlanPathRef.current = planPath;
1557
1707
  // Extract plan steps for progress tracking
@@ -1569,28 +1719,36 @@ export function App(props) {
1569
1719
  setOverlay(null);
1570
1720
  // Rebuild system prompt with the approved plan, then reset the session
1571
1721
  void (async () => {
1572
- const newPrompt = await buildSystemPrompt(props.cwd, props.skills, false, planPath);
1573
- messagesRef.current = [{ role: "system", content: newPrompt }];
1574
- agentLoop.reset();
1575
- persistedIndexRef.current = messagesRef.current.length;
1576
- // Create a new session file
1577
- const sm = sessionManagerRef.current;
1578
- if (sm) {
1579
- const s = await sm.create(props.cwd, currentProvider, currentModel);
1580
- sessionPathRef.current = s.path;
1722
+ try {
1723
+ const newPrompt = await buildSystemPrompt(props.cwd, props.skills, false, planPath);
1724
+ messagesRef.current = [{ role: "system", content: newPrompt }];
1725
+ agentLoop.reset();
1726
+ persistedIndexRef.current = messagesRef.current.length;
1727
+ // Create a new session file
1728
+ const sm = sessionManagerRef.current;
1729
+ if (sm) {
1730
+ const s = await sm.create(props.cwd, currentProvider, currentModel);
1731
+ sessionPathRef.current = s.path;
1732
+ }
1733
+ // Start implementation with a clean context
1734
+ setLiveItems([
1735
+ {
1736
+ kind: "info",
1737
+ text: "Plan approved — starting fresh session for implementation",
1738
+ id: getId(),
1739
+ },
1740
+ ]);
1741
+ setDoneStatus(null);
1742
+ await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
1743
+ }
1744
+ catch (err) {
1745
+ const errMsg = err instanceof Error ? err.message : String(err);
1746
+ log("ERROR", "error", errMsg);
1747
+ setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
1581
1748
  }
1582
- // Start implementation with a clean context
1583
- setLiveItems([
1584
- {
1585
- kind: "info",
1586
- text: "Plan approved — starting fresh session for implementation",
1587
- id: getId(),
1588
- },
1589
- ]);
1590
- setDoneStatus(null);
1591
- await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
1592
1749
  })();
1593
1750
  }, onReject: (planPath, feedback) => {
1751
+ planOverlayPendingRef.current = false;
1594
1752
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1595
1753
  setStaticKey((k) => k + 1);
1596
1754
  setPlanAutoExpand(false);
@@ -1603,10 +1761,15 @@ export function App(props) {
1603
1761
  ...prev,
1604
1762
  { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
1605
1763
  ]);
1606
- void agentLoop.run(msg);
1764
+ void agentLoop.run(msg).catch((err) => {
1765
+ const errMsg = err instanceof Error ? err.message : String(err);
1766
+ log("ERROR", "error", errMsg);
1767
+ setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
1768
+ });
1607
1769
  } })) : (_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"
1608
1770
  ? THINKING_BORDER_COLORS[thinkingBorderFrame]
1609
- : "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, 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 }) })) : (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: () => {
1771
+ : "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 &&
1772
+ !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: () => {
1610
1773
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1611
1774
  setOverlay("tasks");
1612
1775
  }, onToggleSkills: () => {
@@ -1625,6 +1788,6 @@ export function App(props) {
1625
1788
  id: getId(),
1626
1789
  },
1627
1790
  ]);
1628
- }, 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, linesAdded: agentLoop.linesChanged.added, linesRemoved: agentLoop.linesChanged.removed, cwd: props.cwd, gitBranch: gitBranch, thinkingEnabled: thinkingEnabled, planMode: planMode })), buddyEnabled && _jsx(Buddy, { phase: agentLoop.activityPhase }), bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate }))] }))] }));
1791
+ }, cwd: props.cwd, commands: allCommands }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, cwd: props.cwd, gitBranch: gitBranch, thinkingEnabled: thinkingEnabled, planMode: planMode, exitPending: exitPending })), buddyEnabled && _jsx(Buddy, { phase: agentLoop.activityPhase }), bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate }))] }))] }));
1629
1792
  }
1630
1793
  //# sourceMappingURL=App.js.map