@agent-native/core 0.92.2 → 0.92.4

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 (172) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +32 -0
  3. package/corpus/core/docs/content/locales/ar-SA/server.mdx +35 -0
  4. package/corpus/core/docs/content/locales/de-DE/server.mdx +35 -0
  5. package/corpus/core/docs/content/locales/es-ES/server.mdx +35 -0
  6. package/corpus/core/docs/content/locales/fr-FR/server.mdx +35 -0
  7. package/corpus/core/docs/content/locales/hi-IN/server.mdx +35 -0
  8. package/corpus/core/docs/content/locales/ja-JP/server.mdx +35 -0
  9. package/corpus/core/docs/content/locales/ko-KR/server.mdx +35 -0
  10. package/corpus/core/docs/content/locales/pt-BR/server.mdx +35 -0
  11. package/corpus/core/docs/content/locales/zh-CN/server.mdx +35 -0
  12. package/corpus/core/docs/content/locales/zh-TW/server.mdx +35 -0
  13. package/corpus/core/docs/content/server.mdx +35 -0
  14. package/corpus/core/package.json +2 -2
  15. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +14 -4
  16. package/corpus/core/src/agent/engine/anthropic-engine.ts +15 -3
  17. package/corpus/core/src/agent/engine/builder-engine.ts +30 -4
  18. package/corpus/core/src/agent/engine/registry.ts +63 -2
  19. package/corpus/core/src/agent/model-config.ts +0 -2
  20. package/corpus/core/src/agent/production-agent.ts +12 -1
  21. package/corpus/core/src/agent/run-manager.ts +57 -36
  22. package/corpus/core/src/agent/run-store.ts +81 -2
  23. package/corpus/core/src/agent/thread-data-builder.ts +33 -2
  24. package/corpus/core/src/client/AssistantChat.tsx +314 -96
  25. package/corpus/core/src/client/agent-chat-adapter.ts +17 -1
  26. package/corpus/core/src/client/chat/repo-helpers.ts +45 -0
  27. package/corpus/core/src/client/chat/tool-call-display.tsx +36 -27
  28. package/corpus/core/src/client/chat-model-groups.ts +1 -5
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +0 -2
  30. package/corpus/core/src/client/embed-auth.ts +20 -0
  31. package/corpus/core/src/client/settings/SettingsSection.tsx +66 -5
  32. package/corpus/core/src/client/sse-event-processor.ts +49 -17
  33. package/corpus/core/src/client/use-db-sync.ts +31 -9
  34. package/corpus/core/src/collab/agent-presence.ts +6 -1
  35. package/corpus/core/src/collab/awareness-store.ts +185 -0
  36. package/corpus/core/src/collab/awareness.ts +58 -2
  37. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +7 -8
  38. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +7 -18
  39. package/corpus/core/src/server/core-routes-plugin.ts +21 -2
  40. package/corpus/core/src/server/embed-session.ts +20 -4
  41. package/corpus/core/src/styles/agent-native.css +51 -0
  42. package/corpus/core/src/tracking/providers.ts +33 -2
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +8 -1
  44. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +9 -1
  45. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorDetail.tsx +41 -11
  46. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +44 -0
  47. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-action-menus-now-close-cleanly-before-opening-hist.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-email-reports-still-send-when-the-screenshot-captu.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-09-uptime-alerts-now-suppress-recovery-noise-during-flapping-an.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-09-uptime-check-history-now-shows-timing-diagnostics-so-slow-or.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-09-uptime-recovery-alerts-now-send-when-the-original-outage-not.md +6 -0
  52. package/corpus/templates/analytics/docs/uptime-monitoring.md +29 -16
  53. package/corpus/templates/analytics/netlify.toml +3 -0
  54. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +126 -0
  55. package/corpus/templates/analytics/server/db/schema-monitoring.ts +5 -0
  56. package/corpus/templates/analytics/server/jobs/uptime-monitors.ts +5 -1
  57. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +305 -31
  58. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  59. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -1
  60. package/corpus/templates/analytics/server/routes/api/uptime-monitors/run.post.ts +57 -0
  61. package/corpus/templates/clips/actions/list-meetings.ts +12 -0
  62. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminders-now-ignore-obvious-solo-personal-calendar-.md +6 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +1 -0
  64. package/corpus/templates/clips/server/lib/calendar-event-classification.ts +79 -0
  65. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  66. package/dist/agent/engine/ai-sdk-engine.js +13 -4
  67. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  68. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  69. package/dist/agent/engine/anthropic-engine.js +14 -3
  70. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  71. package/dist/agent/engine/builder-engine.d.ts +1 -1
  72. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  73. package/dist/agent/engine/builder-engine.js +26 -4
  74. package/dist/agent/engine/builder-engine.js.map +1 -1
  75. package/dist/agent/engine/registry.d.ts.map +1 -1
  76. package/dist/agent/engine/registry.js +47 -2
  77. package/dist/agent/engine/registry.js.map +1 -1
  78. package/dist/agent/model-config.d.ts +2 -2
  79. package/dist/agent/model-config.d.ts.map +1 -1
  80. package/dist/agent/model-config.js +0 -2
  81. package/dist/agent/model-config.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +12 -1
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/agent/run-manager.d.ts.map +1 -1
  86. package/dist/agent/run-manager.js +52 -33
  87. package/dist/agent/run-manager.js.map +1 -1
  88. package/dist/agent/run-store.d.ts +24 -0
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +64 -2
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  93. package/dist/agent/thread-data-builder.js +28 -2
  94. package/dist/agent/thread-data-builder.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts +6 -0
  96. package/dist/client/AssistantChat.d.ts.map +1 -1
  97. package/dist/client/AssistantChat.js +243 -36
  98. package/dist/client/AssistantChat.js.map +1 -1
  99. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  100. package/dist/client/agent-chat-adapter.js +15 -1
  101. package/dist/client/agent-chat-adapter.js.map +1 -1
  102. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  103. package/dist/client/chat/repo-helpers.js +43 -0
  104. package/dist/client/chat/repo-helpers.js.map +1 -1
  105. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  106. package/dist/client/chat/tool-call-display.js +4 -4
  107. package/dist/client/chat/tool-call-display.js.map +1 -1
  108. package/dist/client/chat-model-groups.d.ts.map +1 -1
  109. package/dist/client/chat-model-groups.js +1 -3
  110. package/dist/client/chat-model-groups.js.map +1 -1
  111. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  112. package/dist/client/composer/TiptapComposer.js +0 -2
  113. package/dist/client/composer/TiptapComposer.js.map +1 -1
  114. package/dist/client/embed-auth.d.ts.map +1 -1
  115. package/dist/client/embed-auth.js +14 -0
  116. package/dist/client/embed-auth.js.map +1 -1
  117. package/dist/client/settings/SettingsSection.d.ts.map +1 -1
  118. package/dist/client/settings/SettingsSection.js +32 -3
  119. package/dist/client/settings/SettingsSection.js.map +1 -1
  120. package/dist/client/sse-event-processor.d.ts.map +1 -1
  121. package/dist/client/sse-event-processor.js +44 -17
  122. package/dist/client/sse-event-processor.js.map +1 -1
  123. package/dist/client/use-db-sync.d.ts.map +1 -1
  124. package/dist/client/use-db-sync.js +29 -9
  125. package/dist/client/use-db-sync.js.map +1 -1
  126. package/dist/collab/agent-presence.d.ts.map +1 -1
  127. package/dist/collab/agent-presence.js +5 -2
  128. package/dist/collab/agent-presence.js.map +1 -1
  129. package/dist/collab/awareness-store.d.ts +1 -1
  130. package/dist/collab/awareness-store.d.ts.map +1 -1
  131. package/dist/collab/awareness-store.js +0 -0
  132. package/dist/collab/awareness-store.js.map +1 -1
  133. package/dist/collab/awareness.d.ts +4 -2
  134. package/dist/collab/awareness.d.ts.map +1 -1
  135. package/dist/collab/awareness.js +46 -2
  136. package/dist/collab/awareness.js.map +1 -1
  137. package/dist/collab/routes.d.ts +1 -1
  138. package/dist/collab/struct-routes.d.ts +1 -1
  139. package/dist/notifications/routes.d.ts +2 -2
  140. package/dist/observability/routes.d.ts +3 -3
  141. package/dist/progress/routes.d.ts +1 -1
  142. package/dist/resources/handlers.d.ts +1 -1
  143. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  144. package/dist/scripts/agent-engines/manage-agent-engine.js +6 -6
  145. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  146. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  147. package/dist/scripts/agent-engines/set-agent-engine.js +5 -13
  148. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  149. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  150. package/dist/server/core-routes-plugin.d.ts +5 -0
  151. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  152. package/dist/server/core-routes-plugin.js +10 -3
  153. package/dist/server/core-routes-plugin.js.map +1 -1
  154. package/dist/server/embed-session.d.ts.map +1 -1
  155. package/dist/server/embed-session.js +17 -4
  156. package/dist/server/embed-session.js.map +1 -1
  157. package/dist/styles/agent-native.css +51 -0
  158. package/dist/tracking/providers.d.ts.map +1 -1
  159. package/dist/tracking/providers.js +23 -5
  160. package/dist/tracking/providers.js.map +1 -1
  161. package/docs/content/locales/ar-SA/server.mdx +35 -0
  162. package/docs/content/locales/de-DE/server.mdx +35 -0
  163. package/docs/content/locales/es-ES/server.mdx +35 -0
  164. package/docs/content/locales/fr-FR/server.mdx +35 -0
  165. package/docs/content/locales/hi-IN/server.mdx +35 -0
  166. package/docs/content/locales/ja-JP/server.mdx +35 -0
  167. package/docs/content/locales/ko-KR/server.mdx +35 -0
  168. package/docs/content/locales/pt-BR/server.mdx +35 -0
  169. package/docs/content/locales/zh-CN/server.mdx +35 -0
  170. package/docs/content/locales/zh-TW/server.mdx +35 -0
  171. package/docs/content/server.mdx +35 -0
  172. package/package.json +2 -2
