@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.
Files changed (134) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
  4. package/corpus/templates/analytics/app/global.css +1 -1
  5. package/corpus/templates/analytics/app/i18n-data.ts +37 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  9. package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  11. package/corpus/templates/assets/app/global.css +1 -1
  12. package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  13. package/corpus/templates/brain/app/global.css +1 -1
  14. package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  15. package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
  17. package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
  18. package/corpus/templates/clips/desktop/src/app.tsx +274 -137
  19. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  20. package/corpus/templates/content/app/global.css +1 -0
  21. package/corpus/templates/crm/app/global.css +1 -1
  22. package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  23. package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  24. package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  25. package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  26. package/corpus/templates/plan/app/global.css +1 -0
  27. package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  28. package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
  29. package/corpus/templates/slides/AGENTS.md +11 -10
  30. package/corpus/templates/slides/actions/add-deck.ts +2 -0
  31. package/corpus/templates/slides/actions/add-slide.ts +15 -1
  32. package/corpus/templates/slides/actions/create-deck.ts +18 -6
  33. package/corpus/templates/slides/actions/get-deck.ts +29 -1
  34. package/corpus/templates/slides/actions/patch-deck.ts +139 -10
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
  37. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
  38. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +10 -1
  39. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  40. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
  41. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  42. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  43. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  44. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  45. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
  46. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
  47. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  48. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  49. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  50. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  51. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  52. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  53. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  54. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  55. package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
  56. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
  57. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  58. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  59. package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
  60. package/corpus/templates/slides/app/pages/Index.tsx +256 -98
  61. package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
  64. package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
  65. package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
  68. package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
  69. package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
  70. package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
  71. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  72. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  73. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  74. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  75. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  76. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  79. package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
  80. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
  81. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  82. package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
  83. package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
  84. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  85. package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
  86. package/corpus/templates/slides/shared/api.ts +2 -1
  87. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  88. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  89. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  90. package/dist/agent/production-agent.d.ts +21 -1
  91. package/dist/agent/production-agent.js +129 -10
  92. package/dist/agent/tool-approval-store.d.ts +18 -0
  93. package/dist/agent/tool-approval-store.js +122 -0
  94. package/dist/agent/types.d.ts +1 -1
  95. package/dist/cli/code-agent-executor.js +11 -2
  96. package/dist/client/AgentPanel.js +31 -6
  97. package/dist/client/AssistantChat.d.ts +3 -2
  98. package/dist/client/AssistantChat.js +7 -20
  99. package/dist/client/agent-chat/index.d.ts +1 -1
  100. package/dist/client/agent-chat-adapter.js +11 -0
  101. package/dist/client/agent-chat.d.ts +1 -1
  102. package/dist/client/chat/markdown-renderer.js +14 -2
  103. package/dist/client/chat/message-components.d.ts +6 -2
  104. package/dist/client/chat/message-components.js +25 -47
  105. package/dist/client/chat/tool-call-display.d.ts +3 -5
  106. package/dist/client/chat/tool-call-display.js +7 -212
  107. package/dist/client/chat-first/apps-rail.js +3 -3
  108. package/dist/client/chat-first/browser-pane.js +4 -1
  109. package/dist/client/chat-first/chat-history.d.ts +3 -1
  110. package/dist/client/chat-first/chat-history.js +2 -2
  111. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  112. package/dist/client/chat-first/surface-tabs.js +52 -41
  113. package/dist/client/chat-first/types.d.ts +3 -0
  114. package/dist/client/chat-first.d.ts +10 -0
  115. package/dist/client/chat-first.js +37 -2
  116. package/dist/client/components/ui/message-scroller.js +4 -1
  117. package/dist/client/i18n.js +3 -1
  118. package/dist/client/progress/RunsTray.js +14 -1
  119. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  120. package/dist/client/sse-event-processor.js +10 -0
  121. package/dist/client/use-chat-models.js +156 -80
  122. package/dist/integrations/webhook-handler.js +1 -1
  123. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  124. package/dist/secrets/routes.d.ts +6 -6
  125. package/dist/server/auth.js +262 -31
  126. package/dist/server/onboarding-html.js +56 -9
  127. package/dist/shared/reasoning-effort.d.ts +8 -8
  128. package/dist/shared/reasoning-effort.js +10 -10
  129. package/dist/shared/streaming-text-smoothing.js +41 -11
  130. package/dist/styles/agent-native.css +19 -32
  131. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  132. package/docs/content/template-chat.mdx +1 -1
  133. package/package.json +2 -2
  134. 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: "shrink-0 border-b border-border bg-card", 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", { role: "tab", tabIndex: activeTabId === tab.id ? 0 : -1, "aria-selected": activeTabId === tab.id, "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
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"), onKeyDown: (event) => {
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", className: "flex min-w-0 flex-1 items-center gap-1.5 truncate px-2 text-start", onClick: () => onActivate(tab), tabIndex: -1, 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) => {
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))) })) : (_jsx("div", { className: "flex flex-col px-2 py-1", "data-surface-empty-state": true, "aria-label": copy("openSideSurfaces"), children: CHAT_FIRST_SURFACE_CATALOG.map((surface) => {
51
- const label = copy(`surface.${surface.kind}.label`);
52
- const reason = surface.availability === "deferred"
53
- ? copy(`surface.${surface.kind}.reason`)
54
- : copy(`surface.${surface.kind}.reason`);
55
- const isDeferred = surface.availability === "deferred";
56
- const row = (_jsxs("div", { className: cn("flex h-7 min-w-0 items-center gap-2 rounded px-2 text-xs transition-colors", surface.kind === "agents" && onOpenSurface
57
- ? "text-foreground hover:bg-accent"
58
- : isDeferred
59
- ? "text-muted-foreground/60"
60
- : "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] }));
61
- if (surface.kind === "agents" && onOpenSurface) {
62
- return (_jsx("button", { type: "button", className: "text-start", title: copy("openActivity"), "aria-label": copy("openActivity"), onClick: () => onOpenSurface(surface.kind), children: row }, surface.kind));
63
- }
64
- if (!isDeferred)
65
- return _jsx("div", { children: row }, surface.kind);
66
- 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));
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
- return { tabs, activeTabId };
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 { ...tab, id, title, appId };
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", hasContentAbove && "message-scroller-viewport--top-fade", className), ...props, children: children }));
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 }));
@@ -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("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"
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
- usePollLoop(refresh, { intervalMs: pollMs, enabled: pollMs > 0 });
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
- return findMcpIntegrationForText(variant === "response" ? contextText : text, integrations);
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
- Promise.all([
114
- callAction("manage-agent-engine", { action: "list" }).catch(() => null),
115
- fetchEnvironmentStatus(),
116
- fetchBuilderStatus(),
117
- ])
118
- .then(([enginesData, envResult, builderResult]) => {
119
- if (!enginesData?.engines) {
120
- // Without a catalog the picker keeps an unvalidated DEFAULT_MODEL,
121
- // which is indistinguishable from a real selection unless we say so.
122
- console.warn("[agent-chat] no engine list; model picker is showing an unvalidated default");
123
- return;
124
- }
125
- if (envResult.state !== "available" ||
126
- builderResult.state !== "available") {
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
- const envKeys = envResult.value;
130
- const builderStatus = builderResult.value;
131
- const configuredKeys = new Set(envKeys.filter((k) => k.configured).map((k) => k.key));
132
- const builderConnected = builderStatus?.configured === true;
133
- const currentEngineName = enginesData.current?.engine;
134
- const currentModel = enginesData.current?.model;
135
- const groups = buildChatModelGroups({
136
- engines: enginesData.engines,
137
- configuredKeys,
138
- builderConnected,
139
- currentEngineName,
140
- currentModel,
141
- });
142
- const nextDefaultModel = currentModel ?? DEFAULT_MODEL;
143
- setAvailableModels(groups);
144
- setDefaultModel(nextDefaultModel);
145
- const selection = selectionRef.current;
146
- // Default only to a CONFIGURED group, and to nothing when there is
147
- // none. `DEFAULT_MODEL` is a builder-gateway id that no group carries
148
- // unless Builder is connected, and unconfigured groups are kept in the
149
- // list for their connect affordance — so both `?? DEFAULT_MODEL` and
150
- // `?? groups[0]` yield a selection the app cannot route, which the
151
- // server silently replaces with its own default. An empty selection
152
- // hides the picker instead of showing a model that will not be used.
153
- const configuredGroups = groups.filter((g) => g.configured);
154
- const resolveRoutableSelection = () => {
155
- const group = configuredGroups.find((g) => g.models.includes(nextDefaultModel)) ??
156
- configuredGroups[0];
157
- if (!group)
158
- return null;
159
- const model = group.models.find((m) => m === nextDefaultModel) ?? group.models[0];
160
- if (!model)
161
- return null;
162
- return {
163
- model,
164
- engine: group.engine,
165
- effort: resolveReasoningEffortSelection(model, selection.selectedEffort),
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
- const applyFallback = (persist) => {
169
- const next = resolveRoutableSelection();
170
- setSelectedModel(next?.model ?? "");
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
- if (!hasExplicitSelectionRef.current) {
179
- applyFallback(false);
180
- return;
181
- }
182
- const selectedGroup = groups.find((group) => group.models.includes(selection.selectedModel) &&
183
- (!selection.selectedEngine ||
184
- group.engine === selection.selectedEngine));
185
- if (selectedGroup) {
186
- // Heal a selection stored without an engine (or with a stale one) so
187
- // later submits carry the pair the catalog resolved.
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
- return;
192
- }
193
- applyFallback(true);
194
- })
195
- .catch(() => { })
196
- .finally(() => setIsLoading(false));
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,