@agent-native/core 0.185.0 → 0.186.0-nightly-20260923163746

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 (44) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/actions/finalize-recording.ts +35 -1
  3. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
  4. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
  5. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
  6. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  7. package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
  8. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
  9. package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
  10. package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
  11. package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
  12. package/dist/agent/engine/provider-endpoint-validation.js +47 -10
  13. package/dist/agent/engine/registry.js +8 -5
  14. package/dist/cli/skills.d.ts +3 -0
  15. package/dist/cli/skills.js +45 -9
  16. package/dist/client/agent-engine-key.d.ts +7 -0
  17. package/dist/client/agent-engine-key.js +37 -0
  18. package/dist/client/agent-provider-catalog.js +1 -1
  19. package/dist/client/settings/ProviderSetupForm.js +80 -7
  20. package/dist/client/settings/SettingsPanel.js +149 -11
  21. package/dist/client/use-chat-models.js +31 -0
  22. package/dist/collab/routes.d.ts +1 -1
  23. package/dist/collab/struct-routes.d.ts +1 -1
  24. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  25. package/dist/localization/default-messages.d.ts +7 -1
  26. package/dist/localization/default-messages.js +7 -1
  27. package/dist/notifications/routes.d.ts +4 -4
  28. package/dist/observability/routes.d.ts +3 -3
  29. package/dist/org/index.d.ts +1 -1
  30. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  31. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  32. package/dist/resources/handlers.d.ts +1 -1
  33. package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
  34. package/dist/secrets/routes.d.ts +3 -3
  35. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  36. package/dist/server/agent-engine-api-key-route.js +8 -5
  37. package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
  38. package/dist/server/agent-engine-ollama-models-route.js +105 -0
  39. package/dist/server/core-routes-plugin.d.ts +1 -1
  40. package/dist/server/core-routes-plugin.js +5 -0
  41. package/dist/server/credential-provider.d.ts +13 -0
  42. package/dist/server/credential-provider.js +19 -0
  43. package/dist/server/transcribe-voice.d.ts +1 -1
  44. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { IconBolt, IconCheck, IconChevronDown, IconDeviceDesktop, IconExternalLink, IconKey, IconLoader2, IconRoute, IconServer2, } from "@tabler/icons-react";
2
+ import { IconBolt, IconCheck, IconChevronDown, IconDeviceDesktop, IconExternalLink, IconKey, IconLoader2, IconRoute, IconSearch, IconServer2, } from "@tabler/icons-react";
3
3
  import { useEffect, useState } from "react";
4
- import { deleteAgentEnginePersonalProviderSettings, getAgentEngineProviderKeyStatus, saveAgentEngineProviderSettings, setAgentEngineProvider, } from "../agent-engine-key.js";
4
+ import { deleteAgentEnginePersonalProviderSettings, fetchOllamaModels, getAgentEngineProviderKeyStatus, saveAgentEngineProviderSettings, setAgentEngineProvider, } from "../agent-engine-key.js";
5
5
  import { getAgentProviderOption, } from "../agent-provider-catalog.js";
6
6
  import { useT } from "../i18n.js";
7
7
  import { cn } from "../utils.js";
@@ -14,13 +14,16 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
14
14
  const [apiKey, setApiKey] = useState("");
15
15
  const [model, setModel] = useState("");
16
16
  const [endpoint, setEndpoint] = useState("");
17
- const [endpointOpen, setEndpointOpen] = useState(initialProvider === "ollama");
17
+ const [endpointOpen, setEndpointOpen] = useState(false);
18
18
  const [saving, setSaving] = useState(false);
19
19
  const [removingPersonalKey, setRemovingPersonalKey] = useState(false);
20
20
  const [saved, setSaved] = useState(false);
21
21
  const [error, setError] = useState(null);
22
22
  const [providerKeyStatus, setProviderKeyStatus] = useState(null);
23
23
  const [statusError, setStatusError] = useState(false);
24
+ const [ollamaModels, setOllamaModels] = useState(null);
25
+ const [ollamaModelsError, setOllamaModelsError] = useState(null);
26
+ const [ollamaModelsLoading, setOllamaModelsLoading] = useState(false);
24
27
  const active = getAgentProviderOption(provider);
25
28
  const refreshProviderKeyStatus = async () => {
26
29
  if (!active.key) {
@@ -66,10 +69,54 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
66
69
  setModel(active.defaultModel);
67
70
  setApiKey("");
68
71
  setEndpoint("");
69
- setEndpointOpen(provider === "ollama");
72
+ setEndpointOpen(false);
70
73
  setError(null);
71
74
  setSaved(false);
75
+ setOllamaModels(null);
76
+ setOllamaModelsError(null);
72
77
  }, [active.defaultModel, provider]);