@@ -854,6 +854,21 @@ function lastActivityTool(
854
854
  return undefined;
855
855
  }
856
856
 
857
+ function lastPreparingActionTool(
858
+ trail: readonly AgentActivityTrailEntry[],
859
+ ): string | undefined {
860
+ for (let i = trail.length - 1; i >= 0; i--) {
861
+ const entry = trail[i];
862
+ const tool = entry?.tool?.trim();
863
+ if (!tool) continue;
864
+ const label = entry.label.trim().toLowerCase();
865
+ if (label.startsWith("preparing ") && label.includes(" action")) {
866
+ return tool;
867
+ }
868
+ }
869
+ return undefined;
870
+ }
871
+
857
872
  function formatActivityTrail(
858
873
  trail: readonly AgentActivityTrailEntry[],
859
874
  ): string | undefined {
@@ -2484,7 +2499,8 @@ export function createAgentChatAdapter(
2484
2499
  part.result !== undefined,
2485
2500
  );
2486
2501
  const currentPreparingToolName =
2487
- lastUnresolvedToolActivity(visibleContent);
2502
+ lastUnresolvedToolActivity(visibleContent) ??
2503
+ lastPreparingActionTool(signal.activityTrail);
2488
2504
  // In-flight tool stall guard. When the same write tool is stuck
2489
2505
  // in-flight because the connection keeps dropping (stream_ended),
2490
2506
  // hasInFlightTool=true keeps madeProgress=true and completely
@@ -207,6 +207,41 @@ function repoTerminalAssistantCount(
207
207
  }).length;
