@agent-native/core 0.102.0 → 0.102.2

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 (118) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +7 -0
  5. package/corpus/core/src/client/AssistantChat.tsx +7 -0
  6. package/corpus/core/src/client/agent-chat-adapter.ts +7 -1
  7. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +1 -1
  8. package/corpus/core/src/client/chat/message-components.tsx +17 -3
  9. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
  10. package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
  11. package/corpus/core/src/client/error-format.ts +24 -0
  12. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
  13. package/corpus/templates/analytics/changelog/2026-07-15-fixed-the-agent-discarding-a-successful-dashboard-edit-and-r.md +6 -0
  14. package/corpus/templates/analytics/server/lib/real-data-actions.ts +30 -1
  15. package/corpus/templates/analytics/server/plugins/agent-chat.ts +15 -0
  16. package/corpus/templates/assets/app/routes/library.tsx +69 -0
  17. package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
  18. package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
  19. package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
  20. package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
  21. package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
  22. package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
  23. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +22 -11
  24. package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
  25. package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
  26. package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
  27. package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
  28. package/corpus/templates/clips/app/components/player/settings-panel.tsx +86 -46
  29. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
  30. package/corpus/templates/clips/app/components/player/video-player.tsx +45 -1
  31. package/corpus/templates/clips/app/i18n/ar-SA.ts +2 -0
  32. package/corpus/templates/clips/app/i18n/de-DE.ts +2 -0
  33. package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
  34. package/corpus/templates/clips/app/i18n/es-ES.ts +2 -0
  35. package/corpus/templates/clips/app/i18n/fr-FR.ts +2 -0
  36. package/corpus/templates/clips/app/i18n/hi-IN.ts +2 -0
  37. package/corpus/templates/clips/app/i18n/ja-JP.ts +2 -0
  38. package/corpus/templates/clips/app/i18n/ko-KR.ts +2 -0
  39. package/corpus/templates/clips/app/i18n/pt-BR.ts +2 -0
  40. package/corpus/templates/clips/app/i18n/zh-CN.ts +2 -0
  41. package/corpus/templates/clips/app/i18n/zh-TW.ts +2 -0
  42. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
  43. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
  44. package/corpus/templates/clips/app/routes/_index.tsx +1 -2
  45. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +71 -31
  46. package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
  47. package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
  56. package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
  57. package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
  59. package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
  60. package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
  61. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
  62. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
  63. package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
  64. package/corpus/templates/content/app/components/editor/DescriptionField.tsx +10 -2
  65. package/corpus/templates/content/changelog/2026-07-15-page-and-database-descriptions-now-use-a-clean-single-line-f.md +6 -0
  66. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
  67. package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
  68. package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
  69. package/corpus/templates/slides/actions/import-file.ts +4 -1
  70. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
  71. package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
  72. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
  73. package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
  74. package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
  75. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  76. package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
  77. package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
  78. package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
  79. package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
  80. package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
  81. package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
  82. package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
  83. package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
  84. package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
  85. package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
  86. package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
  87. package/dist/client/AgentPanel.d.ts.map +1 -1
  88. package/dist/client/AgentPanel.js +5 -0
  89. package/dist/client/AgentPanel.js.map +1 -1
  90. package/dist/client/AssistantChat.d.ts.map +1 -1
  91. package/dist/client/AssistantChat.js +7 -0
  92. package/dist/client/AssistantChat.js.map +1 -1
  93. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  94. package/dist/client/agent-chat-adapter.js +8 -1
  95. package/dist/client/agent-chat-adapter.js.map +1 -1
  96. package/dist/client/agent-page/AgentTabsPage.js +1 -1
  97. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  98. package/dist/client/chat/message-components.d.ts +4 -0
  99. package/dist/client/chat/message-components.d.ts.map +1 -1
  100. package/dist/client/chat/message-components.js +10 -3
  101. package/dist/client/chat/message-components.js.map +1 -1
  102. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  103. package/dist/client/composer/ComposerPlusMenu.js +58 -6
  104. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  105. package/dist/client/composer/asset-picker-url.d.ts +16 -0
  106. package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
  107. package/dist/client/composer/asset-picker-url.js +64 -0
  108. package/dist/client/composer/asset-picker-url.js.map +1 -0
  109. package/dist/client/error-format.d.ts.map +1 -1
  110. package/dist/client/error-format.js +17 -0
  111. package/dist/client/error-format.js.map +1 -1
  112. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  113. package/dist/client/resources/McpIntegrationDialog.js +6 -1
  114. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  115. package/dist/collab/routes.d.ts +1 -1
  116. package/dist/observability/routes.d.ts +1 -1
  117. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  118. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2258
