@prestyj/cli 4.2.16 → 4.2.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -7
  3. package/dist/cli.js +458 -238
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +1 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +66 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +22 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +184 -23
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/agents.d.ts +0 -2
  14. package/dist/core/agents.d.ts.map +1 -1
  15. package/dist/core/agents.js +1 -5
  16. package/dist/core/agents.js.map +1 -1
  17. package/dist/core/auth-storage.d.ts +2 -0
  18. package/dist/core/auth-storage.d.ts.map +1 -1
  19. package/dist/core/auth-storage.js +66 -17
  20. package/dist/core/auth-storage.js.map +1 -1
  21. package/dist/core/auto-update.js +1 -1
  22. package/dist/core/auto-update.js.map +1 -1
  23. package/dist/core/compaction/compactor.d.ts +36 -5
  24. package/dist/core/compaction/compactor.d.ts.map +1 -1
  25. package/dist/core/compaction/compactor.js +222 -63
  26. package/dist/core/compaction/compactor.js.map +1 -1
  27. package/dist/core/compaction/compactor.test.js +377 -32
  28. package/dist/core/compaction/compactor.test.js.map +1 -1
  29. package/dist/core/compaction/token-estimator.d.ts +5 -0
  30. package/dist/core/compaction/token-estimator.d.ts.map +1 -1
  31. package/dist/core/compaction/token-estimator.js +41 -2
  32. package/dist/core/compaction/token-estimator.js.map +1 -1
  33. package/dist/core/compaction/token-estimator.test.js +43 -21
  34. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  35. package/dist/core/event-bus.d.ts +6 -0
  36. package/dist/core/event-bus.d.ts.map +1 -1
  37. package/dist/core/event-bus.js +7 -0
  38. package/dist/core/event-bus.js.map +1 -1
  39. package/dist/core/file-lock.d.ts +6 -0
  40. package/dist/core/file-lock.d.ts.map +1 -0
  41. package/dist/core/file-lock.js +76 -0
  42. package/dist/core/file-lock.js.map +1 -0
  43. package/dist/core/index.d.ts +2 -2
  44. package/dist/core/index.d.ts.map +1 -1
  45. package/dist/core/index.js +1 -1
  46. package/dist/core/index.js.map +1 -1
  47. package/dist/core/mcp/client.d.ts +0 -26
  48. package/dist/core/mcp/client.d.ts.map +1 -1
  49. package/dist/core/mcp/client.js +34 -141
  50. package/dist/core/mcp/client.js.map +1 -1
  51. package/dist/core/mcp/defaults.d.ts.map +1 -1
  52. package/dist/core/mcp/defaults.js +4 -8
  53. package/dist/core/mcp/defaults.js.map +1 -1
  54. package/dist/core/mcp/types.d.ts +0 -2
  55. package/dist/core/mcp/types.d.ts.map +1 -1
  56. package/dist/core/model-registry.d.ts +2 -1
  57. package/dist/core/model-registry.d.ts.map +1 -1
  58. package/dist/core/model-registry.js +16 -5
  59. package/dist/core/model-registry.js.map +1 -1
  60. package/dist/core/oauth/anthropic.d.ts.map +1 -1
  61. package/dist/core/oauth/anthropic.js +3 -3
  62. package/dist/core/oauth/anthropic.js.map +1 -1
  63. package/dist/core/prompt-commands.d.ts.map +1 -1
  64. package/dist/core/prompt-commands.js +21 -0
  65. package/dist/core/prompt-commands.js.map +1 -1
  66. package/dist/core/session-manager.d.ts +34 -2
  67. package/dist/core/session-manager.d.ts.map +1 -1
  68. package/dist/core/session-manager.js +160 -54
  69. package/dist/core/session-manager.js.map +1 -1
  70. package/dist/core/settings-manager.d.ts +2 -2
  71. package/dist/core/skills.d.ts +2 -1
  72. package/dist/core/skills.d.ts.map +1 -1
  73. package/dist/core/skills.js +8 -8
  74. package/dist/core/skills.js.map +1 -1
  75. package/dist/core/slash-commands.d.ts +4 -0
  76. package/dist/core/slash-commands.d.ts.map +1 -1
  77. package/dist/core/slash-commands.js +22 -0
  78. package/dist/core/slash-commands.js.map +1 -1
  79. package/dist/core/telegram.d.ts +94 -0
  80. package/dist/core/telegram.d.ts.map +1 -0
  81. package/dist/core/telegram.js +227 -0
  82. package/dist/core/telegram.js.map +1 -0
  83. package/dist/interactive.d.ts.map +1 -1
  84. package/dist/interactive.js +36 -5
  85. package/dist/interactive.js.map +1 -1
  86. package/dist/modes/index.d.ts +1 -0
  87. package/dist/modes/index.d.ts.map +1 -1
  88. package/dist/modes/index.js +1 -0
  89. package/dist/modes/index.js.map +1 -1
  90. package/dist/modes/json-mode.d.ts.map +1 -1
  91. package/dist/modes/json-mode.js +2 -1
  92. package/dist/modes/json-mode.js.map +1 -1
  93. package/dist/modes/print-mode.d.ts.map +1 -1
  94. package/dist/modes/print-mode.js +2 -1
  95. package/dist/modes/print-mode.js.map +1 -1
  96. package/dist/modes/rpc-mode.d.ts +28 -0
  97. package/dist/modes/rpc-mode.d.ts.map +1 -0
  98. package/dist/modes/rpc-mode.js +146 -0
  99. package/dist/modes/rpc-mode.js.map +1 -0
  100. package/dist/modes/serve-mode.d.ts +21 -0
  101. package/dist/modes/serve-mode.d.ts.map +1 -0
  102. package/dist/modes/serve-mode.js +650 -0
  103. package/dist/modes/serve-mode.js.map +1 -0
  104. package/dist/system-prompt.d.ts +1 -1
  105. package/dist/system-prompt.d.ts.map +1 -1
  106. package/dist/system-prompt.js +45 -10
  107. package/dist/system-prompt.js.map +1 -1
  108. package/dist/tools/bash.d.ts +4 -1
  109. package/dist/tools/bash.d.ts.map +1 -1
  110. package/dist/tools/bash.js +58 -5
  111. package/dist/tools/bash.js.map +1 -1
  112. package/dist/tools/edit.d.ts +4 -1
  113. package/dist/tools/edit.d.ts.map +1 -1
  114. package/dist/tools/edit.js +9 -5
  115. package/dist/tools/edit.js.map +1 -1
  116. package/dist/tools/enter-plan.d.ts +8 -0
  117. package/dist/tools/enter-plan.d.ts.map +1 -0
  118. package/dist/tools/enter-plan.js +28 -0
  119. package/dist/tools/enter-plan.js.map +1 -0
  120. package/dist/tools/exit-plan.d.ts +8 -0
  121. package/dist/tools/exit-plan.d.ts.map +1 -0
  122. package/dist/tools/exit-plan.js +35 -0
  123. package/dist/tools/exit-plan.js.map +1 -0
  124. package/dist/tools/grep.d.ts +2 -1
  125. package/dist/tools/grep.d.ts.map +1 -1
  126. package/dist/tools/grep.js +7 -8
  127. package/dist/tools/grep.js.map +1 -1
  128. package/dist/tools/index.d.ts +17 -0
  129. package/dist/tools/index.d.ts.map +1 -1
  130. package/dist/tools/index.js +26 -7
  131. package/dist/tools/index.js.map +1 -1
  132. package/dist/tools/ls.d.ts +2 -1
  133. package/dist/tools/ls.d.ts.map +1 -1
  134. package/dist/tools/ls.js +4 -4
  135. package/dist/tools/ls.js.map +1 -1
  136. package/dist/tools/operations.d.ts +39 -0
  137. package/dist/tools/operations.d.ts.map +1 -0
  138. package/dist/tools/operations.js +27 -0
  139. package/dist/tools/operations.js.map +1 -0
  140. package/dist/tools/path-utils.d.ts +5 -0
  141. package/dist/tools/path-utils.d.ts.map +1 -1
  142. package/dist/tools/path-utils.js +18 -0
  143. package/dist/tools/path-utils.js.map +1 -1
  144. package/dist/tools/plan-mode.test.d.ts +2 -0
  145. package/dist/tools/plan-mode.test.d.ts.map +1 -0
  146. package/dist/tools/plan-mode.test.js +214 -0
  147. package/dist/tools/plan-mode.test.js.map +1 -0
  148. package/dist/tools/read.d.ts +2 -1
  149. package/dist/tools/read.d.ts.map +1 -1
  150. package/dist/tools/read.js +6 -5
  151. package/dist/tools/read.js.map +1 -1
  152. package/dist/tools/skill.d.ts +10 -0
  153. package/dist/tools/skill.d.ts.map +1 -0
  154. package/dist/tools/skill.js +36 -0
  155. package/dist/tools/skill.js.map +1 -0
  156. package/dist/tools/subagent.d.ts +3 -1
  157. package/dist/tools/subagent.d.ts.map +1 -1
  158. package/dist/tools/subagent.js +46 -4
  159. package/dist/tools/subagent.js.map +1 -1
  160. package/dist/tools/tasks.d.ts +2 -2
  161. package/dist/tools/web-fetch.d.ts.map +1 -1
  162. package/dist/tools/web-fetch.js +47 -0
  163. package/dist/tools/web-fetch.js.map +1 -1
  164. package/dist/tools/write.d.ts +4 -1
  165. package/dist/tools/write.d.ts.map +1 -1
  166. package/dist/tools/write.js +17 -6
  167. package/dist/tools/write.js.map +1 -1
  168. package/dist/ui/App.d.ts +35 -1
  169. package/dist/ui/App.d.ts.map +1 -1
  170. package/dist/ui/App.js +367 -91
  171. package/dist/ui/App.js.map +1 -1
  172. package/dist/ui/components/ActivityIndicator.d.ts +2 -1
  173. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  174. package/dist/ui/components/ActivityIndicator.js +25 -4
  175. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  176. package/dist/ui/components/AnimationContext.d.ts +22 -0
  177. package/dist/ui/components/AnimationContext.d.ts.map +1 -0
  178. package/dist/ui/components/AnimationContext.js +35 -0
  179. package/dist/ui/components/AnimationContext.js.map +1 -0
  180. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  181. package/dist/ui/components/AssistantMessage.js +6 -1
  182. package/dist/ui/components/AssistantMessage.js.map +1 -1
  183. package/dist/ui/components/Banner.d.ts.map +1 -1
  184. package/dist/ui/components/Banner.js +4 -7
  185. package/dist/ui/components/Banner.js.map +1 -1
  186. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  187. package/dist/ui/components/CompactionNotice.js +4 -8
  188. package/dist/ui/components/CompactionNotice.js.map +1 -1
  189. package/dist/ui/components/Footer.d.ts +2 -1
  190. package/dist/ui/components/Footer.d.ts.map +1 -1
  191. package/dist/ui/components/Footer.js +8 -15
  192. package/dist/ui/components/Footer.js.map +1 -1
  193. package/dist/ui/components/InputArea.d.ts +3 -1
  194. package/dist/ui/components/InputArea.d.ts.map +1 -1
  195. package/dist/ui/components/InputArea.js +23 -25
  196. package/dist/ui/components/InputArea.js.map +1 -1
  197. package/dist/ui/components/PlanApproval.d.ts +8 -0
  198. package/dist/ui/components/PlanApproval.d.ts.map +1 -0
  199. package/dist/ui/components/PlanApproval.js +44 -0
  200. package/dist/ui/components/PlanApproval.js.map +1 -0
  201. package/dist/ui/components/PlanBanner.d.ts +6 -0
  202. package/dist/ui/components/PlanBanner.d.ts.map +1 -0
  203. package/dist/ui/components/PlanBanner.js +14 -0
  204. package/dist/ui/components/PlanBanner.js.map +1 -0
  205. package/dist/ui/components/PlanOverlay.d.ts +11 -0
  206. package/dist/ui/components/PlanOverlay.d.ts.map +1 -0
  207. package/dist/ui/components/PlanOverlay.js +263 -0
  208. package/dist/ui/components/PlanOverlay.js.map +1 -0
  209. package/dist/ui/components/ServerToolExecution.d.ts +1 -0
  210. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  211. package/dist/ui/components/ServerToolExecution.js +12 -3
  212. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  213. package/dist/ui/components/SkillsOverlay.d.ts +7 -0
  214. package/dist/ui/components/SkillsOverlay.d.ts.map +1 -0
  215. package/dist/ui/components/SkillsOverlay.js +158 -0
  216. package/dist/ui/components/SkillsOverlay.js.map +1 -0
  217. package/dist/ui/components/Spinner.d.ts.map +1 -1
  218. package/dist/ui/components/Spinner.js +5 -9
  219. package/dist/ui/components/Spinner.js.map +1 -1
  220. package/dist/ui/components/StreamingArea.d.ts +2 -1
  221. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  222. package/dist/ui/components/StreamingArea.js +11 -4
  223. package/dist/ui/components/StreamingArea.js.map +1 -1
  224. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  225. package/dist/ui/components/SubAgentPanel.js +27 -29
  226. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  227. package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
  228. package/dist/ui/components/TaskOverlay.js +7 -3
  229. package/dist/ui/components/TaskOverlay.js.map +1 -1
  230. package/dist/ui/components/ThinkingBlock.d.ts.map +1 -1
  231. package/dist/ui/components/ThinkingBlock.js +4 -1
  232. package/dist/ui/components/ThinkingBlock.js.map +1 -1
  233. package/dist/ui/components/ThinkingIndicator.d.ts.map +1 -1
  234. package/dist/ui/components/ThinkingIndicator.js +8 -33
  235. package/dist/ui/components/ThinkingIndicator.js.map +1 -1
  236. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  237. package/dist/ui/components/ToolExecution.js +95 -41
  238. package/dist/ui/components/ToolExecution.js.map +1 -1
  239. package/dist/ui/components/ToolGroupExecution.d.ts +7 -0
  240. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -0
  241. package/dist/ui/components/ToolGroupExecution.js +115 -0
  242. package/dist/ui/components/ToolGroupExecution.js.map +1 -0
  243. package/dist/ui/components/UserMessage.js +1 -1
  244. package/dist/ui/components/UserMessage.js.map +1 -1
  245. package/dist/ui/components/index.d.ts +1 -0
  246. package/dist/ui/components/index.d.ts.map +1 -1
  247. package/dist/ui/components/index.js +1 -0
  248. package/dist/ui/components/index.js.map +1 -1
  249. package/dist/ui/hooks/useAgentLoop.d.ts +8 -0
  250. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  251. package/dist/ui/hooks/useAgentLoop.js +320 -220
  252. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  253. package/dist/ui/hooks/useTerminalSize.d.ts +21 -9
  254. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  255. package/dist/ui/hooks/useTerminalSize.js +33 -16
  256. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  257. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  258. package/dist/ui/hooks/useTerminalTitle.js +8 -16
  259. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  260. package/dist/ui/live-item-flush.d.ts +11 -1
  261. package/dist/ui/live-item-flush.d.ts.map +1 -1
  262. package/dist/ui/live-item-flush.js +81 -2
  263. package/dist/ui/live-item-flush.js.map +1 -1
  264. package/dist/ui/login.d.ts +1 -1
  265. package/dist/ui/login.d.ts.map +1 -1
  266. package/dist/ui/login.js +30 -6
  267. package/dist/ui/login.js.map +1 -1
  268. package/dist/ui/render.d.ts +11 -0
  269. package/dist/ui/render.d.ts.map +1 -1
  270. package/dist/ui/render.js +24 -10
  271. package/dist/ui/render.js.map +1 -1
  272. package/dist/ui/sessions.d.ts +2 -0
  273. package/dist/ui/sessions.d.ts.map +1 -0
  274. package/dist/ui/sessions.js +208 -0
  275. package/dist/ui/sessions.js.map +1 -0
  276. package/dist/ui/theme/dark.json +3 -1
  277. package/dist/ui/theme/light.json +3 -1
  278. package/dist/ui/theme/theme.d.ts +2 -0
  279. package/dist/ui/theme/theme.d.ts.map +1 -1
  280. package/dist/utils/error-handler.d.ts.map +1 -1
  281. package/dist/utils/error-handler.js +17 -5
  282. package/dist/utils/error-handler.js.map +1 -1
  283. package/dist/utils/image.d.ts.map +1 -1
  284. package/dist/utils/image.js +60 -5
  285. package/dist/utils/image.js.map +1 -1
  286. package/package.json +8 -8
