@agent-native/core 0.98.4 → 0.98.5

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 (217) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +37 -0
  4. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +2 -0
  5. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +2 -0
  6. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +2 -0
  7. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +4 -2
  8. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +2 -0
  9. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +2 -0
  10. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +2 -0
  11. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +2 -0
  12. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +2 -0
  13. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  14. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +2 -0
  15. package/corpus/core/docs/content/tracking.mdx +2 -0
  16. package/corpus/core/package.json +1 -1
  17. package/corpus/core/src/agent/durable-background.ts +16 -16
  18. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +25 -8
  19. package/corpus/core/src/agent/engine/anthropic-engine.ts +17 -3
  20. package/corpus/core/src/agent/engine/output-tokens.ts +6 -6
  21. package/corpus/core/src/agent/production-agent.ts +200 -27
  22. package/corpus/core/src/agent/run-manager.ts +81 -1
  23. package/corpus/core/src/agent/run-store.ts +301 -41
  24. package/corpus/core/src/client/AgentPanel.tsx +6 -2
  25. package/corpus/core/src/client/AssistantChat.tsx +364 -69
  26. package/corpus/core/src/client/agent-chat-adapter.ts +169 -1
  27. package/corpus/core/src/client/chat/message-components.tsx +3 -0
  28. package/corpus/core/src/client/chat/tool-call-display.tsx +20 -36
  29. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +7 -0
  30. package/corpus/core/src/client/extensions/AgentNativeExtensionFrame.tsx +2 -0
  31. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +2 -0
  32. package/corpus/core/src/client/extensions/ExtensionEditor.tsx +9 -2
  33. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +2 -0
  34. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +2 -0
  35. package/corpus/core/src/client/extensions/portable-extension.ts +2 -0
  36. package/corpus/core/src/client/session-replay.ts +361 -29
  37. package/corpus/core/src/client/sse-event-processor.ts +43 -1
  38. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +1 -1
  39. package/corpus/core/src/demo/config.ts +6 -6
  40. package/corpus/core/src/demo/fetch-interceptor.ts +13 -4
  41. package/corpus/core/src/demo/redact.ts +13 -149
  42. package/corpus/core/src/extensions/html-shell.ts +3 -0
  43. package/corpus/core/src/extensions/session-replay-iframe.ts +131 -0
  44. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -20
  45. package/corpus/core/src/session-replay-iframe-protocol.ts +58 -0
  46. package/corpus/core/src/shared/reasoning-effort.ts +35 -0
  47. package/corpus/core/src/styles/agent-conversation.css +26 -0
  48. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  49. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +30 -21
  50. package/corpus/templates/analytics/AGENTS.md +12 -0
  51. package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
  52. package/corpus/templates/analytics/actions/get-session-replay-events.ts +1 -0
  53. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
  54. package/corpus/templates/analytics/actions/list-error-issues.ts +1 -0
  55. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
  56. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -1
  57. package/corpus/templates/analytics/app/global.css +2 -2
  58. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -4
  59. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +52 -203
  60. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +34 -12
  61. package/corpus/templates/analytics/changelog/2026-07-12-analytics-chat-retries-before-showing-a-generic-no-data-mess.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-07-12-analytics-sessions-search-no-longer-loses-fast-keystrokes-an.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-07-12-chart-selection-respects-chat-state.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-anonymizes-error-reporting-emails.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-show-iframe-content.md +6 -0
  66. package/corpus/templates/analytics/netlify.toml +0 -3
  67. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +14 -0
  68. package/corpus/templates/analytics/server/lib/error-capture.ts +59 -2
  69. package/corpus/templates/analytics/server/lib/real-data-actions.ts +24 -0
  70. package/corpus/templates/analytics/server/lib/session-replay.ts +2 -9
  71. package/corpus/templates/analytics/server/plugins/agent-chat.ts +39 -6
  72. package/corpus/templates/assets/app/global.css +1 -1
  73. package/corpus/templates/brain/app/global.css +2 -2
  74. package/corpus/templates/brain/changelog/2026-07-12-fix-long-response-progress.md +6 -0
  75. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +1 -1
  76. package/corpus/templates/forms/app/components/ThemeToggle.tsx +22 -6
  77. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +5 -5
  78. package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +3 -3
  79. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +16 -16
  80. package/corpus/templates/forms/app/global.css +110 -5
  81. package/corpus/templates/forms/app/pages/AskPage.tsx +3 -3
  82. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +134 -52
  83. package/corpus/templates/forms/app/pages/FormFillPage.tsx +44 -11
  84. package/corpus/templates/forms/app/pages/FormsListPage.tsx +42 -24
  85. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +28 -15
  86. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +38 -15
  87. package/corpus/templates/forms/app/routes/_app.settings.tsx +1 -1
  88. package/corpus/templates/forms/changelog/2026-07-12-form-builder-surfaces-are-calmer-fields-stay-stable-on-hover.md +6 -0
  89. package/corpus/templates/forms/changelog/2026-07-12-smoother-more-tactile-controls-throughout-animated-icon-and-.md +6 -0
  90. package/corpus/templates/mail/app/components/email/EmailThread.tsx +21 -13
  91. package/corpus/templates/mail/app/components/email/email-iframe-document.ts +18 -0
  92. package/dist/agent/durable-background.d.ts +10 -10
  93. package/dist/agent/durable-background.d.ts.map +1 -1
  94. package/dist/agent/durable-background.js +16 -16
  95. package/dist/agent/durable-background.js.map +1 -1
  96. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  97. package/dist/agent/engine/ai-sdk-engine.js +19 -8
  98. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  99. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  100. package/dist/agent/engine/anthropic-engine.js +11 -3
  101. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  102. package/dist/agent/engine/output-tokens.js +6 -6
  103. package/dist/agent/engine/output-tokens.js.map +1 -1
  104. package/dist/agent/production-agent.d.ts +52 -0
  105. package/dist/agent/production-agent.d.ts.map +1 -1
  106. package/dist/agent/production-agent.js +154 -26
  107. package/dist/agent/production-agent.js.map +1 -1
  108. package/dist/agent/run-manager.d.ts +9 -0
  109. package/dist/agent/run-manager.d.ts.map +1 -1
  110. package/dist/agent/run-manager.js +71 -1
  111. package/dist/agent/run-manager.js.map +1 -1
  112. package/dist/agent/run-store.d.ts +9 -0
  113. package/dist/agent/run-store.d.ts.map +1 -1
  114. package/dist/agent/run-store.js +243 -48
  115. package/dist/agent/run-store.js.map +1 -1
  116. package/dist/client/AgentPanel.d.ts.map +1 -1
  117. package/dist/client/AgentPanel.js +6 -2
  118. package/dist/client/AgentPanel.js.map +1 -1
  119. package/dist/client/AssistantChat.d.ts +21 -0
  120. package/dist/client/AssistantChat.d.ts.map +1 -1
  121. package/dist/client/AssistantChat.js +298 -67
  122. package/dist/client/AssistantChat.js.map +1 -1
  123. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  124. package/dist/client/agent-chat-adapter.js +146 -1
  125. package/dist/client/agent-chat-adapter.js.map +1 -1
  126. package/dist/client/chat/message-components.d.ts.map +1 -1
  127. package/dist/client/chat/message-components.js +2 -1
  128. package/dist/client/chat/message-components.js.map +1 -1
  129. package/dist/client/chat/tool-call-display.d.ts +3 -1
  130. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  131. package/dist/client/chat/tool-call-display.js +13 -37
  132. package/dist/client/chat/tool-call-display.js.map +1 -1
  133. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +1 -0
  134. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  135. package/dist/client/conversation/use-near-bottom-autoscroll.js +6 -0
  136. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  137. package/dist/client/extensions/AgentNativeExtensionFrame.d.ts.map +1 -1
  138. package/dist/client/extensions/AgentNativeExtensionFrame.js +2 -1
  139. package/dist/client/extensions/AgentNativeExtensionFrame.js.map +1 -1
  140. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  141. package/dist/client/extensions/EmbeddedExtension.js +2 -1
  142. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  143. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  144. package/dist/client/extensions/ExtensionEditor.js +5 -2
  145. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  146. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  147. package/dist/client/extensions/ExtensionViewer.js +2 -1
  148. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  149. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -1
  150. package/dist/client/extensions/InlineExtensionFrame.js +2 -1
  151. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -1
  152. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  153. package/dist/client/extensions/portable-extension.js +2 -0
  154. package/dist/client/extensions/portable-extension.js.map +1 -1
  155. package/dist/client/session-replay.d.ts.map +1 -1
  156. package/dist/client/session-replay.js +308 -29
  157. package/dist/client/session-replay.js.map +1 -1
  158. package/dist/client/sse-event-processor.d.ts +5 -0
  159. package/dist/client/sse-event-processor.d.ts.map +1 -1
  160. package/dist/client/sse-event-processor.js +37 -1
  161. package/dist/client/sse-event-processor.js.map +1 -1
  162. package/dist/collab/routes.d.ts +1 -1
  163. package/dist/collab/struct-routes.d.ts +1 -1
  164. package/dist/demo/actions/toggle-demo-mode.js +1 -1
  165. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  166. package/dist/demo/config.js +6 -6
  167. package/dist/demo/config.js.map +1 -1
  168. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  169. package/dist/demo/fetch-interceptor.js +13 -4
  170. package/dist/demo/fetch-interceptor.js.map +1 -1
  171. package/dist/demo/redact.d.ts +7 -7
  172. package/dist/demo/redact.d.ts.map +1 -1
  173. package/dist/demo/redact.js +8 -145
  174. package/dist/demo/redact.js.map +1 -1
  175. package/dist/extensions/html-shell.d.ts.map +1 -1
  176. package/dist/extensions/html-shell.js +2 -0
  177. package/dist/extensions/html-shell.js.map +1 -1
  178. package/dist/extensions/session-replay-iframe.d.ts +10 -0
  179. package/dist/extensions/session-replay-iframe.d.ts.map +1 -0
  180. package/dist/extensions/session-replay-iframe.js +121 -0
  181. package/dist/extensions/session-replay-iframe.js.map +1 -0
  182. package/dist/progress/routes.d.ts +1 -1
  183. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  184. package/dist/resources/handlers.d.ts +3 -3
  185. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  186. package/dist/scripts/agent-engines/list-agent-engines.js +1 -19
  187. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  188. package/dist/session-replay-iframe-protocol.d.ts +36 -0
  189. package/dist/session-replay-iframe-protocol.d.ts.map +1 -0
  190. package/dist/session-replay-iframe-protocol.js +24 -0
  191. package/dist/session-replay-iframe-protocol.js.map +1 -0
  192. package/dist/shared/reasoning-effort.d.ts +11 -0
  193. package/dist/shared/reasoning-effort.d.ts.map +1 -1
  194. package/dist/shared/reasoning-effort.js +36 -0
  195. package/dist/shared/reasoning-effort.js.map +1 -1
  196. package/dist/styles/agent-conversation.css +26 -0
  197. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  198. package/docs/content/durable-background-runs.mdx +37 -0
  199. package/docs/content/locales/ar-SA/tracking.mdx +2 -0
  200. package/docs/content/locales/de-DE/tracking.mdx +2 -0
  201. package/docs/content/locales/es-ES/tracking.mdx +2 -0
  202. package/docs/content/locales/fr-FR/tracking.mdx +4 -2
  203. package/docs/content/locales/hi-IN/tracking.mdx +2 -0
  204. package/docs/content/locales/ja-JP/tracking.mdx +2 -0
  205. package/docs/content/locales/ko-KR/tracking.mdx +2 -0
  206. package/docs/content/locales/pt-BR/tracking.mdx +2 -0
  207. package/docs/content/locales/zh-CN/tracking.mdx +2 -0
  208. package/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  209. package/docs/content/locales/zh-TW/tracking.mdx +2 -0
  210. package/docs/content/tracking.mdx +2 -0
  211. package/package.json +2 -2
  212. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  213. package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
  214. package/dist/observability/hosted-model-experiment.d.ts +0 -39
  215. package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
  216. package/dist/observability/hosted-model-experiment.js +0 -90
  217. package/dist/observability/hosted-model-experiment.js.map +0 -1