31
- - template files: 5962
30
+ - core files: 2259
31
+ - template files: 5982
@@ -1,5 +1,20 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.102.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 76a9cca: Improve MCP connection status and make provider connection errors actionable.
8
+ - 76a9cca: Keep queued chat follow-ups draining after fast turns and route cross-origin Assets pickers through a top-level sign-in-safe handoff.
9
+ - 76a9cca: Recover the Agent panel when a stale lazy-loaded chat chunk fails to load.
10
+ - 76a9cca: Keep the Builder source-change handoff card visible outside the collapsed chat work summary.
11
+
12
+ ## 0.102.1
13
+
14
+ ### Patch Changes
15
+
16
+ - ab33ae7: Make the Agent Files tab taller and viewport-responsive, and let file previews fill the panel height with natural scrolling.
17
+
3
18
  ## 0.102.0
4
19
 
5
20
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.102.0",
3
+ "version": "0.102.2",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -110,6 +110,7 @@ import type {
110
110
  MultiTabAssistantChatHeaderProps,
111
111
  MultiTabAssistantChatProps,
112
112
  } from "./MultiTabAssistantChat.js";
113
+ import { recoverFromStaleChunkError } from "./route-chunk-recovery.js";
113
114
  import { AgentNativeRouteWarmup } from "./route-warmup.js";
114
115
  import { withBuilderConnectTrackingParams } from "./settings/useBuilderStatus.js";
115
116
  import { useScreenRefreshKey } from "./use-db-sync.js";
@@ -2371,6 +2372,12 @@ class AgentPanelErrorBoundary extends React.Component<
2371
2372
  }
2372
2373
 
2373
2374
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
2375
+ if (recoverFromStaleChunkError(error)) {
2376
+ console.warn(
2377
+ "[agent-native] Recovering agent panel after stale chunk error",
2378
+ );
2379
+ return;
2380
+ }
2374
2381
  const recoverableKind = assistantUiRecoverableRenderErrorKind(error);
2375
2382
  if (recoverableKind) {
2376
2383
  console.warn(
@@ -2354,6 +2354,11 @@ const AssistantChatInner = forwardRef<
2354
2354
  } | null>(null);
2355
2355
  const [authSessionAvailable, setAuthSessionAvailable] = useState(false);
2356
2356
  const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
2357
+ // The dequeue guard briefly stays locked after appending a queued turn so
2358
+ // the adapter has time to claim the run. Wake the effect when that guard
2359
+ // expires; otherwise a fast-completing turn can leave the remaining queue
2360
+ // pending with no state transition left to trigger another dequeue.
2361
+ const [queueWakeVersion, setQueueWakeVersion] = useState(0);
2357
2362
  const queuedMessagesRef = useRef<QueuedMessage[]>([]);
2358
2363
  const queueDirtyRef = useRef(false);
2359
2364
  const queueMutationVersionRef = useRef(0);
@@ -4041,6 +4046,7 @@ const AssistantChatInner = forwardRef<
4041
4046
  if (appended) {
4042
4047
  window.setTimeout(() => {
4043
4048
  dequeueInFlightRef.current = false;
4049
+ setQueueWakeVersion((version) => version + 1);
4044
4050
  }, 500);
4045
4051
  } else {
4046
4052
  dequeueInFlightRef.current = false;
@@ -4062,6 +4068,7 @@ const AssistantChatInner = forwardRef<
4062
4068
  applyLocalQueuedMessages,
4063
4069
  isRestoring,
4064
4070
  isRunning,
4071
+ queueWakeVersion,
4065
4072
  queuedMessages,
4066
4073
  threadId,
4067
4074
  ]);
