@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.
Files changed (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +8 -6
  5. package/corpus/core/src/agent/run-manager.ts +57 -6
  6. package/corpus/core/src/client/AssistantChat.tsx +82 -15
  7. package/corpus/core/src/client/active-run-state.ts +22 -0
  8. package/corpus/core/src/client/chat/message-components.tsx +2 -1
  9. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -2
  10. package/corpus/core/src/client/sse-event-processor.ts +196 -51
  11. package/corpus/core/src/client/tool-display.ts +1 -1
  12. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +15 -13
  13. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +4 -2
  14. package/corpus/templates/clips/app/components/player/share-dialog.tsx +11 -2
  15. package/corpus/templates/clips/app/i18n/ar-SA.ts +4 -0
  16. package/corpus/templates/clips/app/i18n/de-DE.ts +4 -0
  17. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  18. package/corpus/templates/clips/app/i18n/es-ES.ts +4 -0
  19. package/corpus/templates/clips/app/i18n/fr-FR.ts +4 -0
  20. package/corpus/templates/clips/app/i18n/hi-IN.ts +4 -0
  21. package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -0
  22. package/corpus/templates/clips/app/i18n/ko-KR.ts +4 -0
  23. package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -0
  24. package/corpus/templates/clips/app/i18n/zh-CN.ts +4 -0
  25. package/corpus/templates/clips/app/i18n/zh-TW.ts +4 -0
  26. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +31 -3
  27. package/corpus/templates/clips/changelog/2026-07-01-clip-embeds-now-render-as-a-clean-video-only-player-without-.md +6 -0
  28. package/corpus/templates/clips/changelog/2026-07-01-shared-clips-can-now-copy-a-ready-to-send-prompt-that-tells-.md +6 -0
  29. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  30. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +2 -2
  31. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +41 -31
  32. package/dist/agent/production-agent.d.ts.map +1 -1
  33. package/dist/agent/production-agent.js +8 -6
  34. package/dist/agent/production-agent.js.map +1 -1
  35. package/dist/agent/run-manager.d.ts.map +1 -1
  36. package/dist/agent/run-manager.js +56 -4
  37. package/dist/agent/run-manager.js.map +1 -1
  38. package/dist/client/AssistantChat.d.ts +2 -1
  39. package/dist/client/AssistantChat.d.ts.map +1 -1
  40. package/dist/client/AssistantChat.js +74 -18
  41. package/dist/client/AssistantChat.js.map +1 -1
  42. package/dist/client/active-run-state.d.ts +2 -0
  43. package/dist/client/active-run-state.d.ts.map +1 -1
  44. package/dist/client/active-run-state.js +17 -0
  45. package/dist/client/active-run-state.js.map +1 -1
  46. package/dist/client/chat/message-components.d.ts.map +1 -1
  47. package/dist/client/chat/message-components.js +3 -1
  48. package/dist/client/chat/message-components.js.map +1 -1
  49. package/dist/client/chat/tool-call-display.d.ts +3 -1
  50. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  51. package/dist/client/chat/tool-call-display.js +8 -8
  52. package/dist/client/chat/tool-call-display.js.map +1 -1
  53. package/dist/client/sse-event-processor.d.ts +1 -0
  54. package/dist/client/sse-event-processor.d.ts.map +1 -1
  55. package/dist/client/sse-event-processor.js +163 -45
  56. package/dist/client/sse-event-processor.js.map +1 -1
  57. package/dist/client/tool-display.js +1 -1
  58. package/dist/client/tool-display.js.map +1 -1
  59. package/dist/collab/awareness.d.ts +2 -2
  60. package/dist/collab/awareness.d.ts.map +1 -1
  61. package/dist/collab/routes.d.ts +1 -1
  62. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  63. package/dist/notifications/routes.d.ts +3 -3
  64. package/dist/observability/routes.d.ts +6 -6
  65. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  66. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4967
31
+ - template files: 4969
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.23",
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
- if (event.id && event.name) {
2847
- toolInputNames.set(event.id, event.name);
2848
- toolInputBytes.set(event.id, 0);
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 (event.id) {
2857
- const previous = toolInputBytes.get(event.id) ?? 0;
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(event.id, progressBytes);
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
- bumpProgressIfDue();
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
- const statusUpdated = await updateRunStatusIfRunning(
737
- runId,
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 && (isRuntimeRunning || isReconnecting || optimisticRunning);
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 [reconnectAfterSeq, setReconnectAfterSeq] = useState(0);
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 e.g. a tail-resume after the serverless wall, where
1465
- // `scheduleUpdate` intentionally leaves reconnectContent empty — is normal
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
- setReconnectAfterSeq(0);
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(true);
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
- setReconnectAfterSeq(0);
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
- setReconnectFrozen(reconnectContent.length > 0);
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
- reconnectAfterSeq > 0 &&
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
  }