@agent-native/core 0.84.23 → 0.84.25
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +8 -6
- package/corpus/core/src/agent/run-manager.ts +57 -6
- package/corpus/core/src/client/AssistantChat.tsx +82 -15
- package/corpus/core/src/client/active-run-state.ts +22 -0
- package/corpus/core/src/client/chat/message-components.tsx +2 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +18 -2
- package/corpus/core/src/client/sse-event-processor.ts +196 -51
- package/corpus/core/src/client/tool-display.ts +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +15 -13
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +4 -2
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +11 -2
- package/corpus/templates/clips/app/i18n/ar-SA.ts +4 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +4 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +4 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +4 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +4 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +4 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +4 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +31 -3
- package/corpus/templates/clips/changelog/2026-07-01-clip-embeds-now-render-as-a-clean-video-only-player-without-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-shared-clips-can-now-copy-a-ready-to-send-prompt-that-tells-.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +2 -2
- package/corpus/templates/clips/chrome-extension/src/github-preview.ts +41 -31
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +8 -6
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +56 -4
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +2 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +74 -18
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/active-run-state.d.ts +2 -0
- package/dist/client/active-run-state.d.ts.map +1 -1
- package/dist/client/active-run-state.js +17 -0
- package/dist/client/active-run-state.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +3 -1
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +3 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +8 -8
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +1 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +163 -45
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/tool-display.js +1 -1
- package/dist/client/tool-display.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.25
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a11293f: Keep tail-resume reconnect content display-only, recover zero-byte action-prep stalls, and polish Design screen tool labels.
|
|
8
|
+
|
|
9
|
+
## 0.84.24
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- d88a57f: Fix chat tool-call rows shrinking, keep running chats visibly thinking, stream reconnect content incrementally, and collapse exact repeated tool rows.
|
|
14
|
+
|
|
3
15
|
## 0.84.23
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.25",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -2843,22 +2843,24 @@ export async function runAgentLoop(opts: {
|
|
|
2843
2843
|
// reasons, then collapse it when content arrives.
|
|
2844
2844
|
send({ type: "thinking", text: event.text });
|
|
2845
2845
|
} else if (event.type === "tool-input-start") {
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2846
|
+
const key = event.id ?? event.name;
|
|
2847
|
+
if (key && event.name) {
|
|
2848
|
+
toolInputNames.set(key, event.name);
|
|
2849
|
+
toolInputBytes.set(key, 0);
|
|
2849
2850
|
}
|
|
2850
2851
|
sendToolInputActivity(event.name, undefined, true);
|
|
2851
2852
|
} else if (event.type === "tool-input-delta") {
|
|
2853
|
+
const key = event.id ?? event.name;
|
|
2852
2854
|
const toolName =
|
|
2853
2855
|
event.name ??
|
|
2854
2856
|
(event.id ? toolInputNames.get(event.id) : undefined);
|
|
2855
2857
|
let progressBytes: number | undefined;
|
|
2856
|
-
if (
|
|
2857
|
-
const previous = toolInputBytes.get(
|
|
2858
|
+
if (key) {
|
|
2859
|
+
const previous = toolInputBytes.get(key) ?? 0;
|
|
2858
2860
|
progressBytes =
|
|
2859
2861
|
previous +
|
|
2860
2862
|
new TextEncoder().encode(event.text ?? "").byteLength;
|
|
2861
|
-
toolInputBytes.set(
|
|
2863
|
+
toolInputBytes.set(key, progressBytes);
|
|
2862
2864
|
}
|
|
2863
2865
|
sendToolInputActivity(toolName, progressBytes);
|
|
2864
2866
|
} else if (event.type === "gateway-heartbeat") {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
bumpRunProgress,
|
|
17
17
|
reapIfStale,
|
|
18
18
|
reapUnclaimedBackgroundRun,
|
|
19
|
+
reconcileTerminalRunFromEvents,
|
|
19
20
|
ensureTerminalRunEvent,
|
|
20
21
|
setRunError,
|
|
21
22
|
setRunTerminalReason,
|
|
@@ -125,6 +126,12 @@ export const TERMINAL_RUN_RECONNECT_WINDOW_MS = 10 * 60 * 1000;
|
|
|
125
126
|
|
|
126
127
|
const PROVIDER_RATE_LIMITED_ERROR_CODE = "provider_rate_limited";
|
|
127
128
|
|
|
129
|
+
function isPreparingActionActivityEvent(event: AgentChatEvent): boolean {
|
|
130
|
+
if (event.type !== "activity") return false;
|
|
131
|
+
const label = event.label.trim().toLowerCase();
|
|
132
|
+
return label.startsWith("preparing ") && label.includes(" action");
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
function getRunErrorMessage(err: unknown): string {
|
|
129
136
|
if (
|
|
130
137
|
typeof err === "object" &&
|
|
@@ -423,6 +430,7 @@ export function startRun(
|
|
|
423
430
|
// chunk. The stuck-detector threshold is on the order of tens of seconds,
|
|
424
431
|
// so 1s resolution is plenty.
|
|
425
432
|
let lastProgressBumpAt = 0;
|
|
433
|
+
const preparingActivityBytes = new Map<string, number>();
|
|
426
434
|
let eventPersistenceErrorCaptured = false;
|
|
427
435
|
const bumpProgressIfDue = () => {
|
|
428
436
|
const now = Date.now();
|
|
@@ -430,6 +438,41 @@ export function startRun(
|
|
|
430
438
|
lastProgressBumpAt = now;
|
|
431
439
|
bumpRunProgress(runId).catch(() => {});
|
|
432
440
|
};
|
|
441
|
+
const shouldBumpProgressForEvent = (event: AgentChatEvent): boolean => {
|
|
442
|
+
if (event.type === "stream_keepalive") return false;
|
|
443
|
+
if (event.type === "activity" && isPreparingActionActivityEvent(event)) {
|
|
444
|
+
const toolKey = event.tool?.trim() || event.label.trim();
|
|
445
|
+
const progressBytes =
|
|
446
|
+
typeof event.progressBytes === "number" &&
|
|
447
|
+
Number.isFinite(event.progressBytes) &&
|
|
448
|
+
event.progressBytes >= 0
|
|
449
|
+
? Math.floor(event.progressBytes)
|
|
450
|
+
: undefined;
|
|
451
|
+
if (progressBytes === undefined) return false;
|
|
452
|
+
const previousBytes = preparingActivityBytes.get(toolKey) ?? 0;
|
|
453
|
+
if (progressBytes <= previousBytes) {
|
|
454
|
+
preparingActivityBytes.set(
|
|
455
|
+
toolKey,
|
|
456
|
+
Math.max(previousBytes, progressBytes),
|
|
457
|
+
);
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
preparingActivityBytes.set(toolKey, progressBytes);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (event.type === "tool_start" || event.type === "tool_done") {
|
|
464
|
+
const tool = event.tool?.trim();
|
|
465
|
+
if (tool) preparingActivityBytes.delete(tool);
|
|
466
|
+
}
|
|
467
|
+
if (
|
|
468
|
+
event.type === "clear" ||
|
|
469
|
+
event.type === "done" ||
|
|
470
|
+
event.type === "error"
|
|
471
|
+
) {
|
|
472
|
+
preparingActivityBytes.clear();
|
|
473
|
+
}
|
|
474
|
+
return true;
|
|
475
|
+
};
|
|
433
476
|
|
|
434
477
|
// Periodic SQL abort check interval (for cross-isolate abort on Workers).
|
|
435
478
|
// Also self-aborts when our row is no longer status='running' — catches the
|
|
@@ -539,8 +582,12 @@ export function startRun(
|
|
|
539
582
|
// Bump the durable progress timestamp. Distinct from the heartbeat:
|
|
540
583
|
// heartbeat = "process is up", progress = "real work is happening." The
|
|
541
584
|
// gap between them is what the client-side stuck-detector reads to tell
|
|
542
|
-
// a hung run from a healthy one.
|
|
543
|
-
|
|
585
|
+
// a hung run from a healthy one. Keepalive and zero-byte action prep are
|
|
586
|
+
// liveness only; streamed input bytes, text, and tool lifecycle events are
|
|
587
|
+
// real progress.
|
|
588
|
+
if (shouldBumpProgressForEvent(runEvent.event)) {
|
|
589
|
+
bumpProgressIfDue();
|
|
590
|
+
}
|
|
544
591
|
|
|
545
592
|
// Persist event to SQL. Events are chained through persistenceChain so
|
|
546
593
|
// inserts commit in seq order — an out-of-order commit would advance the
|
|
@@ -733,12 +780,16 @@ export function startRun(
|
|
|
733
780
|
try {
|
|
734
781
|
await insertRunPromise;
|
|
735
782
|
if (!terminalPersistenceError) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
finalStatus
|
|
739
|
-
|
|
783
|
+
let statusUpdated = false;
|
|
784
|
+
try {
|
|
785
|
+
statusUpdated = await updateRunStatusIfRunning(runId, finalStatus);
|
|
786
|
+
} catch {
|
|
787
|
+
statusUpdated = false;
|
|
788
|
+
}
|
|
740
789
|
if (statusUpdated) {
|
|
741
790
|
await setRunTerminalReason(runId, terminalReason);
|
|
791
|
+
} else {
|
|
792
|
+
await reconcileTerminalRunFromEvents(runId).catch(() => false);
|
|
742
793
|
}
|
|
743
794
|
}
|
|
744
795
|
} catch {
|
|
@@ -37,10 +37,13 @@ import React, {
|
|
|
37
37
|
import { LLM_MISSING_CREDENTIALS_MESSAGE } from "../agent/engine/credential-errors.js";
|
|
38
38
|
import type { ReasoningEffort } from "../shared/reasoning-effort.js";
|
|
39
39
|
import {
|
|
40
|
+
ACTIVE_RUN_STATE_EVENT,
|
|
41
|
+
clearActiveRunIfMatches,
|
|
40
42
|
getActiveRunActivityTool,
|
|
41
43
|
getActiveRun,
|
|
42
44
|
resolveReconnectAfterSeq,
|
|
43
45
|
setActiveRun,
|
|
46
|
+
type ActiveRunState,
|
|
44
47
|
updateActiveRunActivity,
|
|
45
48
|
updateActiveRunSeq,
|
|
46
49
|
} from "./active-run-state.js";
|
|
@@ -282,6 +285,13 @@ export function reconnectProgressTimedOut(args: {
|
|
|
282
285
|
return args.now - args.lastProgressAt >= threshold;
|
|
283
286
|
}
|
|
284
287
|
|
|
288
|
+
function activeRunMatchesThread(
|
|
289
|
+
state: ActiveRunState | null,
|
|
290
|
+
threadId: string | undefined,
|
|
291
|
+
): boolean {
|
|
292
|
+
return Boolean(threadId && state?.threadId === threadId && state.runId);
|
|
293
|
+
}
|
|
294
|
+
|
|
285
295
|
function isAssistantUiDuplicateMessageIdError(error: unknown): boolean {
|
|
286
296
|
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
287
297
|
return (
|
|
@@ -627,15 +637,21 @@ export function resolveAssistantChatRunningState({
|
|
|
627
637
|
isReconnecting,
|
|
628
638
|
optimisticRunning,
|
|
629
639
|
isAutoResuming,
|
|
640
|
+
hasActiveServerRun,
|
|
630
641
|
}: {
|
|
631
642
|
forceStopped: boolean;
|
|
632
643
|
isRuntimeRunning: boolean;
|
|
633
644
|
isReconnecting: boolean;
|
|
634
645
|
optimisticRunning: boolean;
|
|
635
646
|
isAutoResuming: boolean;
|
|
647
|
+
hasActiveServerRun?: boolean;
|
|
636
648
|
}): { isRunning: boolean; showRunningInUI: boolean } {
|
|
637
649
|
const isRunning =
|
|
638
|
-
!forceStopped &&
|
|
650
|
+
!forceStopped &&
|
|
651
|
+
(isRuntimeRunning ||
|
|
652
|
+
isReconnecting ||
|
|
653
|
+
optimisticRunning ||
|
|
654
|
+
Boolean(hasActiveServerRun));
|
|
639
655
|
return {
|
|
640
656
|
isRunning,
|
|
641
657
|
// During auto-continuation, assistant-ui can briefly mark the message done
|
|
@@ -1440,13 +1456,34 @@ const AssistantChatInner = forwardRef<
|
|
|
1440
1456
|
// When stop is clicked during reconnect, keep content visible (don't wipe it)
|
|
1441
1457
|
const [reconnectFrozen, setReconnectFrozen] = useState(false);
|
|
1442
1458
|
const reconnectRunIdRef = useRef<string | null>(null);
|
|
1443
|
-
const
|
|
1459
|
+
const reconnectCanMaterializeRef = useRef(false);
|
|
1444
1460
|
const reconnectAbortRef = useRef<AbortController | null>(null);
|
|
1445
1461
|
// Nuclear stop: user clicked stop. Clears the stop button/indicator AND
|
|
1446
1462
|
// lets new submissions go through immediately — prevents the "stuck
|
|
1447
1463
|
// queueing forever" state where isReconnecting or isRuntimeRunning gets
|
|
1448
1464
|
// wedged (e.g. after a tab refresh + stop during reconnect).
|
|
1449
1465
|
const [forceStopped, setForceStopped] = useState(false);
|
|
1466
|
+
const [hasActiveServerRun, setHasActiveServerRun] = useState(() =>
|
|
1467
|
+
activeRunMatchesThread(getActiveRun(), threadId),
|
|
1468
|
+
);
|
|
1469
|
+
useEffect(() => {
|
|
1470
|
+
const syncActiveRun = (state = getActiveRun()) => {
|
|
1471
|
+
setHasActiveServerRun(activeRunMatchesThread(state, threadId));
|
|
1472
|
+
};
|
|
1473
|
+
syncActiveRun();
|
|
1474
|
+
const handler = (event: Event) => {
|
|
1475
|
+
const state = (event as CustomEvent<{ state?: ActiveRunState | null }>)
|
|
1476
|
+
.detail?.state;
|
|
1477
|
+
syncActiveRun(state ?? null);
|
|
1478
|
+
};
|
|
1479
|
+
const storageHandler = () => syncActiveRun();
|
|
1480
|
+
window.addEventListener(ACTIVE_RUN_STATE_EVENT, handler);
|
|
1481
|
+
window.addEventListener("storage", storageHandler);
|
|
1482
|
+
return () => {
|
|
1483
|
+
window.removeEventListener(ACTIVE_RUN_STATE_EVENT, handler);
|
|
1484
|
+
window.removeEventListener("storage", storageHandler);
|
|
1485
|
+
};
|
|
1486
|
+
}, [threadId]);
|
|
1450
1487
|
// Real running state drives submission/queue gating; UI running also covers
|
|
1451
1488
|
// short auto-continuation gaps so the latest assistant message does not flash
|
|
1452
1489
|
// into a done state while the agent is still working.
|
|
@@ -1456,14 +1493,14 @@ const AssistantChatInner = forwardRef<
|
|
|
1456
1493
|
isReconnecting,
|
|
1457
1494
|
optimisticRunning,
|
|
1458
1495
|
isAutoResuming,
|
|
1496
|
+
hasActiveServerRun,
|
|
1459
1497
|
});
|
|
1460
1498
|
const textStreaming = showRunningInUI || externalStreaming;
|
|
1461
1499
|
// A revealed activity label wins; otherwise stay a steady "Thinking" by
|
|
1462
1500
|
// default. We only surface "Reconnecting" while we are actively replaying
|
|
1463
1501
|
// recovered content (reconnectContent populated). A bare reconnect with no
|
|
1464
|
-
// replayed content
|
|
1465
|
-
//
|
|
1466
|
-
// ongoing work, so it must read as "Thinking", never a perpetual "Working".
|
|
1502
|
+
// replayed content is normal ongoing work, so it must read as "Thinking",
|
|
1503
|
+
// never a perpetual "Working".
|
|
1467
1504
|
const runningStatusLabel = runningActivityLabel
|
|
1468
1505
|
? runningActivityLabel
|
|
1469
1506
|
: isAutoResuming
|
|
@@ -1738,9 +1775,9 @@ const AssistantChatInner = forwardRef<
|
|
|
1738
1775
|
|
|
1739
1776
|
reconnectRunIdRef.current = runId;
|
|
1740
1777
|
const afterSeq = resolveReconnectAfterSeq(threadId, runId);
|
|
1778
|
+
reconnectCanMaterializeRef.current = afterSeq === 0;
|
|
1741
1779
|
const storedActivityTool = getActiveRunActivityTool(threadId, runId);
|
|
1742
1780
|
setRunningActivityTool(storedActivityTool);
|
|
1743
|
-
setReconnectAfterSeq(afterSeq);
|
|
1744
1781
|
setActiveRun({
|
|
1745
1782
|
threadId,
|
|
1746
1783
|
runId,
|
|
@@ -1830,7 +1867,6 @@ const AssistantChatInner = forwardRef<
|
|
|
1830
1867
|
let rafPending = false;
|
|
1831
1868
|
let latestSnapshot: ContentPart[] = [];
|
|
1832
1869
|
const scheduleUpdate = (snapshot: ContentPart[]) => {
|
|
1833
|
-
if (afterSeq > 0) return;
|
|
1834
1870
|
latestSnapshot = snapshot;
|
|
1835
1871
|
if (rafPending) return;
|
|
1836
1872
|
rafPending = true;
|
|
@@ -1909,6 +1945,7 @@ const AssistantChatInner = forwardRef<
|
|
|
1909
1945
|
settleInterruptedToolCalls(latestContent);
|
|
1910
1946
|
setReconnectContent([...latestContent]);
|
|
1911
1947
|
setReconnectFrozen(latestContent.length > 0);
|
|
1948
|
+
reconnectCanMaterializeRef.current = latestContent.length > 0;
|
|
1912
1949
|
}
|
|
1913
1950
|
setRunErrorInfo({
|
|
1914
1951
|
message:
|
|
@@ -1918,10 +1955,13 @@ const AssistantChatInner = forwardRef<
|
|
|
1918
1955
|
runId,
|
|
1919
1956
|
});
|
|
1920
1957
|
setDismissedRunErrorKey(null);
|
|
1958
|
+
clearActiveRunIfMatches(threadId, runId);
|
|
1921
1959
|
reconnectAbortRef.current = null;
|
|
1922
1960
|
setIsReconnecting(false);
|
|
1923
1961
|
reconnectRunIdRef.current = null;
|
|
1924
|
-
|
|
1962
|
+
if (afterSeq > 0) {
|
|
1963
|
+
reconnectCanMaterializeRef.current = false;
|
|
1964
|
+
}
|
|
1925
1965
|
window.dispatchEvent(
|
|
1926
1966
|
new CustomEvent("agentNative.chatRunning", {
|
|
1927
1967
|
detail: { isRunning: false, tabId: tabId || threadId },
|
|
@@ -1930,7 +1970,7 @@ const AssistantChatInner = forwardRef<
|
|
|
1930
1970
|
return;
|
|
1931
1971
|
}
|
|
1932
1972
|
|
|
1933
|
-
setReconnectFrozen(
|
|
1973
|
+
setReconnectFrozen(afterSeq === 0);
|
|
1934
1974
|
let loaded = false;
|
|
1935
1975
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
1936
1976
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -1939,16 +1979,24 @@ const AssistantChatInner = forwardRef<
|
|
|
1939
1979
|
if (repoHasAssistantMessage(repo)) {
|
|
1940
1980
|
setReconnectContent([]);
|
|
1941
1981
|
setReconnectFrozen(false);
|
|
1982
|
+
reconnectCanMaterializeRef.current = false;
|
|
1942
1983
|
loaded = true;
|
|
1943
1984
|
break;
|
|
1944
1985
|
}
|
|
1945
1986
|
}
|
|
1946
1987
|
|
|
1947
1988
|
if (reconnectRunIdRef.current === runId) {
|
|
1989
|
+
if (afterSeq > 0) {
|
|
1990
|
+
setReconnectContent([]);
|
|
1991
|
+
setReconnectFrozen(false);
|
|
1992
|
+
}
|
|
1993
|
+
clearActiveRunIfMatches(threadId, runId);
|
|
1948
1994
|
reconnectAbortRef.current = null;
|
|
1949
1995
|
setIsReconnecting(false);
|
|
1950
1996
|
reconnectRunIdRef.current = null;
|
|
1951
|
-
|
|
1997
|
+
if (loaded || afterSeq > 0 || latestContent.length === 0) {
|
|
1998
|
+
reconnectCanMaterializeRef.current = false;
|
|
1999
|
+
}
|
|
1952
2000
|
window.dispatchEvent(
|
|
1953
2001
|
new CustomEvent("agentNative.chatRunning", {
|
|
1954
2002
|
detail: { isRunning: false, tabId: tabId || threadId },
|
|
@@ -1956,7 +2004,12 @@ const AssistantChatInner = forwardRef<
|
|
|
1956
2004
|
);
|
|
1957
2005
|
}
|
|
1958
2006
|
if (!loaded) {
|
|
1959
|
-
await refreshThreadFromServer();
|
|
2007
|
+
const repo = await refreshThreadFromServer();
|
|
2008
|
+
if (afterSeq > 0 || repoHasAssistantMessage(repo)) {
|
|
2009
|
+
setReconnectContent([]);
|
|
2010
|
+
setReconnectFrozen(false);
|
|
2011
|
+
reconnectCanMaterializeRef.current = false;
|
|
2012
|
+
}
|
|
1960
2013
|
}
|
|
1961
2014
|
};
|
|
1962
2015
|
|
|
@@ -2650,6 +2703,7 @@ const AssistantChatInner = forwardRef<
|
|
|
2650
2703
|
if (reconnectFrozen) {
|
|
2651
2704
|
setReconnectFrozen(false);
|
|
2652
2705
|
setReconnectContent([]);
|
|
2706
|
+
reconnectCanMaterializeRef.current = false;
|
|
2653
2707
|
}
|
|
2654
2708
|
if (forceStopped) {
|
|
2655
2709
|
setForceStopped(false);
|
|
@@ -2670,6 +2724,11 @@ const AssistantChatInner = forwardRef<
|
|
|
2670
2724
|
|
|
2671
2725
|
const materializeFrozenReconnectContent = useCallback(() => {
|
|
2672
2726
|
if (!reconnectFrozen || reconnectContent.length === 0) return;
|
|
2727
|
+
if (!reconnectCanMaterializeRef.current) {
|
|
2728
|
+
setReconnectFrozen(false);
|
|
2729
|
+
setReconnectContent([]);
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2673
2732
|
try {
|
|
2674
2733
|
const frozenContent = cloneContentParts(reconnectContent);
|
|
2675
2734
|
settleInterruptedToolCalls(frozenContent);
|
|
@@ -2710,6 +2769,7 @@ const AssistantChatInner = forwardRef<
|
|
|
2710
2769
|
threadRuntime.import(ensureMessageMetadata(repo));
|
|
2711
2770
|
setReconnectFrozen(false);
|
|
2712
2771
|
setReconnectContent([]);
|
|
2772
|
+
reconnectCanMaterializeRef.current = false;
|
|
2713
2773
|
} catch (err) {
|
|
2714
2774
|
captureError(err, {
|
|
2715
2775
|
tags: {
|
|
@@ -2748,6 +2808,7 @@ const AssistantChatInner = forwardRef<
|
|
|
2748
2808
|
setRunErrorInfo(null);
|
|
2749
2809
|
setDismissedRunErrorKey(null);
|
|
2750
2810
|
if (runIdToAbort) {
|
|
2811
|
+
if (threadId) clearActiveRunIfMatches(threadId, runIdToAbort);
|
|
2751
2812
|
fetch(`${apiUrl}/runs/${encodeURIComponent(runIdToAbort)}/abort`, {
|
|
2752
2813
|
method: "POST",
|
|
2753
2814
|
}).catch(() => {});
|
|
@@ -2756,9 +2817,16 @@ const AssistantChatInner = forwardRef<
|
|
|
2756
2817
|
reconnectAbortRef.current?.abort();
|
|
2757
2818
|
reconnectAbortRef.current = null;
|
|
2758
2819
|
reconnectRunIdRef.current = null;
|
|
2759
|
-
setReconnectAfterSeq(0);
|
|
2760
2820
|
setIsReconnecting(false);
|
|
2761
|
-
|
|
2821
|
+
const shouldFreezeReconnectContent =
|
|
2822
|
+
reconnectCanMaterializeRef.current && reconnectContent.length > 0;
|
|
2823
|
+
if (shouldFreezeReconnectContent) {
|
|
2824
|
+
setReconnectFrozen(true);
|
|
2825
|
+
} else {
|
|
2826
|
+
setReconnectFrozen(false);
|
|
2827
|
+
setReconnectContent([]);
|
|
2828
|
+
reconnectCanMaterializeRef.current = false;
|
|
2829
|
+
}
|
|
2762
2830
|
}
|
|
2763
2831
|
threadRuntime.cancelRun();
|
|
2764
2832
|
if (typeof window !== "undefined") {
|
|
@@ -3610,12 +3678,11 @@ const AssistantChatInner = forwardRef<
|
|
|
3610
3678
|
/>
|
|
3611
3679
|
)}
|
|
3612
3680
|
{(isReconnecting || reconnectFrozen) &&
|
|
3613
|
-
reconnectAfterSeq === 0 &&
|
|
3614
3681
|
reconnectContent.length > 0 && (
|
|
3615
3682
|
<ReconnectStreamMessage content={reconnectContent} />
|
|
3616
3683
|
)}
|
|
3617
3684
|
{(isReconnecting || reconnectFrozen) &&
|
|
3618
|
-
|
|
3685
|
+
reconnectContent.length === 0 &&
|
|
3619
3686
|
reconnectActivityContent.length > 0 && (
|
|
3620
3687
|
<ReconnectStreamMessage
|
|
3621
3688
|
content={reconnectActivityContent}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const STORAGE_KEY = "agent-chat-active-run";
|
|
2
|
+
export const ACTIVE_RUN_STATE_EVENT = "agent-chat:active-run-state-change";
|
|
2
3
|
|
|
3
4
|
export interface ActiveRunState {
|
|
4
5
|
threadId: string;
|
|
@@ -7,6 +8,19 @@ export interface ActiveRunState {
|
|
|
7
8
|
activityTool?: string | null;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
function notifyActiveRunStateChanged(state: ActiveRunState | null): void {
|
|
12
|
+
if (
|
|
13
|
+
typeof window === "undefined" ||
|
|
14
|
+
typeof window.dispatchEvent !== "function" ||
|
|
15
|
+
typeof CustomEvent === "undefined"
|
|
16
|
+
) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
window.dispatchEvent(
|
|
20
|
+
new CustomEvent(ACTIVE_RUN_STATE_EVENT, { detail: { state } }),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
function normalizeActivityTool(toolName: unknown): string | null {
|
|
11
25
|
if (typeof toolName !== "string") return null;
|
|
12
26
|
const tool = toolName.trim();
|
|
@@ -17,6 +31,7 @@ export function setActiveRun(state: ActiveRunState): void {
|
|
|
17
31
|
try {
|
|
18
32
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
19
33
|
} catch {}
|
|
34
|
+
notifyActiveRunStateChanged(state);
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
export function getActiveRun(): ActiveRunState | null {
|
|
@@ -64,6 +79,13 @@ export function clearActiveRun(): void {
|
|
|
64
79
|
try {
|
|
65
80
|
sessionStorage.removeItem(STORAGE_KEY);
|
|
66
81
|
} catch {}
|
|
82
|
+
notifyActiveRunStateChanged(null);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function clearActiveRunIfMatches(threadId: string, runId: string): void {
|
|
86
|
+
const state = getActiveRun();
|
|
87
|
+
if (state?.threadId !== threadId || state.runId !== runId) return;
|
|
88
|
+
clearActiveRun();
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
/** Resume reconnect SSE after the last seen event (0 = replay from the start). */
|
|
@@ -807,6 +807,7 @@ export function shouldShowAssistantMessageFooter({
|
|
|
807
807
|
hasUnresolvedTool?: boolean;
|
|
808
808
|
}): boolean {
|
|
809
809
|
if (!hasRenderableContent) return false;
|
|
810
|
+
if (chatRunning) return false;
|
|
810
811
|
if (!isLast) return true;
|
|
811
812
|
if (hasUnresolvedTool) return false;
|
|
812
813
|
return !chatRunning && statusIsTerminal;
|
|
@@ -909,7 +910,7 @@ export function AssistantMessage() {
|
|
|
909
910
|
className="group relative"
|
|
910
911
|
style={{ contentVisibility: isComplete ? "auto" : "visible" }}
|
|
911
912
|
>
|
|
912
|
-
<div className="max-w-[95%] text-sm leading-relaxed text-foreground">
|
|
913
|
+
<div className="w-full max-w-[95%] text-sm leading-relaxed text-foreground">
|
|
913
914
|
<MessagePrimitive.Parts
|
|
914
915
|
components={{
|
|
915
916
|
Text: MarkdownText,
|
|
@@ -418,6 +418,7 @@ export function ToolCallDisplay({
|
|
|
418
418
|
isRunning,
|
|
419
419
|
structuredMeta,
|
|
420
420
|
approval,
|
|
421
|
+
repeatCount,
|
|
421
422
|
}: {
|
|
422
423
|
toolName: string;
|
|
423
424
|
argsText?: string;
|
|
@@ -428,6 +429,7 @@ export function ToolCallDisplay({
|
|
|
428
429
|
isRunning: boolean;
|
|
429
430
|
structuredMeta?: Record<string, unknown>;
|
|
430
431
|
approval?: { approvalKey: string; dismissed?: boolean };
|
|
432
|
+
repeatCount?: number;
|
|
431
433
|
}) {
|
|
432
434
|
// Delegate to bespoke cells when structured metadata is present.
|
|
433
435
|
// These must be separate components so hook order in ToolCallDisplayGeneric
|
|
@@ -474,6 +476,7 @@ export function ToolCallDisplay({
|
|
|
474
476
|
chatUI={chatUI}
|
|
475
477
|
isRunning={isRunning}
|
|
476
478
|
approval={approval}
|
|
479
|
+
repeatCount={repeatCount}
|
|
477
480
|
/>
|
|
478
481
|
);
|
|
479
482
|
}
|
|
@@ -487,6 +490,7 @@ function ToolCallDisplayGeneric({
|
|
|
487
490
|
chatUI,
|
|
488
491
|
isRunning,
|
|
489
492
|
approval,
|
|
493
|
+
repeatCount,
|
|
490
494
|
}: {
|
|
491
495
|
toolName: string;
|
|
492
496
|
argsText?: string;
|
|
@@ -496,6 +500,7 @@ function ToolCallDisplayGeneric({
|
|
|
496
500
|
chatUI?: ActionChatUIConfig;
|
|
497
501
|
isRunning: boolean;
|
|
498
502
|
approval?: { approvalKey: string; dismissed?: boolean };
|
|
503
|
+
repeatCount?: number;
|
|
499
504
|
}) {
|
|
500
505
|
const streamRef = useRef<HTMLDivElement>(null);
|
|
501
506
|
|
|
@@ -614,7 +619,7 @@ function ToolCallDisplayGeneric({
|
|
|
614
619
|
const isExpanded = isAgentCall ? hasStreamText && expanded : expanded;
|
|
615
620
|
|
|
616
621
|
return (
|
|
617
|
-
<div className="my-1 overflow-hidden">
|
|
622
|
+
<div className="my-1 w-full overflow-hidden">
|
|
618
623
|
{mcpApp && <McpAppRenderer app={mcpApp} className="mb-1.5" />}
|
|
619
624
|
<button
|
|
620
625
|
onClick={() => canExpand && setExpanded(!isExpanded)}
|
|
@@ -640,6 +645,14 @@ function ToolCallDisplayGeneric({
|
|
|
640
645
|
<span className="truncate min-w-0">
|
|
641
646
|
<span className="font-medium">{displayName}</span>
|
|
642
647
|
</span>
|
|
648
|
+
{repeatCount && repeatCount > 1 && (
|
|
649
|
+
<span
|
|
650
|
+
className="shrink-0 rounded border border-border/60 px-1.5 py-0.5 text-[10px] leading-none text-muted-foreground"
|
|
651
|
+
title={`Repeated ${repeatCount} times`}
|
|
652
|
+
>
|
|
653
|
+
{repeatCount}x
|
|
654
|
+
</span>
|
|
655
|
+
)}
|
|
643
656
|
{canExpand && (
|
|
644
657
|
<IconChevronDown
|
|
645
658
|
className={cn(
|
|
@@ -694,6 +707,7 @@ export function ToolCallFallback({
|
|
|
694
707
|
structuredMeta?: Record<string, unknown>;
|
|
695
708
|
activity?: boolean;
|
|
696
709
|
approval?: { approvalKey: string; dismissed?: boolean };
|
|
710
|
+
repeatCount?: number;
|
|
697
711
|
}) {
|
|
698
712
|
const chatRunning = React.useContext(ChatRunningContext);
|
|
699
713
|
const isRunning =
|
|
@@ -715,6 +729,7 @@ export function ToolCallFallback({
|
|
|
715
729
|
structuredMeta={rest.structuredMeta}
|
|
716
730
|
isRunning={isRunning}
|
|
717
731
|
approval={rest.approval}
|
|
732
|
+
repeatCount={rest.repeatCount}
|
|
718
733
|
/>
|
|
719
734
|
);
|
|
720
735
|
}
|
|
@@ -734,7 +749,7 @@ export function ReconnectStreamMessage({
|
|
|
734
749
|
|
|
735
750
|
return (
|
|
736
751
|
<div className="flex justify-start">
|
|
737
|
-
<div className="max-w-[95%] text-sm leading-relaxed text-foreground space-y-1">
|
|
752
|
+
<div className="w-full max-w-[95%] text-sm leading-relaxed text-foreground space-y-1">
|
|
738
753
|
{content.map((part, i) => {
|
|
739
754
|
if (part.type === "text") {
|
|
740
755
|
const partStreaming = chatRunning && i === streamingTextPartIndex;
|
|
@@ -764,6 +779,7 @@ export function ReconnectStreamMessage({
|
|
|
764
779
|
(chatRunning || part.activity === true)
|
|
765
780
|
}
|
|
766
781
|
approval={part.approval}
|
|
782
|
+
repeatCount={part.repeatCount}
|
|
767
783
|
/>
|
|
768
784
|
);
|
|
769
785
|
}
|