@mragentix/cli 4.2.37 → 4.2.38
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.
- package/dist/cli.js +180 -9
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts +1 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +8 -1
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/skills.d.ts +2 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +8 -8
- package/dist/core/skills.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +17 -5
- package/dist/interactive.js.map +1 -1
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +2 -1
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +2 -1
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc-mode.js +2 -1
- package/dist/modes/rpc-mode.js.map +1 -1
- package/dist/modes/serve-mode.d.ts.map +1 -1
- package/dist/modes/serve-mode.js +7 -6
- package/dist/modes/serve-mode.js.map +1 -1
- package/dist/system-prompt.d.ts +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +40 -2
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/bash.d.ts +3 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +4 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit-diff.test.d.ts +2 -0
- package/dist/tools/edit-diff.test.d.ts.map +1 -0
- package/dist/tools/edit-diff.test.js +67 -0
- package/dist/tools/edit-diff.test.js.map +1 -0
- package/dist/tools/edit.d.ts +3 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +4 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.d.ts +2 -0
- package/dist/tools/edit.test.d.ts.map +1 -0
- package/dist/tools/edit.test.js +93 -0
- package/dist/tools/edit.test.js.map +1 -0
- package/dist/tools/enter-plan.d.ts +8 -0
- package/dist/tools/enter-plan.d.ts.map +1 -0
- package/dist/tools/enter-plan.js +28 -0
- package/dist/tools/enter-plan.js.map +1 -0
- package/dist/tools/exit-plan.d.ts +8 -0
- package/dist/tools/exit-plan.d.ts.map +1 -0
- package/dist/tools/exit-plan.js +36 -0
- package/dist/tools/exit-plan.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +20 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/path-utils.test.d.ts +2 -0
- package/dist/tools/path-utils.test.d.ts.map +1 -0
- package/dist/tools/path-utils.test.js +49 -0
- package/dist/tools/path-utils.test.js.map +1 -0
- package/dist/tools/plan-mode.test.d.ts +2 -0
- package/dist/tools/plan-mode.test.d.ts.map +1 -0
- package/dist/tools/plan-mode.test.js +214 -0
- package/dist/tools/plan-mode.test.js.map +1 -0
- package/dist/tools/read.test.d.ts +2 -0
- package/dist/tools/read.test.d.ts.map +1 -0
- package/dist/tools/read.test.js +81 -0
- package/dist/tools/read.test.js.map +1 -0
- package/dist/tools/skill.d.ts +10 -0
- package/dist/tools/skill.d.ts.map +1 -0
- package/dist/tools/skill.js +36 -0
- package/dist/tools/skill.js.map +1 -0
- package/dist/tools/subagent.d.ts +3 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +4 -1
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/truncate-utils.test.d.ts +2 -0
- package/dist/tools/truncate-utils.test.d.ts.map +1 -0
- package/dist/tools/truncate-utils.test.js +93 -0
- package/dist/tools/truncate-utils.test.js.map +1 -0
- package/dist/tools/write.d.ts +3 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +11 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +65 -52
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts +18 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +234 -31
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts +2 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +25 -4
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
- package/dist/ui/components/AssistantMessage.js +6 -1
- package/dist/ui/components/AssistantMessage.js.map +1 -1
- package/dist/ui/components/Banner.d.ts.map +1 -1
- package/dist/ui/components/Banner.js +13 -4
- package/dist/ui/components/Banner.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +2 -1
- package/dist/ui/components/Footer.d.ts.map +1 -1
- package/dist/ui/components/Footer.js +30 -16
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/components/InputArea.d.ts +3 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +27 -17
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/Markdown.d.ts +5 -1
- package/dist/ui/components/Markdown.d.ts.map +1 -1
- package/dist/ui/components/Markdown.js +14 -6
- package/dist/ui/components/Markdown.js.map +1 -1
- package/dist/ui/components/PlanApproval.d.ts +8 -0
- package/dist/ui/components/PlanApproval.d.ts.map +1 -0
- package/dist/ui/components/PlanApproval.js +58 -0
- package/dist/ui/components/PlanApproval.js.map +1 -0
- package/dist/ui/components/PlanBanner.d.ts +6 -0
- package/dist/ui/components/PlanBanner.d.ts.map +1 -0
- package/dist/ui/components/PlanBanner.js +28 -0
- package/dist/ui/components/PlanBanner.js.map +1 -0
- package/dist/ui/components/PlanOverlay.d.ts +11 -0
- package/dist/ui/components/PlanOverlay.d.ts.map +1 -0
- package/dist/ui/components/PlanOverlay.js +271 -0
- package/dist/ui/components/PlanOverlay.js.map +1 -0
- package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ServerToolExecution.js +11 -3
- package/dist/ui/components/ServerToolExecution.js.map +1 -1
- package/dist/ui/components/SkillsOverlay.d.ts +7 -0
- package/dist/ui/components/SkillsOverlay.d.ts.map +1 -0
- package/dist/ui/components/SkillsOverlay.js +162 -0
- package/dist/ui/components/SkillsOverlay.js.map +1 -0
- package/dist/ui/components/Spinner.js +1 -1
- package/dist/ui/components/Spinner.js.map +1 -1
- package/dist/ui/components/StreamingArea.d.ts +2 -1
- package/dist/ui/components/StreamingArea.d.ts.map +1 -1
- package/dist/ui/components/StreamingArea.js +7 -2
- package/dist/ui/components/StreamingArea.js.map +1 -1
- package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
- package/dist/ui/components/SubAgentPanel.js +21 -11
- package/dist/ui/components/SubAgentPanel.js.map +1 -1
- package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
- package/dist/ui/components/TaskOverlay.js +10 -6
- package/dist/ui/components/TaskOverlay.js.map +1 -1
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +33 -13
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/components/ToolGroupExecution.js +1 -1
- package/dist/ui/components/ToolGroupExecution.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +8 -0
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +318 -236
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useTerminalSize.d.ts +18 -16
- package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalSize.js +31 -24
- package/dist/ui/hooks/useTerminalSize.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +1 -5
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/render.d.ts +11 -0
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +7 -2
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/sessions.d.ts.map +1 -1
- package/dist/ui/sessions.js +1 -5
- package/dist/ui/sessions.js.map +1 -1
- package/dist/ui/theme/dark.json +3 -1
- package/dist/ui/theme/light.json +3 -1
- package/dist/ui/theme/theme.d.ts +2 -0
- package/dist/ui/theme/theme.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
-
import { agentLoop } from "@mragentix/agent";
|
|
2
|
+
import { agentLoop, isAbortError } from "@mragentix/agent";
|
|
3
3
|
/** Rough token estimate from message content (~4 chars per token). */
|
|
4
4
|
function estimateTokens(msgs) {
|
|
5
5
|
let chars = 0;
|
|
@@ -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,254 +210,304 @@ 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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if
|
|
231
|
-
|
|
232
|
-
thinkingStartRef.current
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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);
|
|
316
418
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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;
|
|
322
443
|
}
|
|
323
|
-
case "tool_call_end": {
|
|
324
|
-
const tc = activeToolCallsRef.current.find((t) => t.toolCallId === event.toolCallId);
|
|
325
|
-
const toolName = tc?.name ?? "unknown";
|
|
326
|
-
const durationMs = tc ? Date.now() - tc.startTime : 0;
|
|
327
|
-
onToolEnd?.(event.toolCallId, toolName, event.result, event.isError, durationMs, event.details);
|
|
328
|
-
activeToolCallsRef.current = activeToolCallsRef.current.filter((t) => t.toolCallId !== event.toolCallId);
|
|
329
|
-
setActiveToolCalls(activeToolCallsRef.current);
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
case "server_tool_call":
|
|
333
|
-
onServerToolCall?.(event.id, event.name, event.input);
|
|
334
|
-
break;
|
|
335
|
-
case "server_tool_result":
|
|
336
|
-
onServerToolResult?.(event.toolUseId, event.resultType, event.data);
|
|
337
|
-
break;
|
|
338
|
-
case "turn_end":
|
|
339
|
-
onTurnEnd?.(event.turn, event.stopReason, event.usage);
|
|
340
|
-
setCurrentTurn(event.turn);
|
|
341
|
-
setTotalTokens((prev) => ({
|
|
342
|
-
input: prev.input + event.usage.inputTokens,
|
|
343
|
-
output: prev.output + event.usage.outputTokens,
|
|
344
|
-
}));
|
|
345
|
-
// Latest turn's input tokens = current context window fill
|
|
346
|
-
// With prompt caching, input_tokens only counts non-cached tokens.
|
|
347
|
-
// Total context = input + cache_read + cache_write.
|
|
348
|
-
setContextUsed(event.usage.inputTokens +
|
|
349
|
-
(event.usage.cacheRead ?? 0) +
|
|
350
|
-
(event.usage.cacheWrite ?? 0));
|
|
351
|
-
// Replace char-based estimate with real output tokens
|
|
352
|
-
realTokensAccumRef.current += event.usage.outputTokens;
|
|
353
|
-
charCountRef.current = 0;
|
|
354
|
-
setStreamedTokenEstimate(realTokensAccumRef.current);
|
|
355
|
-
// Reset phase for next turn
|
|
356
|
-
phaseRef.current = "waiting";
|
|
357
|
-
setActivityPhase("waiting");
|
|
358
|
-
// Flush all pending text before completing turn
|
|
359
|
-
flushAllText();
|
|
360
|
-
if (textVisibleRef.current) {
|
|
361
|
-
onTurnText?.(textVisibleRef.current, thinkingBufferRef.current, thinkingAccumRef.current);
|
|
362
|
-
}
|
|
363
|
-
// Reset streaming buffers for next turn
|
|
364
|
-
textPendingRef.current = "";
|
|
365
|
-
textVisibleRef.current = "";
|
|
366
|
-
thinkingBufferRef.current = "";
|
|
367
|
-
thinkingPendingRef.current = "";
|
|
368
|
-
thinkingVisibleRef.current = "";
|
|
369
|
-
setStreamingText("");
|
|
370
|
-
setStreamingThinking("");
|
|
371
|
-
break;
|
|
372
|
-
case "agent_done":
|
|
373
|
-
flushAllText();
|
|
374
|
-
// Batch ALL completion state into a single render so Ink
|
|
375
|
-
// processes the live-area change atomically. Previously
|
|
376
|
-
// isRunning, activityPhase, and onDone landed in separate
|
|
377
|
-
// render batches, causing multiple live-area height changes
|
|
378
|
-
// that confused Ink's cursor math and clipped content.
|
|
379
|
-
setIsRunning(false);
|
|
380
|
-
phaseRef.current = "idle";
|
|
381
|
-
setActivityPhase("idle");
|
|
382
|
-
// Call onDone HERE (not in finally) so its state updates
|
|
383
|
-
// (doneStatus, flushing items to Static) are batched too.
|
|
384
|
-
onDone?.(Date.now() - runStartRef.current, [...toolsUsedRef.current]);
|
|
385
|
-
doneCalledRef.current = true;
|
|
386
|
-
break;
|
|
387
444
|
}
|
|
388
445
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (!isAbort) {
|
|
393
|
-
throw err;
|
|
394
|
-
}
|
|
395
|
-
wasAborted = true;
|
|
396
|
-
}
|
|
397
|
-
finally {
|
|
398
|
-
setIsRunning(false);
|
|
399
|
-
abortRef.current = null;
|
|
400
|
-
stopReveal();
|
|
401
|
-
stopThinkingReveal();
|
|
402
|
-
if (elapsedTimerRef.current) {
|
|
403
|
-
clearInterval(elapsedTimerRef.current);
|
|
404
|
-
elapsedTimerRef.current = null;
|
|
405
|
-
}
|
|
406
|
-
phaseRef.current = "idle";
|
|
407
|
-
setActivityPhase("idle");
|
|
408
|
-
if (wasAborted) {
|
|
409
|
-
// Flush any visible streaming text so onAborted (which adds
|
|
410
|
-
// "Request was stopped.") lands AFTER the agent's partial text
|
|
411
|
-
// in liveItems — not above it.
|
|
412
|
-
flushAllText();
|
|
413
|
-
if (textVisibleRef.current) {
|
|
414
|
-
onTurnText?.(textVisibleRef.current, thinkingBufferRef.current, thinkingAccumRef.current);
|
|
446
|
+
catch (err) {
|
|
447
|
+
if (!isAbortError(err)) {
|
|
448
|
+
throw err;
|
|
415
449
|
}
|
|
416
|
-
|
|
417
|
-
textVisibleRef.current = "";
|
|
418
|
-
thinkingBufferRef.current = "";
|
|
419
|
-
thinkingPendingRef.current = "";
|
|
420
|
-
thinkingVisibleRef.current = "";
|
|
421
|
-
setStreamingText("");
|
|
422
|
-
setStreamingThinking("");
|
|
423
|
-
onAborted?.();
|
|
450
|
+
wasAborted = true;
|
|
424
451
|
}
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
452
|
+
finally {
|
|
453
|
+
// If the signal was aborted but the loop exited normally (e.g.
|
|
454
|
+
// agent_done fired right before the abort), treat it as aborted so
|
|
455
|
+
// the user sees "Request was stopped." instead of a duration verb.
|
|
456
|
+
if (!wasAborted && ac.signal.aborted) {
|
|
457
|
+
wasAborted = true;
|
|
458
|
+
}
|
|
459
|
+
setIsRunning(false);
|
|
460
|
+
abortRef.current = null;
|
|
461
|
+
stopReveal();
|
|
462
|
+
stopThinkingReveal();
|
|
463
|
+
if (elapsedTimerRef.current) {
|
|
464
|
+
clearInterval(elapsedTimerRef.current);
|
|
465
|
+
elapsedTimerRef.current = null;
|
|
466
|
+
}
|
|
467
|
+
phaseRef.current = "idle";
|
|
468
|
+
setActivityPhase("idle");
|
|
469
|
+
if (wasAborted) {
|
|
470
|
+
// Flush any visible streaming text so onAborted (which adds
|
|
471
|
+
// "Request was stopped.") lands AFTER the agent's partial text
|
|
472
|
+
// in liveItems — not above it.
|
|
473
|
+
flushAllText();
|
|
474
|
+
if (textVisibleRef.current) {
|
|
475
|
+
onTurnText?.(textVisibleRef.current, thinkingBufferRef.current, thinkingAccumRef.current);
|
|
476
|
+
}
|
|
477
|
+
textPendingRef.current = "";
|
|
478
|
+
textVisibleRef.current = "";
|
|
479
|
+
thinkingBufferRef.current = "";
|
|
480
|
+
thinkingPendingRef.current = "";
|
|
481
|
+
thinkingVisibleRef.current = "";
|
|
482
|
+
setStreamingText("");
|
|
483
|
+
setStreamingThinking("");
|
|
484
|
+
onAborted?.();
|
|
485
|
+
}
|
|
486
|
+
else if (!doneCalledRef.current) {
|
|
487
|
+
// Safety fallback — normally agent_done calls onDone in-band
|
|
488
|
+
const durationMs = Date.now() - runStartRef.current;
|
|
489
|
+
onDone?.(durationMs, [...toolsUsedRef.current]);
|
|
490
|
+
}
|
|
491
|
+
// Notify parent of new messages
|
|
492
|
+
const newMsgs = messages.current.slice(startIndex);
|
|
493
|
+
onComplete?.(newMsgs);
|
|
429
494
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
495
|
+
return wasAborted;
|
|
496
|
+
}; // end runSingle
|
|
497
|
+
// Run the initial message
|
|
498
|
+
const aborted = await runSingle(userContent);
|
|
499
|
+
// Drain the queue: process follow-up messages that arrived after agent_done.
|
|
500
|
+
// Most queued messages are consumed mid-run via getSteeringMessages, but
|
|
501
|
+
// messages that arrive after the agent finishes (no more tool calls to
|
|
502
|
+
// trigger steering) land here. Batch all remaining into a single run.
|
|
503
|
+
if (!aborted && queueRef.current.length > 0) {
|
|
504
|
+
const batch = queueRef.current.splice(0);
|
|
505
|
+
setQueuedCount(0);
|
|
506
|
+
const merged = mergeUserContent(batch);
|
|
507
|
+
// Let React process the onDone state updates before starting next run
|
|
508
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
509
|
+
onQueuedStart?.(merged);
|
|
510
|
+
await runSingle(merged);
|
|
433
511
|
}
|
|
434
512
|
}, [
|
|
435
513
|
messages,
|
|
@@ -444,6 +522,7 @@ export function useAgentLoop(messages, options, callbacks) {
|
|
|
444
522
|
onTurnEnd,
|
|
445
523
|
onDone,
|
|
446
524
|
onAborted,
|
|
525
|
+
onQueuedStart,
|
|
447
526
|
startReveal,
|
|
448
527
|
stopReveal,
|
|
449
528
|
startThinkingReveal,
|
|
@@ -466,6 +545,9 @@ export function useAgentLoop(messages, options, callbacks) {
|
|
|
466
545
|
run,
|
|
467
546
|
abort,
|
|
468
547
|
reset,
|
|
548
|
+
queueMessage,
|
|
549
|
+
queuedCount,
|
|
550
|
+
clearQueue,
|
|
469
551
|
isRunning,
|
|
470
552
|
streamingText,
|
|
471
553
|
streamingThinking,
|