@@ -22,6 +22,31 @@ function estimateTokens(msgs) {
22
22
  }
23
23
  return Math.round(chars / 4);
24
24
  }
25
+ /**
26
+ * Merge multiple UserContent items into a single one.
27
+ * Text-only items are joined with newlines. Mixed content (text + images)
28
+ * is flattened into a content array preserving all parts.
29
+ */
30
+ function mergeUserContent(items) {
31
+ if (items.length === 1)
32
+ return items[0];
33
+ const hasArrayContent = items.some((c) => Array.isArray(c));
34
+ if (!hasArrayContent) {
35
+ // All items are strings — join with newlines
36
+ return items.join("\n");
37
+ }
38
+ // Flatten into a single content array
39
+ const parts = [];
40
+ for (const item of items) {
41
+ if (typeof item === "string") {
42
+ parts.push({ type: "text", text: item });
43
+ }
44
+ else {
45
+ parts.push(...item);
46
+ }
47
+ }
48
+ return parts;
49
+ }
25
50
  export function useAgentLoop(messages, options, callbacks) {
26
51
  const onComplete = callbacks?.onComplete;
27
52
  const onTurnText = callbacks?.onTurnText;
@@ -33,6 +58,7 @@ export function useAgentLoop(messages, options, callbacks) {
33
58
  const onTurnEnd = callbacks?.onTurnEnd;
34
59
  const onDone = callbacks?.onDone;
35
60
  const onAborted = callbacks?.onAborted;
61
+ const onQueuedStart = callbacks?.onQueuedStart;
36
62
  const [isRunning, setIsRunning] = useState(false);
37
63
  const [streamingText, setStreamingText] = useState("");
38
64
  const [streamingThinking, setStreamingThinking] = useState("");
@@ -46,6 +72,8 @@ export function useAgentLoop(messages, options, callbacks) {
46
72
  const [isThinking, setIsThinking] = useState(false);
47
73
  const [streamedTokenEstimate, setStreamedTokenEstimate] = useState(0);
48
74
  const abortRef = useRef(null);
75
+ const queueRef = useRef([]);
76
+ const [queuedCount, setQueuedCount] = useState(0);
49
77
  const activeToolCallsRef = useRef([]);
50
78
  const textPendingRef = useRef("");
51
79
  const textVisibleRef = useRef("");
@@ -182,237 +210,305 @@ export function useAgentLoop(messages, options, callbacks) {
182
210
  setThinkingMs(0);
183
211
  setIsThinking(false);
184
212
  setStreamedTokenEstimate(0);
213
+ queueRef.current = [];
214
+ setQueuedCount(0);
215
+ }, []);
216
+ const queueMessage = useCallback((content) => {
217
+ queueRef.current.push(content);
218
+ setQueuedCount(queueRef.current.length);
219
+ }, []);
220
+ const clearQueue = useCallback(() => {
221
+ queueRef.current = [];
222
+ setQueuedCount(0);
185
223
  }, []);
186
224
  const run = useCallback(async (userContent) => {
187
- const ac = new AbortController();
188
- abortRef.current = ac;
189
- let wasAborted = false;
190
- // Reset state
191
- doneCalledRef.current = false;
192
- textPendingRef.current = "";
193
- textVisibleRef.current = "";
194
- thinkingBufferRef.current = "";
195
- thinkingPendingRef.current = "";
196
- thinkingVisibleRef.current = "";
197
- runStartRef.current = Date.now();
198
- toolsUsedRef.current = new Set();
199
- charCountRef.current = 0;
200
- realTokensAccumRef.current = 0;
201
- thinkingAccumRef.current = 0;
202
- thinkingStartRef.current = null;
203
- phaseRef.current = "waiting";
204
- setStreamingText("");
205
- setStreamingThinking("");
206
- setActiveToolCalls([]);
207
- setActivityPhase("waiting");
208
- setElapsedMs(0);
209
- setThinkingMs(0);
210
- setIsThinking(false);
211
- setStreamedTokenEstimate(0);
212
- setIsRunning(true);
213
- // Start elapsed timer (ticks every 1000ms — less frequent to reduce
214
- // Ink re-renders which cause live-area flickering and viewport snapping)
215
- if (elapsedTimerRef.current)
216
- clearInterval(elapsedTimerRef.current);
217
- const timerStart = Date.now();
218
- elapsedTimerRef.current = setInterval(() => {
219
- const now = Date.now();
220
- setElapsedMs(now - timerStart);
221
- // Update live thinking time if currently thinking
222
- if (thinkingStartRef.current !== null) {
223
- setThinkingMs(thinkingAccumRef.current + (now - thinkingStartRef.current));
224
- }
225
- // Update token estimate
226
- setStreamedTokenEstimate(realTokensAccumRef.current + Math.ceil(charCountRef.current / 4));
227
- }, 1000);
228
- /** Freeze thinking time if currently in thinking phase */
229
- const freezeThinking = () => {
230
- if (thinkingStartRef.current !== null) {
231
- thinkingAccumRef.current += Date.now() - thinkingStartRef.current;
232
- thinkingStartRef.current = null;
233
- setThinkingMs(thinkingAccumRef.current);
234
- setIsThinking(false);
235
- }
236
- };
237
- // Push user message
238
- const userMsg = { role: "user", content: userContent };
239
- messages.current.push(userMsg);
240
- const startIndex = messages.current.length;
241
- try {
242
- // Resolve fresh credentials (handles OAuth token refresh)
243
- let apiKey = options.apiKey;
244
- let accountId = options.accountId;
245
- if (options.resolveCredentials) {
246
- const creds = await options.resolveCredentials();
247
- apiKey = creds.apiKey;
248
- accountId = creds.accountId;
249
- }
250
- const generator = agentLoop(messages.current, {
251
- provider: options.provider,
252
- model: options.model,
253
- tools: options.tools,
254
- webSearch: options.webSearch,
255
- maxTokens: options.maxTokens,
256
- thinking: options.thinking,
257
- apiKey,
258
- baseUrl: options.baseUrl,
259
- accountId,
260
- signal: ac.signal,
261
- transformContext: options.transformContext,
262
- });
263
- for await (const event of generator) {
264
- switch (event.type) {
265
- case "text_delta":
266
- textPendingRef.current += event.text;
267
- charCountRef.current += event.text.length;
268
- startReveal();
269
- if (phaseRef.current !== "generating") {
225
+ /** Run a single user message through the agent loop. Returns true if aborted. */
226
+ const runSingle = async (content) => {
227
+ const ac = new AbortController();
228
+ abortRef.current = ac;
229
+ let wasAborted = false;
230
+ // Reset state
231
+ doneCalledRef.current = false;
232
+ textPendingRef.current = "";
233
+ textVisibleRef.current = "";
234
+ thinkingBufferRef.current = "";
235
+ thinkingPendingRef.current = "";
236
+ thinkingVisibleRef.current = "";
237
+ runStartRef.current = Date.now();
238
+ toolsUsedRef.current = new Set();
239
+ charCountRef.current = 0;
240
+ realTokensAccumRef.current = 0;
241
+ thinkingAccumRef.current = 0;
242
+ thinkingStartRef.current = null;
243
+ phaseRef.current = "waiting";
244
+ setStreamingText("");
245
+ setStreamingThinking("");
246
+ setActiveToolCalls([]);
247
+ setActivityPhase("waiting");
248
+ setElapsedMs(0);
249
+ setThinkingMs(0);
250
+ setIsThinking(false);
251
+ setStreamedTokenEstimate(0);
252
+ setIsRunning(true);
253
+ // Start elapsed timer (ticks every 1000ms — less frequent to reduce
254
+ // Ink re-renders which cause live-area flickering and viewport snapping)
255
+ if (elapsedTimerRef.current)
256
+ clearInterval(elapsedTimerRef.current);
257
+ const timerStart = Date.now();
258
+ elapsedTimerRef.current = setInterval(() => {
259
+ const now = Date.now();
260
+ setElapsedMs(now - timerStart);
261
+ // Update live thinking time if currently thinking
262
+ if (thinkingStartRef.current !== null) {
263
+ setThinkingMs(thinkingAccumRef.current + (now - thinkingStartRef.current));
264
+ }
265
+ // Update token estimate
266
+ setStreamedTokenEstimate(realTokensAccumRef.current + Math.ceil(charCountRef.current / 4));
267
+ }, 1000);
268
+ /** Freeze thinking time if currently in thinking phase */
269
+ const freezeThinking = () => {
270
+ if (thinkingStartRef.current !== null) {
271
+ thinkingAccumRef.current += Date.now() - thinkingStartRef.current;
272
+ thinkingStartRef.current = null;
273
+ setThinkingMs(thinkingAccumRef.current);
274
+ setIsThinking(false);
275
+ }
276
+ };
277
+ // Push user message
278
+ const userMsg = { role: "user", content: content };
279
+ messages.current.push(userMsg);
280
+ const startIndex = messages.current.length;
281
+ try {
282
+ // Resolve fresh credentials (handles OAuth token refresh)
283
+ let apiKey = options.apiKey;
284
+ let accountId = options.accountId;
285
+ if (options.resolveCredentials) {
286
+ const creds = await options.resolveCredentials();
287
+ apiKey = creds.apiKey;
288
+ accountId = creds.accountId;
289
+ }
290
+ const generator = agentLoop(messages.current, {
291
+ provider: options.provider,
292
+ model: options.model,
293
+ tools: options.tools,
294
+ webSearch: options.webSearch,
295
+ maxTokens: options.maxTokens,
296
+ thinking: options.thinking,
297
+ apiKey,
298
+ baseUrl: options.baseUrl,
299
+ accountId,
300
+ signal: ac.signal,
301
+ transformContext: options.transformContext,
302
+ // Drain queued messages as steering — injected between tool calls
303
+ // and before the agent would stop, so the LLM sees user guidance
304
+ // within the same run instead of waiting for a new one.
305
+ getSteeringMessages: () => {
306
+ if (queueRef.current.length === 0)
307
+ return null;
308
+ const batch = queueRef.current.splice(0);
309
+ setQueuedCount(0);
310
+ const merged = mergeUserContent(batch);
311
+ onQueuedStart?.(merged);
312
+ return [{ role: "user", content: merged }];
313
+ },
314
+ });
315
+ for await (const event of generator) {
316
+ switch (event.type) {
317
+ case "text_delta":
318
+ textPendingRef.current += event.text;
319
+ charCountRef.current += event.text.length;
320
+ startReveal();
321
+ if (phaseRef.current !== "generating") {
322
+ freezeThinking();
323
+ phaseRef.current = "generating";
324
+ setActivityPhase("generating");
325
+ }
326
+ break;
327
+ case "thinking_delta":
328
+ thinkingBufferRef.current += event.text;
329
+ thinkingPendingRef.current += event.text;
330
+ charCountRef.current += event.text.length;
331
+ startThinkingReveal();
332
+ if (phaseRef.current !== "thinking") {
333
+ thinkingStartRef.current = Date.now();
334
+ setIsThinking(true);
335
+ phaseRef.current = "thinking";
336
+ setActivityPhase("thinking");
337
+ }
338
+ break;
339
+ case "tool_call_start": {
270
340
  freezeThinking();
271
- phaseRef.current = "generating";
272
- setActivityPhase("generating");
341
+ if (phaseRef.current !== "tools") {
342
+ phaseRef.current = "tools";
343
+ setActivityPhase("tools");
344
+ }
345
+ const newTc = {
346
+ toolCallId: event.toolCallId,
347
+ name: event.name,
348
+ args: event.args,
349
+ startTime: Date.now(),
350
+ updates: [],
351
+ };
352
+ onToolStart?.(event.toolCallId, event.name, event.args);
353
+ toolsUsedRef.current.add(event.name);
354
+ activeToolCallsRef.current = [...activeToolCallsRef.current, newTc];
355
+ setActiveToolCalls(activeToolCallsRef.current);
356
+ break;
273
357
  }
274
- break;
275
- case "thinking_delta":
276
- thinkingBufferRef.current += event.text;
277
- thinkingPendingRef.current += event.text;
278
- charCountRef.current += event.text.length;
279
- startThinkingReveal();
280
- if (phaseRef.current !== "thinking") {
281
- thinkingStartRef.current = Date.now();
282
- setIsThinking(true);
283
- phaseRef.current = "thinking";
284
- setActivityPhase("thinking");
358
+ case "tool_call_update": {
359
+ onToolUpdate?.(event.toolCallId, event.update);
360
+ // Mutate the matching tool call in-place to avoid allocating
361
+ // a new array + new objects on every update event. Over a 5h
362
+ // session with thousands of tool calls this prevents significant
363
+ // GC pressure from spread-copy churn.
364
+ const target = activeToolCallsRef.current.find((tc) => tc.toolCallId === event.toolCallId);
365
+ if (target) {
366
+ if (target.updates.length >= 20) {
367
+ target.updates.shift();
368
+ }
369
+ target.updates.push(event.update);
370
+ }
371
+ // Spread once to create a new array reference for React state
372
+ setActiveToolCalls([...activeToolCallsRef.current]);
373
+ break;
285
374
  }
286
- break;
287
- case "tool_call_start": {
288
- freezeThinking();
289
- if (phaseRef.current !== "tools") {
290
- phaseRef.current = "tools";
291
- setActivityPhase("tools");
375
+ case "tool_call_end": {
376
+ const tc = activeToolCallsRef.current.find((t) => t.toolCallId === event.toolCallId);
377
+ const toolName = tc?.name ?? "unknown";
378
+ const durationMs = tc ? Date.now() - tc.startTime : 0;
379
+ onToolEnd?.(event.toolCallId, toolName, event.result, event.isError, durationMs, event.details);
380
+ activeToolCallsRef.current = activeToolCallsRef.current.filter((t) => t.toolCallId !== event.toolCallId);
381
+ setActiveToolCalls(activeToolCallsRef.current);
382
+ break;
292
383
  }
293
- const newTc = {
294
- toolCallId: event.toolCallId,
295
- name: event.name,
296
- args: event.args,
297
- startTime: Date.now(),
298
- updates: [],
299
- };
300
- onToolStart?.(event.toolCallId, event.name, event.args);
301
- toolsUsedRef.current.add(event.name);
302
- activeToolCallsRef.current = [...activeToolCallsRef.current, newTc];
303
- setActiveToolCalls(activeToolCallsRef.current);
304
- break;
305
- }
306
- case "tool_call_update": {
307
- onToolUpdate?.(event.toolCallId, event.update);
308
- activeToolCallsRef.current = activeToolCallsRef.current.map((tc) => tc.toolCallId === event.toolCallId
309
- ? {
310
- ...tc,
311
- // Keep only the last 20 updates to prevent unbounded memory growth
312
- updates: tc.updates.length >= 20
313
- ? [...tc.updates.slice(-19), event.update]
314
- : [...tc.updates, event.update],
384
+ case "server_tool_call":
385
+ onServerToolCall?.(event.id, event.name, event.input);
386
+ break;
387
+ case "server_tool_result":
388
+ onServerToolResult?.(event.toolUseId, event.resultType, event.data);
389
+ break;
390
+ case "steering_message":
391
+ // Steering message was injected — UI already notified via
392
+ // onQueuedStart inside getSteeringMessages callback.
393
+ break;
394
+ case "turn_end":
395
+ onTurnEnd?.(event.turn, event.stopReason, event.usage);
396
+ setCurrentTurn(event.turn);
397
+ setTotalTokens((prev) => ({
398
+ input: prev.input + event.usage.inputTokens,
399
+ output: prev.output + event.usage.outputTokens,
400
+ }));
401
+ // Latest turn's input tokens = current context window fill
402
+ // With prompt caching, input_tokens only counts non-cached tokens.
403
+ // Total context = input + cache_read + cache_write.
404
+ setContextUsed(event.usage.inputTokens +
405
+ (event.usage.cacheRead ?? 0) +
406
+ (event.usage.cacheWrite ?? 0));
407
+ // Replace char-based estimate with real output tokens
408
+ realTokensAccumRef.current += event.usage.outputTokens;
409
+ charCountRef.current = 0;
410
+ setStreamedTokenEstimate(realTokensAccumRef.current);
411
+ // Reset phase for next turn
412
+ phaseRef.current = "waiting";
413
+ setActivityPhase("waiting");
414
+ // Flush all pending text before completing turn
415
+ flushAllText();
416
+ if (textVisibleRef.current) {
417
+ onTurnText?.(textVisibleRef.current, thinkingBufferRef.current, thinkingAccumRef.current);
315
418
  }
316
- : tc);
317
- setActiveToolCalls(activeToolCallsRef.current);
318
- break;
319
- }
320
- case "tool_call_end": {
321
- const tc = activeToolCallsRef.current.find((t) => t.toolCallId === event.toolCallId);
322
- const toolName = tc?.name ?? "unknown";
323
- const durationMs = tc ? Date.now() - tc.startTime : 0;
324
- onToolEnd?.(event.toolCallId, toolName, event.result, event.isError, durationMs, event.details);
325
- activeToolCallsRef.current = activeToolCallsRef.current.filter((t) => t.toolCallId !== event.toolCallId);
326
- setActiveToolCalls(activeToolCallsRef.current);
327
- break;
419
+ // Reset streaming buffers for next turn
420
+ textPendingRef.current = "";
421
+ textVisibleRef.current = "";
422
+ thinkingBufferRef.current = "";
423
+ thinkingPendingRef.current = "";
424
+ thinkingVisibleRef.current = "";
425
+ setStreamingText("");
426
+ setStreamingThinking("");
427
+ break;
428
+ case "agent_done":
429
+ flushAllText();
430
+ // Batch ALL completion state into a single render so Ink
431
+ // processes the live-area change atomically. Previously
432
+ // isRunning, activityPhase, and onDone landed in separate
433
+ // render batches, causing multiple live-area height changes
434
+ // that confused Ink's cursor math and clipped content.
435
+ setIsRunning(false);
436
+ phaseRef.current = "idle";
437
+ setActivityPhase("idle");
438
+ // Call onDone HERE (not in finally) so its state updates
439
+ // (doneStatus, flushing items to Static) are batched too.
440
+ onDone?.(Date.now() - runStartRef.current, [...toolsUsedRef.current]);
441
+ doneCalledRef.current = true;
442
+ break;
328
443
  }
329
- case "server_tool_call":
330
- onServerToolCall?.(event.id, event.name, event.input);
331
- break;
332
- case "server_tool_result":
333
- onServerToolResult?.(event.toolUseId, event.resultType, event.data);
334
- break;
335
- case "turn_end":
336
- onTurnEnd?.(event.turn, event.stopReason, event.usage);
337
- setCurrentTurn(event.turn);
338
- setTotalTokens((prev) => ({
339
- input: prev.input + event.usage.inputTokens,
340
- output: prev.output + event.usage.outputTokens,
341
- }));
342
- // Latest turn's input tokens = current context window fill
343
- // With prompt caching, input_tokens only counts non-cached tokens.
344
- // Total context = input + cache_read + cache_write.
345
- setContextUsed(event.usage.inputTokens +
346
- (event.usage.cacheRead ?? 0) +
347
- (event.usage.cacheWrite ?? 0));
348
- // Replace char-based estimate with real output tokens
349
- realTokensAccumRef.current += event.usage.outputTokens;
350
- charCountRef.current = 0;
351
- setStreamedTokenEstimate(realTokensAccumRef.current);
352
- // Reset phase for next turn
353
- phaseRef.current = "waiting";
354
- setActivityPhase("waiting");
355
- // Flush all pending text before completing turn
356
- flushAllText();
357
- if (textVisibleRef.current) {
358
- onTurnText?.(textVisibleRef.current, thinkingBufferRef.current, thinkingAccumRef.current);
359
- }
360
- // Reset streaming buffers for next turn
361
- textPendingRef.current = "";
362
- textVisibleRef.current = "";
363
- thinkingBufferRef.current = "";
364
- thinkingPendingRef.current = "";
365
- thinkingVisibleRef.current = "";
366
- setStreamingText("");
367
- setStreamingThinking("");
368
- break;
369
- case "agent_done":
370
- flushAllText();
371
- // Batch ALL completion state into a single render so Ink
372
- // processes the live-area change atomically. Previously
373
- // isRunning, activityPhase, and onDone landed in separate
374
- // render batches, causing multiple live-area height changes
375
- // that confused Ink's cursor math and clipped content.
376
- setIsRunning(false);
377
- phaseRef.current = "idle";
378
- setActivityPhase("idle");
379
- // Call onDone HERE (not in finally) so its state updates
380
- // (doneStatus, flushing items to Static) are batched too.
381
- onDone?.(Date.now() - runStartRef.current, [...toolsUsedRef.current]);
382
- doneCalledRef.current = true;
383
- break;
384
444
  }
385
445
  }
386
- }
387
- catch (err) {
388
- const isAbort = err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"));
389
- if (!isAbort) {
390
- throw err;
391
- }
392
- wasAborted = true;
393
- }
394
- finally {
395
- setIsRunning(false);
396
- abortRef.current = null;
397
- stopReveal();
398
- stopThinkingReveal();
399
- if (elapsedTimerRef.current) {
400
- clearInterval(elapsedTimerRef.current);
401
- elapsedTimerRef.current = null;
402
- }
403
- phaseRef.current = "idle";
404
- setActivityPhase("idle");
405
- if (wasAborted) {
406
- onAborted?.();
446
+ catch (err) {
447
+ const isAbort = err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"));
448
+ if (!isAbort) {
449
+ throw err;
450
+ }
451
+ wasAborted = true;
407
452
  }
408
- else if (!doneCalledRef.current) {
409
- // Safety fallback normally agent_done calls onDone in-band
410
- const durationMs = Date.now() - runStartRef.current;
411
- onDone?.(durationMs, [...toolsUsedRef.current]);
453
+ finally {
454
+ // If the signal was aborted but the loop exited normally (e.g.
455
+ // agent_done fired right before the abort), treat it as aborted so
456
+ // the user sees "Request was stopped." instead of a duration verb.
457
+ if (!wasAborted && ac.signal.aborted) {
458
+ wasAborted = true;
459
+ }
460
+ setIsRunning(false);
461
+ abortRef.current = null;
462
+ stopReveal();
463
+ stopThinkingReveal();
464
+ if (elapsedTimerRef.current) {
465
+ clearInterval(elapsedTimerRef.current);
466
+ elapsedTimerRef.current = null;
467
+ }
468
+ phaseRef.current = "idle";
469
+ setActivityPhase("idle");
470
+ if (wasAborted) {
471
+ // Flush any visible streaming text so onAborted (which adds
472
+ // "Request was stopped.") lands AFTER the agent's partial text
473
+ // in liveItems — not above it.
474
+ flushAllText();
475
+ if (textVisibleRef.current) {
476
+ onTurnText?.(textVisibleRef.current, thinkingBufferRef.current, thinkingAccumRef.current);
477
+ }
478
+ textPendingRef.current = "";
479
+ textVisibleRef.current = "";
480
+ thinkingBufferRef.current = "";
481
+ thinkingPendingRef.current = "";
482
+ thinkingVisibleRef.current = "";
483
+ setStreamingText("");
484
+ setStreamingThinking("");
485
+ onAborted?.();
486
+ }
487
+ else if (!doneCalledRef.current) {
488
+ // Safety fallback — normally agent_done calls onDone in-band
489
+ const durationMs = Date.now() - runStartRef.current;
490
+ onDone?.(durationMs, [...toolsUsedRef.current]);
491
+ }
492
+ // Notify parent of new messages
493
+ const newMsgs = messages.current.slice(startIndex);
494
+ onComplete?.(newMsgs);
412
495
  }
413
- // Notify parent of new messages
414
- const newMsgs = messages.current.slice(startIndex);
415
- onComplete?.(newMsgs);
496
+ return wasAborted;
497
+ }; // end runSingle
498
+ // Run the initial message
499
+ const aborted = await runSingle(userContent);
500
+ // Drain the queue: process follow-up messages that arrived after agent_done.
501
+ // Most queued messages are consumed mid-run via getSteeringMessages, but
502
+ // messages that arrive after the agent finishes (no more tool calls to
503
+ // trigger steering) land here. Batch all remaining into a single run.
504
+ if (!aborted && queueRef.current.length > 0) {
505
+ const batch = queueRef.current.splice(0);
506
+ setQueuedCount(0);
507
+ const merged = mergeUserContent(batch);
508
+ // Let React process the onDone state updates before starting next run
509
+ await new Promise((r) => setTimeout(r, 100));
510
+ onQueuedStart?.(merged);
511
+ await runSingle(merged);
416
512
  }
417
513
  }, [
418
514
  messages,
@@ -427,6 +523,7 @@ export function useAgentLoop(messages, options, callbacks) {
427
523
  onTurnEnd,
428
524
  onDone,
429
525
  onAborted,
526
+ onQueuedStart,
430
527
  startReveal,
431
528
  stopReveal,
432
529
  startThinkingReveal,
@@ -444,11 +541,14 @@ export function useAgentLoop(messages, options, callbacks) {
444
541
  elapsedTimerRef.current = null;
445
542
  }
446
543
  };
447
- }, [stopReveal]);
544
+ }, [stopReveal, stopThinkingReveal]);
448
545
  return {
449
546
  run,
450
547
  abort,
451
548
  reset,
549
+ queueMessage,
550
+ queuedCount,
551
+ clearQueue,
452
552
  isRunning,
453
553
  streamingText,
454
554
  streamingThinking,