@agent-native/core 0.149.0 → 0.149.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 +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
- package/corpus/templates/analytics/app/global.css +1 -1
- package/corpus/templates/analytics/app/i18n-data.ts +37 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
- package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/assets/app/global.css +1 -1
- package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/brain/app/global.css +1 -1
- package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +274 -137
- package/corpus/templates/clips/desktop/src/styles.css +35 -0
- package/corpus/templates/content/app/global.css +1 -0
- package/corpus/templates/crm/app/global.css +1 -1
- package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -0
- package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
- package/corpus/templates/slides/AGENTS.md +11 -10
- package/corpus/templates/slides/actions/add-deck.ts +2 -0
- package/corpus/templates/slides/actions/add-slide.ts +15 -1
- package/corpus/templates/slides/actions/create-deck.ts +18 -6
- package/corpus/templates/slides/actions/get-deck.ts +29 -1
- package/corpus/templates/slides/actions/patch-deck.ts +139 -10
- package/corpus/templates/slides/actions/save-deck.ts +24 -1
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
- package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +10 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
- package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
- package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
- package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
- package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
- package/corpus/templates/slides/app/pages/Index.tsx +256 -98
- package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
- package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
- package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
- package/corpus/templates/slides/shared/api.ts +2 -1
- package/corpus/templates/slides/shared/deck-title.ts +135 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
- package/dist/agent/engine/ai-sdk-engine.js +1 -1
- package/dist/agent/production-agent.d.ts +21 -1
- package/dist/agent/production-agent.js +129 -10
- package/dist/agent/tool-approval-store.d.ts +18 -0
- package/dist/agent/tool-approval-store.js +122 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/cli/code-agent-executor.js +11 -2
- package/dist/client/AgentPanel.js +31 -6
- package/dist/client/AssistantChat.d.ts +3 -2
- package/dist/client/AssistantChat.js +7 -20
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat-adapter.js +11 -0
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/chat/markdown-renderer.js +14 -2
- package/dist/client/chat/message-components.d.ts +6 -2
- package/dist/client/chat/message-components.js +25 -47
- package/dist/client/chat/tool-call-display.d.ts +3 -5
- package/dist/client/chat/tool-call-display.js +7 -212
- package/dist/client/chat-first/apps-rail.js +3 -3
- package/dist/client/chat-first/browser-pane.js +4 -1
- package/dist/client/chat-first/chat-history.d.ts +3 -1
- package/dist/client/chat-first/chat-history.js +2 -2
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +52 -41
- package/dist/client/chat-first/types.d.ts +3 -0
- package/dist/client/chat-first.d.ts +10 -0
- package/dist/client/chat-first.js +37 -2
- package/dist/client/components/ui/message-scroller.js +4 -1
- package/dist/client/i18n.js +3 -1
- package/dist/client/progress/RunsTray.js +14 -1
- package/dist/client/resources/McpConnectionSuggestion.js +9 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/use-chat-models.js +156 -80
- package/dist/integrations/webhook-handler.js +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/auth.js +262 -31
- package/dist/server/onboarding-html.js +56 -9
- package/dist/shared/reasoning-effort.d.ts +8 -8
- package/dist/shared/reasoning-effort.js +10 -10
- package/dist/shared/streaming-text-smoothing.js +41 -11
- package/dist/styles/agent-native.css +19 -32
- package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/docs/content/template-chat.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
|
@@ -17,54 +17,65 @@ function SurfaceIcon({ kind }) {
|
|
|
17
17
|
const Icon = ICONS[kind];
|
|
18
18
|
return _jsx(Icon, { size: 14, "aria-hidden": "true" });
|
|
19
19
|
}
|
|
20
|
-
export function ChatFirstSurfaceTabs({ tabs, activeTabId, onActivate, onClose, onCloseOthers, onCloseToRight, onCloseAll, onOpenSurface, copy = defaultChatFirstCopy, }) {
|
|
21
|
-
return (_jsx(TooltipProvider, { delayDuration: 400, children: _jsx("div", { "data-chat-first-surface-tabs": true, className: "
|
|
20
|
+
export function ChatFirstSurfaceTabs({ tabs, activeTabId, onActivate, onClose, onCloseOthers, onCloseToRight, onCloseAll, onOpenSurface, apps = [], onOpenApp, renderAppIcon, copy = defaultChatFirstCopy, }) {
|
|
21
|
+
return (_jsx(TooltipProvider, { delayDuration: 400, children: _jsx("div", { "data-chat-first-surface-tabs": true, className: cn("min-w-0 border-b border-border bg-card", tabs.length > 0
|
|
22
|
+
? "shrink-0"
|
|
23
|
+
: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"), children: tabs.length > 0 ? (_jsx("div", { className: "flex min-w-0 items-center gap-1 overflow-x-auto px-2 py-1.5", role: "tablist", "aria-label": copy("openSideSurfaces"), children: tabs.map((tab, index) => (_jsxs(ContextMenu, { children: [_jsx(ContextMenuTrigger, { asChild: true, children: _jsxs("div", { "data-surface-tab-id": tab.id, "data-active": activeTabId === tab.id ? "true" : "false", className: cn("group flex h-8 min-w-0 max-w-48 shrink-0 items-center rounded-md text-xs transition-colors", activeTabId === tab.id
|
|
22
24
|
? "bg-accent text-foreground"
|
|
23
|
-
: "text-muted-foreground hover:bg-accent hover:text-foreground"),
|
|
24
|
-
const nextIndex = event.key === "ArrowRight"
|
|
25
|
-
? (index + 1) % tabs.length
|
|
26
|
-
: event.key === "ArrowLeft"
|
|
27
|
-
? (index - 1 + tabs.length) % tabs.length
|
|
28
|
-
: event.key === "Home"
|
|
29
|
-
? 0
|
|
30
|
-
: event.key === "End"
|
|
31
|
-
? tabs.length - 1
|
|
32
|
-
: -1;
|
|
33
|
-
if (nextIndex < 0)
|
|
34
|
-
return;
|
|
35
|
-
event.preventDefault();
|
|
36
|
-
onActivate(tabs[nextIndex]);
|
|
37
|
-
requestAnimationFrame(() => {
|
|
38
|
-
document
|
|
39
|
-
.querySelector(`[data-chat-first-surface-tab-id="${CSS.escape(tabs[nextIndex].id)}"]`)
|
|
40
|
-
?.focus();
|
|
41
|
-
});
|
|
42
|
-
}, "data-chat-first-surface-tab-id": tab.id, onMouseDown: (event) => {
|
|
25
|
+
: "text-muted-foreground hover:bg-accent hover:text-foreground"), onMouseDown: (event) => {
|
|
43
26
|
if (event.button === 1) {
|
|
44
27
|
event.preventDefault();
|
|
45
28
|
onClose(tab);
|
|
46
29
|
}
|
|
47
|
-
}, title: tab.title, children: [_jsxs("button", { type: "button",
|
|
30
|
+
}, title: tab.title, children: [_jsxs("button", { type: "button", role: "tab", tabIndex: activeTabId === tab.id ? 0 : -1, "aria-selected": activeTabId === tab.id, "data-chat-first-surface-tab-id": tab.id, className: "flex min-w-0 flex-1 items-center gap-1.5 truncate rounded-md px-2 text-start focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onKeyDown: (event) => {
|
|
31
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
onActivate(tab);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const nextIndex = event.key === "ArrowRight"
|
|
37
|
+
? (index + 1) % tabs.length
|
|
38
|
+
: event.key === "ArrowLeft"
|
|
39
|
+
? (index - 1 + tabs.length) % tabs.length
|
|
40
|
+
: event.key === "Home"
|
|
41
|
+
? 0
|
|
42
|
+
: event.key === "End"
|
|
43
|
+
? tabs.length - 1
|
|
44
|
+
: -1;
|
|
45
|
+
if (nextIndex < 0)
|
|
46
|
+
return;
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
onActivate(tabs[nextIndex]);
|
|
49
|
+
requestAnimationFrame(() => {
|
|
50
|
+
document
|
|
51
|
+
.querySelector(`[data-chat-first-surface-tab-id="${CSS.escape(tabs[nextIndex].id)}"]`)
|
|
52
|
+
?.focus();
|
|
53
|
+
});
|
|
54
|
+
}, onClick: () => onActivate(tab), children: [_jsx(SurfaceIcon, { kind: tab.kind }), _jsx("span", { className: "truncate", children: tab.title })] }), _jsx("button", { type: "button", className: "mr-1 flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground/65 opacity-0 transition-opacity hover:bg-background/70 hover:text-foreground group-hover:opacity-100 focus-visible:opacity-100", "aria-label": copy("closeTab", { name: tab.title }), title: copy("closeTab", { name: tab.title }), onClick: (event) => {
|
|
48
55
|
event.stopPropagation();
|
|
49
56
|
onClose(tab);
|
|
50
|
-
}, children: _jsx(IconX, { size: 12, "aria-hidden": "true" }) })] }) }), _jsxs(ContextMenuContent, { children: [_jsx(ContextMenuItem, { onSelect: () => onClose(tab), children: copy("close") }), _jsx(ContextMenuItem, { disabled: tabs.length < 2, onSelect: () => onCloseOthers(tab), children: copy("closeOthers") }), _jsx(ContextMenuItem, { disabled: tabs[tabs.length - 1]?.id === tab.id, onSelect: () => onCloseToRight(tab), children: copy("closeToRight") }), _jsx(ContextMenuSeparator, {}), _jsx(ContextMenuItem, { onSelect: onCloseAll, children: copy("closeAll") })] })] }, tab.id))) })) : (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
}, children: _jsx(IconX, { size: 12, "aria-hidden": "true" }) })] }) }), _jsxs(ContextMenuContent, { children: [_jsx(ContextMenuItem, { onSelect: () => onClose(tab), children: copy("close") }), _jsx(ContextMenuItem, { disabled: tabs.length < 2, onSelect: () => onCloseOthers(tab), children: copy("closeOthers") }), _jsx(ContextMenuItem, { disabled: tabs[tabs.length - 1]?.id === tab.id, onSelect: () => onCloseToRight(tab), children: copy("closeToRight") }), _jsx(ContextMenuSeparator, {}), _jsx(ContextMenuItem, { onSelect: onCloseAll, children: copy("closeAll") })] })] }, tab.id))) })) : (_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto px-2 py-1", "data-surface-empty-state": true, "aria-label": copy("openSideSurfaces"), children: [CHAT_FIRST_SURFACE_CATALOG.map((surface) => {
|
|
58
|
+
const label = copy(`surface.${surface.kind}.label`);
|
|
59
|
+
const reason = surface.availability === "deferred"
|
|
60
|
+
? copy(`surface.${surface.kind}.reason`)
|
|
61
|
+
: copy(`surface.${surface.kind}.reason`);
|
|
62
|
+
const isDeferred = surface.availability === "deferred";
|
|
63
|
+
const canOpenSurface = onOpenSurface &&
|
|
64
|
+
(surface.kind === "browser" ||
|
|
65
|
+
surface.kind === "side-chat" ||
|
|
66
|
+
surface.kind === "agents");
|
|
67
|
+
const row = (_jsxs("div", { className: cn("flex h-7 min-w-0 items-center gap-2 rounded px-2 text-xs transition-colors", canOpenSurface
|
|
68
|
+
? "text-foreground hover:bg-accent"
|
|
69
|
+
: isDeferred
|
|
70
|
+
? "text-muted-foreground/60"
|
|
71
|
+
: "text-muted-foreground hover:bg-accent hover:text-foreground"), title: !isDeferred ? reason : undefined, "aria-label": `${label}: ${reason}`, children: [_jsx(SurfaceIcon, { kind: surface.kind }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), isDeferred ? (_jsx("span", { "data-surface-availability": "deferred", className: "size-1.5 shrink-0 rounded-full bg-muted-foreground/45", "aria-label": copy("deferred") })) : null] }));
|
|
72
|
+
if (canOpenSurface) {
|
|
73
|
+
return (_jsx("button", { type: "button", className: "block w-full text-start", title: surface.kind === "browser" ? reason : copy("openActivity"), "aria-label": surface.kind === "browser" ? label : copy("openActivity"), onClick: () => onOpenSurface(surface.kind === "side-chat" ? "agents" : surface.kind), children: row }, surface.kind));
|
|
74
|
+
}
|
|
75
|
+
if (!isDeferred)
|
|
76
|
+
return _jsx("div", { children: row }, surface.kind);
|
|
77
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("div", { className: "text-start", children: row }) }), _jsx(TooltipContent, { side: "left", className: "max-w-72", children: reason })] }, surface.kind));
|
|
78
|
+
}), apps.length > 0 && onOpenApp ? (_jsxs("div", { className: "mt-1 border-t border-border pt-1", children: [_jsx("p", { className: "px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: copy("workspaceApps") }), _jsx("div", { className: "space-y-0.5", children: apps.map((app) => (_jsxs("button", { type: "button", "data-chat-first-surface-app": app.id, className: "flex h-8 w-full min-w-0 items-center gap-2 rounded px-2 text-start text-xs text-foreground transition-colors hover:bg-accent", title: copy("openApp", { name: app.name }), "aria-label": copy("openApp", { name: app.name }), onClick: () => onOpenApp(app), children: [renderAppIcon?.(app) ?? _jsx(SurfaceIcon, { kind: "app" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: app.name })] }, app.id))) })] })) : null] })) }) }));
|
|
68
79
|
}
|
|
69
80
|
export function ChatFirstSurfaceContent({ tabs, activeTabId, renderTab, }) {
|
|
70
81
|
return (_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: tabs.map((tab) => {
|
|
@@ -31,6 +31,9 @@ export interface ChatFirstSurfaceTabsProps {
|
|
|
31
31
|
onCloseToRight: (tab: ChatFirstSurfaceTab) => void;
|
|
32
32
|
onCloseAll: () => void;
|
|
33
33
|
onOpenSurface?: (kind: ChatFirstSurfaceKind) => void;
|
|
34
|
+
apps?: readonly ChatFirstAppItem[];
|
|
35
|
+
onOpenApp?: (app: ChatFirstAppItem) => void;
|
|
36
|
+
renderAppIcon?: (app: ChatFirstAppItem) => ReactNode;
|
|
34
37
|
copy?: ChatFirstCopy;
|
|
35
38
|
}
|
|
36
39
|
export interface ChatFirstAppRailProps {
|
|
@@ -36,11 +36,14 @@ export interface ChatFirstSessionWatchStore {
|
|
|
36
36
|
close: () => void;
|
|
37
37
|
}
|
|
38
38
|
export type ChatFirstSurfaceKind = "app" | "browser" | "terminal" | "diff" | "files" | "side-chat" | "agents";
|
|
39
|
+
export type ChatFirstAppSurfacePlacement = "main" | "side";
|
|
39
40
|
export interface ChatFirstSurfaceTab {
|
|
40
41
|
id: string;
|
|
41
42
|
kind: ChatFirstSurfaceKind;
|
|
42
43
|
title: string;
|
|
43
44
|
appId?: string;
|
|
45
|
+
/** App tabs can either replace the main chat or sit beside it. */
|
|
46
|
+
placement?: ChatFirstAppSurfacePlacement;
|
|
44
47
|
path?: string;
|
|
45
48
|
view?: string;
|
|
46
49
|
url?: string;
|
|
@@ -128,6 +131,7 @@ export type ChatFirstBrowserResolution = {
|
|
|
128
131
|
target: {
|
|
129
132
|
url: string;
|
|
130
133
|
title?: string;
|
|
134
|
+
openExternally?: boolean;
|
|
131
135
|
};
|
|
132
136
|
} | {
|
|
133
137
|
status: "unresolved";
|
|
@@ -202,6 +206,12 @@ export declare function subscribeChatFirstOpenApp(listener: ChatFirstOpenAppList
|
|
|
202
206
|
export declare function emitChatFirstOpenApp(detail: ChatFirstOpenAppDetail): ChatFirstOpenAppDelivery;
|
|
203
207
|
export declare function subscribeChatFirstOpenBrowser(listener: ChatFirstOpenBrowserListener): () => void;
|
|
204
208
|
export declare function emitChatFirstOpenBrowser(detail: ChatFirstOpenBrowserDetail): ChatFirstOpenAppDelivery;
|
|
209
|
+
/**
|
|
210
|
+
* Builder's web app sends frame-blocking headers. Its branch/editor URLs are
|
|
211
|
+
* open-browser targets, not the iframe-backed Fusion preview URLs served from
|
|
212
|
+
* builder.cloud, so keep them out of the chat-first browser iframe.
|
|
213
|
+
*/
|
|
214
|
+
export declare function shouldOpenChatFirstBrowserExternally(value: string): boolean;
|
|
205
215
|
export declare function resolveChatFirstAppTarget(detail: ChatFirstOpenAppDetail, apps: readonly ChatFirstAppRegistration[], options?: {
|
|
206
216
|
currentOrigin?: string;
|
|
207
217
|
}): ChatFirstAppResolution;
|
|
@@ -338,6 +338,9 @@ export function resolveChatFirstBrowserTarget(detail) {
|
|
|
338
338
|
target: {
|
|
339
339
|
url: url.href,
|
|
340
340
|
...(detail.title?.trim() ? { title: detail.title.trim() } : {}),
|
|
341
|
+
...(shouldOpenChatFirstBrowserExternally(url.href)
|
|
342
|
+
? { openExternally: true }
|
|
343
|
+
: {}),
|
|
341
344
|
},
|
|
342
345
|
};
|
|
343
346
|
}
|
|
@@ -464,9 +467,16 @@ function readChatFirstSurfaceTabsState(scope) {
|
|
|
464
467
|
return { tabs: [], activeTabId: null };
|
|
465
468
|
}
|
|
466
469
|
const value = scoped;
|
|
470
|
+
let migratedLegacyPlacement = false;
|
|
467
471
|
const tabs = Array.isArray(value.tabs)
|
|
468
472
|
? value.tabs.flatMap((candidate) => {
|
|
469
473
|
const tab = normalizePersistedChatFirstSurfaceTab(candidate);
|
|
474
|
+
if (tab?.kind === "app" &&
|
|
475
|
+
candidate !== null &&
|
|
476
|
+
typeof candidate === "object" &&
|
|
477
|
+
!("placement" in candidate)) {
|
|
478
|
+
migratedLegacyPlacement = true;
|
|
479
|
+
}
|
|
470
480
|
return tab ? [tab] : [];
|
|
471
481
|
})
|
|
472
482
|
: [];
|
|
@@ -474,7 +484,10 @@ function readChatFirstSurfaceTabsState(scope) {
|
|
|
474
484
|
tabs.some((tab) => tab.id === value.activeTabId)
|
|
475
485
|
? value.activeTabId
|
|
476
486
|
: (tabs[0]?.id ?? null);
|
|
477
|
-
|
|
487
|
+
const state = { tabs, activeTabId };
|
|
488
|
+
if (migratedLegacyPlacement)
|
|
489
|
+
writeChatFirstSurfaceTabsState(scope, state);
|
|
490
|
+
return state;
|
|
478
491
|
}
|
|
479
492
|
catch {
|
|
480
493
|
return { tabs: [], activeTabId: null };
|
|
@@ -498,11 +511,23 @@ function normalizePersistedChatFirstSurfaceTab(candidate) {
|
|
|
498
511
|
const appId = typeof value.appId === "string" ? value.appId.trim() : "";
|
|
499
512
|
if (!appId)
|
|
500
513
|
return null;
|
|
514
|
+
const placement = value.placement;
|
|
515
|
+
if (placement !== undefined &&
|
|
516
|
+
placement !== "main" &&
|
|
517
|
+
placement !== "side") {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
501
520
|
if (value.path !== undefined &&
|
|
502
521
|
(typeof value.path !== "string" || normalizedPath(value.path) === null)) {
|
|
503
522
|
return null;
|
|
504
523
|
}
|
|
505
|
-
return {
|
|
524
|
+
return {
|
|
525
|
+
...tab,
|
|
526
|
+
id,
|
|
527
|
+
title,
|
|
528
|
+
appId,
|
|
529
|
+
placement: placement ?? "main",
|
|
530
|
+
};
|
|
506
531
|
}
|
|
507
532
|
if (kind === "browser") {
|
|
508
533
|
const url = typeof value.url === "string" ? value.url.trim() : "";
|
|
@@ -700,6 +725,16 @@ function parseAbsoluteHttpUrl(value) {
|
|
|
700
725
|
return null;
|
|
701
726
|
}
|
|
702
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Builder's web app sends frame-blocking headers. Its branch/editor URLs are
|
|
730
|
+
* open-browser targets, not the iframe-backed Fusion preview URLs served from
|
|
731
|
+
* builder.cloud, so keep them out of the chat-first browser iframe.
|
|
732
|
+
*/
|
|
733
|
+
export function shouldOpenChatFirstBrowserExternally(value) {
|
|
734
|
+
const url = parseAbsoluteHttpUrl(value);
|
|
735
|
+
return Boolean(url &&
|
|
736
|
+
(url.hostname === "builder.io" || url.hostname.endsWith(".builder.io")));
|
|
737
|
+
}
|
|
703
738
|
function registrationUrls(app, currentOrigin) {
|
|
704
739
|
const values = [app.url, app.devUrl];
|
|
705
740
|
if (app.path && currentOrigin)
|
|
@@ -10,7 +10,10 @@ function MessageScroller({ className, ...props }) {
|
|
|
10
10
|
}
|
|
11
11
|
function MessageScrollerViewport({ className, children, ...props }) {
|
|
12
12
|
const { start: hasContentAbove } = useMessageScrollerScrollable();
|
|
13
|
-
return (_jsx(ShadcnMessageScroller.Viewport, { className: cn("min-h-0 flex-1 overflow-y-auto overflow-x-hidden",
|
|
13
|
+
return (_jsx(ShadcnMessageScroller.Viewport, { className: cn("min-h-0 flex-1 overflow-y-auto overflow-x-hidden",
|
|
14
|
+
// The base class carries the mask itself so it is never added or
|
|
15
|
+
// removed; the modifier only widens the fade. See agent-native.css.
|
|
16
|
+
"message-scroller-viewport", hasContentAbove && "message-scroller-viewport--top-fade", className), ...props, children: children }));
|
|
14
17
|
}
|
|
15
18
|
function MessageScrollerContent({ className, spacerClassName, ...props }) {
|
|
16
19
|
return (_jsx(ShadcnMessageScroller.Content, { className: cn("flex flex-col", className), spacerClassName: cn("shrink-0", spacerClassName), ...props }));
|
package/dist/client/i18n.js
CHANGED
|
@@ -549,7 +549,9 @@ export function LanguagePicker({ className, includeSystem = true, label, variant
|
|
|
549
549
|
? "flex h-8 w-8 items-center justify-center"
|
|
550
550
|
: variant === "select"
|
|
551
551
|
? "flex h-9 w-full items-center justify-between gap-2 px-3 text-start text-sm"
|
|
552
|
-
: null), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(IconLanguage, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), variant === "select" ? (_jsx("span", { className: "truncate", children: selectedLabel })) : (_jsx("span", { className: "sr-only", children: triggerLabel }))] }), variant === "select" ? (_jsx(IconChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": "true" })) : null] }) }), _jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { align: variant === "select" ? "start" : "end", sideOffset: 6, role: "menu", className: cn(
|
|
552
|
+
: null), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(IconLanguage, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), variant === "select" ? (_jsx("span", { className: "truncate", children: selectedLabel })) : (_jsx("span", { className: "sr-only", children: triggerLabel }))] }), variant === "select" ? (_jsx(IconChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": "true" })) : null] }) }), _jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { align: variant === "select" ? "start" : "end", sideOffset: 6, role: "menu", className: cn(
|
|
553
|
+
// compositing-ok: popover content unmounts on close.
|
|
554
|
+
"z-[9999] max-h-[min(20rem,var(--radix-popover-content-available-height))] overflow-y-auto rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none will-change-[transform,opacity] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:duration-100 data-[state=open]:duration-150 data-[state=closed]:ease-in data-[state=open]:ease-out data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1", variant === "icon" || variant === "ghost-icon"
|
|
553
555
|
? "min-w-56"
|
|
554
556
|
: "w-[min(20rem,calc(100vw-2rem))] min-w-[var(--radix-popover-trigger-width)]"), children: options.map((option) => {
|
|
555
557
|
const optionSelected = option.value === preference;
|
|
@@ -9,6 +9,15 @@ import { useChangeVersion } from "../use-change-version.js";
|
|
|
9
9
|
import { usePollLoop } from "../use-poll-loop.js";
|
|
10
10
|
import { cn } from "../utils.js";
|
|
11
11
|
const RUN_CHANGE_SETTLE_MS = 250;
|
|
12
|
+
/**
|
|
13
|
+
* Cadence used while a run still reads as active, even for hosts that opted
|
|
14
|
+
* out of idle polling with `pollMs={0}`. Those hosts only refresh on mount and
|
|
15
|
+
* on a `runs` change event, and a run abandoned mid-flight (budget exhausted,
|
|
16
|
+
* dead worker) emits neither — so the spinner has no path back to a terminal
|
|
17
|
+
* status. Polling only while something looks active keeps the idle cost at
|
|
18
|
+
* zero and still lets the server's stale sweep terminalize the row.
|
|
19
|
+
*/
|
|
20
|
+
const ACTIVE_RUN_POLL_MS = 5000;
|
|
12
21
|
function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showRecent, }) {
|
|
13
22
|
const t = useT();
|
|
14
23
|
const [runs, setRuns] = useState([]);
|
|
@@ -39,7 +48,11 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
|
|
|
39
48
|
const timeout = window.setTimeout(() => void refresh(), RUN_CHANGE_SETTLE_MS);
|
|
40
49
|
return () => window.clearTimeout(timeout);
|
|
41
50
|
}, [refresh, runsVersion]);
|
|
42
|
-
|
|
51
|
+
const hasActiveRun = runs.some((run) => run.status === "running");
|
|
52
|
+
usePollLoop(refresh, {
|
|
53
|
+
intervalMs: pollMs > 0 ? pollMs : ACTIVE_RUN_POLL_MS,
|
|
54
|
+
enabled: pollMs > 0 || hasActiveRun,
|
|
55
|
+
});
|
|
43
56
|
const dismissRun = useCallback(async (runId) => {
|
|
44
57
|
setRuns((current) => current.filter((run) => run.id !== runId));
|
|
45
58
|
try {
|
|
@@ -9,8 +9,16 @@ import { buildMcpOAuthStartUrl, findMcpIntegrationForText, getMcpIntegrationApiF
|
|
|
9
9
|
import { McpIntegrationDialog } from "./McpIntegrationDialog.js";
|
|
10
10
|
import { McpIntegrationLogo } from "./McpIntegrationLogo.js";
|
|
11
11
|
import { useCreateMcpServer, useMcpServers, } from "./use-mcp-servers.js";
|
|
12
|
+
function visibleUserAuthoredText(text) {
|
|
13
|
+
return text
|
|
14
|
+
.replace(/<context\b[^>]*>[\s\S]*?<\/context>\n?/gi, "")
|
|
15
|
+
.replace(/<context\b[^>]*>[\s\S]*$/gi, "")
|
|
16
|
+
.replace(/<\/context>/gi, "")
|
|
17
|
+
.trim();
|
|
18
|
+
}
|
|
12
19
|
export function findMcpConnectionSuggestionIntegration({ text, contextText = "", variant = "composer", integrations = getDefaultMcpIntegrations(), }) {
|
|
13
|
-
|
|
20
|
+
const sourceText = variant === "response" ? contextText : text;
|
|
21
|
+
return findMcpIntegrationForText(visibleUserAuthoredText(sourceText), integrations);
|
|
14
22
|
}
|
|
15
23
|
export function shouldRenderMcpIntegrationFallback(logoUrl, logoLoadFailed) {
|
|
16
24
|
return !logoUrl || logoLoadFailed;
|
|
@@ -1291,6 +1291,9 @@ export function processEvent(ev, content, toolCallCounter, tabId, state) {
|
|
|
1291
1291
|
detail: { ...runError, tabId },
|
|
1292
1292
|
}));
|
|
1293
1293
|
}
|
|
1294
|
+
// This event is terminal. There may be no visible text or tool_done after
|
|
1295
|
+
// the last preparation activity, so do not leave its label mounted.
|
|
1296
|
+
dispatchActivityClear(tabId);
|
|
1294
1297
|
settleInterruptedToolCalls(content, undefined, { includeActivity: true });
|
|
1295
1298
|
content.push({
|
|
1296
1299
|
type: "text",
|
|
@@ -1386,6 +1389,9 @@ export function processEvent(ev, content, toolCallCounter, tabId, state) {
|
|
|
1386
1389
|
...(ev.errorCode ? { errorCode: ev.errorCode } : {}),
|
|
1387
1390
|
...(ev.recoverable ? { recoverable: ev.recoverable } : {}),
|
|
1388
1391
|
};
|
|
1392
|
+
// Non-recoverable errors end the turn. Recoverable errors return above as
|
|
1393
|
+
// auto-continue signals and must keep their activity state alive.
|
|
1394
|
+
dispatchActivityClear(tabId);
|
|
1389
1395
|
if (typeof window !== "undefined") {
|
|
1390
1396
|
window.dispatchEvent(new CustomEvent("agent-chat:run-error", {
|
|
1391
1397
|
detail: { ...runError, tabId },
|
|
@@ -1406,6 +1412,10 @@ export function processEvent(ev, content, toolCallCounter, tabId, state) {
|
|
|
1406
1412
|
};
|
|
1407
1413
|
}
|
|
1408
1414
|
if (ev.type === "done") {
|
|
1415
|
+
// `done` is authoritative even when the final model chunk contains only
|
|
1416
|
+
// a wrap-up marker. Clear any preparation label before inspecting pending
|
|
1417
|
+
// tools so both success and interrupted-terminal paths settle the UI.
|
|
1418
|
+
dispatchActivityClear(tabId);
|
|
1409
1419
|
const interruptedTools = pendingToolNames(content);
|
|
1410
1420
|
const allInterruptedTools = [
|
|
1411
1421
|
...interruptedTools.running,
|
|
@@ -6,6 +6,22 @@ import { fetchBuilderStatus, fetchEnvironmentStatus, } from "./client-status-req
|
|
|
6
6
|
import { callAction } from "./use-action.js";
|
|
7
7
|
const DEFAULT_STORAGE_KEY = "agent-native:chat-models:selection";
|
|
8
8
|
export const CHAT_MODEL_SELECTION_CHANGED_EVENT = "agent-native:chat-model-selection-changed";
|
|
9
|
+
const MODEL_DISCOVERY_RETRY_DELAYS_MS = [250, 1_000];
|
|
10
|
+
async function fetchEngineCatalog() {
|
|
11
|
+
try {
|
|
12
|
+
const result = (await callAction("manage-agent-engine", { action: "list" }));
|
|
13
|
+
if (!result ||
|
|
14
|
+
typeof result !== "object" ||
|
|
15
|
+
!("engines" in result) ||
|
|
16
|
+
!Array.isArray(result.engines)) {
|
|
17
|
+
return { state: "unavailable" };
|
|
18
|
+
}
|
|
19
|
+
return { state: "available", value: result };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { state: "unavailable" };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
function readPersisted(key) {
|
|
10
26
|
if (!key || typeof window === "undefined")
|
|
11
27
|
return {};
|
|
@@ -50,6 +66,9 @@ export function useChatModels({ storageKey = DEFAULT_STORAGE_KEY, enabled = true
|
|
|
50
66
|
selectedEngine,
|
|
51
67
|
selectedEffort,
|
|
52
68
|
});
|
|
69
|
+
const mountedRef = useRef(true);
|
|
70
|
+
const refreshGenerationRef = useRef(0);
|
|
71
|
+
const retryTimerRef = useRef(null);
|
|
53
72
|
useEffect(() => {
|
|
54
73
|
selectionRef.current = {
|
|
55
74
|
selectedModel,
|
|
@@ -109,98 +128,155 @@ export function useChatModels({ storageKey = DEFAULT_STORAGE_KEY, enabled = true
|
|
|
109
128
|
const refreshEngines = useCallback(() => {
|
|
110
129
|
if (!enabled)
|
|
111
130
|
return;
|
|
131
|
+
refreshGenerationRef.current += 1;
|
|
132
|
+
const refreshGeneration = refreshGenerationRef.current;
|
|
133
|
+
if (retryTimerRef.current !== null) {
|
|
134
|
+
clearTimeout(retryTimerRef.current);
|
|
135
|
+
retryTimerRef.current = null;
|
|
136
|
+
}
|
|
112
137
|
setIsLoading(true);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
const isCurrentRefresh = () => mountedRef.current && refreshGenerationRef.current === refreshGeneration;
|
|
139
|
+
const finish = () => {
|
|
140
|
+
if (isCurrentRefresh())
|
|
141
|
+
setIsLoading(false);
|
|
142
|
+
};
|
|
143
|
+
function scheduleRetry(attempt) {
|
|
144
|
+
const delay = MODEL_DISCOVERY_RETRY_DELAYS_MS[attempt];
|
|
145
|
+
if (delay === undefined || !isCurrentRefresh())
|
|
146
|
+
return false;
|
|
147
|
+
retryTimerRef.current = setTimeout(() => {
|
|
148
|
+
retryTimerRef.current = null;
|
|
149
|
+
load(attempt + 1);
|
|
150
|
+
}, delay);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
function load(attempt) {
|
|
154
|
+
if (!isCurrentRefresh())
|
|
127
155
|
return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
Promise.all([
|
|
157
|
+
fetchEngineCatalog(),
|
|
158
|
+
fetchEnvironmentStatus(),
|
|
159
|
+
fetchBuilderStatus(),
|
|
160
|
+
])
|
|
161
|
+
.then(([engineResult, envResult, builderResult]) => {
|
|
162
|
+
if (!isCurrentRefresh())
|
|
163
|
+
return;
|
|
164
|
+
if (engineResult.state !== "available" ||
|
|
165
|
+
envResult.state !== "available" ||
|
|
166
|
+
builderResult.state !== "available") {
|
|
167
|
+
if (scheduleRetry(attempt))
|
|
168
|
+
return;
|
|
169
|
+
if (engineResult.state !== "available") {
|
|
170
|
+
// Without a catalog the picker keeps an unvalidated DEFAULT_MODEL,
|
|
171
|
+
// which is indistinguishable from a real selection unless we say so.
|
|
172
|
+
console.warn("[agent-chat] engine list unavailable; model picker is showing an unvalidated default");
|
|
173
|
+
}
|
|
174
|
+
finish();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const enginesData = engineResult.value;
|
|
178
|
+
const envKeys = envResult.value;
|
|
179
|
+
const builderStatus = builderResult.value;
|
|
180
|
+
const configuredKeys = new Set(envKeys.filter((k) => k.configured).map((k) => k.key));
|
|
181
|
+
const builderConnected = builderStatus?.configured === true;
|
|
182
|
+
const currentEngineName = enginesData.current?.engine;
|
|
183
|
+
const currentModel = enginesData.current?.model;
|
|
184
|
+
const groups = buildChatModelGroups({
|
|
185
|
+
engines: enginesData.engines,
|
|
186
|
+
configuredKeys,
|
|
187
|
+
builderConnected,
|
|
188
|
+
currentEngineName,
|
|
189
|
+
currentModel,
|
|
190
|
+
});
|
|
191
|
+
const nextDefaultModel = currentModel ?? DEFAULT_MODEL;
|
|
192
|
+
setAvailableModels(groups);
|
|
193
|
+
setDefaultModel(nextDefaultModel);
|
|
194
|
+
const selection = selectionRef.current;
|
|
195
|
+
// Default only to a CONFIGURED group, and to nothing when there is
|
|
196
|
+
// none. `DEFAULT_MODEL` is a builder-gateway id that no group carries
|
|
197
|
+
// unless Builder is connected, and unconfigured groups are kept in the
|
|
198
|
+
// list for their connect affordance — so both `?? DEFAULT_MODEL` and
|
|
199
|
+
// `?? groups[0]` yield a selection the app cannot route, which the
|
|
200
|
+
// server silently replaces with its own default. An empty selection
|
|
201
|
+
// hides the picker instead of showing a model that will not be used.
|
|
202
|
+
const configuredGroups = groups.filter((g) => g.configured);
|
|
203
|
+
const resolveRoutableSelection = () => {
|
|
204
|
+
const group = configuredGroups.find((g) => g.models.includes(nextDefaultModel)) ?? configuredGroups[0];
|
|
205
|
+
if (!group)
|
|
206
|
+
return null;
|
|
207
|
+
const model = group.models.find((m) => m === nextDefaultModel) ??
|
|
208
|
+
group.models[0];
|
|
209
|
+
if (!model)
|
|
210
|
+
return null;
|
|
211
|
+
return {
|
|
212
|
+
model,
|
|
213
|
+
engine: group.engine,
|
|
214
|
+
effort: resolveReasoningEffortSelection(model, selection.selectedEffort),
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
const applyFallback = (persist) => {
|
|
218
|
+
const next = resolveRoutableSelection();
|
|
219
|
+
setSelectedModel(next?.model ?? "");
|
|
220
|
+
setSelectedEngine(next?.engine ?? "");
|
|
221
|
+
if (next) {
|
|
222
|
+
setSelectedEffort(next.effort);
|
|
223
|
+
if (persist)
|
|
224
|
+
writePersisted(storageKey, next);
|
|
225
|
+
}
|
|
166
226
|
};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
setSelectedEngine(next?.engine ?? "");
|
|
172
|
-
if (next) {
|
|
173
|
-
setSelectedEffort(next.effort);
|
|
174
|
-
if (persist)
|
|
175
|
-
writePersisted(storageKey, next);
|
|
227
|
+
if (!hasExplicitSelectionRef.current) {
|
|
228
|
+
applyFallback(false);
|
|
229
|
+
finish();
|
|
230
|
+
return;
|
|
176
231
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (selection.selectedEngine !== selectedGroup.engine) {
|
|
189
|
-
setSelectedEngine(selectedGroup.engine);
|
|
232
|
+
const selectedGroup = groups.find((group) => group.models.includes(selection.selectedModel) &&
|
|
233
|
+
(!selection.selectedEngine ||
|
|
234
|
+
group.engine === selection.selectedEngine));
|
|
235
|
+
if (selectedGroup) {
|
|
236
|
+
// Heal a selection stored without an engine (or with a stale one) so
|
|
237
|
+
// later submits carry the pair the catalog resolved.
|
|
238
|
+
if (selection.selectedEngine !== selectedGroup.engine) {
|
|
239
|
+
setSelectedEngine(selectedGroup.engine);
|
|
240
|
+
}
|
|
241
|
+
finish();
|
|
242
|
+
return;
|
|
190
243
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
244
|
+
applyFallback(true);
|
|
245
|
+
finish();
|
|
246
|
+
})
|
|
247
|
+
.catch(() => {
|
|
248
|
+
if (!isCurrentRefresh())
|
|
249
|
+
return;
|
|
250
|
+
if (!scheduleRetry(attempt))
|
|
251
|
+
finish();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
load(0);
|
|
197
255
|
}, [enabled, storageKey]);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
mountedRef.current = true;
|
|
258
|
+
return () => {
|
|
259
|
+
mountedRef.current = false;
|
|
260
|
+
refreshGenerationRef.current += 1;
|
|
261
|
+
if (retryTimerRef.current !== null) {
|
|
262
|
+
clearTimeout(retryTimerRef.current);
|
|
263
|
+
retryTimerRef.current = null;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}, []);
|
|
198
267
|
useEffect(() => {
|
|
199
268
|
if (!enabled) {
|
|
269
|
+
refreshGenerationRef.current += 1;
|
|
270
|
+
if (retryTimerRef.current !== null) {
|
|
271
|
+
clearTimeout(retryTimerRef.current);
|
|
272
|
+
retryTimerRef.current = null;
|
|
273
|
+
}
|
|
200
274
|
setIsLoading(false);
|
|
201
275
|
return;
|
|
202
276
|
}
|
|
203
277
|
refreshEngines();
|
|
278
|
+
window.addEventListener("agent-engine:configured-changed", refreshEngines);
|
|
279
|
+
return () => window.removeEventListener("agent-engine:configured-changed", refreshEngines);
|
|
204
280
|
}, [enabled, refreshEngines]);
|
|
205
281
|
return {
|
|
206
282
|
availableModels,
|