208
208
  }
209
209
 
210
+ function toolCallProgressScore(part: RepoMessageContent): number {
211
+ if (part.type !== "tool-call") return 0;
212
+ let score = 1;
213
+ if (part.activity !== true) score += 1;
214
+ if ("result" in part) score += 4;
215
+ return score;
216
+ }
217
+
218
+ function repoToolCallProgress(repo: NormalizedRepo | null | undefined): {
219
+ total: number;
220
+ materialized: number;
221
+ completed: number;
222
+ score: number;
223
+ } {
224
+ const progress = {
225
+ total: 0,
226
+ materialized: 0,
227
+ completed: 0,
228
+ score: 0,
229
+ };
230
+ for (const entry of getRepoMessages(repo)) {
231
+ const message = getRepoMessage(entry);
232
+ const content = message?.content;
233
+ if (!Array.isArray(content)) continue;
234
+ for (const part of content) {
235
+ if (part?.type !== "tool-call") continue;
236
+ progress.total += 1;
237
+ if (part.activity !== true) progress.materialized += 1;
238
+ if ("result" in part) progress.completed += 1;
239
+ progress.score += toolCallProgressScore(part);
240
+ }
241
+ }
242
+ return progress;
243
+ }
244
+
210
245
  export function shouldImportServerThreadData(
211
246
  currentRepo: NormalizedRepo | null | undefined,
212
247
  incomingRepo: NormalizedRepo | null | undefined,
@@ -230,6 +265,16 @@ export function shouldImportServerThreadData(
230
265
  ) {
231
266
  return false;
232
267
  }
268
+ const currentTools = repoToolCallProgress(currentRepo);
269
+ const incomingTools = repoToolCallProgress(incomingRepo);
270
+ if (
271
+ incomingTools.total < currentTools.total ||
272
+ incomingTools.materialized < currentTools.materialized ||
273
+ incomingTools.completed < currentTools.completed ||
274
+ incomingTools.score < currentTools.score
275
+ ) {
276
+ return false;
277
+ }
233
278
  }
