@prestyj/cli 4.2.56 → 4.2.75

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 (195) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +4 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +6 -5
  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 +11 -0
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/compaction/compactor.d.ts +3 -1
  12. package/dist/core/compaction/compactor.d.ts.map +1 -1
  13. package/dist/core/compaction/compactor.js +28 -5
  14. package/dist/core/compaction/compactor.js.map +1 -1
  15. package/dist/core/compaction/compactor.test.js +86 -10
  16. package/dist/core/compaction/compactor.test.js.map +1 -1
  17. package/dist/core/compaction/token-estimator.test.js +1 -1
  18. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  19. package/dist/core/model-registry.js +8 -8
  20. package/dist/core/model-registry.js.map +1 -1
  21. package/dist/core/settings-manager.d.ts +1 -0
  22. package/dist/core/settings-manager.d.ts.map +1 -1
  23. package/dist/core/settings-manager.js +2 -0
  24. package/dist/core/settings-manager.js.map +1 -1
  25. package/dist/core/slash-commands.d.ts.map +1 -1
  26. package/dist/core/slash-commands.js +18 -4
  27. package/dist/core/slash-commands.js.map +1 -1
  28. package/dist/core/telegram.d.ts.map +1 -1
  29. package/dist/core/telegram.js +3 -0
  30. package/dist/core/telegram.js.map +1 -1
  31. package/dist/interactive.d.ts.map +1 -1
  32. package/dist/interactive.js +1 -0
  33. package/dist/interactive.js.map +1 -1
  34. package/dist/system-prompt.d.ts.map +1 -1
  35. package/dist/system-prompt.js +21 -3
  36. package/dist/system-prompt.js.map +1 -1
  37. package/dist/tools/bash.d.ts.map +1 -1
  38. package/dist/tools/bash.js +4 -7
  39. package/dist/tools/bash.js.map +1 -1
  40. package/dist/tools/edit.d.ts.map +1 -1
  41. package/dist/tools/edit.js +4 -1
  42. package/dist/tools/edit.js.map +1 -1
  43. package/dist/tools/overflow.d.ts +6 -0
  44. package/dist/tools/overflow.d.ts.map +1 -0
  45. package/dist/tools/overflow.js +13 -0
  46. package/dist/tools/overflow.js.map +1 -0
  47. package/dist/tools/read.d.ts.map +1 -1
  48. package/dist/tools/read.js +6 -2
  49. package/dist/tools/read.js.map +1 -1
  50. package/dist/tools/truncate-utils.test.js +11 -11
  51. package/dist/tools/truncate-utils.test.js.map +1 -1
  52. package/dist/tools/truncate.d.ts +6 -4
  53. package/dist/tools/truncate.d.ts.map +1 -1
  54. package/dist/tools/truncate.js +12 -10
  55. package/dist/tools/truncate.js.map +1 -1
  56. package/dist/ui/App.d.ts +1 -0
  57. package/dist/ui/App.d.ts.map +1 -1
  58. package/dist/ui/App.js +261 -62
  59. package/dist/ui/App.js.map +1 -1
  60. package/dist/ui/buddy/Buddy.d.ts +15 -0
  61. package/dist/ui/buddy/Buddy.d.ts.map +1 -0
  62. package/dist/ui/buddy/Buddy.js +118 -0
  63. package/dist/ui/buddy/Buddy.js.map +1 -0
  64. package/dist/ui/buddy/gacha.d.ts +8 -0
  65. package/dist/ui/buddy/gacha.d.ts.map +1 -0
  66. package/dist/ui/buddy/gacha.js +94 -0
  67. package/dist/ui/buddy/gacha.js.map +1 -0
  68. package/dist/ui/buddy/species.d.ts +52 -0
  69. package/dist/ui/buddy/species.d.ts.map +1 -0
  70. package/dist/ui/buddy/species.js +257 -0
  71. package/dist/ui/buddy/species.js.map +1 -0
  72. package/dist/ui/components/ActivityIndicator.d.ts +10 -2
  73. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  74. package/dist/ui/components/ActivityIndicator.js +79 -22
  75. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  76. package/dist/ui/components/AnimationContext.d.ts +14 -2
  77. package/dist/ui/components/AnimationContext.d.ts.map +1 -1
  78. package/dist/ui/components/AnimationContext.js +54 -8
  79. package/dist/ui/components/AnimationContext.js.map +1 -1
  80. package/dist/ui/components/AssistantMessage.d.ts +2 -1
  81. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  82. package/dist/ui/components/AssistantMessage.js +5 -4
  83. package/dist/ui/components/AssistantMessage.js.map +1 -1
  84. package/dist/ui/components/BackgroundTasksBar.js +2 -2
  85. package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
  86. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  87. package/dist/ui/components/CompactionNotice.js +2 -1
  88. package/dist/ui/components/CompactionNotice.js.map +1 -1
  89. package/dist/ui/components/Footer.d.ts +3 -1
  90. package/dist/ui/components/Footer.d.ts.map +1 -1
  91. package/dist/ui/components/Footer.js +23 -48
  92. package/dist/ui/components/Footer.js.map +1 -1
  93. package/dist/ui/components/InputArea.d.ts.map +1 -1
  94. package/dist/ui/components/InputArea.js +2 -1
  95. package/dist/ui/components/InputArea.js.map +1 -1
  96. package/dist/ui/components/Markdown.d.ts +11 -6
  97. package/dist/ui/components/Markdown.d.ts.map +1 -1
  98. package/dist/ui/components/Markdown.js +170 -18
  99. package/dist/ui/components/Markdown.js.map +1 -1
  100. package/dist/ui/components/MessageResponse.d.ts +25 -0
  101. package/dist/ui/components/MessageResponse.d.ts.map +1 -0
  102. package/dist/ui/components/MessageResponse.js +42 -0
  103. package/dist/ui/components/MessageResponse.js.map +1 -0
  104. package/dist/ui/components/NoSelect.d.ts +20 -0
  105. package/dist/ui/components/NoSelect.d.ts.map +1 -0
  106. package/dist/ui/components/NoSelect.js +14 -0
  107. package/dist/ui/components/NoSelect.js.map +1 -0
  108. package/dist/ui/components/PlanProgress.d.ts +7 -0
  109. package/dist/ui/components/PlanProgress.d.ts.map +1 -0
  110. package/dist/ui/components/PlanProgress.js +30 -0
  111. package/dist/ui/components/PlanProgress.js.map +1 -0
  112. package/dist/ui/components/Ratchet.d.ts +20 -0
  113. package/dist/ui/components/Ratchet.d.ts.map +1 -0
  114. package/dist/ui/components/Ratchet.js +30 -0
  115. package/dist/ui/components/Ratchet.js.map +1 -0
  116. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  117. package/dist/ui/components/ServerToolExecution.js +5 -6
  118. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  119. package/dist/ui/components/Spinner.d.ts +8 -2
  120. package/dist/ui/components/Spinner.d.ts.map +1 -1
  121. package/dist/ui/components/Spinner.js +52 -5
  122. package/dist/ui/components/Spinner.js.map +1 -1
  123. package/dist/ui/components/StreamingArea.d.ts +2 -1
  124. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  125. package/dist/ui/components/StreamingArea.js +9 -44
  126. package/dist/ui/components/StreamingArea.js.map +1 -1
  127. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  128. package/dist/ui/components/SubAgentPanel.js +6 -4
  129. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  130. package/dist/ui/components/ThinkingBlock.d.ts +2 -1
  131. package/dist/ui/components/ThinkingBlock.d.ts.map +1 -1
  132. package/dist/ui/components/ThinkingBlock.js +2 -2
  133. package/dist/ui/components/ThinkingBlock.js.map +1 -1
  134. package/dist/ui/components/ThinkingIndicator.d.ts.map +1 -1
  135. package/dist/ui/components/ThinkingIndicator.js +2 -1
  136. package/dist/ui/components/ThinkingIndicator.js.map +1 -1
  137. package/dist/ui/components/ToolExecution.d.ts +1 -0
  138. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  139. package/dist/ui/components/ToolExecution.js +44 -15
  140. package/dist/ui/components/ToolExecution.js.map +1 -1
  141. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  142. package/dist/ui/components/ToolGroupExecution.js +21 -17
  143. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  144. package/dist/ui/components/ToolUseLoader.d.ts +19 -0
  145. package/dist/ui/components/ToolUseLoader.d.ts.map +1 -0
  146. package/dist/ui/components/ToolUseLoader.js +42 -0
  147. package/dist/ui/components/ToolUseLoader.js.map +1 -0
  148. package/dist/ui/components/index.d.ts +1 -1
  149. package/dist/ui/components/index.d.ts.map +1 -1
  150. package/dist/ui/components/index.js +1 -1
  151. package/dist/ui/components/index.js.map +1 -1
  152. package/dist/ui/constants/figures.d.ts +30 -0
  153. package/dist/ui/constants/figures.d.ts.map +1 -0
  154. package/dist/ui/constants/figures.js +41 -0
  155. package/dist/ui/constants/figures.js.map +1 -0
  156. package/dist/ui/hooks/useAgentLoop.d.ts +16 -1
  157. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  158. package/dist/ui/hooks/useAgentLoop.js +49 -141
  159. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  160. package/dist/ui/hooks/useBlink.d.ts +11 -0
  161. package/dist/ui/hooks/useBlink.d.ts.map +1 -0
  162. package/dist/ui/hooks/useBlink.js +19 -0
  163. package/dist/ui/hooks/useBlink.js.map +1 -0
  164. package/dist/ui/hooks/useMinDisplayTime.d.ts +11 -0
  165. package/dist/ui/hooks/useMinDisplayTime.d.ts.map +1 -0
  166. package/dist/ui/hooks/useMinDisplayTime.js +34 -0
  167. package/dist/ui/hooks/useMinDisplayTime.js.map +1 -0
  168. package/dist/ui/hooks/useTerminalProgress.d.ts +2 -0
  169. package/dist/ui/hooks/useTerminalProgress.d.ts.map +1 -0
  170. package/dist/ui/hooks/useTerminalProgress.js +98 -0
  171. package/dist/ui/hooks/useTerminalProgress.js.map +1 -0
  172. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  173. package/dist/ui/hooks/useTerminalTitle.js +2 -1
  174. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  175. package/dist/ui/spinner-frames.d.ts +1 -0
  176. package/dist/ui/spinner-frames.d.ts.map +1 -1
  177. package/dist/ui/spinner-frames.js +15 -5
  178. package/dist/ui/spinner-frames.js.map +1 -1
  179. package/dist/ui/theme/dark.json +2 -1
  180. package/dist/ui/theme/light.json +2 -1
  181. package/dist/ui/theme/theme.d.ts +1 -0
  182. package/dist/ui/theme/theme.d.ts.map +1 -1
  183. package/dist/ui/utils/markdown-cache.d.ts +12 -0
  184. package/dist/ui/utils/markdown-cache.d.ts.map +1 -0
  185. package/dist/ui/utils/markdown-cache.js +43 -0
  186. package/dist/ui/utils/markdown-cache.js.map +1 -0
  187. package/dist/ui/utils/word-diff.d.ts +11 -0
  188. package/dist/ui/utils/word-diff.d.ts.map +1 -0
  189. package/dist/ui/utils/word-diff.js +23 -0
  190. package/dist/ui/utils/word-diff.js.map +1 -0
  191. package/dist/utils/plan-steps.d.ts +41 -0
  192. package/dist/utils/plan-steps.d.ts.map +1 -0
  193. package/dist/utils/plan-steps.js +91 -0
  194. package/dist/utils/plan-steps.js.map +1 -0
  195. package/package.json +3 -3