@@ -21,6 +21,7 @@ import {
21
21
  IconPlayerStopFilled,
22
22
  IconTerminal,
23
23
  IconAlertTriangle,
24
+ IconChevronDown,
24
25
  IconRefresh,
25
26
  } from "@tabler/icons-react";
26
27
  import React, {
@@ -126,7 +127,6 @@ import {
126
127
  import { useReconnectReaderOwner } from "./chat/use-reconnect-reader-owner.js";
127
128
  import {
128
129
  MessageScroller,
129
- MessageScrollerButton,
130
130
  MessageScrollerContent,
131
131
  MessageScrollerItem,
132
132
  MessageScrollerProvider,
@@ -162,6 +162,7 @@ import type {
162
162
  Reference,
163
163
  } from "./composer/types.js";
164
164
  import { ContextMeter } from "./context-xray/ContextMeter.js";
165
+ import { useNearBottomAutoscroll } from "./conversation/index.js";
165
166
  import {
166
167
  useAgentDynamicSuggestionsResult,
167
168
  type AgentDynamicSuggestionsOption,
@@ -725,6 +726,75 @@ function getMessageText(message: unknown): string {
725
726
  return typeof content === "string" ? displayableUserMessageText(content) : "";
726
727
  }
727
728
 
729
+ function contentPartFollowKey(part: unknown): string {
730
+ if (!part || typeof part !== "object") return "unknown";
731
+ const candidate = part as {
732
+ type?: unknown;
733
+ text?: unknown;
734
+ toolCallId?: unknown;
735
+ toolName?: unknown;
736
+ status?: { type?: unknown };
737
+ argsText?: unknown;
738
+ result?: unknown;
739
+ image?: unknown;
740
+ };
741
+ const type = typeof candidate.type === "string" ? candidate.type : "unknown";
742
+ if (type === "text" || type === "reasoning") {
743
+ return `${type}:${String(candidate.text ?? "").length}`;
744
+ }
745
+ if (type === "tool-call") {
746
+ return [
747
+ type,
748
+ candidate.toolCallId ?? "",
749
+ candidate.toolName ?? "",
750
+ candidate.status?.type ?? "",
751
+ String(candidate.argsText ?? "").length,
752
+ String(candidate.result ?? "").length,
753
+ ].join(":");
754
+ }
755
+ if (type === "image") return `image:${String(candidate.image ?? "").length}`;
756
+ return `${type}:${String(candidate.text ?? candidate.result ?? "").length}`;
757
+ }
758
+
759
+ function contentFollowKey(content: unknown): string {
760
+ if (typeof content === "string") return `text:${content.length}`;
761
+ if (!Array.isArray(content)) return "";
762
+ return content.map(contentPartFollowKey).join("|");
763
+ }
764
+
765
+ function messageFollowKey(message: unknown): string {
766
+ const candidate = ((message as { message?: unknown })?.message ??
767
+ message) as {
768
+ id?: unknown;
769
+ role?: unknown;
770
+ status?: { type?: unknown; reason?: unknown };
771
+ content?: unknown;
772
+ };
773
+ return [
774
+ candidate.id ?? "",
775
+ candidate.role ?? "",
776
+ candidate.status?.type ?? "",
777
+ candidate.status?.reason ?? "",
778
+ contentFollowKey(candidate.content),
779
+ ].join(",");
780
+ }
781
+
782
+ function queuedMessageFollowKey(message: QueuedMessage): string {
783
+ return [
784
+ message.id,
785
+ message.text.length,
786
+ message.images?.length ?? 0,
787
+ message.attachments?.length ?? 0,
788
+ message.references?.length ?? 0,
789
+ message.requestMode ?? "",
790
+ message.recoveryAction ?? "",
791
+ ].join(":");
792
+ }
793
+
794
+ function reconnectContentFollowKey(content: readonly ContentPart[]): string {
795
+ return content.map(contentPartFollowKey).join("|");
796
+ }
797
+
728
798
  export function reconnectActivityFallbackContent(
729
799
  toolName: string | null | undefined,
730
800
  ): ContentPart[] {
@@ -932,6 +1002,91 @@ function latestRenderedAssistantText(messages: readonly unknown[]): string {
932
1002
  return assistantTextFromContent(latestMessage.content);
933
1003
  }
934
1004
 
1005
+ function latestRenderedAssistantReasoning(
1006
+ messages: readonly unknown[],
1007
+ ): string[] {
1008
+ const latestEntry = messages.at(-1);
1009
+ const latestMessage = getRepoMessage(latestEntry as any);
1010
+ if (
1011
+ latestMessage?.role !== "assistant" ||
1012
+ !Array.isArray(latestMessage.content)
1013
+ ) {
1014
+ return [];
1015
+ }
1016
+ return latestMessage.content.flatMap((part) =>
1017
+ part?.type === "reasoning" &&
1018
+ typeof part.text === "string" &&
1019
+ part.text.length > 0
1020
+ ? [part.text]
1021
+ : [],
1022
+ );
1023
+ }
1024
+
1025
+ function trimReconnectReasoningAlreadyRendered(
1026
+ content: ContentPart[],
1027
+ renderedReasoning: readonly string[],
1028
+ options?: { trimTailOverlap?: boolean },
1029
+ ): ContentPart[] {
1030
+ if (renderedReasoning.length === 0) return content;
1031
+ const firstReconnectReasoning = content.find(
1032
+ (part): part is Extract<ContentPart, { type: "reasoning" }> =>
1033
+ part.type === "reasoning" && part.text.length > 0,
1034
+ )?.text;
1035
+ let renderedOffset = 0;
1036
+ if (options?.trimTailOverlap && firstReconnectReasoning) {
1037
+ for (let index = renderedReasoning.length - 1; index >= 0; index -= 1) {
1038
+ const rendered = renderedReasoning[index];
1039
+ if (
1040
+ rendered === firstReconnectReasoning ||
1041
+ rendered.startsWith(firstReconnectReasoning) ||
1042
+ firstReconnectReasoning.startsWith(rendered) ||
1043
+ longestSuffixPrefixOverlap(rendered, firstReconnectReasoning) > 0
1044
+ ) {
1045
+ renderedOffset = index;
1046
+ break;
1047
+ }
1048
+ }
1049
+ }
1050
+ let reasoningIndex = 0;
1051
+ let changed = false;
1052
+ const next: ContentPart[] = [];
1053
+
1054
+ for (const part of content) {
1055
+ if (part.type !== "reasoning") {
1056
+ next.push(part);
1057
+ continue;
1058
+ }
1059
+ const rendered = renderedReasoning[renderedOffset + reasoningIndex];
1060
+ reasoningIndex += 1;
1061
+ if (!rendered) {
1062
+ next.push(part);
1063
+ continue;
1064
+ }
1065
+ if (rendered === part.text || rendered.startsWith(part.text)) {
1066
+ changed = true;
1067
+ continue;
1068
+ }
1069
+ if (part.text.startsWith(rendered)) {
1070
+ const tail = part.text.slice(rendered.length);
1071
+ if (tail) next.push({ ...part, text: tail });
1072
+ changed = true;
1073
+ continue;
1074
+ }
1075
+ if (options?.trimTailOverlap) {
1076
+ const overlap = longestSuffixPrefixOverlap(rendered, part.text);
1077
+ if (overlap > 0) {
1078
+ const tail = part.text.slice(overlap);
1079
+ if (tail) next.push({ ...part, text: tail });
1080
+ changed = true;
1081
+ continue;
1082
+ }
1083
+ }
1084
+ next.push(part);
1085
+ }
1086
+
1087
+ return changed ? next : content;
1088
+ }
1089
+
935
1090
  function dedupePendingToolCallReplaysWithinContent(
936
1091
  content: ContentPart[],
937
1092
  ): ContentPart[] {
@@ -1118,11 +1273,13 @@ export function dedupeReconnectContentAgainstMessages(
1118
1273
  const { byId, latestAssistantByFingerprint, latestAssistantByName } =
1119
1274
  collectRenderedToolCallStates(messages);
1120
1275
  const renderedAssistantText = latestRenderedAssistantText(messages);
1276
+ const renderedAssistantReasoning = latestRenderedAssistantReasoning(messages);
1121
1277
  if (
1122
1278
  byId.size === 0 &&
1123
1279
  latestAssistantByFingerprint.size === 0 &&
1124
1280
  latestAssistantByName.size === 0 &&
1125
- !renderedAssistantText
1281
+ !renderedAssistantText &&
1282
+ renderedAssistantReasoning.length === 0
1126
1283
  ) {
1127
1284
  return changed ? snapshotDeduped : content;
1128
1285
  }
@@ -1243,12 +1400,18 @@ export function dedupeReconnectContentAgainstMessages(
1243
1400
  return true;
1244
1401
  })
1245
1402
  : snapshotDeduped;
1246
- const textDeduped = trimReconnectTextAlreadyRendered(
1403
+ const reasoningDeduped = trimReconnectReasoningAlreadyRendered(
1247
1404
  filtered,
1405
+ renderedAssistantReasoning,
1406
+ { trimTailOverlap: options?.trimTailTextOverlap },
1407
+ );
1408
+ if (reasoningDeduped !== filtered) changed = true;
1409
+ const textDeduped = trimReconnectTextAlreadyRendered(
1410
+ reasoningDeduped,
1248
1411
  renderedAssistantText,
1249
1412
  { trimTailOverlap: options?.trimTailTextOverlap },
1250
1413
  );
1251
- if (textDeduped !== filtered) changed = true;
1414
+ if (textDeduped !== reasoningDeduped) changed = true;
1252
1415
  return changed ? textDeduped : content;
1253
1416
  }
1254
1417
 
@@ -1450,7 +1613,7 @@ function AssistantChatUserMessageItem() {
1450
1613
  const message = messageRuntime.getState();
1451
1614
  if (isHiddenUserMessage(message)) return null;
1452
1615
  return (
1453
- <MessageScrollerItem messageId={message.id} scrollAnchor>
1616
+ <MessageScrollerItem messageId={message.id}>
1454
1617
  <UserMessage />
1455
1618
  </MessageScrollerItem>
1456
1619
  );
@@ -1883,6 +2046,87 @@ function stripBase64FromRepo(repo: unknown): unknown {
1883
2046
  return { ...r, messages };
1884
2047
  }
1885
2048
 
2049
+ /**
2050
+ * Owns the "Resuming…" status shown during the adapter's auto-continuation
2051
+ * window — the gap between the end of one serverless chunk and the POST for
2052
+ * the next. Set true the moment the adapter dispatches
2053
+ * `agent-chat:auto-continue`. Cleared here as soon as the successor chunk
2054
+ * produces real output (`agent-chat:stream-progress` — dispatched by the SSE
2055
+ * processor for non-empty text/reasoning deltas) or the run is force-stopped;
2056
+ * a 30s failsafe timer covers any case neither fires. The caller clears it
2057
+ * via the returned `clearAutoResume` for other real-progress signals (tool
2058
+ * activity, an accepted run error, an explicit stop) that live outside this
2059
+ * hook.
2060
+ *
2061
+ * Deliberately NOT cleared by `agent-chat:activity-clear` or merely-idle
2062
+ * `isRunning`: both also fire for old-chunk `tool_done` replays / server
2063
+ * retries, and clearing on those would re-expose terminal message controls
2064
+ * during the exact gap this state exists to cover.
2065
+ */
2066
+ export function useAutoResumeStatus(
2067
+ tabId: string | undefined,
2068
+ forceStopped: boolean,
2069
+ ): { isAutoResuming: boolean; clearAutoResume: () => void } {
2070
+ const [isAutoResuming, setIsAutoResuming] = useState(false);
2071
+ const autoResumeTimerRef = useRef<number | null>(null);
2072
+
2073
+ const clearAutoResume = useCallback(() => {
2074
+ if (autoResumeTimerRef.current !== null) {
2075
+ window.clearTimeout(autoResumeTimerRef.current);
2076
+ autoResumeTimerRef.current = null;
2077
+ }
2078
+ setIsAutoResuming(false);
2079
+ }, []);
2080
+
2081
+ useEffect(() => {
2082
+ const handler = (e: Event) => {
2083
+ const detail = (e as CustomEvent).detail as { tabId?: string };
2084
+ if (tabId && detail?.tabId && detail.tabId !== tabId) return;
2085
+ if (autoResumeTimerRef.current !== null) {
2086
+ window.clearTimeout(autoResumeTimerRef.current);
2087
+ }
2088
+ setIsAutoResuming(true);
2089
+ autoResumeTimerRef.current = window.setTimeout(() => {
2090
+ autoResumeTimerRef.current = null;
2091
+ setIsAutoResuming(false);
2092
+ }, AUTO_RESUME_STATUS_TIMEOUT_MS);
2093
+ };
2094
+ window.addEventListener("agent-chat:auto-continue", handler);
2095
+ return () => {
2096
+ if (autoResumeTimerRef.current !== null) {
2097
+ window.clearTimeout(autoResumeTimerRef.current);
2098
+ autoResumeTimerRef.current = null;
2099
+ }
2100
+ window.removeEventListener("agent-chat:auto-continue", handler);
2101
+ };
2102
+ }, [tabId]);
2103
+
2104
+ // Real forward progress in the current chunk (visible text or reasoning
2105
+ // deltas) means the run is not stuck between chunks — clear the indicator
2106
+ // immediately rather than waiting on the 30s failsafe. This is the
2107
+ // plain-text-continuation case: no tool call fires `agent-chat:activity`
2108
+ // to clear it, so without this listener "Resuming" would linger for up to
2109
+ // AUTO_RESUME_STATUS_TIMEOUT_MS after the run already resumed or finished.
2110
+ useEffect(() => {
2111
+ const handler = (e: Event) => {
2112
+ const detail = (e as CustomEvent).detail as { tabId?: string };
2113
+ if (tabId && detail?.tabId && detail.tabId !== tabId) return;
2114
+ clearAutoResume();
2115
+ };
2116
+ window.addEventListener("agent-chat:stream-progress", handler);
2117
+ return () =>
2118
+ window.removeEventListener("agent-chat:stream-progress", handler);
2119
+ }, [clearAutoResume, tabId]);
2120
+
2121
+ useEffect(() => {
2122
+ if (forceStopped) {
2123
+ clearAutoResume();
2124
+ }
2125
+ }, [clearAutoResume, forceStopped]);
2126
+
2127
+ return { isAutoResuming, clearAutoResume };
2128
+ }
2129
+
1886
2130
  const AssistantChatInner = forwardRef<
1887
2131
  AssistantChatHandle,
1888
2132
  AssistantChatProps & { apiUrl: string }
@@ -2248,15 +2492,6 @@ const AssistantChatInner = forwardRef<
2248
2492
  runId?: string;
2249
2493
  } | null>(null);
2250
2494
  const [isReconnecting, setIsReconnecting] = useState(false);
2251
- // True during the 250ms continuation window and startup of the next chunk
2252
- // (adapter's auto-continue delay before POSTing the next chunk).
2253
- const [isAutoResuming, setIsAutoResuming] = useState(false);
2254
- // Latest-value ref for the same single-reader checks as isRuntimeRunningRef:
2255
- // during an adapter auto-continuation the runtime can flick false between
2256
- // chunks while the adapter is still driving the turn.
2257
- const isAutoResumingRef = useRef(isAutoResuming);
2258
- isAutoResumingRef.current = isAutoResuming;
2259
- const autoResumeTimerRef = useRef<number | null>(null);
2260
2495
  const [optimisticRunning, setOptimisticRunning] = useState(false);
2261
2496
  const [runningActivityLabel, setRunningActivityLabel] = useState<
2262
2497
  string | null
@@ -2303,6 +2538,17 @@ const AssistantChatInner = forwardRef<
2303
2538
  // queueing forever" state where isReconnecting or isRuntimeRunning gets
2304
2539
  // wedged (e.g. after a tab refresh + stop during reconnect).
2305
2540
  const [forceStopped, setForceStopped] = useState(false);
2541
+ // True during the 250ms continuation window and startup of the next chunk
2542
+ // (adapter's auto-continue delay before POSTing the next chunk).
2543
+ const { isAutoResuming, clearAutoResume } = useAutoResumeStatus(
2544
+ tabId,
2545
+ forceStopped,
2546
+ );
2547
+ // Latest-value ref for the same single-reader checks as isRuntimeRunningRef:
2548
+ // during an adapter auto-continuation the runtime can flick false between
2549
+ // chunks while the adapter is still driving the turn.
2550
+ const isAutoResumingRef = useRef(isAutoResuming);
2551
+ isAutoResumingRef.current = isAutoResuming;
2306
2552
  const [hasActiveServerRun, setHasActiveServerRun] = useState(() =>
2307
2553
  activeRunMatchesThread(getActiveRun(), threadId),
2308
2554
  );
@@ -2365,6 +2611,10 @@ const AssistantChatInner = forwardRef<
2365
2611
  const stopActiveRunRef = useRef<
2366
2612
  (options?: { preserveQueuedMessages?: boolean }) => void
2367
2613
  >(() => {});
2614
+ // addToQueue is declared before the autoscroll hook because it also feeds
2615
+ // the reconnect/imperative APIs. Keep a stable bridge so an accepted visible
2616
+ // submit can explicitly reattach bottom-following without closure churn.
2617
+ const resumeFollowingRef = useRef<() => void>(() => {});
2368
2618
 
2369
2619
  const markOptimisticRunning = useCallback(() => {
2370
2620
  setOptimisticRunning(true);
@@ -3624,10 +3874,13 @@ const AssistantChatInner = forwardRef<
3624
3874
  ...(detail.recoverable ? { recoverable: detail.recoverable } : {}),
3625
3875
  });
3626
3876
  setDismissedRunErrorKey(null);
3877
+ // An errored continuation must not keep showing "Resuming" — there is
3878
+ // no further chunk coming to clear it.
3879
+ clearAutoResume();
3627
3880
  };
3628
3881
  window.addEventListener("agent-chat:run-error", handler);
3629
3882
  return () => window.removeEventListener("agent-chat:run-error", handler);
3630
- }, [latestAssistantRunId, tabId, threadId]);
3883
+ }, [clearAutoResume, latestAssistantRunId, tabId, threadId]);
3631
3884
 
3632
3885
  // Real activity means the next chunk has started. Surface longer-lived
3633
3886
  // activity such as "Still generating image" so active-run reconnects do not
@@ -3644,7 +3897,7 @@ const AssistantChatInner = forwardRef<
3644
3897
  typeof detail?.label === "string" ? detail.label.trim() : "";
3645
3898
  if (!label) return;
3646
3899
  const tool = typeof detail?.tool === "string" ? detail.tool.trim() : "";
3647
- setIsAutoResuming(false);
3900
+ clearAutoResume();
3648
3901
  setRunningActivityTool(tool || null);
3649
3902
  updateActiveRunActivity(tool || null);
3650
3903
  latestActivityLabelRef.current = label;
@@ -3679,50 +3932,23 @@ const AssistantChatInner = forwardRef<
3679
3932
  window.removeEventListener("agent-chat:activity", handler);
3680
3933
  window.removeEventListener("agent-chat:activity-clear", clear);
3681
3934
  };