78
+ // Ask the Ollama server itself which models it has pulled, instead of only
79
+ // offering the static suggestion list. Triggered explicitly by the "Find
80
+ // models" button rather than on every keystroke, so the request always
81
+ // uses the address the user actually meant to check.
82
+ const handleFindOllamaModels = () => {
83
+ setOllamaModelsLoading(true);
84
+ setOllamaModelsError(null);
85
+ const typedEndpoint = endpoint.trim();
86
+ void fetchOllamaModels(typedEndpoint || undefined)
87
+ .then(async (models) => {
88
+ setOllamaModels(models);
89
+ setOllamaModelsError(null);
90
+ // A successful check is the only signal this address actually works.
91
+ // Persist it as soon as it's confirmed — other surfaces that read
92
+ // the saved Ollama endpoint (like the chat composer's model picker)
93
+ // have no address field of their own, so without this they keep
94
+ // falling back to the http://localhost:11434 default until the main
95
+ // "Use Ollama" button below is also clicked.
96
+ if (typedEndpoint && active.endpointKey) {
97
+ try {
98
+ await saveAgentEngineProviderSettings({
99
+ provider,
100
+ key: active.endpointKey,
101
+ baseUrl: typedEndpoint,
102
+ });
103
+ void refreshProviderKeyStatus();
104
+ }
105
+ catch {
106
+ // coercion-ok: the connectivity check itself still succeeded and
107
+ // the found models are shown; the address just wasn't persisted
108
+ // (e.g. a dropped session). The main submit button below retries
109
+ // the save, so this is never reported to the user as a clean
110
+ // success — it's a silent retry opportunity, not a lost error.
111
+ }
112
+ }
113
+ })
114
+ .catch((err) => {
115
+ setOllamaModels(null);
116
+ setOllamaModelsError(err instanceof Error ? err.message : String(err));
117
+ })
118
+ .finally(() => setOllamaModelsLoading(false));
119
+ };
73
120
  const handleProviderChange = (nextProvider) => {
74
121
  setProvider(nextProvider);
75
122
  };
@@ -142,6 +189,7 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
142
189
  // newly released provider models usable before the catalog is refreshed.
143
190
  const modelInputVisible = Boolean(active.key) || active.supportsCustomModel;
144
191
  const endpointVisible = active.supportsEndpoint;