package/dist/ui/App.js CHANGED
@@ -27,8 +27,9 @@ import { TaskOverlay } from "./components/TaskOverlay.js";
27
27
  import { SkillsOverlay } from "./components/SkillsOverlay.js";
28
28
  import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
29
29
  import { useTheme } from "./theme/theme.js";
30
- import { useAnimationTick, deriveFrame } from "./components/AnimationContext.js";
30
+ import { useAnimationTick, useAnimationActive, deriveFrame, } from "./components/AnimationContext.js";
31
31
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
32
+ import { useTerminalProgress } from "./hooks/useTerminalProgress.js";
32
33
  import { getGitBranch } from "../utils/git.js";
33
34
  import { getModel, getContextWindow } from "../core/model-registry.js";
34
35
  import { SessionManager } from "../core/session-manager.js";
@@ -39,8 +40,10 @@ import { estimateConversationTokens } from "../core/compaction/token-estimator.j
39
40
  import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
40
41
  import { loadCustomCommands } from "../core/custom-commands.js";
41
42
  import { buildSystemPrompt } from "../system-prompt.js";
43
+ import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, stripDoneMarkers, } from "../utils/plan-steps.js";
42
44
  import { getMCPServers } from "../core/mcp/index.js";
43
45
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd } from "./live-item-flush.js";
46
+ import { Buddy } from "./buddy/Buddy.js";
44
47
  // ── Provider Error Hints ──────────────────────────────────
