@agent-native/core 0.95.0 → 0.95.1

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 (135) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +10 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +13 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +18 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +16 -0
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +17 -0
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +16 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +17 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +15 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +16 -0
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +11 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +12 -0
  13. package/corpus/core/docs/content/template-design.mdx +16 -0
  14. package/corpus/core/package.json +2 -2
  15. package/corpus/core/src/agent/production-agent.ts +20 -0
  16. package/corpus/core/src/agent/thread-data-builder.ts +41 -17
  17. package/corpus/core/src/client/AssistantChat.tsx +250 -25
  18. package/corpus/core/src/client/chat/tool-call-display.tsx +1 -1
  19. package/corpus/core/src/client/chat/use-reconnect-reader-owner.ts +28 -0
  20. package/corpus/core/src/client/guided-questions.tsx +9 -1
  21. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +6 -7
  22. package/corpus/core/src/client/sse-event-processor.ts +90 -21
  23. package/corpus/core/src/connections/catalog.ts +2 -2
  24. package/corpus/core/src/deploy/build.ts +3 -0
  25. package/corpus/core/src/integrations/catalog.ts +2 -2
  26. package/corpus/core/src/server/auth.ts +1 -1
  27. package/corpus/core/src/server/credential-provider.ts +7 -3
  28. package/corpus/core/src/shared/cache-control.ts +10 -4
  29. package/corpus/templates/analytics/app/lib/data-sources.ts +5 -4
  30. package/corpus/templates/analytics/changelog/2026-07-10-slack-token-setup-marked-legacy.md +6 -0
  31. package/corpus/templates/analytics/server/lib/credential-keys.ts +6 -2
  32. package/corpus/templates/brain/changelog/2026-07-10-slack-backfill-token-marked-legacy.md +6 -0
  33. package/corpus/templates/brain/server/register-secrets.ts +2 -2
  34. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -1
  35. package/corpus/templates/clips/app/components/library/page-header.tsx +19 -3
  36. package/corpus/templates/clips/app/routes/_app.library._index.tsx +16 -12
  37. package/corpus/templates/clips/changelog/2026-07-10-new-recording-appears-once-when-the-library-sidebar-is-open.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-10-slack-token-fallback-guidance.md +6 -0
  39. package/corpus/templates/clips/server/register-secrets.ts +1 -1
  40. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +0 -66
  41. package/corpus/templates/content/app/routes/_app.local-files.tsx +7 -47
  42. package/corpus/templates/content/changelog/2026-07-10-local-folders-now-wait-for-an-explicit-pull-before-content-r.md +6 -0
  43. package/corpus/templates/content/package.json +1 -1
  44. package/corpus/templates/design/actions/generate-design.ts +128 -31
  45. package/corpus/templates/design/actions/present-design-variants.ts +37 -9
  46. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +9 -3
  47. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +32 -10
  48. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +2 -2
  49. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +9 -1
  50. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -5
  51. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
  52. package/corpus/templates/design/app/global.css +10 -0
  53. package/corpus/templates/design/app/hooks/use-question-flow.ts +9 -2
  54. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  55. package/corpus/templates/design/app/i18n-data.ts +21 -11
  56. package/corpus/templates/design/changelog/2026-07-10-editor-panels-are-easier-to-distinguish-and-design-intake-qu.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-10-interact-mode-now-stays-on-the-full-canvas-matches-edit-visu.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-10-new-web-designs-open-at-desktop-size-and-adapt-across-mobile.md +6 -0
  59. package/corpus/templates/design/changelog/2026-07-10-tweaks-now-link-to-full-documentation.md +6 -0
  60. package/corpus/templates/design/server/plugins/agent-chat.ts +2 -0
  61. package/corpus/templates/mail/app/i18n/ar-SA.ts +3 -3
  62. package/corpus/templates/mail/app/i18n/de-DE.ts +3 -3
  63. package/corpus/templates/mail/app/i18n/en-US.ts +4 -3
  64. package/corpus/templates/mail/app/i18n/es-ES.ts +3 -3
  65. package/corpus/templates/mail/app/i18n/fr-FR.ts +3 -3
  66. package/corpus/templates/mail/app/i18n/hi-IN.ts +3 -3
  67. package/corpus/templates/mail/app/i18n/ja-JP.ts +3 -3
  68. package/corpus/templates/mail/app/i18n/ko-KR.ts +3 -3
  69. package/corpus/templates/mail/app/i18n/pt-BR.ts +3 -3
  70. package/corpus/templates/mail/app/i18n/zh-CN.ts +3 -3
  71. package/corpus/templates/mail/app/i18n/zh-TW.ts +3 -3
  72. package/corpus/templates/mail/changelog/2026-07-10-slack-intake-marked-legacy.md +6 -0
  73. package/corpus/templates/mail/server/lib/env-config.ts +5 -4
  74. package/corpus/templates/plan/package.json +1 -1
  75. package/dist/agent/production-agent.d.ts.map +1 -1
  76. package/dist/agent/production-agent.js +20 -0
  77. package/dist/agent/production-agent.js.map +1 -1
  78. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  79. package/dist/agent/thread-data-builder.js +36 -18
  80. package/dist/agent/thread-data-builder.js.map +1 -1
  81. package/dist/client/AssistantChat.d.ts +3 -1
  82. package/dist/client/AssistantChat.d.ts.map +1 -1
  83. package/dist/client/AssistantChat.js +200 -28
  84. package/dist/client/AssistantChat.js.map +1 -1
  85. package/dist/client/chat/tool-call-display.js +1 -1
  86. package/dist/client/chat/tool-call-display.js.map +1 -1
  87. package/dist/client/chat/use-reconnect-reader-owner.d.ts +11 -0
  88. package/dist/client/chat/use-reconnect-reader-owner.d.ts.map +1 -0
  89. package/dist/client/chat/use-reconnect-reader-owner.js +21 -0
  90. package/dist/client/chat/use-reconnect-reader-owner.js.map +1 -0
  91. package/dist/client/guided-questions.d.ts.map +1 -1
  92. package/dist/client/guided-questions.js +5 -1
  93. package/dist/client/guided-questions.js.map +1 -1
  94. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  95. package/dist/client/integrations/IntegrationsPanel.js +5 -7
  96. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  97. package/dist/client/sse-event-processor.d.ts.map +1 -1
  98. package/dist/client/sse-event-processor.js +67 -20
  99. package/dist/client/sse-event-processor.js.map +1 -1
  100. package/dist/collab/routes.d.ts +1 -1
  101. package/dist/connections/catalog.d.ts +2 -2
  102. package/dist/connections/catalog.js +2 -2
  103. package/dist/connections/catalog.js.map +1 -1
  104. package/dist/deploy/build.d.ts.map +1 -1
  105. package/dist/deploy/build.js +3 -1
  106. package/dist/deploy/build.js.map +1 -1
  107. package/dist/integrations/catalog.js +2 -2
  108. package/dist/integrations/catalog.js.map +1 -1
  109. package/dist/notifications/routes.d.ts +2 -2
  110. package/dist/observability/routes.d.ts +1 -1
  111. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  112. package/dist/resources/handlers.d.ts +1 -1
  113. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  114. package/dist/server/auth.js +1 -1
  115. package/dist/server/auth.js.map +1 -1
  116. package/dist/server/credential-provider.d.ts +5 -3
  117. package/dist/server/credential-provider.d.ts.map +1 -1
  118. package/dist/server/credential-provider.js +7 -3
  119. package/dist/server/credential-provider.js.map +1 -1
  120. package/dist/shared/cache-control.d.ts +8 -6
  121. package/dist/shared/cache-control.d.ts.map +1 -1
  122. package/dist/shared/cache-control.js +8 -3
  123. package/dist/shared/cache-control.js.map +1 -1
  124. package/docs/content/locales/ar-SA/template-design.mdx +13 -0
  125. package/docs/content/locales/de-DE/template-design.mdx +18 -0
  126. package/docs/content/locales/es-ES/template-design.mdx +16 -0
  127. package/docs/content/locales/fr-FR/template-design.mdx +17 -0
  128. package/docs/content/locales/hi-IN/template-design.mdx +16 -0
  129. package/docs/content/locales/ja-JP/template-design.mdx +17 -0
  130. package/docs/content/locales/ko-KR/template-design.mdx +15 -0
  131. package/docs/content/locales/pt-BR/template-design.mdx +16 -0
  132. package/docs/content/locales/zh-CN/template-design.mdx +11 -0
  133. package/docs/content/locales/zh-TW/template-design.mdx +12 -0
  134. package/docs/content/template-design.mdx +16 -0
  135. package/package.json +2 -2