192
+ const isOllama = provider === "ollama";
145
193
  return (_jsxs("form", { className: cn("space-y-3", className), onSubmit: (event) => {
146
194
  event.preventDefault();
147
195
  void handleSave();
@@ -159,9 +207,34 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
159
207
  setError(null);
160
208
  }, className: cn("w-full rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") })] })) : (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-border/70 bg-background/60 px-2.5 py-2", children: [_jsx(IconServer2, { size: isPage ? 15 : 13, className: "mt-0.5 shrink-0 text-muted-foreground" }), _jsx("p", { className: "text-[11px] leading-relaxed text-muted-foreground", children: t("agentPanel.noApiKeyNeeded", {
161
209
  defaultValue: "No API key required.",
162
- }) })] })), modelInputVisible ? (_jsxs("label", { className: "block space-y-1.5", children: [_jsx("span", { className: cn("font-medium text-foreground", isPage ? "text-xs" : "text-[11px]"), children: t("agentPanel.modelId", { defaultValue: "Model ID" }) }), _jsx("input", { type: "text", value: model, list: `agent-provider-models-${provider}`, disabled: saving, spellCheck: false, autoComplete: "off", placeholder: active.defaultModel, onChange: (event) => setModel(event.target.value), className: cn("w-full rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") }), _jsx("datalist", { id: `agent-provider-models-${provider}`, children: active.supportedModels.map((modelOption) => (_jsx("option", { value: modelOption }, modelOption))) })] })) : null, endpointVisible ? (_jsxs("div", { className: "border-t border-border/70 pt-2", children: [_jsxs("button", { type: "button", onClick: () => setEndpointOpen((open) => !open), className: "flex w-full items-center justify-between gap-2 text-start text-[11px] font-medium text-foreground", "aria-expanded": endpointOpen, title: provider === "ollama"
163
- ? "Defaults to Ollama at http://localhost:11434."
164
- : "Use for LiteLLM or another OpenAI-compatible gateway.", children: [_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(IconChevronDown, { size: 13, className: cn("text-muted-foreground transition-transform", !endpointOpen && "-rotate-90") }), t("agentPanel.endpointUrl", {
210
+ }) })] })), isOllama && endpointVisible ? (_jsxs("div", { className: "space-y-1.5", children: [_jsx("span", { className: cn("font-medium text-foreground", isPage ? "text-xs" : "text-[11px]"), children: t("agentPanel.endpointUrl", { defaultValue: "Endpoint URL" }) }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("input", { type: "url", value: endpoint, disabled: saving, spellCheck: false, autoComplete: "off", placeholder: active.endpointPlaceholder, onChange: (event) => {
211
+ setEndpoint(event.target.value);
212
+ setOllamaModels(null);
213
+ setOllamaModelsError(null);
214
+ }, className: cn("min-w-0 flex-1 rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") }), _jsxs("button", { type: "button", disabled: saving || ollamaModelsLoading, onClick: handleFindOllamaModels, className: cn("inline-flex shrink-0 items-center gap-1.5 rounded-md border border-input bg-background font-medium text-foreground transition-colors hover:bg-accent/40 disabled:cursor-not-allowed disabled:opacity-50", isPage ? "h-10 px-3 text-xs" : "h-8 px-2.5 text-[11px]"), children: [ollamaModelsLoading ? (_jsx(IconLoader2, { size: 13, className: "animate-spin" })) : (_jsx(IconSearch, { size: 13 })), t("agentPanel.findModels", { defaultValue: "Find models" })] })] })] })) : null, modelInputVisible ? (_jsxs("label", { className: "block space-y-1.5", children: [_jsx("span", { className: cn("font-medium text-foreground", isPage ? "text-xs" : "text-[11px]"), children: t("agentPanel.modelId", { defaultValue: "Model ID" }) }), _jsx("input", { type: "text", value: model, list: isOllama ? undefined : `agent-provider-models-${provider}`, disabled: saving, spellCheck: false, autoComplete: "off", placeholder: active.defaultModel, onChange: (event) => setModel(event.target.value), className: cn("w-full rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") }), isOllama ? null : (_jsx("datalist", { id: `agent-provider-models-${provider}`, children: active.supportedModels.map((modelOption) => (_jsx("option", { value: modelOption }, modelOption))) })), isOllama ? (_jsxs("div", { className: "space-y-1.5", children: [ollamaModels && ollamaModels.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1.5", children: ollamaModels.map((modelOption) => (_jsx("button", { type: "button", disabled: saving, onClick: () => setModel(modelOption), "aria-pressed": model === modelOption, className: cn("rounded-md border px-2 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50", model === modelOption
215
+ ? "border-primary bg-primary/10 text-primary"
216
+ : "border-border bg-background text-foreground hover:bg-accent/40"), children: modelOption }, modelOption))) })) : null, _jsx("p", { className: "text-[10px] leading-relaxed text-muted-foreground", children: ollamaModelsLoading
217
+ ? t("agentPanel.ollamaModelsChecking", {
218
+ defaultValue: "Checking installed models…",
219
+ })
220
+ : ollamaModels && ollamaModels.length > 0
221
+ ? t("agentPanel.ollamaModelsFound", {
222
+ count: ollamaModels.length,
223
+ })
224
+ : ollamaModels
225
+ ? t("agentPanel.ollamaModelsNone", {
226
+ defaultValue: "Connected, but no models are pulled yet — run `ollama pull llama3.1`.",
227
+ })
228
+ : ollamaModelsError
229
+ ? t("agentPanel.ollamaModelsError", {
230
+ error: ollamaModelsError,
231
+ defaultValue: `${ollamaModelsError} Showing example model names below.`,
232
+ })
233
+ : t("agentPanel.ollamaModelsPrompt", {
234
+ defaultValue: 'Click "Find models" above to list what your Ollama server actually has installed.',
235
+ }) })] })) : null] })) : null, !isOllama && endpointVisible ? (_jsxs("div", { className: "border-t border-border/70 pt-2", children: [_jsxs("button", { type: "button", onClick: () => setEndpointOpen((open) => !open), className: "flex w-full items-center justify-between gap-2 text-start text-[11px] font-medium text-foreground", "aria-expanded": endpointOpen, title: t("agentPanel.compatibleEndpointHint", {
236
+ defaultValue: "Use this for LiteLLM or another OpenAI-compatible gateway.",
237
+ }), children: [_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(IconChevronDown, { size: 13, className: cn("text-muted-foreground transition-transform", !endpointOpen && "-rotate-90") }), t("agentPanel.endpointUrl", {
165
238
  defaultValue: "Endpoint URL",
166
239
  })] }), _jsx("span", { className: "text-[10px] font-normal text-muted-foreground", children: t("agentPanel.optional", { defaultValue: "Optional" }) })] }), endpointOpen ? (_jsx("div", { className: "mt-2 space-y-1.5", children: _jsx("input", { type: "url", value: endpoint, disabled: saving, spellCheck: false, autoComplete: "off", placeholder: active.endpointPlaceholder, onChange: (event) => setEndpoint(event.target.value), className: cn("w-full rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") }) })) : null] })) : null, _jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-0.5", children: [_jsx("button", { type: "submit", disabled: saving || Boolean(active.key && !apiKey.trim()), className: cn("inline-flex items-center justify-center gap-1.5 rounded-md bg-foreground font-medium text-background transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50", isPage ? "h-9 px-3 text-xs" : "h-8 px-3 text-[11px]"), children: saving ? (_jsxs(_Fragment, { children: [_jsx(IconLoader2, { size: isPage ? 14 : 11, className: "animate-spin" }), t("agentPanel.savingProvider", {
167
240
  defaultValue: "Saving...",
@@ -2,13 +2,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Checkbox, Picker, Skeleton, TextField, } from "@agent-native/toolkit/design-system";
3
3
  import { Button as ToolkitButton } from "@agent-native/toolkit/ui/button";
4
4
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@agent-native/toolkit/ui/command";
5
- import { IconChevronDown, IconChevronRight, IconCheck, IconExternalLink, IconBrain, IconHierarchy2, IconBrowser, IconGitBranch, IconCloud, IconDatabase, IconFolder, IconShield, IconPlugConnected, IconTopologyRing2, IconLoader2, IconUpload, IconCoin, IconMail, IconKey, IconMicrophone, IconEyeOff, IconBolt, IconGauge, IconApps, IconUsersGroup, IconTool, IconAlertCircle, } from "@tabler/icons-react";
5
+ import { IconChevronDown, IconChevronRight, IconCheck, IconExternalLink, IconBrain, IconHierarchy2, IconBrowser, IconGitBranch, IconCloud, IconDatabase, IconFolder, IconShield, IconPlugConnected, IconTopologyRing2, IconLoader2, IconUpload, IconCoin, IconMail, IconKey, IconMicrophone, IconEyeOff, IconBolt, IconGauge, IconApps, IconUsersGroup, IconTool, IconAlertCircle, IconSearch, } from "@tabler/icons-react";
6
6
  import React, { Suspense, lazy, useState, useEffect, useCallback, useMemo, useRef, } from "react";