45
48
  /** Detect provider-side errors and return a user-facing hint. */
46
49
  function getProviderErrorHint(message) {
@@ -102,6 +105,41 @@ function compactHistory(items) {
102
105
  return compacted;
103
106
  }
104
107
  // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
108
+ /** Check whether an item is still active (running spinner, pending result). */
109
+ function isActiveItem(item) {
110
+ switch (item.kind) {
111
+ case "tool_start":
112
+ case "server_tool_start":
113
+ case "compacting":
114
+ return true;
115
+ case "tool_group":
116
+ return item.tools.some((t) => t.status === "running");
117
+ case "subagent_group":
118
+ return item.agents.some((a) => a.status === "running");
119
+ default:
120
+ return false;
121
+ }
122
+ }
123
+ /**
124
+ * Partition live items into completed (flushable to Static) and still-active.
125
+ * Completed items precede active ones — we flush the longest contiguous prefix
126
+ * of completed items to keep ordering stable.
127
+ */
128
+ function partitionCompleted(items) {
129
+ // Find the first active item — everything before it is safe to flush
130
+ const firstActiveIdx = items.findIndex(isActiveItem);
131
+ if (firstActiveIdx === -1) {
132
+ // All items are completed
133
+ return { flushed: items, remaining: [] };
134
+ }
135
+ if (firstActiveIdx === 0) {
136
+ return { flushed: [], remaining: items };
137
+ }
138
+ return {
139
+ flushed: items.slice(0, firstActiveIdx),
140
+ remaining: items.slice(firstActiveIdx),
141
+ };
142
+ }
105
143
  // ── Duration summary ─────────────────────────────────────
106
144
  function formatDuration(ms) {
107
145
  const totalSec = Math.round(ms / 1000);
@@ -276,10 +314,16 @@ export function App(props) {
276
314
  const messagesRef = useRef(props.messages);
277
315
  const [planAutoExpand, setPlanAutoExpand] = useState(false);
278
316
  const approvedPlanPathRef = useRef(undefined);
317
+ const planStepsRef = useRef([]);
318
+ const [planSteps, setPlanSteps] = useState([]);
279
319
  const nextIdRef = useRef(0);
280
320
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
281
321
  const sessionPathRef = useRef(props.sessionPath);
282
322
  const persistedIndexRef = useRef(messagesRef.current.length);
323
+ /** Last actual API-reported input token count (from turn_end). */
324
+ const lastActualTokensRef = useRef(0);
325
+ /** Timestamp of last compaction — used for time-based cooldown. */
326
+ const lastCompactionTimeRef = useRef(0);
283
327
  const getId = () => String(nextIdRef.current++);
284
328
  // Two-phase flush: items waiting to be moved to Static history after the
285
329
  // live area has been cleared and Ink has committed the smaller output.
@@ -379,13 +423,15 @@ export function App(props) {
379
423
  persistedIndexRef.current = allMsgs.length;
380
424
  }, []);
381
425
  // ── Compaction ─────────────────────────────────────────
382
- // Load settings for auto-compaction
426
+ // Load settings for auto-compaction + buddy
383
427
  const settingsRef = useRef(null);
428
+ const [buddyEnabled, setBuddyEnabled] = useState(false);
384
429
  useEffect(() => {
385
430
  if (props.settingsFile) {
386
431
  const sm = new SettingsManager(props.settingsFile);
387
432
  sm.load().then(() => {
388
433
  settingsRef.current = sm;
434
+ setBuddyEnabled(sm.get("buddyEnabled") ?? false);
389
435
  });
390
436
  }
391
437
  }, [props.settingsFile]);
@@ -442,6 +488,11 @@ export function App(props) {
442
488
  * transformContext callback for the agent loop.
443
489
  * Called before each LLM call and on context overflow.
444
490
  * Checks if auto-compaction is needed and runs it.
491
+ *
492
+ * Uses actual API-reported token counts (from previous turn_end) when
493
+ * available, falling back to the character-based estimate. A 30-second
494
+ * cooldown prevents repeated compaction — matching the pattern used by
495
+ * Mysti, openclaw, and other real-world agent frameworks.
445
496
  */
446
497
  const transformContext = useCallback(async (messages, options) => {
447
498
  const settings = settingsRef.current;
@@ -449,13 +500,26 @@ export function App(props) {
449
500
  const threshold = settings?.get("compactThreshold") ?? 0.8;
450
501
  // Force-compact on context overflow regardless of settings
451
502
  if (options?.force) {
452
- return compactConversation(messages);
503
+ const result = await compactConversation(messages);
504
+ lastCompactionTimeRef.current = Date.now();
505
+ lastActualTokensRef.current = 0; // Reset stale pre-compaction count
506
+ return result;
453
507
  }
454
508
  if (!autoCompact)
455
509
  return messages;
510
+ // Time-based cooldown: skip if compaction ran within the last 30 seconds
511
+ if (Date.now() - lastCompactionTimeRef.current < 30_000) {
512
+ log("INFO", "compaction", `Skipping compaction — cooldown active`);
513
+ return messages;
514
+ }
456
515
  const contextWindow = getContextWindow(currentModel);
457
- if (shouldCompact(messages, contextWindow, threshold)) {
458
- return compactConversation(messages);
516
+ // Prefer actual API-reported tokens over char-based estimate
517
+ const actualTokens = lastActualTokensRef.current > 0 ? lastActualTokensRef.current : undefined;
518
+ if (shouldCompact(messages, contextWindow, threshold, actualTokens)) {
519
+ const result = await compactConversation(messages);
520
+ lastCompactionTimeRef.current = Date.now();
521
+ lastActualTokensRef.current = 0; // Reset stale pre-compaction count
522
+ return result;
459
523
  }
460
524
  return messages;
461
525
  }, [currentModel, compactConversation]);
@@ -535,8 +599,33 @@ export function App(props) {
535
599
  }, {
536
600
  onComplete: useCallback(() => {
537
601
  persistNewMessages();
538
- }, [persistNewMessages]),
602
+ // Auto-clear plan progress and approved plan when all steps are completed
603
+ const steps = planStepsRef.current;
604
+ if (steps.length > 0 && steps.every((s) => s.completed)) {
605
+ planStepsRef.current = [];
606
+ setPlanSteps([]);
607
+ approvedPlanPathRef.current = undefined;
608
+ // Rebuild system prompt to remove the completed plan from context
609
+ void (async () => {
610
+ const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
611
+ if (messagesRef.current[0]?.role === "system") {
612
+ messagesRef.current[0] = { role: "system", content: newPrompt };
613
+ }
614
+ })();
615
+ }
616
+ }, [persistNewMessages, planMode, props.cwd, props.skills]),
539
617
  onTurnText: useCallback((text, thinking, thinkingMs) => {
618
+ // Track [DONE:n] markers for plan step progress
619
+ if (planStepsRef.current.length > 0) {
620
+ const completed = findCompletedMarkers(text);
621
+ if (completed.size > 0) {
622
+ const updated = markStepsCompleted(planStepsRef.current, completed);
623
+ if (updated !== planStepsRef.current) {
624
+ planStepsRef.current = updated;
625
+ setPlanSteps(updated);
626
+ }
627
+ }
628
+ }
540
629
  // Flush all completed items from the previous turn to Static history.
541
630
  // This keeps liveItems bounded per-turn, preventing Ink's live area from
542
631
  // growing unbounded, which makes Ink's live-area re-renders expensive.
@@ -545,11 +634,22 @@ export function App(props) {
545
634
  if (flushed.length > 0) {
546
635
  setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
547
636
  }
548
- return [{ kind: "assistant", text, thinking, thinkingMs, id: getId() }];
637
+ const displayText = planStepsRef.current.length > 0 ? stripDoneMarkers(text) : text;
638
+ return [{ kind: "assistant", text: displayText, thinking, thinkingMs, id: getId() }];
549
639
  });
550
640
  }, []),
551
641
  onToolStart: useCallback((toolCallId, name, args) => {
552
642
  log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
643
+ // Flush completed items (assistant text, finished tools) to Static
644
+ // before adding tool UI. Keeping both in the live area makes it tall
645
+ // and causes Ink's cursor math to clip the top.
646
+ setLiveItems((prev) => {
647
+ const { flushed, remaining } = partitionCompleted(prev);
648
+ if (flushed.length > 0) {
649
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
650
+ }
651
+ return remaining;
652
+ });
553
653
  if (name === "subagent") {
554
654
  // Create or update the sub-agent group item
555
655
  const newAgent = {
@@ -656,7 +756,12 @@ export function App(props) {
656
756
  };
657
757
  const next = [...prev];
658
758
  next[groupIdx] = { ...group, agents: updatedAgents };
659
- return next;
759
+ // Flush completed items to Static to keep the live area small
760
+ const { flushed, remaining } = partitionCompleted(next);
761
+ if (flushed.length > 0) {
762
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
763
+ }
764
+ return remaining;
660
765
  });
661
766
  }
662
767
  else {
@@ -664,59 +769,87 @@ export function App(props) {
664
769
  // Check if this tool is in a tool_group
665
770
  const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
666
771
  item.tools.some((t) => t.toolCallId === toolCallId));
772
+ let updated;
667
773
  if (groupIdx !== -1) {
668
774
  const group = prev[groupIdx];
669
- const next = [...prev];
670
- next[groupIdx] = {
775
+ updated = [...prev];
776
+ updated[groupIdx] = {
671
777
  ...group,
672
778
  tools: group.tools.map((t) => t.toolCallId === toolCallId
673
779
  ? { ...t, status: "done", result, isError }
674
780
  : t),
675
781
  };
676
- return next;
677
782
  }
678
- // Find the matching tool_start and replace it with tool_done
679
- const startIdx = prev.findIndex((item) => item.kind === "tool_start" && item.toolCallId === toolCallId);
680
- if (startIdx !== -1) {
681
- const startItem = prev[startIdx];
682
- const doneItem = {
683
- kind: "tool_done",
684
- name,
685
- args: startItem.args,
686
- result,
687
- isError,
688
- durationMs,
689
- id: startItem.id,
690
- };
691
- const next = [...prev];
692
- next[startIdx] = doneItem;
693
- return next;
783
+ else {
784
+ // Find the matching tool_start and replace it with tool_done
785
+ const startIdx = prev.findIndex((item) => item.kind === "tool_start" && item.toolCallId === toolCallId);
786
+ if (startIdx !== -1) {
787
+ const startItem = prev[startIdx];
788
+ const doneItem = {
789
+ kind: "tool_done",
790
+ name,
791
+ args: startItem.args,
792
+ result,
793
+ isError,
794
+ durationMs,
795
+ details,
796
+ id: startItem.id,
797
+ };
798
+ updated = [...prev];
799
+ updated[startIdx] = doneItem;
800
+ }
801
+ else {
802
+ // Fallback: just append
803
+ updated = [
804
+ ...prev,
805
+ {
806
+ kind: "tool_done",
807
+ name,
808
+ args: {},
809
+ result,
810
+ isError,
811
+ durationMs,
812
+ details,
813
+ id: getId(),
814
+ },
815
+ ];
816
+ }
694
817
  }
695
- // Fallback: just append
696
- return [
697
- ...prev,
698
- { kind: "tool_done", name, args: {}, result, isError, durationMs, id: getId() },
699
- ];
818
+ // Flush completed items to Static to keep the live area small
819
+ const { flushed, remaining } = partitionCompleted(updated);
820
+ if (flushed.length > 0) {
821
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
822
+ }
823
+ return remaining;
700
824
  });
701
825
  }
702
826
  }, []),
703
827
  onServerToolCall: useCallback((id, name, input) => {
704
828
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
705
- setLiveItems((prev) => [
706
- ...prev,
707
- {
708
- kind: "server_tool_start",
709
- serverToolCallId: id,
710
- name,
711
- input,
712
- startedAt: Date.now(),
713
- id: getId(),
714
- },
715
- ]);
829
+ // Flush completed items (including assistant text) to Static before
830
+ // adding server tool UI — same rationale as onToolStart.
831
+ setLiveItems((prev) => {
832
+ const { flushed, remaining } = partitionCompleted(prev);
833
+ if (flushed.length > 0) {
834
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
835
+ }
836
+ return [
837
+ ...remaining,
838
+ {
839
+ kind: "server_tool_start",
840
+ serverToolCallId: id,
841
+ name,
842
+ input,
843
+ startedAt: Date.now(),
844
+ id: getId(),
845
+ },
846
+ ];
847
+ });
716
848
  }, []),
717
849
  onServerToolResult: useCallback((toolUseId, resultType, data) => {
718
850
  log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
719
851
  setLiveItems((prev) => {
852
+ let updated;
720
853
  const startIdx = prev.findIndex((item) => item.kind === "server_tool_start" && item.serverToolCallId === toolUseId);
721
854
  if (startIdx !== -1) {
722
855
  const startItem = prev[startIdx];
@@ -729,22 +862,29 @@ export function App(props) {
729
862
  durationMs: Date.now() - startItem.startedAt,
730
863
  id: startItem.id,
731
864
  };
732
- const next = [...prev];
733
- next[startIdx] = doneItem;
734
- return next;
865
+ updated = [...prev];
866
+ updated[startIdx] = doneItem;
735
867
  }
736
- return [
737
- ...prev,
738
- {
739
- kind: "server_tool_done",
740
- name: "unknown",
741
- input: {},
742
- resultType,
743
- data,
744
- durationMs: 0,
745
- id: getId(),
746
- },
747
- ];
868
+ else {
869
+ updated = [
870
+ ...prev,
871
+ {
872
+ kind: "server_tool_done",
873
+ name: "unknown",
874
+ input: {},
875
+ resultType,
876
+ data,
877
+ durationMs: 0,
878
+ id: getId(),
879
+ },
880
+ ];
881
+ }
882
+ // Flush completed items to Static
883
+ const { flushed, remaining } = partitionCompleted(updated);
884
+ if (flushed.length > 0) {
885
+ setHistory((h) => compactHistory([...h, ...trimFlushedItems(flushed)]));
886
+ }
887
+ return remaining;
748
888
  });
749
889
  }, []),
750
890
  onTurnEnd: useCallback((turn, stopReason, usage) => {
@@ -755,6 +895,9 @@ export function App(props) {
755
895
  ...(usage.cacheRead != null && { cacheRead: String(usage.cacheRead) }),
756
896
  ...(usage.cacheWrite != null && { cacheWrite: String(usage.cacheWrite) }),
757
897
  });
898
+ // Track actual token count for compaction decisions
899
+ lastActualTokensRef.current =
900
+ usage.inputTokens + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
758
901
  // For tool-only turns (no text), flush completed items to Static so
759
902
  // liveItems doesn't grow unbounded across consecutive tool-only turns.
760
903
  setLiveItems((prev) => {
@@ -901,7 +1044,10 @@ export function App(props) {
901
1044
  setTitleRunning(agentLoop.isRunning);
902
1045
  setTitleToolNames(agentLoop.activeToolCalls.map((tc) => tc.name));
903
1046
  }, [agentLoop.activityPhase, agentLoop.isRunning, activeToolNamesKey]);
1047
+ // Terminal progress bar (OSC 9;4) — pulsing bar in supported terminals
1048
+ useTerminalProgress(agentLoop.isRunning, agentLoop.activeToolCalls.length > 0);
904
1049
  // Animated thinking border — derived from global animation tick
1050
+ useAnimationActive();
905
1051
  const animTick = useAnimationTick();
906
1052
  const thinkingBorderFrame = agentLoop.activityPhase === "thinking"
907
1053
  ? deriveFrame(animTick, 1000, THINKING_BORDER_COLORS.length)
@@ -941,7 +1087,14 @@ export function App(props) {
941
1087
  setHistory([{ kind: "banner", id: "banner" }]);
942
1088
  setLiveItems([]);
943
1089
  setDoneStatus(null);
944
- messagesRef.current = messagesRef.current.slice(0, 1); // keep system prompt
1090
+ approvedPlanPathRef.current = undefined;
1091
+ planStepsRef.current = [];
1092
+ setPlanSteps([]);
1093
+ // Rebuild system prompt without the approved plan
1094
+ void (async () => {
1095
+ const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
1096
+ messagesRef.current = [{ role: "system", content: newPrompt }];
1097
+ })();
945
1098
  agentLoop.reset();
946
1099
  setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
947
1100
  return;
@@ -968,6 +1121,40 @@ export function App(props) {
968
1121
  ]);
969
1122
  return;
970
1123
  }
1124
+ // Handle /clearplan — dismiss the approved plan
1125
+ if (trimmed === "/clearplan") {
1126
+ approvedPlanPathRef.current = undefined;
1127
+ planStepsRef.current = [];
1128
+ setPlanSteps([]);
1129
+ // Rebuild system prompt without the plan
1130
+ void (async () => {
1131
+ const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
1132
+ if (messagesRef.current[0]?.role === "system") {
1133
+ messagesRef.current[0] = { role: "system", content: newPrompt };
1134
+ }
1135
+ })();
1136
+ setLiveItems([{ kind: "info", text: "Approved plan dismissed.", id: getId() }]);
1137
+ return;
1138
+ }
1139
+ // Handle /buddy — toggle companion
1140
+ if (trimmed === "/buddy") {
1141
+ const next = !buddyEnabled;
1142
+ setBuddyEnabled(next);
1143
+ if (settingsRef.current) {
1144
+ settingsRef.current.set("buddyEnabled", next);
1145
+ }
1146
+ setLiveItems((items) => [
1147
+ ...items,
1148
+ {
1149
+ kind: "info",
1150
+ text: next
1151
+ ? "Buddy enabled! Your companion will appear near the prompt."
1152
+ : "Buddy disabled.",
1153
+ id: getId(),
1154
+ },
1155
+ ]);
1156
+ return;
1157
+ }
971
1158
  // Handle /plans — open plan pane
972
1159
  if (trimmed === "/plans") {
973
1160
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
@@ -1113,6 +1300,12 @@ export function App(props) {
1113
1300
  };
1114
1301
  setLastUserMessage(input);
1115
1302
  setDoneStatus(null);
1303
+ // Clear stale plan progress if there's no active approved plan
1304
+ // (avoids lingering progress from a completed or abandoned plan run)
1305
+ if (planStepsRef.current.length > 0 && !approvedPlanPathRef.current) {
1306
+ planStepsRef.current = [];
1307
+ setPlanSteps([]);
1308
+ }
1116
1309
  setLiveItems([userItem]);
1117
1310
  // Run agent
1118
1311
  try {
@@ -1249,7 +1442,7 @@ export function App(props) {
1249
1442
  case "tool_start":
1250
1443
  return _jsx(ToolExecution, { status: "running", name: item.name, args: item.args }, item.id);
1251
1444
  case "tool_done":
1252
- return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError }, item.id));
1445
+ return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
1253
1446
  case "tool_group":
1254
1447
  return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
1255
1448
  case "server_tool_start":
@@ -1361,6 +1554,12 @@ export function App(props) {
1361
1554
  }, onApprove: (planPath) => {
1362
1555
  // Store approved plan path — will be injected into the new system prompt
1363
1556
  approvedPlanPathRef.current = planPath;
1557
+ // Extract plan steps for progress tracking
1558
+ void import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8").then((content) => {
1559
+ const steps = extractPlanSteps(content);
1560
+ planStepsRef.current = steps;
1561
+ setPlanSteps(steps);
1562
+ }));
1364
1563
  // Clear session for a fresh context focused on the plan
1365
1564
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1366
1565
  setHistory([{ kind: "banner", id: "banner" }]);
@@ -1407,7 +1606,7 @@ export function App(props) {
1407
1606
  void agentLoop.run(msg);
1408
1607
  } })) : (_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"
1409
1608
  ? THINKING_BORDER_COLORS[thinkingBorderFrame]
1410
- : "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, 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: () => {
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: () => {
1411
1610
  stdout?.write("\x1b[2J\x1b[3J\x1b[H");
1412
1611
  setOverlay("tasks");
1413
1612
  }, onToggleSkills: () => {
@@ -1426,6 +1625,6 @@ export function App(props) {
1426
1625
  id: getId(),
1427
1626
  },
1428
1627
  ]);
1429
- }, 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 }))] }))] }));
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 }))] }))] }));
1430
1629
  }
1431
1630
  //# sourceMappingURL=App.js.map