3682
- }, [resetRunningActivity, tabId]);
3683
-
3684
- // Show "Resuming…" during the adapter's auto-continuation window (the
3685
- // ~250ms gap between the end of one serverless chunk and the POST for the
3686
- // next). The adapter dispatches `agent-chat:auto-continue` at that moment.
3935
+ }, [clearAutoResume, resetRunningActivity, tabId]);
3936
+
3937
+ // "Resuming…" itself (auto-continue stream-progress/forceStopped clear,
3938
+ // plus the 30s failsafe) is owned by useAutoResumeStatus above. Real
3939
+ // next-chunk activity (tool start/activity events) clears it in the
3940
+ // activity handler above; real streamed output (text/reasoning) clears it
3941
+ // via that hook's own stream-progress listener. This effect only handles
3942
+ // the running-activity reset once both running signals go idle — it does
3943
+ // NOT clear auto-resume merely because `isRunning` is still true: the
3944
+ // adapter dispatches auto-continue before the old chunk's runtime flips
3945
+ // idle, so doing that would expose terminal message controls during the
3946
+ // exact gap this state is meant to cover.
3687
3947
  useEffect(() => {
3688
- const handler = (e: Event) => {
3689
- const detail = (e as CustomEvent).detail as { tabId?: string };
3690
- if (tabId && detail?.tabId && detail.tabId !== tabId) return;
3691
- if (autoResumeTimerRef.current !== null) {
3692
- window.clearTimeout(autoResumeTimerRef.current);
3693
- }
3694
- setIsAutoResuming(true);
3695
- autoResumeTimerRef.current = window.setTimeout(() => {
3696
- autoResumeTimerRef.current = null;
3697
- setIsAutoResuming(false);
3698
- }, AUTO_RESUME_STATUS_TIMEOUT_MS);
3699
- };
3700
- window.addEventListener("agent-chat:auto-continue", handler);
3701
- return () => {
3702
- if (autoResumeTimerRef.current !== null) {
3703
- window.clearTimeout(autoResumeTimerRef.current);
3704
- autoResumeTimerRef.current = null;
3705
- }
3706
- window.removeEventListener("agent-chat:auto-continue", handler);
3707
- };
3708
- }, [tabId]);
3709
-
3710
- // Clear auto-resume once the next chunk has visibly started or the user stops.
3711
- // Do not clear solely because `isRunning` is false: auto-resume intentionally
3712
- // covers that between-chunk gap so the chat never looks idle while work is
3713
- // still scheduled.
3714
- useEffect(() => {
3715
- if (isRunning || forceStopped) {
3716
- if (autoResumeTimerRef.current !== null) {
3717
- window.clearTimeout(autoResumeTimerRef.current);
3718
- autoResumeTimerRef.current = null;
3719
- }
3720
- setIsAutoResuming(false);
3721
- }
3722
3948
  if (!isRunning && !isAutoResuming) {
3723
3949
  resetRunningActivity();
3724
3950
  }
3725
- }, [forceStopped, isAutoResuming, isRunning, resetRunningActivity]);
3951
+ }, [isAutoResuming, isRunning, resetRunningActivity]);
3726
3952
 