7
7
  import { CHATGPT_SUBSCRIPTION_DEFAULT_MODEL, CHATGPT_SUBSCRIPTION_ENGINE_NAME, CHATGPT_SUBSCRIPTION_LAB_KEY, } from "../../agent/chatgpt-subscription-contract.js";
8
8
  import { PROVIDER_ENV_PLACEHOLDERS } from "../../agent/engine/provider-env-vars.js";
9
9
  import { buildSettingsRoute, STANDARD_APP_ROUTES, } from "../../navigation/index.js";
10
10
  import { docsUrl } from "../../shared/docs-url.js";
11
- import { saveAgentEngineProviderSettings, setAgentEngineProvider, } from "../agent-engine-key.js";
11
+ import { fetchOllamaModels, saveAgentEngineProviderSettings, setAgentEngineProvider, } from "../agent-engine-key.js";
12
12
  import { AgentWorkspaceContent } from "../agent-page/AgentWorkspaceContent.js";
13
13
  import { AGENT_PROVIDER_CATALOG, getAgentProviderOption, providerIdForEngine, } from "../agent-provider-catalog.js";
14
14
  import { agentNativePath, appMountedPath } from "../api-path.js";
@@ -393,6 +393,9 @@ function LLMSectionInner({ builderFlow, builderLoading, builderStatusAvailable,
393
393
  const [engineCatalogAvailable, setEngineCatalogAvailable] = useState(false);
394
394
  const [statusProbeAvailable, setStatusProbeAvailable] = useState(false);
395
395
  const probeGenerationRef = useRef({ env: 0, status: 0 });
396
+ const [ollamaModels, setOllamaModels] = useState(null);
397
+ const [ollamaModelsError, setOllamaModelsError] = useState(null);
398
+ const [ollamaModelsLoading, setOllamaModelsLoading] = useState(false);
396
399
  const initialLoading = !enginesLoaded || !!builderLoading;
397
400
  const refreshEnvKeys = useCallback(() => {
398
401
  const generation = ++probeGenerationRef.current.env;
@@ -576,8 +579,47 @@ function LLMSectionInner({ builderFlow, builderLoading, builderStatusAvailable,
576
579
  const manualSetupHint = "Select a provider.";
577
580
  const engineChanged = selectedEngine !== currentEngine || selectedModel !== currentModel;
578
581
  const isEndpointProvider = selectedProviderOption.supportsEndpoint === true;
582
+ const isOllama = selectedProvider === "ollama";
579
583
  const endpointChanged = isEndpointProvider && (!!baseUrl.trim() || clearBaseUrl);
580
584
  const providerSettingsChanged = !!apiKey.trim() || endpointChanged;
585
+ // Ask the Ollama server itself which models it has pulled, instead of only
586
+ // offering the static suggestion list. Triggered explicitly by the "Find
587
+ // models" button, mirroring the same flow in `AgentProviderSetupForm`.
588
+ const handleFindOllamaModels = () => {
589
+ setOllamaModelsLoading(true);
590
+ setOllamaModelsError(null);
591
+ const typedEndpoint = baseUrl.trim();
592
+ void fetchOllamaModels(typedEndpoint || undefined)
593
+ .then(async (models) => {
594
+ setOllamaModels(models);
595
+ setOllamaModelsError(null);
596
+ // A successful check is the only signal this address actually works;
597
+ // persist it immediately so other surfaces reading the saved Ollama
598
+ // endpoint (the chat composer's model picker) don't keep falling back
599
+ // to the localhost default until "Save" is also clicked.
600
+ if (typedEndpoint) {
601
+ try {
602
+ await saveAgentEngineProviderSettings({
603
+ provider: selectedProvider,
604
+ ...(envVar ? { key: envVar } : {}),
605
+ baseUrl: typedEndpoint,
606
+ });
607
+ setBaseUrlConfigured(true);
608
+ }
609
+ catch {
610
+ // coercion-ok: the connectivity check itself still succeeded and
611
+ // the found models are shown; the address just wasn't persisted
612
+ // (e.g. a dropped session). The "Save endpoint" button below
613
+ // retries it, so this is never reported as a clean success.
614
+ }
615
+ }
616
+ })
617
+ .catch((err) => {
618
+ setOllamaModels(null);
619
+ setOllamaModelsError(err instanceof Error ? err.message : String(err));
620
+ })
621
+ .finally(() => setOllamaModelsLoading(false));
622
+ };
581
623
  const modelOptions = (selectedEngineInfo?.supportedModels ?? []).map((m) => ({ value: m, label: friendlyModelName(m) }));
582
624
  const handleSave = async () => {
583
625
  if (!providerSettingsChanged || (!envVar && !isEndpointProvider))
@@ -735,7 +777,33 @@ function LLMSectionInner({ builderFlow, builderLoading, builderStatusAvailable,
735
777
  setApplyError(null);
736
778
  setApplyNote(false);
737
779
  setTestResult(null);
738
- } }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: fieldLabelClass(isPage), children: "Model" }), _jsx("input", { type: "text", list: `model-suggestions-${selectedEngine}`, value: selectedModel, onChange: (e) => {
780
+ setOllamaModels(null);
781
+ setOllamaModelsError(null);
782
+ } }), isOllama && isEndpointProvider ? (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "text-[12px] font-medium text-foreground", children: "Endpoint URL" }), _jsx("span", { className: "text-[10px] text-muted-foreground", children: baseUrlConfigured ? "Configured" : "Optional" })] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("input", { type: "url", value: baseUrl, onChange: (e) => {
783
+ const baseUrl = e.target.value;
784
+ setSelectionState((previous) => ({
785
+ ...previous,
786
+ baseUrl,
787
+ clearBaseUrl: baseUrl.trim()
788
+ ? false
789
+ : previous.clearBaseUrl,
790
+ }));
791
+ setOllamaModels(null);
792
+ setOllamaModelsError(null);
793
+ }, onKeyDown: (e) => {
794
+ if (e.key === "Enter")
795
+ void handleSave();
796
+ }, placeholder: baseUrlConfigured
797
+ ? "Leave blank to keep current endpoint"
798
+ : "http://localhost:11434 or local network address like http://192.168.1.123:11434", disabled: clearBaseUrl, spellCheck: false, autoComplete: "off", className: "flex h-9 min-w-0 flex-1 rounded-md border border-border bg-background px-3 text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-accent disabled:opacity-50 placeholder:text-muted-foreground/50", style: CONTROL_STYLE }), _jsxs(Button, { type: "button", intent: "neutral", emphasis: "outline", disabled: saving || ollamaModelsLoading || clearBaseUrl, onClick: handleFindOllamaModels, className: "inline-flex h-9 shrink-0 items-center gap-1.5 rounded-md border border-border bg-background px-2.5 text-[11px] font-medium text-foreground hover:bg-accent/40 disabled:cursor-not-allowed disabled:opacity-50", children: [ollamaModelsLoading ? (_jsx(IconLoader2, { size: 12, className: "animate-spin" })) : (_jsx(IconSearch, { size: 12 })), "Find models"] })] }), baseUrlConfigured && (_jsxs("label", { className: "flex items-center gap-1.5 text-[10px] text-muted-foreground", children: [_jsx(Checkbox, { checked: clearBaseUrl, onChange: (checked) => {
799
+ setSelectionState((previous) => ({
800
+ ...previous,
801
+ clearBaseUrl: checked,
802
+ baseUrl: checked ? "" : previous.baseUrl,
803
+ }));
804
+ }, "aria-label": "Clear saved endpoint override", className: "shrink-0" }), "Clear saved endpoint override"] })), endpointChanged && (_jsx(Button, { type: "button", intent: "neutral", emphasis: "solid", onClick: handleSave, disabled: saving, className: "rounded bg-accent px-2.5 py-1 text-[10px] font-medium text-foreground hover:bg-accent/80 disabled:opacity-40", children: saving ? (_jsx(IconLoader2, { size: 10, className: "animate-spin" })) : saved ? (_jsx(IconCheck, { size: 10 })) : ("Save endpoint") }))] })) : null, _jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: fieldLabelClass(isPage), children: "Model" }), _jsx("input", { type: "text", list: isOllama
805
+ ? undefined
806
+ : `model-suggestions-${selectedEngine}`, value: selectedModel, onChange: (e) => {
739
807
  const model = e.target.value;
740
808
  setSelectionState((previous) => ({
741
809
  ...previous,
@@ -744,9 +812,25 @@ function LLMSectionInner({ builderFlow, builderLoading, builderStatusAvailable,
744
812
  setApplyError(null);
745
813
  setApplyNote(false);
746
814
  setTestResult(null);
747
- }, placeholder: selectedEngineInfo?.defaultModel ?? "e.g. model-id", spellCheck: false, autoComplete: "off", className: textInputClass(isPage), style: isPage ? CONTROL_STYLE_PAGE : CONTROL_STYLE }), modelOptions.length > 0 && (_jsx("datalist", { id: `model-suggestions-${selectedEngine}`, children: modelOptions.map((opt) => (_jsx("option", { value: opt.value, label: opt.label }, opt.value))) }))] }), isEndpointProvider && (_jsxs("div", { className: "border-t border-border/70 pt-2", children: [_jsxs(Button, { type: "button", onClick: () => setAdvancedOpen((v) => !v), className: "flex w-full cursor-pointer items-center justify-between gap-2 rounded px-0.5 py-1 text-left hover:text-foreground", children: [_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5 text-[11px] font-medium text-foreground", children: [advancedOpen ? (_jsx(IconChevronDown, { size: 12 })) : (_jsx(IconChevronRight, { size: 12, className: "rtl:-scale-x-100" })), "Advanced"] }), _jsx("span", { className: "truncate text-[10px] text-muted-foreground", children: selectedProvider === "ollama"
748
- ? "Local Ollama endpoint"
749
- : "OpenAI-compatible endpoint" })] }), advancedOpen && (_jsxs("div", { className: "mt-1.5 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "text-[12px] font-medium text-foreground", children: "Endpoint URL" }), _jsx("span", { className: "text-[10px] text-muted-foreground", children: baseUrlConfigured ? "Configured" : "Optional" })] }), _jsx("input", { type: "url", value: baseUrl, onChange: (e) => {
815
+ }, placeholder: selectedEngineInfo?.defaultModel ?? "e.g. model-id", spellCheck: false, autoComplete: "off", className: textInputClass(isPage), style: isPage ? CONTROL_STYLE_PAGE : CONTROL_STYLE }), !isOllama && modelOptions.length > 0 && (_jsx("datalist", { id: `model-suggestions-${selectedEngine}`, children: modelOptions.map((opt) => (_jsx("option", { value: opt.value, label: opt.label }, opt.value))) })), isOllama ? (_jsxs("div", { className: "space-y-1.5", children: [ollamaModels && ollamaModels.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1.5", children: ollamaModels.map((modelOption) => (_jsx(Button, { type: "button", disabled: saving, onClick: () => {
816
+ setSelectionState((previous) => ({
817
+ ...previous,
818
+ selectedModel: modelOption,
819
+ }));
820
+ setApplyError(null);
821
+ setApplyNote(false);
822
+ setTestResult(null);
823
+ }, "aria-pressed": selectedModel === modelOption, className: cn("rounded-md border px-2 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50", selectedModel === modelOption
824
+ ? "border-primary bg-primary/10 text-primary"
825
+ : "border-border bg-background text-foreground hover:bg-accent/40"), children: modelOption }, modelOption))) })) : null, _jsx("p", { className: "text-[10px] leading-relaxed text-muted-foreground", children: ollamaModelsLoading
826
+ ? "Checking installed models…"
827
+ : ollamaModels && ollamaModels.length > 0
828
+ ? `Found ${ollamaModels.length} installed model${ollamaModels.length === 1 ? "" : "s"}.`
829
+ : ollamaModels
830
+ ? "Connected, but no models are pulled yet — run `ollama pull llama3.1`."
831
+ : ollamaModelsError
832
+ ? `${ollamaModelsError} Showing example model names below.`
833
+ : 'Click "Find models" above to list what your Ollama server actually has installed.' })] })) : null] }), !isOllama && isEndpointProvider && (_jsxs("div", { className: "border-t border-border/70 pt-2", children: [_jsxs(Button, { type: "button", onClick: () => setAdvancedOpen((v) => !v), className: "flex w-full cursor-pointer items-center justify-between gap-2 rounded px-0.5 py-1 text-left hover:text-foreground", children: [_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5 text-[11px] font-medium text-foreground", children: [advancedOpen ? (_jsx(IconChevronDown, { size: 12 })) : (_jsx(IconChevronRight, { size: 12, className: "rtl:-scale-x-100" })), "Advanced"] }), _jsx("span", { className: "truncate text-[10px] text-muted-foreground", children: "OpenAI-compatible endpoint" })] }), advancedOpen && (_jsxs("div", { className: "mt-1.5 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "text-[12px] font-medium text-foreground", children: "Endpoint URL" }), _jsx("span", { className: "text-[10px] text-muted-foreground", children: baseUrlConfigured ? "Configured" : "Optional" })] }), _jsx("input", { type: "url", value: baseUrl, onChange: (e) => {
750
834
  const baseUrl = e.target.value;
751
835
  setSelectionState((previous) => ({
752
836
  ...previous,
@@ -760,9 +844,7 @@ function LLMSectionInner({ builderFlow, builderLoading, builderStatusAvailable,
760
844
  void handleSave();
761
845
  }, placeholder: baseUrlConfigured
762
846
  ? "Leave blank to keep current endpoint"
763
- : selectedProvider === "ollama"
764
- ? "http://localhost:11434"
765
- : "https://gateway.example/v1", disabled: clearBaseUrl, spellCheck: false, autoComplete: "off", className: "flex h-9 w-full rounded-md border border-border bg-background px-3 text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-accent disabled:opacity-50 placeholder:text-muted-foreground/50", style: CONTROL_STYLE }), baseUrlConfigured && (_jsxs("label", { className: "flex items-center gap-1.5 text-[10px] text-muted-foreground", children: [_jsx(Checkbox, { checked: clearBaseUrl, onChange: (checked) => {
847
+ : "https://gateway.example/v1", disabled: clearBaseUrl, spellCheck: false, autoComplete: "off", className: "flex h-9 w-full rounded-md border border-border bg-background px-3 text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-accent disabled:opacity-50 placeholder:text-muted-foreground/50", style: CONTROL_STYLE }), baseUrlConfigured && (_jsxs("label", { className: "flex items-center gap-1.5 text-[10px] text-muted-foreground", children: [_jsx(Checkbox, { checked: clearBaseUrl, onChange: (checked) => {
766
848
  setSelectionState((previous) => ({
767
849
  ...previous,
768
850
  clearBaseUrl: checked,
@@ -793,7 +875,34 @@ function friendlyAppName(appId) {
793
875
  }
794
876
  function AppDefaultModelPicker({ engines, value, disabled, onChange, }) {
795
877
  const [open, setOpen] = useState(false);
878
+ const [ollamaModels, setOllamaModels] = useState(null);
796
879
  const visibleEngines = engines.filter((engine) => engine.name !== "ai-sdk:anthropic");
880
+ // The static catalog only has the curated suggestion models for Ollama.
881
+ // Ask the configured Ollama server what it actually has installed and
882
+ // swap those in — a second, later render, so it never blocks this
883
+ // picker's first paint on a local network round trip. Gated on the
884
+ // popover actually being opened (a deliberate user action), not merely
885
+ // Ollama's presence in the catalog, which it always has by default —
886
+ // an unconditional probe would 502 for the vast majority of setups that
887
+ // never touched Ollama and never even open this picker.
888
+ useEffect(() => {
889
+ if (!open)
890
+ return;
891
+ if (!engines.some((engine) => engine.name === "ai-sdk:ollama"))
892
+ return;
893
+ let cancelled = false;
894
+ void fetchOllamaModels()
895
+ .then((models) => {
896
+ if (!cancelled && models.length > 0)
897
+ setOllamaModels(models);
898
+ })
899
+ .catch(() => {
900
+ // No local Ollama server reachable — keep the static suggestions.
901
+ });
902
+ return () => {
903
+ cancelled = true;
904
+ };
905
+ }, [open, engines]);
797
906
  const selectedModel = value.includes("::")
798
907
  ? value.slice(value.indexOf("::") + 2)
799
908
  : null;
@@ -817,7 +926,9 @@ function AppDefaultModelPicker({ engines, value, disabled, onChange, }) {
817
926
  const providerLabel = engine.name === "builder"
818
927
  ? "Builder.io"
819
928
  : engine.label || engine.name;
820
- const modelIds = latestModelsOnly(engine.supportedModels);
929
+ const modelIds = engine.name === "ai-sdk:ollama" && ollamaModels?.length
930
+ ? ollamaModels
931
+ : latestModelsOnly(engine.supportedModels);
821
932
  const models = modelIds.length
822
933
  ? modelIds
823
934
  : engine.defaultModel
@@ -842,6 +953,7 @@ function AppModelDefaultsSectionInner({ grouped = false, open, onToggle, }) {
842
953
  const [saving, setSaving] = useState(false);
843
954
  const [saved, setSaved] = useState(false);
844
955
  const [error, setError] = useState(null);
956
+ const [ollamaModels, setOllamaModels] = useState(null);
845
957
  const load = useCallback(() => {
846
958
  let cancelled = false;
847
959
  setLoading(true);
@@ -868,9 +980,35 @@ function AppModelDefaultsSectionInner({ grouped = false, open, onToggle, }) {
868
980
  };
869
981
  }, []);
870
982
  useEffect(() => load(), [load]);
983
+ // The static catalog only has the curated suggestion models for Ollama.
984
+ // Ask the configured Ollama server what it actually has installed and
985
+ // swap those in — a second, later render, so it never blocks this
986
+ // section's first paint on a local network round trip. Gated on Ollama
987
+ // actually being the selected engine here (not merely present in the
988
+ // catalog, which it always is by default) — an unconditional probe would
989
+ // 502 for the vast majority of setups that never touched Ollama.
990
+ useEffect(() => {
991
+ if (selectedEngine !== "ai-sdk:ollama")
992
+ return;
993
+ let cancelled = false;
994
+ void fetchOllamaModels()
995
+ .then((models) => {
996
+ if (!cancelled && models.length > 0)
997
+ setOllamaModels(models);
998
+ })
999
+ .catch(() => {
1000
+ // No local Ollama server reachable — keep the static suggestions.
1001
+ });
1002
+ return () => {
1003
+ cancelled = true;
1004
+ };
1005
+ }, [selectedEngine]);
871
1006
  if (!loading && !settings)
872
1007
  return null;
873
1008
  const selectedEngineInfo = settings?.engines.find((engine) => engine.name === selectedEngine) ?? null;
1009
+ const selectedEngineModels = selectedEngine === "ai-sdk:ollama" && ollamaModels?.length
1010
+ ? ollamaModels
1011
+ : (selectedEngineInfo?.supportedModels ?? []);
874
1012
  const engineOptions = (settings?.engines ?? [])
875
1013
  .filter((engine) => engine.name === selectedEngine || engine.name !== "ai-sdk:anthropic")
876
1014
  .map((engine) => ({
@@ -990,7 +1128,7 @@ function AppModelDefaultsSectionInner({ grouped = false, open, onToggle, }) {
990
1128
  const info = settings.engines.find((engine) => engine.name === value);
991
1129
  setSelectedModel(info?.defaultModel ?? "");
992
1130
  setError(null);
993
- } }), _jsx(AppDefaultModelField, { engine: selectedEngine, models: selectedEngineInfo?.supportedModels ?? [], value: selectedModel, defaultModel: selectedEngineInfo?.defaultModel, disabled: !settings.canUpdate || saving, onValueChange: (value) => {
1131
+ } }), _jsx(AppDefaultModelField, { engine: selectedEngine, models: selectedEngineModels, value: selectedModel, defaultModel: selectedEngineInfo?.defaultModel, disabled: !settings.canUpdate || saving, onValueChange: (value) => {
994
1132
  setSelectedModel(value);
995
1133
  setError(null);
996
1134
  }, onEnter: () => {
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { DEFAULT_MODEL } from "../agent/default-model.js";
3
3
  import { DEFAULT_REASONING_EFFORT, getReasoningEffortOptionsForModel, resolveReasoningEffortSelection, } from "../shared/reasoning-effort.js";
4
+ import { fetchOllamaModels } from "./agent-engine-key.js";
4
5
  import { buildChatModelGroups, } from "./chat-model-groups.js";
5
6
  import { fetchBuilderStatus, fetchEnvironmentStatus, } from "./client-status-requests.js";
6
7
  import { callAction } from "./use-action.js";
@@ -201,6 +202,36 @@ export function useChatModels({ storageKey = DEFAULT_STORAGE_KEY, enabled = true
201
202
  const nextDefaultModel = currentModel ?? DEFAULT_MODEL;
202
203
  setAvailableModels(groups);
203
204
  setDefaultModel(nextDefaultModel);
205
+ // The static catalog only has the curated suggestion models for
206
+ // Ollama. Once the engine list is in, ask the configured Ollama
207
+ // server what it actually has installed and swap those in — a
208
+ // second, later render, so it never blocks the picker's first
209
+ // paint on a local network round trip. Gated on Ollama actually
210
+ // being the current engine (not merely present in the catalog,
211
+ // which it always is): every app registers it by default, so an
212
+ // unconditional probe would 502 on every chat load for the vast
213
+ // majority of setups that never touched Ollama.
214
+ if (currentEngineName === "ai-sdk:ollama") {
215
+ void fetchOllamaModels()
216
+ .then((liveModels) => {
217
+ if (!isCurrentRefresh() || liveModels.length === 0)
218
+ return;
219
+ const liveEngines = enginesData.engines.map((engine) => engine.name === "ai-sdk:ollama"
220
+ ? { ...engine, supportedModels: liveModels }
221
+ : engine);
222
+ setAvailableModels(buildChatModelGroups({
223
+ engines: liveEngines,
224
+ configuredKeys,
225
+ builderConnected,
226
+ currentEngineName,
227
+ currentModel,
228
+ }));
229
+ })
230
+ .catch(() => {
231
+ // No local Ollama server reachable — keep the static
232
+ // suggestion list already rendered above.
233
+ });
234
+ }
204
235
  const selection = selectionRef.current;
205
236
  // Default only to a CONFIGURED group, and to nothing when there is
206
237
  // none. `DEFAULT_MODEL` is a builder-gateway id that no group carries
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -44,9 +44,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
44
44
  url: string;
45
45
  id: string;
46
46
  provider: string;
47
+ error?: undefined;
47
48
  configured?: undefined;
48
49
  connectPath?: undefined;
49
- error?: undefined;
50
50
  } | {
51
51
  url?: undefined;
52
52
  id?: undefined;
@@ -643,8 +643,14 @@ declare const messages: {
643
643
  modelId: string;
644
644
  endpointUrl: string;
645
645
  optional: string;
646
- ollamaEndpointHint: string;
647
646
  compatibleEndpointHint: string;
647
+ findModels: string;
648
+ ollamaModelsChecking: string;
649
+ ollamaModelsFound_one: string;
650
+ ollamaModelsFound_other: string;
651
+ ollamaModelsNone: string;
652
+ ollamaModelsError: string;
653
+ ollamaModelsPrompt: string;
648
654
  savingProvider: string;
649
655
  providerSaved: string;
650
656
  useProvider: string;
@@ -650,8 +650,14 @@ const messages = {
650
650
  modelId: "Model ID",
651
651
  endpointUrl: "Endpoint URL",
652
652
  optional: "Optional",
653
- ollamaEndpointHint: "Defaults to Ollama at http://localhost:11434.",
654
653
  compatibleEndpointHint: "Use this for LiteLLM or another OpenAI-compatible gateway.",
654
+ findModels: "Find models",
655
+ ollamaModelsChecking: "Checking installed models…",
656
+ ollamaModelsFound_one: "Found {{count}} installed model.",
657
+ ollamaModelsFound_other: "Found {{count}} installed models.",
658
+ ollamaModelsNone: "Connected, but no models are pulled yet — run `ollama pull llama3.1`.",
659
+ ollamaModelsError: "{{error}} Showing example model names below.",
660
+ ollamaModelsPrompt: 'Click "Find models" above to list what your Ollama server actually has installed.',
655
661
  savingProvider: "Saving...",
656
662
  providerSaved: "Connected",
657
663
  useProvider: "Use {{provider}}",
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- count?: undefined;
20
19
  updated: number;
21
20
  error?: undefined;
22
21
  ok?: undefined;
23
- } | {
24
22
  count?: undefined;
23
+ } | {
25
24
  updated?: undefined;
26
25
  error: string;
27
26
  ok?: undefined;
28
- } | {
29
27
  count?: undefined;
28
+ } | {
30
29
  updated?: undefined;
31
- error?: undefined;
32
30
  ok: boolean;
31
+ error?: undefined;
32
+ count?: undefined;
33
33
  }>>;
@@ -41,11 +41,11 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ id?: undefined;
44
45
  error?: undefined;
45
46
  ok?: undefined;
46
47
  summary: import("./types.js").TraceSummary;
47
48
  spans: import("./types.js").TraceSpan[];
48
- id?: undefined;
49
49
  } | {
50
50
  error?: undefined;
51
51
  ok?: undefined;
@@ -53,15 +53,15 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
53
53
  spans?: undefined;
54
54
  id: string;
55
55
  } | {
56
+ id?: undefined;
56
57
  ok?: undefined;
57
58
  summary?: undefined;
58
59
  spans?: undefined;
59
- id?: undefined;
60
60
  error: any;
61
61
  } | {
62
+ id?: undefined;
62
63
  error?: undefined;
63
64
  summary?: undefined;
64
65
  spans?: undefined;
65
- id?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -136,8 +136,8 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
136
136
  export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
137
137
  name?: undefined;
138
138
  success: boolean;
139
- id?: undefined;
140
139
  role?: undefined;
140
+ id?: undefined;
141
141
  } | {
142
142
  success?: undefined;
143
143
  id: string;