@@ -18,6 +18,7 @@ import { repoHasAssistantMessage, getRepoMessages, getRepoMessage, shouldImportS
18
18
  import { BuilderSetupContent, LoopLimitContinueCard, RunErrorRecoveryCard, PlanModeCallout, getLoopLimitMetadata, getRunErrorMetadata, getRequestModeMetadata, } from "./chat/run-recovery.js";
19
19
  import { createAgentChatRuntimeAdapter, } from "./chat/runtime.js";
20
20
  import { ChatRunningContext, ApprovalContext, ReconnectStreamMessage, } from "./chat/tool-call-display.js";
21
+ import { useReconnectReaderOwner } from "./chat/use-reconnect-reader-owner.js";
21
22
  import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, } from "./components/ui/message-scroller.js";
22
23
  import { Popover, PopoverContent, PopoverTrigger, } from "./components/ui/popover.js";
23
24
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./components/ui/tooltip.js";
@@ -396,6 +397,17 @@ function toolCallNameFromContentPart(part) {
396
397
  function toolCallIdsRepresentSameLocalCall(renderedId, reconnectId) {
397
398
  return renderedId === reconnectId || renderedId.endsWith(`:${reconnectId}`);
398
399
  }
400
+ function toolCallIdIsReaderLocal(id) {
401
+ return (/^tc_\d+$/.test(id) ||
402
+ /:tc_\d+$/.test(id) ||
403
+ id.startsWith("reconnect-activity:"));
404
+ }
405
+ function toolCallReaderLocalKey(id) {
406
+ const counterMatch = id.match(/(?:^|:)(tc_\d+)$/);
407
+ if (counterMatch?.[1])
408
+ return counterMatch[1];
409
+ return id.startsWith("reconnect-activity:") ? id : null;
410
+ }
399
411
  function collectRenderedToolCallStates(messages) {
400
412
  const byId = new Map();
401
413
  for (const message of messages) {
@@ -470,7 +482,7 @@ function latestRenderedAssistantText(messages) {
470
482
  return assistantTextFromContent(latestMessage.content);
471
483
  }
472
484
  function dedupePendingToolCallReplaysWithinContent(content) {
473
- const seenLaterFingerprints = new Set();
485
+ const laterStatesByFingerprint = new Map();
474
486
  let changed = false;
475
487
  const nextReversed = [];
476
488
  for (let i = content.length - 1; i >= 0; i -= 1) {
@@ -478,44 +490,134 @@ function dedupePendingToolCallReplaysWithinContent(content) {
478
490
  if (!part)
479
491
  continue;
480
492
  const fingerprint = toolCallFingerprintFromContentPart(part);
481
- if (fingerprint &&
482
- !toolCallPartHasResult(part) &&
483
- seenLaterFingerprints.has(fingerprint)) {
493
+ const laterState = fingerprint
494
+ ? laterStatesByFingerprint.get(fingerprint)
495
+ : undefined;
496
+ const id = toolCallIdFromContentPart(part);
497
+ const readerLocalKey = id ? toolCallReaderLocalKey(id) : null;
498
+ const isReaderReplay = Boolean(id &&
499
+ laterState &&
500
+ (laterState.ids.has(id) ||
501
+ (readerLocalKey && laterState.readerLocalKeys.has(readerLocalKey)) ||
502
+ (toolCallIdIsReaderLocal(id) && laterState.hasStableId)));
503
+ if (fingerprint && !toolCallPartHasResult(part) && isReaderReplay) {
484
504
  changed = true;
485
505
  continue;
486
506
  }
487
- if (fingerprint)
488
- seenLaterFingerprints.add(fingerprint);
507
+ if (fingerprint) {
508
+ const state = laterState ?? {
509
+ ids: new Set(),
510
+ readerLocalKeys: new Set(),
511
+ hasStableId: false,
512
+ };
513
+ if (id) {
514
+ state.ids.add(id);
515
+ const key = toolCallReaderLocalKey(id);
516
+ if (key)
517
+ state.readerLocalKeys.add(key);
518
+ else
519
+ state.hasStableId = true;
520
+ }
521
+ laterStatesByFingerprint.set(fingerprint, state);
522
+ }
489
523
  nextReversed.push(part);
490
524
  }
491
525
  return changed ? nextReversed.reverse() : content;
492
526
  }
493
- function trimReconnectTextAlreadyRendered(content, renderedAssistantText) {
527
+ function pendingToolCallCountsByName(content) {
528
+ const counts = new Map();
529
+ for (const part of content) {
530
+ const name = toolCallNameFromContentPart(part);
531
+ if (!name || toolCallProgressRank(part) >= 4)
532
+ continue;
533
+ counts.set(name, (counts.get(name) ?? 0) + 1);
534
+ }
535
+ return counts;
536
+ }
537
+ /**
538
+ * Linear-time longest overlap where a suffix of `renderedText` equals a prefix
539
+ * of `reconnectText`. The reconnect overlay is recalculated during render on
540
+ * every stream tick, so a descending slice/endsWith scan becomes quadratic on
541
+ * large repeated output.
542
+ */
543
+ function longestSuffixPrefixOverlap(renderedText, reconnectText) {
544
+ const maxOverlap = Math.min(renderedText.length, reconnectText.length);
545
+ if (maxOverlap === 0)
546
+ return 0;
547
+ const pattern = reconnectText.slice(0, maxOverlap);
548
+ const prefixLengths = new Array(pattern.length).fill(0);
549
+ for (let index = 1; index < pattern.length; index += 1) {
550
+ let matched = prefixLengths[index - 1] ?? 0;
551
+ while (matched > 0 && pattern[index] !== pattern[matched]) {
552
+ matched = prefixLengths[matched - 1] ?? 0;
553
+ }
554
+ if (pattern[index] === pattern[matched])
555
+ matched += 1;
556
+ prefixLengths[index] = matched;
557
+ }
558
+ let matched = 0;
559
+ const renderedStart = renderedText.length - maxOverlap;
560
+ for (let index = renderedStart; index < renderedText.length; index += 1) {
561
+ const character = renderedText[index];
562
+ while (matched > 0 && character !== pattern[matched]) {
563
+ matched = prefixLengths[matched - 1] ?? 0;
564
+ }
565
+ if (character === pattern[matched])
566
+ matched += 1;
567
+ if (matched === pattern.length && index < renderedText.length - 1) {
568
+ matched = prefixLengths[matched - 1] ?? 0;
569
+ }
570
+ }
571
+ return matched;
572
+ }
573
+ function trimReconnectTextAlreadyRendered(content, renderedAssistantText, options) {
494
574
  if (!renderedAssistantText)
495
575
  return content;
496
- let remainingOverlap = renderedAssistantText;
576
+ const reconnectText = assistantTextFromContent(content);
577
+ if (!reconnectText)
578
+ return content;
579
+ let overlapLength = 0;
580
+ if (reconnectText.startsWith(renderedAssistantText)) {
581
+ overlapLength = renderedAssistantText.length;
582
+ }
583
+ else if (renderedAssistantText.startsWith(reconnectText)) {
584
+ overlapLength = reconnectText.length;
585
+ }
586
+ else if (options?.trimTailOverlap) {
587
+ const tailOverlap = longestSuffixPrefixOverlap(renderedAssistantText, reconnectText);
588
+ if (tailOverlap > 0) {
589
+ // Tail reconnects begin after the last remembered event, so their first
590
+ // text can be the final suffix of a thread snapshot imported in parallel.
591
+ // For a partial overlap, require a whole word/phrase boundary so a
592
+ // coincidental shared character at the join cannot eat legitimate text.
593
+ const renderedBeforeOverlap = renderedAssistantText[renderedAssistantText.length - tailOverlap - 1];
594
+ const reconnectAfterOverlap = reconnectText[tailOverlap];
595
+ const isWordCharacter = (value) => value ? /[\p{L}\p{N}_]/u.test(value) : false;
596
+ const isWholeBoundary = !isWordCharacter(renderedBeforeOverlap) &&
597
+ !isWordCharacter(reconnectAfterOverlap);
598
+ if (tailOverlap === reconnectText.length || isWholeBoundary) {
599
+ overlapLength = tailOverlap;
600
+ }
601
+ }
602
+ }
603
+ if (overlapLength === 0)
604
+ return content;
605
+ let remainingOverlap = overlapLength;
497
606
  let changed = false;
498
607
  const next = [];
499
608
  for (const part of content) {
500
- if (part.type !== "text" || remainingOverlap.length === 0) {
609
+ if (part.type !== "text" || remainingOverlap === 0) {
501
610
  next.push(part);
502
611
  continue;
503
612
  }
504
- if (remainingOverlap.startsWith(part.text)) {
505
- remainingOverlap = remainingOverlap.slice(part.text.length);
506
- changed = true;
507
- continue;
508
- }
509
- if (part.text.startsWith(remainingOverlap)) {
510
- const text = part.text.slice(remainingOverlap.length);
511
- remainingOverlap = "";
613
+ if (part.text.length <= remainingOverlap) {
614
+ remainingOverlap -= part.text.length;
512
615
  changed = true;
513
- if (text)
514
- next.push({ ...part, text });
515
616
  continue;
516
617
  }
517
- remainingOverlap = "";
518
- next.push(part);
618
+ next.push({ ...part, text: part.text.slice(remainingOverlap) });
619
+ remainingOverlap = 0;
620
+ changed = true;
519
621
  }
520
622
  return changed ? next : content;
521
623
  }
@@ -523,6 +625,7 @@ export function dedupeReconnectContentAgainstMessages(content, messages, options
523
625
  if (content.length === 0)
524
626
  return content;
525
627
  const snapshotDeduped = dedupePendingToolCallReplaysWithinContent(content);
628
+ const reconnectPendingCounts = pendingToolCallCountsByName(snapshotDeduped);
526
629
  let changed = snapshotDeduped !== content;
527
630
  if (messages.length === 0)
528
631
  return changed ? snapshotDeduped : content;
@@ -595,6 +698,25 @@ export function dedupeReconnectContentAgainstMessages(content, messages, options
595
698
  const latestByName = name
596
699
  ? latestAssistantByName.get(name)
597
700
  : undefined;
701
+ const reconnectPendingCount = name
702
+ ? (reconnectPendingCounts.get(name) ?? 0)
703
+ : 0;
704
+ const hasReaderLocalIdentity = Boolean((id && toolCallIdIsReaderLocal(id)) ||
705
+ (latestByName &&
706
+ Array.from(latestByName.pendingIds).some(toolCallIdIsReaderLocal)));
707
+ if (options?.suppressToolRepeats &&
708
+ latestByName &&
709
+ latestByName.pendingIds.size === 1 &&
710
+ reconnectPendingCount === 1 &&
711
+ hasReaderLocalIdentity &&
712
+ latestByName.pendingRank >= reconnectRank) {
713
+ // During reconnect -> live-adapter handoff both accumulators own
714
+ // the same run. Their pre-tool-start activity cards can have
715
+ // unrelated reader-local ids and no args fingerprint, so name +
716
+ // pending progress is the only stable identity available.
717
+ changed = true;
718
+ return false;
719
+ }
598
720
  const matchesRenderedLocalId = id && latestByName
599
721
  ? Array.from(latestByName.ids).some((renderedId) => toolCallIdsRepresentSameLocalCall(renderedId, id))
600
722
  : false;
@@ -612,7 +734,7 @@ export function dedupeReconnectContentAgainstMessages(content, messages, options
612
734
  return true;
613
735
  })
614
736
  : snapshotDeduped;
615
- const textDeduped = trimReconnectTextAlreadyRendered(filtered, renderedAssistantText);
737
+ const textDeduped = trimReconnectTextAlreadyRendered(filtered, renderedAssistantText, { trimTailOverlap: options?.trimTailTextOverlap });
616
738
  if (textDeduped !== filtered)
617
739
  changed = true;
618
740
  return changed ? textDeduped : content;
@@ -687,16 +809,45 @@ function contentHasVisibleReasoning(content) {
687
809
  candidate.text.trim().length > 0);
688
810
  });
689
811
  }
690
- export function shouldShowGlobalRunningStatus({ showRunningInUI, runningActivityLabel, latestMessage, reconnectContent, }) {
812
+ function contentHasUnresolvedToolCall(content, toolName) {
813
+ if (!Array.isArray(content))
814
+ return false;
815
+ return content.some((part) => {
816
+ if (!part || typeof part !== "object")
817
+ return false;
818
+ const candidate = part;
819
+ return (candidate.type === "tool-call" &&
820
+ candidate.result === undefined &&
821
+ (!toolName || candidate.toolName === toolName));
822
+ });
823
+ }
824
+ export function shouldShowGlobalRunningStatus({ showRunningInUI, runningActivityLabel, runningActivityTool, latestMessage, reconnectContent, }) {
691
825
  if (!showRunningInUI)
692
826
  return false;
693
- if (runningActivityLabel)
694
- return true;
695
827
  const message = latestMessage && typeof latestMessage === "object"
696
828
  ? latestMessage
697
829
  : null;
698
830
  const latestMessageHasReasoning = message?.role === "assistant" &&
699
831
  contentHasVisibleReasoning(message.content);
832
+ const latestMessageHasUnresolvedTool = message?.role === "assistant" &&
833
+ contentHasUnresolvedToolCall(message.content);
834
+ const matchingActivityToolIsVisible = Boolean(runningActivityTool &&
835
+ ((message?.role === "assistant" &&
836
+ contentHasUnresolvedToolCall(message.content, runningActivityTool)) ||
837
+ contentHasUnresolvedToolCall(reconnectContent, runningActivityTool)));
838
+ // A pending card for the same tool is already the running indicator.
839
+ // Rendering its global activity label as well shows the logical call twice
840
+ // (for example, `generate design` plus `Writing generate design...`).
841
+ if (runningActivityLabel && matchingActivityToolIsVisible) {
842
+ return false;
843
+ }
844
+ if (!runningActivityLabel &&
845
+ (latestMessageHasUnresolvedTool ||
846
+ contentHasUnresolvedToolCall(reconnectContent))) {
847
+ return false;
848
+ }
849
+ if (runningActivityLabel)
850
+ return true;
700
851
  return (!latestMessageHasReasoning && !contentHasVisibleReasoning(reconnectContent));
701
852
  }
702
853
  function AssistantChatUserMessageItem() {
@@ -1198,6 +1349,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1198
1349
  const reconnectCanMaterializeRef = useRef(false);
1199
1350
  const reconnectAbortRef = useRef(null);
1200
1351
  const reconnectAutoRecoveryCountRef = useRef(0);
1352
+ const reconnectOwnerMountedRef = useReconnectReaderOwner(reconnectRunIdRef, reconnectAbortRef);
1201
1353
  const [pendingReconnectRecovery, setPendingReconnectRecovery] = useState(null);
1202
1354
  // Nuclear stop: user clicked stop. Clears the stop button/indicator AND
1203
1355
  // lets new submissions go through immediately — prevents the "stuck
@@ -1494,7 +1646,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1494
1646
  (!stopped.runId || !runId || stopped.runId === runId));
1495
1647
  }, []);
1496
1648
  const startReconnectToRun = useCallback((runInfo) => {
1497
- if (!threadId ||
1649
+ if (!reconnectOwnerMountedRef.current ||
1650
+ !threadId ||
1498
1651
  !runInfo.runId ||
1499
1652
  (runInfo.status !== "running" && !isReplayableTerminalRun(runInfo))) {
1500
1653
  return false;
@@ -1687,6 +1840,10 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1687
1840
  rafPending = true;
1688
1841
  requestAnimationFrame(() => {
1689
1842
  rafPending = false;
1843
+ if (!reconnectOwnerMountedRef.current ||
1844
+ reconnectRunIdRef.current !== runId) {
1845
+ return;
1846
+ }
1690
1847
  setReconnectContent(latestSnapshot);
1691
1848
  });
1692
1849
  };
@@ -1745,6 +1902,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1745
1902
  clearInterval(watchdog);
1746
1903
  clearInterval(idleCheck);
1747
1904
  }
1905
+ // A newer reader, live adapter, stop action, or component unmount took
1906
+ // ownership while this async loop was unwinding. Do not let the stale
1907
+ // generation refresh/import or update reconnect state afterwards.
1908
+ if (reconnectRunIdRef.current !== runId)
1909
+ return;
1748
1910
  if (noProgressDuringReconnect && reconnectRunIdRef.current === runId) {
1749
1911
  const reconnectErrorCode = reconnectTerminalReason === "run_timeout"
1750
1912
  ? "run_timeout"
@@ -2656,6 +2818,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2656
2818
  }
2657
2819
  const stillNeeded = dedupeReconnectContentAgainstMessages(reconnectContent, messages, {
2658
2820
  suppressToolRepeats: true,
2821
+ trimTailTextOverlap: true,
2659
2822
  }).length > 0;
2660
2823
  if (!stillNeeded) {
2661
2824
  setReconnectContent([]);
@@ -3139,7 +3302,10 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3139
3302
  // Do not memoize this on `messages` identity. assistant-ui can update the
3140
3303
  // live assistant message content in place while streaming, and the reconnect
3141
3304
  // overlay must hide as soon as that live message has caught up.
3142
- const visibleReconnectContent = dedupeReconnectContentAgainstMessages(reconnectContent, messages, { suppressToolRepeats: adapterHandoffPending });
3305
+ const visibleReconnectContent = dedupeReconnectContentAgainstMessages(reconnectContent, messages, {
3306
+ suppressToolRepeats: adapterHandoffPending,
3307
+ trimTailTextOverlap: adapterHandoffPending || reconnectTailOnlyRef.current,
3308
+ });
3143
3309
  const chatScrollResetKey = `${tabId ?? ""}:${threadId ?? ""}`;
3144
3310
  const { isDevMode: cpDevMode } = useDevMode(apiUrl);
3145
3311
  const checkpointCtx = useMemo(() => ({ apiUrl, devMode: cpDevMode, threadId }), [apiUrl, cpDevMode, threadId]);
@@ -3159,11 +3325,17 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3159
3325
  const lastUserText = useMemo(() => latestNonRecoveryUserMessageText(messages), [messages]);
3160
3326
  const latestMessage = messages[messages.length - 1];
3161
3327
  const latestMessageRole = latestMessage?.role;
3328
+ const reconnectStatusContent = visibleReconnectContent.length > 0
3329
+ ? visibleReconnectContent
3330
+ : reconnectContent.length === 0
3331
+ ? reconnectActivityContent
3332
+ : [];
3162
3333
  const showGlobalRunningStatus = shouldShowGlobalRunningStatus({
3163
3334
  showRunningInUI,
3164
3335
  runningActivityLabel,
3336
+ runningActivityTool,
3165
3337
  latestMessage,
3166
- reconnectContent: visibleReconnectContent,
3338
+ reconnectContent: reconnectStatusContent,
3167
3339
  });
3168
3340
  const latestAssistantWasPlan = latestMessageRole === "assistant" &&
3169
3341
  getRequestModeMetadata(latestMessage) === "plan";