3727
3953
  // Auto-dequeue: when the agent is idle, send the next queued message. This
3728
3954
  // intentionally does not depend on observing the running -> idle transition:
@@ -4025,11 +4251,7 @@ const AssistantChatInner = forwardRef<
4025
4251
  setOptimisticRunning(false);
4026
4252
  setHasActiveServerRun(false);
4027
4253
  setPendingReconnectRecovery(null);
4028
- setIsAutoResuming(false);
4029
- if (autoResumeTimerRef.current !== null) {
4030
- window.clearTimeout(autoResumeTimerRef.current);
4031
- autoResumeTimerRef.current = null;
4032
- }
4254
+ clearAutoResume();
4033
4255
  resetRunningActivity();
4034
4256
  if (!options?.preserveQueuedMessages) {
4035
4257
  queueStopVersionRef.current += 1;
@@ -4087,6 +4309,7 @@ const AssistantChatInner = forwardRef<
4087
4309
  [
4088
4310
  apiUrl,
4089
4311
  applyLocalQueuedMessages,
4312
+ clearAutoResume,
4090
4313
  isReconnecting,
4091
4314
  resetRunningActivity,
4092
4315
  reconnectContent,
@@ -4133,9 +4356,6 @@ const AssistantChatInner = forwardRef<
4133
4356
  // Selection context attached via Cmd+I is one-shot — clear it as soon
4134
4357
  // as the user actually sends a message so it can't be re-used.
4135
4358
  clearPendingSelection();
4136
- // Sending a message is an explicit user action. The scroller anchors the
4137
- // appended user turn so the new message and reply land in view without
4138
- // re-pinning the viewport during ordinary streaming.
4139
4359
  const submitted = includeComposerContext
4140
4360
  ? buildComposerContextSubmission(text)
4141
4361
  : { text, includesContext: false };
@@ -4344,6 +4564,10 @@ const AssistantChatInner = forwardRef<
4344
4564
  // intentionally reported before the agent's response resolves: a
4345
4565
  // caller like sendToAgentChatAndConfirm only needs to know the submit
4346
4566
  // wasn't silently dropped, not whether the run itself later succeeds.
4567
+ // A visible submit is explicit user intent to see the new turn. Reattach
4568
+ // following only after the message was queued/appended successfully;
4569
+ // hidden reconnect recovery turns must leave the user's viewport alone.
4570
+ if (!hideUserMessage) resumeFollowingRef.current();
4347
4571
  reportAgentChatSubmitResult(submitMessageId, true);
4348
4572
  if (submitted.includesContext) {
4349
4573
  updateComposerContextItems(() => []);
@@ -4510,6 +4734,62 @@ const AssistantChatInner = forwardRef<
4510
4734
  adapterHandoffPending || reconnectTailOnlyRef.current,
4511
4735
  },
4512
4736
  );
4737
+ const autoscrollFollowKey = [
4738
+ messages.map(messageFollowKey).join(";"),
4739
+ `q:${queuedMessages.map(queuedMessageFollowKey).join("|")}`,
4740
+ `r:${reconnectContentFollowKey(visibleReconnectContent)}`,
4741
+ `status:${showRunningInUI ? runningStatusLabel : "idle"}`,
4742
+ ].join(";;");
4743
+ const {
4744
+ scrollRef,
4745
+ isNearBottomRef,
4746
+ showScrollToBottom,
4747
+ scrollToBottom,
4748
+ scrollToBottomAfterPaint,
4749
+ resumeFollowing,
4750
+ } = useNearBottomAutoscroll<HTMLDivElement>({
4751
+ followKey: autoscrollFollowKey,
4752
+ streaming: textStreaming,
4753
+ });
4754
+ resumeFollowingRef.current = resumeFollowing;
4755
+
4756
+ const scrollToBottomWhileLayoutSettles = useCallback(() => {
4757
+ scrollToBottomAfterPaint();
4758
+ const element = scrollRef.current;
4759
+ if (!element || typeof ResizeObserver === "undefined") return undefined;
4760
+
4761
+ let stopped = false;
4762
+ const observer = new ResizeObserver(() => {
4763
+ if (!stopped && isNearBottomRef.current) scrollToBottom();
4764
+ });
4765
+ observer.observe(element);
4766
+ const timeout = window.setTimeout(() => {
4767
+ stopped = true;
4768
+ observer.disconnect();
4769
+ if (isNearBottomRef.current) scrollToBottom();
4770
+ }, 1600);
4771
+
4772
+ return () => {
4773
+ stopped = true;
4774
+ window.clearTimeout(timeout);
4775
+ observer.disconnect();
4776
+ };
4777
+ }, [isNearBottomRef, scrollRef, scrollToBottom, scrollToBottomAfterPaint]);
4778
+
4779
+ const wasRestoringRef = useRef(isRestoring);
4780
+ useEffect(() => {
4781
+ const wasRestoring = wasRestoringRef.current;
4782
+ wasRestoringRef.current = isRestoring;
4783
+ if (wasRestoring && !isRestoring) {
4784
+ return scrollToBottomWhileLayoutSettles();
4785
+ }
4786
+ }, [isRestoring, scrollToBottomWhileLayoutSettles]);
4787
+
4788
+ useEffect(() => {
4789
+ if (!textStreaming && isNearBottomRef.current) {
4790
+ scrollToBottomAfterPaint();
4791
+ }
4792
+ }, [isNearBottomRef, scrollToBottomAfterPaint, textStreaming]);
4513
4793
  const chatScrollResetKey = `${tabId ?? ""}:${threadId ?? ""}`;
4514
4794
 
4515
4795
  const { isDevMode: cpDevMode } = useDevMode(apiUrl);
@@ -4661,6 +4941,7 @@ const AssistantChatInner = forwardRef<
4661
4941
  showComposerSlot ||
4662
4942
  showCenteredEmptyThreadFooterSlot ||
4663
4943
  (guidedQuestions && guidedQuestions.length > 0) ||
4944
+ showScrollToBottom ||
4664
4945
  composerContextItems.length > 0 ||
4665
4946
  showPlanModeCallout,
4666
4947
  );
