@agent-native/core 0.102.1 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +9 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +7 -0
- package/corpus/core/src/client/AssistantChat.tsx +7 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +7 -1
- package/corpus/core/src/client/chat/message-components.tsx +17 -3
- package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
- package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
- package/corpus/core/src/client/error-format.ts +24 -0
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
- package/corpus/templates/assets/app/routes/library.tsx +69 -0
- package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
- package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
- package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
- package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
- package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
- package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +22 -11
- package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
- package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
- package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
- package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
- package/corpus/templates/clips/app/components/player/settings-panel.tsx +86 -46
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
- package/corpus/templates/clips/app/components/player/video-player.tsx +45 -1
- package/corpus/templates/clips/app/i18n/ar-SA.ts +2 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +2 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +2 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +2 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +2 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +2 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +2 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +2 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +2 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
- package/corpus/templates/clips/app/routes/_index.tsx +1 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +71 -31
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
- package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
- package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
- package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
- package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
- package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
- package/corpus/templates/slides/actions/import-file.ts +4 -1
- package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
- package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -0
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +7 -0
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +8 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +4 -0
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +10 -3
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.js +58 -6
- package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/client/composer/asset-picker-url.d.ts +16 -0
- package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
- package/dist/client/composer/asset-picker-url.js +64 -0
- package/dist/client/composer/asset-picker-url.js.map +1 -0
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +17 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +6 -1
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
3
12
|
## 0.102.1
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.102.
|
|
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
|
-
|
|
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 {
|
|
@@ -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
|
|
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
|
|
1048
|
-
(p
|
|
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)
|
|
762
|
-
|
|
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 (
|
|
804
|
+
if (
|
|
805
|
+
!targetOrigin ||
|
|
806
|
+
(!open && !standaloneWindowRef.current) ||
|
|
807
|
+
(externalPicker && !standaloneHandoffId)
|
|
808
|
+
)
|
|
809
|
+
return;
|
|
766
810
|
|
|
767
811
|
const handleMessage = (event: MessageEvent) => {
|
|
768
|
-
|
|
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
|
-
}, [
|
|
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
|
-
{
|
|
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
|
-
|
|
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(
|
|
@@ -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);
|
|
@@ -13,10 +13,11 @@ import { writeAppState } from "@agent-native/core/application-state";
|
|
|
13
13
|
import { deleteAppSecret, readAppSecret } from "@agent-native/core/secrets";
|
|
14
14
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
15
15
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
16
|
-
import { eq } from "drizzle-orm";
|
|
16
|
+
import { and, eq, inArray, isNull, or } from "drizzle-orm";
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
|
|
19
19
|
import { getDb, schema } from "../server/db/index.js";
|
|
20
|
+
import { lockCalendarAccount } from "../server/lib/calendar-event-meetings.js";
|
|
20
21
|
import { revokeToken } from "../server/lib/google-calendar-client.js";
|
|
21
22
|
|
|
22
23
|
export default defineAction({
|
|
@@ -90,15 +91,62 @@ export default defineAction({
|
|
|
90
91
|
}).catch(() => {});
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.
|
|
96
|
-
|
|
94
|
+
await db.transaction(async (tx) => {
|
|
95
|
+
// Materialization takes this same account-row write lock before it
|
|
96
|
+
// snapshots/claims an event and inserts a meeting. Taking it before
|
|
97
|
+
// this cleanup snapshot prevents a new unrecorded meeting from being
|
|
98
|
+
// inserted after the rows below have been inspected.
|
|
99
|
+
if (!(await lockCalendarAccount(tx, args.id, account.ownerEmail))) {
|
|
100
|
+
throw new Error(`Calendar account not found: ${args.id}`);
|
|
101
|
+
}
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
|
|
103
|
+
// Preserve recorded meetings, but hide unrecorded calendar placeholders
|
|
104
|
+
// tied to this account. Without this cleanup, those materialized rows
|
|
105
|
+
// stay visible after the account and its event cache are removed.
|
|
106
|
+
const syncedEvents = await tx
|
|
107
|
+
.select({
|
|
108
|
+
id: schema.calendarEvents.id,
|
|
109
|
+
meetingId: schema.calendarEvents.meetingId,
|
|
110
|
+
})
|
|
111
|
+
.from(schema.calendarEvents)
|
|
112
|
+
.where(eq(schema.calendarEvents.calendarAccountId, args.id));
|
|
113
|
+
const syncedCalendarEventIds = syncedEvents.map((event) => event.id);
|
|
114
|
+
const syncedMeetingIds = syncedEvents
|
|
115
|
+
.map((event) => event.meetingId)
|
|
116
|
+
.filter((meetingId): meetingId is string => Boolean(meetingId));
|
|
117
|
+
if (syncedCalendarEventIds.length > 0 || syncedMeetingIds.length > 0) {
|
|
118
|
+
await tx
|
|
119
|
+
.update(schema.meetings)
|
|
120
|
+
.set({ trashedAt: new Date().toISOString() })
|
|
121
|
+
.where(
|
|
122
|
+
and(
|
|
123
|
+
or(
|
|
124
|
+
syncedMeetingIds.length > 0
|
|
125
|
+
? inArray(schema.meetings.id, syncedMeetingIds)
|
|
126
|
+
: undefined,
|
|
127
|
+
syncedCalendarEventIds.length > 0
|
|
128
|
+
? inArray(
|
|
129
|
+
schema.meetings.calendarEventId,
|
|
130
|
+
syncedCalendarEventIds,
|
|
131
|
+
)
|
|
132
|
+
: undefined,
|
|
133
|
+
)!,
|
|
134
|
+
isNull(schema.meetings.recordingId),
|
|
135
|
+
isNull(schema.meetings.trashedAt),
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Drop the synced events for this account so the meetings tab clears.
|
|
141
|
+
await tx
|
|
142
|
+
.delete(schema.calendarEvents)
|
|
143
|
+
.where(eq(schema.calendarEvents.calendarAccountId, args.id));
|
|
144
|
+
|
|
145
|
+
// Drop the account row itself.
|
|
146
|
+
await tx
|
|
147
|
+
.delete(schema.calendarAccounts)
|
|
148
|
+
.where(eq(schema.calendarAccounts.id, args.id));
|
|
149
|
+
});
|
|
102
150
|
|
|
103
151
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
104
152
|
return { id: args.id, disconnected: true };
|