@@ -3392,7 +3392,13 @@ export function createAgentChatAdapter(
3392
3392
  text: formatChatErrorText(
3393
3393
  message,
3394
3394
  preservedError?.upgradeUrl,
3395
- errorCode,
3395
+ // `message` is already user-facing here: either a
3396
+ // recovery-specific explanation from exhaustedRecoveryMessage
3397
+ // or a normalized gateway message from errorInfo. Passing
3398
+ // the fallback connection_error code back through the
3399
+ // formatter would replace the useful recovery guidance
3400
+ // with the generic interruption copy.
3401
+ preservedError ? errorCode : undefined,
3396
3402
  ),
3397
3403
  });
3398
3404
  yield {
@@ -152,7 +152,7 @@ function EmptySlot({ label }: { label: string }) {
152
152
 
153
153
  function FilesTab({ scope }: AgentPageTabProps) {
154
154
  return (
155
- <div className="h-full min-h-[360px]">
155
+ <div className="h-[calc(100vh-14rem)] min-h-[480px]">
156
156
  <ResourcesPanel
157
157
  key={scope}
158
158
  showMcpServers={false}
@@ -909,10 +909,24 @@ function ReasoningMessagePart() {
909
909
  );
910
910
  }
911
911
 
912
+ const ALWAYS_VISIBLE_ASSISTANT_TOOLS = new Set(["connect-builder"]);
913
+
914
+ export function isCollapsibleAssistantWorkPart(part: {
915
+ type?: string;
916
+ toolName?: string;
917
+ }): boolean {
918
+ if (part.type === "reasoning") return true;
919
+ return (
920
+ part.type === "tool-call" &&
921
+ !ALWAYS_VISIBLE_ASSISTANT_TOOLS.has(part.toolName ?? "")
922
+ );
923
+ }
924
+
912
925
  function groupAssistantWorkParts(part: {
913
926
  type?: string;
927
+ toolName?: string;
914
928
  }): ["group-work"] | null {
915
- if (part.type === "reasoning" || part.type === "tool-call") {
929
+ if (isCollapsibleAssistantWorkPart(part)) {
916
930
  return ["group-work"];
917
931
  }
918
932
  return null;
@@ -1044,8 +1058,8 @@ export function AssistantMessage() {
1044
1058
  Array.isArray(msgContent) &&
1045
1059
  msgContent.some(
1046
1060
  (p) =>
1047
- p.type === "reasoning" ||
1048
- (p.type === "tool-call" && p.activity !== true),
1061
+ (p.type !== "tool-call" || p.activity !== true) &&
1062
+ isCollapsibleAssistantWorkPart(p),
1049
1063
  );
1050
1064
 
1051
1065
  if (!hasRenderableContent) return null;
@@ -42,6 +42,11 @@ import {
42
42
  type McpServerScope,
43
43
  } from "../resources/use-mcp-servers.js";
44
44
  import { cn } from "../utils.js";
45
+ import {
46
+ createAssetPickerHandoffId,
47
+ isExternalAssetPickerUrl,
48
+ standaloneAssetPickerUrl,
49
+ } from "./asset-picker-url.js";
45
50
  import type { ComposerMode } from "./types.js";
46
51
 
47
52
  interface ComposerPlusMenuProps {
@@ -73,6 +78,7 @@ interface EmbedEnvelope<TPayload = unknown> {
73
78
 
74
79
  interface AssetPickerPayload {
75
80
  assetId?: unknown;
81
+ handoffId?: unknown;
76
82
  url?: unknown;
77
83
  previewUrl?: unknown;
78
84
  downloadUrl?: unknown;
@@ -742,8 +748,31 @@ function AssetsPickerModal({
742
748
  onOpenChange: (open: boolean) => void;
743
749
  }) {
744
750
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
751
+ const standaloneWindowRef = useRef<Window | null>(null);
745
752
  const [pickerReady, setPickerReady] = useState(false);
753
+ const [standaloneHandoffId, setStandaloneHandoffId] = useState<string | null>(
754
+ null,
755
+ );
746
756
  const sourceUrl = useMemo(() => assetPickerUrl(), []);
757
+ const externalPicker = useMemo(
758
+ () =>
759
+ typeof window !== "undefined" &&
760
+ isExternalAssetPickerUrl(sourceUrl, window.location.origin),
761
+ [sourceUrl],
762
+ );
763
+ const standaloneUrl = useMemo(
764
+ () =>
765
+ standaloneAssetPickerUrl(
766
+ sourceUrl,
767
+ typeof window !== "undefined" ? window.location.href : undefined,
768
+ {
769
+ handoffId: standaloneHandoffId ?? undefined,
770
+ returnOrigin:
771
+ typeof window !== "undefined" ? window.location.origin : undefined,
772
+ },
773
+ ),
774
+ [sourceUrl, standaloneHandoffId],
775
+ );
747
776
  const iframeUrl = useMemo(() => withEmbeddedParams(sourceUrl), [sourceUrl]);
748
777
  const targetOrigin = useMemo(() => assetPickerOrigin(iframeUrl), [iframeUrl]);
749
778
  const configurePicker = useCallback(() => {
@@ -758,14 +787,32 @@ function AssetsPickerModal({
758
787
  }, [targetOrigin]);
759
788
 
760
789
  useEffect(() => {
761
- if (open) setPickerReady(false);
762
- }, [iframeUrl, open]);
790
+ if (open) {
791
+ setPickerReady(false);
792
+ if (externalPicker) {
793
+ standaloneWindowRef.current = null;
794
+ setStandaloneHandoffId(createAssetPickerHandoffId());
795
+ } else {
796
+ setStandaloneHandoffId(null);
797
+ }
798
+ return;
799
+ }
800
+ if (!standaloneWindowRef.current) setStandaloneHandoffId(null);
801
+ }, [externalPicker, iframeUrl, open]);
763
802
 
764
803
  useEffect(() => {
765
- if (!open || !targetOrigin) return;
804
+ if (
805
+ !targetOrigin ||
806
+ (!open && !standaloneWindowRef.current) ||
807
+ (externalPicker && !standaloneHandoffId)
808
+ )
809
+ return;
766
810
 
767
811
  const handleMessage = (event: MessageEvent) => {
768
- if (event.source !== iframeRef.current?.contentWindow) return;
812
+ const expectedSource = externalPicker
813
+ ? standaloneWindowRef.current
814
+ : iframeRef.current?.contentWindow;
815
+ if (!expectedSource || event.source !== expectedSource) return;
769
816
  if (event.origin !== targetOrigin) return;
770
817
  if (!isEmbedEnvelope(event.data)) return;
771
818
 
@@ -776,6 +823,14 @@ function AssetsPickerModal({
776
823
  }
777
824
 
778
825
  if (event.data.type !== "message") return;
826
+ if (externalPicker) {
827
+ const payload = event.data.payload;
828
+ const handoffId =
829
+ payload && typeof payload === "object"
830
+ ? assetString((payload as AssetPickerPayload).handoffId)
831
+ : null;
832
+ if (handoffId !== standaloneHandoffId) return;
833
+ }
779
834
  if (event.data.name === "close") {
780
835
  onOpenChange(false);
781
836
  return;
@@ -798,6 +853,8 @@ function AssetsPickerModal({
798
853
  title: `Image: ${title}`,
799
854
  context: assetContext(event.data.payload, url),
800
855
  });
856
+ standaloneWindowRef.current = null;
857
+ setStandaloneHandoffId(null);
801
858
  onOpenChange(false);
802
859
  };
803
860
 
@@ -811,7 +868,26 @@ function AssetsPickerModal({
811
868
  window.removeEventListener("message", handleMessage);
812
869
  window.removeEventListener("keydown", handleKey);
813
870
  };
814
- }, [configurePicker, onOpenChange, open, targetOrigin]);
871
+ }, [
872
+ configurePicker,
873
+ externalPicker,
874
+ onOpenChange,
875
+ open,
876
+ standaloneHandoffId,
877
+ targetOrigin,
878
+ ]);
879
+
880
+ const openStandalonePicker = useCallback(
881
+ (event: React.MouseEvent<HTMLAnchorElement>) => {
882
+ if (!standaloneHandoffId) return;
883
+ event.preventDefault();
884
+ const pickerWindow = window.open(standaloneUrl, "_blank");
885
+ if (!pickerWindow) return;
886
+ standaloneWindowRef.current = pickerWindow;
887
+ onOpenChange(false);
888
+ },
889
+ [onOpenChange, standaloneHandoffId, standaloneUrl],
890
+ );
815
891
 
816
892
  if (!open || typeof document === "undefined") return null;
817
893
 
@@ -842,7 +918,21 @@ function AssetsPickerModal({
842
918
  <IconX className="h-4 w-4" />
843
919
  </button>
844
920
  </div>
845
- {targetOrigin ? (
921
+ {externalPicker ? (
922
+ <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-8 text-center">
923
+ <div className="max-w-md text-sm text-muted-foreground">
924
+ Open Assets in a new tab to sign in and choose an image securely.
925
+ </div>
926
+ <a
927
+ href={standaloneUrl}
928
+ target="_blank"
929
+ onClick={openStandalonePicker}
930
+ className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90"
931
+ >
932
+ Open Assets image picker
933
+ </a>
934
+ </div>
935
+ ) : targetOrigin ? (
846
936
  <div className="relative min-h-0 flex-1 overflow-hidden bg-background">
847
937
  {!pickerReady && <AssetsPickerLoadingSkeleton />}
848
938
  <iframe
@@ -0,0 +1,80 @@
1
+ const FALLBACK_BASE_URL = "http://agent-native.invalid";
2
+
3
+ export const ASSET_PICKER_HANDOFF_PARAM = "__an_asset_picker_handoff";
4
+ export const ASSET_PICKER_RETURN_ORIGIN_PARAM =
5
+ "__an_asset_picker_return_origin";
6
+
7
+ export interface StandaloneAssetPickerOptions {
8
+ handoffId?: string;
9
+ returnOrigin?: string;
10
+ }
11
+
12
+ export function createAssetPickerHandoffId(): string {
13
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
14
+ return crypto.randomUUID();
15
+ }
16
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
17
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
18
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
19
+ "",
20
+ );
21
+ }
22
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
23
+ }
24
+
25
+ /**
26
+ * Cross-origin Assets pages cannot inherit the host app's authenticated
27
+ * session inside an iframe. Treat those URLs as link-out targets so provider
28
+ * sign-in runs in a normal top-level browser context.
29
+ */
30
+ export function isExternalAssetPickerUrl(
31
+ value: string,
32
+ currentOrigin: string,
33
+ ): boolean {
34
+ try {
35
+ return new URL(value, currentOrigin).origin !== currentOrigin;
36
+ } catch {
37
+ // A malformed configured URL should fail closed instead of being loaded in
38
+ // an iframe with an unknown auth boundary.
39
+ return true;
40
+ }
41
+ }
42
+
43
+ /** Build a top-level picker URL without iframe-only auth flags. */
44
+ export function standaloneAssetPickerUrl(
45
+ value: string,
46
+ baseUrl = FALLBACK_BASE_URL,
47
+ options: StandaloneAssetPickerOptions = {},
48
+ ): string {
49
+ try {
50
+ const parsed = new URL(value, baseUrl);
51
+ parsed.searchParams.delete("embedded");
52
+ parsed.searchParams.delete("__an_embed_token");
53
+ parsed.searchParams.set("mediaType", "image");
54
+ let returnOrigin: string | null = null;
55
+ if (options.returnOrigin) {
56
+ try {
57
+ const parsedOrigin = new URL(options.returnOrigin);
58
+ if (
59
+ parsedOrigin.protocol === "http:" ||
60
+ parsedOrigin.protocol === "https:"
61
+ ) {
62
+ returnOrigin = parsedOrigin.origin;
63
+ }
64
+ } catch {
65
+ // Omit an invalid return target rather than emitting an unverified
66
+ // cross-origin callback URL.
67
+ }
68
+ }
69
+ if (options.handoffId && returnOrigin) {
70
+ parsed.searchParams.set(ASSET_PICKER_HANDOFF_PARAM, options.handoffId);
71
+ parsed.searchParams.set(ASSET_PICKER_RETURN_ORIGIN_PARAM, returnOrigin);
72
+ } else {
73
+ parsed.searchParams.delete(ASSET_PICKER_HANDOFF_PARAM);
74
+ parsed.searchParams.delete(ASSET_PICKER_RETURN_ORIGIN_PARAM);
75
+ }
76
+ return parsed.toString();
77
+ } catch {
78
+ return value;
79
+ }
80
+ }
@@ -102,6 +102,18 @@ function isProviderAuthenticationError(
102
102
  );
103
103
  }
104
104
 
105
+ function isConnectionError(text: string, errorCode?: string): boolean {
106
+ const code = normalizeErrorCode(errorCode);
107
+ return (
108
+ code === "provider_network_error" ||
109
+ code === "connection_error" ||
110
+ code === "network_error" ||
111
+ /^(?:provider_network_error|connection_error|network_error)$/i.test(
112
+ text.trim(),
113
+ )
114
+ );
115
+ }
116
+
105
117
  export function normalizeChatError(
106
118
  errorMessage: string,
107
119
  errorCode?: string,
@@ -126,6 +138,18 @@ export function normalizeChatError(
126
138
  };
127
139
  }
128
140
 
141
+ if (isConnectionError(text, errorCode)) {
142
+ const providerNetworkError =
143
+ normalizeErrorCode(errorCode) === "provider_network_error" ||
144
+ /provider_network_error/i.test(text);
145
+ return {
146
+ message: providerNetworkError
147
+ ? "The model provider could not be reached. Check your connection and retry."
148
+ : "The agent connection was interrupted. Check your connection and retry.",
149
+ details: text,
150
+ };
151
+ }
152
+
129
153
  if (/^Gateway error \(no detail; raw event:/i.test(text)) {
130
154
  // The previous copy promised auto-recovery and suggested switching models,
131
155
  // but neither helps for this code: the server already retried once and
@@ -115,7 +115,14 @@ export function McpIntegrationDialog({
115
115
  ...(mcpServersQuery.data?.user ?? []),
116
116
  ...(mcpServersQuery.data?.org ?? []),
117
117
  ];
118
- return new Set(servers.map((server) => compareUrl(server.url)));
118
+ // A saved server is not necessarily a working connection. The settings
119
+ // page reports failed and unknown health states separately, so only mark
120
+ // catalog entries as connected after the health probe succeeds.
121
+ return new Set(
122
+ servers
123
+ .filter((server) => server.status.state === "connected")
124
+ .map((server) => compareUrl(server.url)),
125
+ );
119
126
  }, [mcpServersQuery.data]);
120
127
 
121
128
  const filteredIntegrations = useMemo(
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Fixed the agent discarding a successful dashboard edit and replacing it with an unrelated data-source message
@@ -76,6 +76,20 @@ export const DASHBOARD_CONSTRUCTION_ACTIONS = new Set([
76
76
  "get-extension",
77
77
  ]);
78
78
 
79
+ // Dashboard authoring/save actions. Unlike the read actions above, running one
80
+ // of these is proof the turn actually edited or built a dashboard/extension —
81
+ // which is a legitimate non-query completion. A saved SQL panel the user runs
82
+ // themselves is not a fabricated metric, so the guard only needs to keep
83
+ // blocking drafts that state invented numbers (draftClaimsAnalyticsMetrics).
84
+ export const DASHBOARD_MUTATION_ACTIONS = new Set([
85
+ "mutate-dashboard",
86
+ "update-dashboard",
87
+ "compose-dashboard",
88
+ "install-dashboard-template",
89
+ "create-extension",
90
+ "update-extension",
91
+ ]);
92
+
79
93
  const RUN_CODE_BRIDGE_TOOLS_USED = /^bridgeToolsUsed:\s*(.+)$/im;
80
94
 
81
95
  const MCP_DATA_SOURCE_TOKENS = [
@@ -120,11 +134,15 @@ function isDashboardConstructionActionName(name: string): boolean {
120
134
  return DASHBOARD_CONSTRUCTION_ACTIONS.has(normalizeActionToolName(name));
121
135
  }
122
136
 
137
+ function isDashboardMutationActionName(name: string): boolean {
138
+ return DASHBOARD_MUTATION_ACTIONS.has(normalizeActionToolName(name));
139
+ }
140
+
123
141
  // "Build/clone/template" language targeting a dashboard/extension/panel is
124
142
  // dashboard construction, distinct from an analytics-result question. Turns
125
143
  // like this may inspect and clone a template without running a metric query.
126
144
  const DASHBOARD_CONSTRUCTION_INTENT_TERMS =
127
- /\b(build|create|make|clone|copy|duplicate|adapt|template|based (?:off|on)|using .{1,80}? as a template)\b/i;
145
+ /\b(build|create|make|clone|copy|duplicate|adapt|update|edit|change|modify|rename|adjust|simplify|switch|template|based (?:off|on)|using .{1,80}? as a template)\b/i;
128
146
 
129
147
  const DASHBOARD_CONSTRUCTION_TARGET_TERMS =
130
148
  /\b(dashboard|extension|panel|widget)\b/i;
@@ -151,6 +169,17 @@ export function hasDashboardConstructionAttempt(
151
169
  });
152
170
  }
153
171
 
172
+ export function hasDashboardMutationAttempt(
173
+ toolResults:
174
+ | Array<{ name?: string; isError?: boolean; content?: string }>
175
+ | undefined,
176
+ ): boolean {
177
+ return (toolResults ?? []).some((result) => {
178
+ if (result.isError) return false;
179
+ return isDashboardMutationActionName(String(result.name ?? ""));
180
+ });
181
+ }
182
+
154
183
  function isCorpusSourceActionName(name: string): boolean {
155
184
  return CORPUS_SOURCE_ACTIONS.has(normalizeActionToolName(name));
156
185
  }
@@ -17,6 +17,7 @@ import {
17
17
  draftClaimsAnalyticsMetrics,
18
18
  failedDataQueryAttemptMessage,
19
19
  hasDashboardConstructionAttempt,
20
+ hasDashboardMutationAttempt,
20
21
  hasExplicitPartialDisclosure,
21
22
  hasFailedCorpusWorkflowEvidence,
22
23
  hasDataQueryAttempt,
@@ -525,6 +526,20 @@ export function realDataFinalGuard(
525
526
  "I can't make a confident exhaustive analytics claim yet because part of the source evidence was aborted, truncated, or still paginated. I need to recover the missing coverage or state the answer as partial with the inspected sample size.",
526
527
  };
527
528
  }
529
+ // Dashboard EDIT turns: the user asked to change an existing dashboard and
530
+ // the agent actually saved a mutation (mutate-dashboard/update-dashboard/
531
+ // etc.). This is a legitimate non-query completion regardless of how the
532
+ // request was phrased ("update the panels" does not match the construction
533
+ // intent regex), so it must not be steered into a data-source query. Anchor
534
+ // on tool evidence, not user wording, and still block any draft that states
535
+ // invented numbers via draftClaimsAnalyticsMetrics. A saved SQL panel the
536
+ // user runs themselves is not a fabricated metric.
537
+ if (
538
+ hasDashboardMutationAttempt(context.toolResults) &&
539
+ !draftClaimsAnalyticsMetrics(context.text)
540
+ ) {
541
+ return null;
542
+ }
528
543
  // Dashboard construction/template-clone turns may inspect and clone an
529
544
  // existing dashboard/extension without running a metric query, as long as
530
545
  // the draft does not invent numbers. Check this before the generic
@@ -16,6 +16,8 @@ import {
16
16
  writeClientAppState,
17
17
  } from "@agent-native/core/client";
18
18
  import {
19
+ AGENT_NATIVE_EMBED_MESSAGE_TYPES,
20
+ createAgentNativeEmbedEnvelope,
19
21
  createEmbeddedAppBridge,
20
22
  type EmbeddedAppBridge,
21
23
  } from "@agent-native/core/embedding";
@@ -244,6 +246,31 @@ function isEmbeddedWindow() {
244
246
  }
245
247
  }
246
248
 
249
+ interface StandalonePickerHandoff {
250
+ handoffId: string;
251
+ returnOrigin: string;
252
+ }
253
+
254
+ function standalonePickerHandoff(): StandalonePickerHandoff | null {
255
+ if (typeof window === "undefined") return null;
256
+ const params = new URLSearchParams(window.location.search);
257
+ const handoffId = params.get("__an_asset_picker_handoff")?.trim();
258
+ const rawReturnOrigin = params.get("__an_asset_picker_return_origin")?.trim();
259
+ if (!handoffId || handoffId.length > 128 || !rawReturnOrigin) return null;
260
+ try {
261
+ const parsed = new URL(rawReturnOrigin);
262
+ if (
263
+ (parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
264
+ parsed.origin !== rawReturnOrigin
265
+ ) {
266
+ return null;
267
+ }
268
+ return { handoffId, returnOrigin: parsed.origin };
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+
247
274
  function normalizeCount(value: unknown): number {
248
275
  if (value === null || value === undefined || value === "") return 3;
249
276
  const count = Number(value);
@@ -2206,6 +2233,10 @@ export function AssetPickerSurface() {
2206
2233
  isEmbedAuthActive(),
2207
2234
  [searchParams],
2208
2235
  );
2236
+ const standaloneHandoff = useMemo(
2237
+ () => (embedded ? null : standalonePickerHandoff()),
2238
+ [embedded],
2239
+ );
2209
2240
  const pickerVariantScopeId = useMemo(
2210
2241
  () =>
2211
2242
  typeof window === "undefined" ? null : `picker:${getBrowserTabId()}`,
@@ -2552,6 +2583,35 @@ export function AssetPickerSurface() {
2552
2583
  [],
2553
2584
  );
2554
2585
 
2586
+ const postStandaloneSelectionMessage = useCallback(
2587
+ (payload: ReturnType<typeof assetPayload>) => {
2588
+ if (
2589
+ !standaloneHandoff ||
2590
+ typeof window === "undefined" ||
2591
+ !window.opener ||
2592
+ window.opener.closed
2593
+ ) {
2594
+ return false;
2595
+ }
2596
+ try {
2597
+ window.opener.postMessage(
2598
+ createAgentNativeEmbedEnvelope(
2599
+ AGENT_NATIVE_EMBED_MESSAGE_TYPES.MESSAGE,
2600
+ {
2601
+ name: "chooseAsset",
2602
+ payload: { ...payload, handoffId: standaloneHandoff.handoffId },
2603
+ },
2604
+ ),
2605
+ standaloneHandoff.returnOrigin,
2606
+ );
2607
+ return true;
2608
+ } catch {
2609
+ return false;
2610
+ }
2611
+ },
2612
+ [standaloneHandoff],
2613
+ );
2614
+
2555
2615
  const chooseAsset = (asset: Asset) => {
2556
2616
  const payload = assetPayload(asset, mediaType);
2557
2617
  if (embedded) {
@@ -2574,6 +2634,15 @@ export function AssetPickerSurface() {
2574
2634
  });
2575
2635
  return;
2576
2636
  }
2637
+ if (postStandaloneSelectionMessage(payload)) {
2638
+ toast.success(
2639
+ t("assetPicker.selectedAsset", {
2640
+ title: selectedAssetLabel(payload),
2641
+ }),
2642
+ );
2643
+ window.setTimeout(() => window.close(), 0);
2644
+ return;
2645
+ }
2577
2646
  setStandaloneSelection(payload);
2578
2647
  setStandaloneCopyOk(false);
2579
2648
  void copyStandaloneSelection(payload);
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Fixed top-level image picker selections returning to the composer