@@ -4764,9 +5045,12 @@ const AssistantChatInner = forwardRef<
4764
5045
  )}
4765
5046
 
4766
5047
  {/* Messages area */}
4767
- <MessageScrollerProvider key={chatScrollResetKey} autoScroll>
5048
+ <MessageScrollerProvider
5049
+ key={chatScrollResetKey}
5050
+ autoScroll={false}
5051
+ >
4768
5052
  <MessageScroller className="agent-chat-scroll">
4769
- <MessageScrollerViewport>
5053
+ <MessageScrollerViewport ref={scrollRef}>
4770
5054
  {authError ? (
4771
5055
  <div className="flex flex-col items-center justify-center h-full px-4 gap-3">
4772
5056
  <div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
@@ -5028,7 +5312,6 @@ const AssistantChatInner = forwardRef<
5028
5312
  <MessageScrollerItem
5029
5313
  key={msg.id}
5030
5314
  messageId={msg.id}
5031
- scrollAnchor
5032
5315
  >
5033
5316
  <div className="group flex items-start justify-end gap-1.5">
5034
5317
  <button
@@ -5072,8 +5355,20 @@ const AssistantChatInner = forwardRef<
5072
5355
  </MessageScrollerContent>
5073
5356
  )}
5074
5357
  </MessageScrollerViewport>
5075
- {!authError && !isRestoring && !showEmptyState ? (
5076
- <MessageScrollerButton />
5358
+ {!authError &&
5359
+ !isRestoring &&
5360
+ !showEmptyState &&
5361
+ showScrollToBottom ? (
5362
+ <div className="shrink-0 flex justify-center -mb-1">
5363
+ <button
5364
+ type="button"
5365
+ onClick={scrollToBottom}
5366
+ className="flex h-7 w-7 items-center justify-center rounded-full border border-border bg-background shadow-sm hover:bg-accent"
5367
+ aria-label="Scroll to bottom"
5368
+ >
5369
+ <IconChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
5370
+ </button>
5371
+ </div>
5077
5372
  ) : null}
5078
5373
  </MessageScroller>
5079
5374
  </MessageScrollerProvider>