234
279
 
235
280
  return true;
@@ -29,7 +29,11 @@ import type { ActionChatUIConfig } from "../../action-ui.js";
29
29
  import type { AgentMcpAppPayload } from "../../mcp-client/app-result.js";
30
30
  import { AgentTaskCard } from "../AgentTaskCard.js";
31
31
  import { writeClipboardText } from "../clipboard.js";
32
- import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog.js";
32
+ import {
33
+ Popover,
34
+ PopoverContent,
35
+ PopoverTrigger,
36
+ } from "../components/ui/popover.js";
33
37
  import { ConnectBuilderCard } from "../ConnectBuilderCard.js";
34
38
  import { McpAppRenderer } from "../mcp-apps/McpAppRenderer.js";
35
39
  import type { ContentPart } from "../sse-event-processor.js";
@@ -306,16 +310,18 @@ function SimpleCodeViewer({
306
310
  );
307
311
  }
308
312
 
309
- function ToolOutputModal({
313
+ function ToolOutputPopover({
310
314
  open,
311
315
  onOpenChange,
312
316
  title,
313
317
  payload,
318
+ children,
314
319
  }: {
315
320
  open: boolean;
316
321
  onOpenChange: (open: boolean) => void;
317
322
  title: string;
318
323
  payload: ToolDetailPayload;
324
+ children: React.ReactNode;
319
325
  }) {
320
326
  const [copied, setCopied] = useState(false);
321
327
  const copyResetRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -339,12 +345,17 @@ function ToolOutputModal({
339
345
  }, [payload.copyText]);
340
346
 
341
347
  return (
342
- <Dialog open={open} onOpenChange={onOpenChange}>
343
- <DialogContent className="flex h-[min(80vh,720px)] w-[min(92vw,760px)] max-w-[760px] flex-col gap-0 overflow-hidden p-0">
344
- <div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-5 py-3 pr-12">
345
- <DialogTitle className="truncate text-sm font-medium">
346
- {title}
347
- </DialogTitle>
348
+ <Popover open={open} onOpenChange={onOpenChange}>
349
+ <PopoverTrigger asChild>{children}</PopoverTrigger>
350
+ <PopoverContent
351
+ align="start"
352
+ side="bottom"
353
+ sideOffset={6}
354
+ collisionPadding={12}
355
+ className="flex w-[min(calc(100vw-2rem),760px)] flex-col gap-0 overflow-hidden p-0"
356
+ >
357
+ <div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
358
+ <div className="truncate text-sm font-medium">{title}</div>
348
359
  <button
349
360
  type="button"
350
361
  onClick={copyValue}
@@ -354,15 +365,15 @@ function ToolOutputModal({
354
365
  {copied ? "Copied" : "Copy"}
355
366
  </button>
356
367
  </div>
357
- <div className="min-h-0 flex-1 overflow-auto p-4">
368
+ <div className="p-3">
358
369
  <SimpleCodeViewer
359
370
  text={payload.text}
360
371
  lang={payload.lang}
361
- maxHeightClass="max-h-none"
372
+ maxHeightClass="max-h-[75vh]"
362
373
  />
363
374
  </div>
364
- </DialogContent>
365
- </Dialog>
375
+ </PopoverContent>
376
+ </Popover>
366
377
  );
367
378
  }
368
379
 
@@ -800,25 +811,23 @@ function ToolCallDisplayGeneric({
800
811
  />
801
812
  )}
802
813
  {resultPayload && (
803
- <button
804
- type="button"
805
- onClick={() => setOutputOpen(true)}
806
- aria-label={`View ${toolName} output`}
807
- className="inline-flex size-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground"
814
+ <ToolOutputPopover
815
+ open={outputOpen}
816
+ onOpenChange={setOutputOpen}
817
+ title={outputTitle}
818
+ payload={resultPayload}
808
819
  >
809
- <IconCode className="size-3.5" />
810
- </button>
820
+ <button
821
+ type="button"
822
+ aria-label={`View ${toolName} output`}
823
+ className="inline-flex size-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground"
824
+ >
825
+ <IconCode className="size-3.5" />
826
+ </button>
827
+ </ToolOutputPopover>
811
828
  )}
812
829
  </div>
813
830
  </AnimatedCollapse>
814
- {resultPayload && (
815
- <ToolOutputModal
816
- open={outputOpen}
817
- onOpenChange={setOutputOpen}
818
- title={outputTitle}
819
- payload={resultPayload}
820
- />
821
- )}
822
831
  {approval && (
823
832
  <ApprovalAffordance toolName={toolName} approval={approval} />
824
833
  )}
@@ -28,11 +28,7 @@ function addCurrentModel(
28
28
  currentModel?: string,
29
29
  ): string[] {
30
30
  const next = [...models];
31
- if (
32
- engineName === currentEngineName &&
33
- currentModel &&
34
- !next.includes(currentModel)
35
- ) {
31
+ if (engineName === currentEngineName && currentModel && next.length === 0) {
36
32
  next.unshift(currentModel);
37
33
  }
38
34
  return next;
@@ -761,8 +761,6 @@ function ModeSelector({
761
761
  const FRIENDLY_MODEL_NAMES: Record<string, string> = {
762
762
  auto: "Default model",
763
763
  "claude-fable-5": "Fable 5",
764
- "grok-code-fast": "Grok Code Fast",
765
- "qwen3-coder": "Qwen3 Coder",
766
764
  "kimi-k2-5": "Kimi K2.5",
767
765
  "deepseek-v3-1": "DeepSeek v3.1",
768
766
  "z-ai/glm-5.2": "GLM 5.2",
@@ -335,6 +335,14 @@ function sameOrigin(input: RequestInfo | URL, win: Window): boolean {
335
335
  return !!url && !!origin && url.origin === origin;
336
336
  }
337
337
 
338
+ function isAgentNativeRuntimePath(pathname: string): boolean {
339
+ return (
340
+ pathname === "/_agent-native" ||
341
+ pathname.endsWith("/_agent-native") ||
342
+ pathname.includes("/_agent-native/")
343
+ );
344
+ }
345
+
338
346
  function requestMethod(input: RequestInfo | URL, init?: RequestInit): string {
339
347
  return (
340
348
  init?.method ??
@@ -444,6 +452,18 @@ function withEmbedAuthHeaders(
444
452
  token: string,
445
453
  win: Window,
446
454
  ): [RequestInfo | URL, RequestInit | undefined] {
455
+ const method = requestMethod(input, init);
456
+ const url = inputUrl(input, win);
457
+ if (
458
+ url &&
459
+ sameOrigin(input, win) &&
460
+ GUARDED_METHODS.has(method) &&
461
+ isAgentNativeRuntimePath(url.pathname)
462
+ ) {
463
+ url.searchParams.set(EMBED_TOKEN_QUERY_PARAM, token);
464
+ return [url.toString(), init];
465
+ }
466
+
447
467
  const headers = new Headers(
448
468
  init?.headers ?? (input instanceof Request ? input.headers : undefined),
449
469
  );
@@ -1,5 +1,13 @@
1
1
  import { IconChevronDown, IconCheck } from "@tabler/icons-react";
2
- import { createContext, useContext, type ReactNode } from "react";
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ type ReactNode,
9
+ type TransitionEvent,
10
+ } from "react";
3
11
 
4
12
  import { cn } from "../utils.js";
5
13
 
@@ -100,6 +108,58 @@ function StatusBadge({
100
108
  );
101
109
  }
102
110
 
111
+ function SettingsSectionBody({
112
+ open,
113
+ children,
114
+ }: {
115
+ open: boolean;
116
+ children: ReactNode;
117
+ }) {
118
+ const [present, setPresent] = useState(open);
119
+
120
+ useEffect(() => {
121
+ if (open) {
122
+ setPresent(true);
123
+ return;
124
+ }
125
+ if (!present) return;
126
+
127
+ // Fallback for reduced-motion and older transition engines that may not
128
+ // emit a height transitionend event.
129
+ const timeout = window.setTimeout(() => setPresent(false), 260);
130
+ return () => window.clearTimeout(timeout);
131
+ }, [open, present]);
132
+
133
+ const handleTransitionEnd = useCallback(
134
+ (event: TransitionEvent<HTMLDivElement>) => {
135
+ if (event.target !== event.currentTarget) return;
136
+ if (open) return;
137
+ if (
138
+ event.propertyName !== "height" &&
139
+ event.propertyName !== "max-height"
140
+ ) {
141
+ return;
142
+ }
143
+ setPresent(false);
144
+ },
145
+ [open],
146
+ );
147
+
148
+ if (!present) return null;
149
+
150
+ return (
151
+ <div
152
+ data-state={open ? "open" : "closed"}
153
+ aria-hidden={open ? undefined : true}
154
+ inert={open ? undefined : true}
155
+ className="agent-native-settings-section-body"
156
+ onTransitionEnd={handleTransitionEnd}
157
+ >
158
+ {children}
159
+ </div>
160
+ );
161
+ }
162
+
103
163
  function PageSettingsSection({
104
164
  id,
105
165
  icon,
@@ -156,7 +216,7 @@ function PageSettingsSection({
156
216
  )}
157
217
  />
158
218
  </button>
159
- {open && (
219
+ <SettingsSectionBody open={open}>
160
220
  <div
161
221
  // `data-agent-native-settings-page` lets the shared stylesheet nudge
162
222
  // the smallest fixed type (authored dense for the compact sidebar) up
@@ -167,7 +227,7 @@ function PageSettingsSection({
167
227
  >
168
228
  {children}
169
229
  </div>
170
- )}
230
+ </SettingsSectionBody>
171
231
  </div>
172
232
  );
173
233
  }
@@ -189,6 +249,7 @@ function SidebarSettingsSection({
189
249
  <button
190
250
  type="button"
191
251
  onClick={onToggle}
252
+ aria-expanded={open}
192
253
  className="flex w-full cursor-pointer items-center justify-between px-3 py-2.5 text-start rounded-lg hover:bg-accent/40 transition-colors"
193
254
  >
194
255
  <div className="flex items-center gap-2 min-w-0">
@@ -207,7 +268,7 @@ function SidebarSettingsSection({
207
268
  className={`shrink-0 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}`}
208
269
  />
209
270
  </button>
210
- {open && (
271
+ <SettingsSectionBody open={open}>
211
272
  <div className="border-t border-border px-3 pb-3 pt-2.5">
212
273
  {subtitle && (
213
274
  <p className="text-[10px] text-muted-foreground mb-2.5">
@@ -216,7 +277,7 @@ function SidebarSettingsSection({
216
277
  )}
217
278
  {children}
218
279
  </div>
219
- )}
280
+ </SettingsSectionBody>
220
281
  </div>
221
282
  );
222
283
  }
@@ -577,6 +577,7 @@ function isAutoRecoverableError(ev: SSEEvent, errMsg: string): boolean {
577
577
  if (
578
578
  code === "builder_gateway_network_error" ||
579
579
  code === "builder_gateway_timeout" ||
580
+ code === "provider_network_error" ||
580
581
  code === "stale_run" ||
581
582
  code === "timeout" ||
582
583
  code === "timeout_error" ||
@@ -962,7 +963,7 @@ export function processEvent(
962
963
  };
963
964
  }
964
965
 
965
- if (ev.type === "thinking") {
966
+ if (ev.type === "thinking" || ev.type === "reasoning") {
966
967
  // Model chain-of-thought. Coalesce consecutive deltas into one reasoning
967
968
  // part so the UI can render a single collapsible "Thinking" cell.
968
969
  const delta = ev.text ?? "";
@@ -1001,14 +1002,27 @@ export function processEvent(
1001
1002
 
1002
1003
  const pendingToolCallIndex = findPendingToolCallIndex(content, tool);
1003
1004
  if (pendingToolCallIndex === -1) {
1004
- content.push({
1005
- type: "tool-call",
1006
- toolCallId: `tc_${++toolCallCounter.value}`,
1007
- toolName: tool,
1008
- argsText: "",
1009
- args: {},
1010
- activity: true,
1011
- });
1005
+ // Only surface a placeholder spinner when this tool has no card yet. A
1006
+ // trailing activity heartbeat that arrives after the matching tool_done
1007
+ // (e.g. reordered reconnect replay) must NOT resurrect a spinner for an
1008
+ // already-completed call — that is the "pop back to an older state"
1009
+ // flicker. The real card reappears on the next tool_start regardless.
1010
+ const hasCompletedSameTool = content.some(
1011
+ (part) =>
1012
+ part.type === "tool-call" &&
1013
+ part.toolName === tool &&
1014
+ part.result !== undefined,
1015
+ );
1016
+ if (!hasCompletedSameTool) {
1017
+ content.push({
1018
+ type: "tool-call",
1019
+ toolCallId: `tc_${++toolCallCounter.value}`,
1020
+ toolName: tool,
1021
+ argsText: "",
1022
+ args: {},
1023
+ activity: true,
1024
+ });
1025
+ }
1012
1026
  }
1013
1027
  return {
1014
1028
  action: "yield",
@@ -1043,17 +1057,28 @@ export function processEvent(
1043
1057
  const pendingToolCallIndex = findPendingToolCallIndex(content, tool, ev.id);
1044
1058
  const pendingToolCall =
1045
1059
  pendingToolCallIndex >= 0 ? content[pendingToolCallIndex] : undefined;
1060
+ const pendingIsActivityPlaceholder =
1061
+ pendingToolCall?.type === "tool-call" &&
1062
+ pendingToolCall.activity === true &&
1063
+ pendingToolCall.argsText === "" &&
1064
+ Object.keys(pendingToolCall.args).length === 0;
1065
+ // A re-emitted start for the SAME id — a retry/auto-continue clear that
1066
+ // keeps the in-flight card mounted, or a reconnect replay — must update the
1067
+ // existing card in place instead of pushing a duplicate. Matching on id
1068
+ // keeps genuinely parallel same-name calls, which carry distinct ids,
1069
+ // separate.
1070
+ const pendingIsSameIdReplay =
1071
+ pendingToolCall?.type === "tool-call" &&
1072
+ ev.id !== undefined &&
1073
+ pendingToolCall.toolCallId === ev.id;
1046
1074
  if (
1047
1075
  pendingToolCall &&
1048
1076
  pendingToolCall.type === "tool-call" &&
1049
- pendingToolCall.activity === true &&
1050
- pendingToolCall.argsText === "" &&
1051
- Object.keys(pendingToolCall.args).length === 0
1077
+ (pendingIsActivityPlaceholder || pendingIsSameIdReplay)
1052
1078
  ) {
1053
- // Upgrade the pending activity card in place. Prefer the server-assigned
1054
- // id so the subsequent tool_done can match it precisely (parallel
1055
- // same-name calls each carry their own id). Fall back to the
1056
- // locally-generated id already on the card.
1079
+ // Upgrade the pending card in place. Prefer the server-assigned id so the
1080
+ // subsequent tool_done can match it precisely (parallel same-name calls
1081
+ // each carry their own id). Fall back to the locally-generated id.
1057
1082
  content[pendingToolCallIndex] = {
1058
1083
  type: "tool-call",
1059
1084
  toolCallId: ev.id ?? pendingToolCall.toolCallId,
@@ -1432,7 +1457,14 @@ function clearAssistantDraftContent(content: ContentPart[]): void {
1432
1457
  continue;
1433
1458
  }
1434
1459
  if (part.type === "tool-call" && part.result === undefined) {
1435
- content.splice(index, 1);
1460
+ // Only drop ephemeral placeholders. Materialized in-flight tool cards
1461
+ // (real args from tool_start) stay mounted so a retry/auto-continue clear
1462
+ // does not hide→show the same call when the next chunk re-emits it.
1463
+ const isEphemeral =
1464
+ part.activity === true ||
1465
+ part.argsText === "" ||
1466
+ Object.keys(part.args ?? {}).length === 0;
1467
+ if (isEphemeral) content.splice(index, 1);
1436
1468
  }
1437
1469
  }
1438
1470
  }
@@ -777,16 +777,38 @@ export function useDbSync(
777
777
  // Suppressed-action-only batches skip this whole list (their
778
778
  // mutations perform their own narrow invalidation) — but events must
779
779
  // STILL reach the onEvent forwarding below, so guard, don't return.
780
+ //
781
+ // Invalidation is scoped by source. Data-query prefixes (action,
782
+ // extension, tool) refetch only when the batch carries an event that
783
+ // can actually change action/extension-backed data — action
784
+ // mutations, settings, extension, collab, screen-refresh, etc.
785
+ // `app-state` events (agent/UI navigation, selection, and the
786
+ // set-url/questions command channel) drive their OWN keys below and
787
+ // must NEVER fan out into "refetch every action query": an active
788
+ // agent session mirrors navigation + selection into application_state
789
+ // continuously, and the serverless poll path replays those writes
790
+ // back to the originating tab (the DB-scan fallback cannot preserve
791
+ // `requestSource`, so `ignoreSource` can't filter them). Fanning each
792
+ // one into a full `["action"]` refetch turned a normal session into a
793
+ // client fetch storm that exhausted the DB connection pool — which in
794
+ // turn starved run heartbeat writes and surfaced as `stale_run`.
780
795
  if (!suppressesWholeBatch) {
781
- queryClient.invalidateQueries({ queryKey: ["action"] });
782
- queryClient.invalidateQueries({ queryKey: ["extension"] });
783
- queryClient.invalidateQueries({ queryKey: ["extensions"] });
784
- queryClient.invalidateQueries({ queryKey: ["extension-slots"] });
785
- queryClient.invalidateQueries({ queryKey: ["slot-installs"] });
786
- queryClient.invalidateQueries({ queryKey: ["slot-available"] });
787
- queryClient.invalidateQueries({ queryKey: ["tool"] });
788
- queryClient.invalidateQueries({ queryKey: ["tools"] });
789
- queryClient.invalidateQueries({ queryKey: ["app-state"] });
796
+ const hasDataChangingEvent = invalidating.some(
797
+ (evt) => evt.source !== "app-state",
798
+ );
799
+ if (hasDataChangingEvent) {
800
+ queryClient.invalidateQueries({ queryKey: ["action"] });
801
+ queryClient.invalidateQueries({ queryKey: ["extension"] });
802
+ queryClient.invalidateQueries({ queryKey: ["extensions"] });
803
+ queryClient.invalidateQueries({ queryKey: ["extension-slots"] });
804
+ queryClient.invalidateQueries({ queryKey: ["slot-installs"] });
805
+ queryClient.invalidateQueries({ queryKey: ["slot-available"] });
806
+ queryClient.invalidateQueries({ queryKey: ["tool"] });
807
+ queryClient.invalidateQueries({ queryKey: ["tools"] });
808
+ }
809
+ if (invalidating.some((evt) => evt.source === "app-state")) {
810
+ queryClient.invalidateQueries({ queryKey: ["app-state"] });
811
+ }
790
812
  if (hasAppStateEvent(invalidating, "navigate")) {
791
813
  queryClient.invalidateQueries({ queryKey: ["navigate-command"] });
792
814
  }
@@ -21,7 +21,9 @@ import { AGENT_CLIENT_ID, DEFAULT_AGENT_IDENTITY } from "./agent-identity.js";
21
21
  import { deleteAwarenessRow, upsertAwarenessRow } from "./awareness-store.js";
22
22
  import {
23
23
  emitAwarenessChange,
24
+ forgetAwarenessClear,
24
25
  getDocAwareness,
26
+ rememberAwarenessClear,
25
27
  type AwarenessEntry,
26
28
  } from "./awareness.js";
27
29
  import { appendRecentEdit, type RecentEdit } from "./recent-edits.js";
@@ -49,11 +51,13 @@ function cancelLinger(docId: string): void {
49
51
 
50
52
  function removeAgentPresence(docId: string): void {
51
53
  cancelLinger(docId);
54
+ const clearedAt = Date.now();
55
+ rememberAwarenessClear(docId, AGENT_CLIENT_ID, clearedAt);
52
56
  const map = getDocAwareness(docId);
53
57
  map.delete(AGENT_CLIENT_ID);
54
58
  emitAwarenessChange(docId, currentAwarenessStates(map));
55
59
  // Cross-instance removal (serverless) — best-effort, never blocks.
56
- void deleteAwarenessRow(docId, AGENT_CLIENT_ID);
60
+ void deleteAwarenessRow(docId, AGENT_CLIENT_ID, clearedAt);
57
61
 
58
62
  const interval = _heartbeats.get(docId);
59
63
  if (interval) {
@@ -133,6 +137,7 @@ function readAgentState(docId: string): Record<string, unknown> {
133
137
  }
134
138
 
135
139
  function writeAgentState(docId: string, state: Record<string, unknown>): void {
140
+ forgetAwarenessClear(docId, AGENT_CLIENT_ID);
136
141
  const entry: AwarenessEntry = {
137
142
  clientId: AGENT_CLIENT_ID,
138
143
  state: JSON.stringify(state),