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

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 (63) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/delete-event.ts +6 -4
  3. package/corpus/templates/calendar/actions/delete-events.ts +10 -11
  4. package/corpus/templates/calendar/actions/event-action-helpers.ts +53 -0
  5. package/corpus/templates/calendar/actions/get-event.ts +59 -14
  6. package/corpus/templates/calendar/actions/rsvp-event.ts +5 -3
  7. package/corpus/templates/calendar/actions/update-event.ts +15 -7
  8. package/corpus/templates/calendar/actions/update-events.ts +29 -11
  9. package/corpus/templates/calendar/changelog/2026-09-21-events-stay-editable-when-their-calendar-is-connected-throug.md +6 -0
  10. package/corpus/templates/calendar/server/lib/google-calendar.ts +70 -18
  11. package/corpus/templates/calendar/shared/google-calendar-sources.ts +47 -0
  12. package/corpus/templates/clips/actions/finalize-recording.ts +35 -1
  13. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
  14. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
  15. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
  16. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  17. package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
  18. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
  19. package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +9 -1
  20. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +16 -2
  21. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +61 -40
  22. package/corpus/templates/content/app/i18n-data.ts +12 -0
  23. package/corpus/templates/content/app/routes/_app.settings.tsx +36 -9
  24. package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +1 -1
  25. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
  26. package/corpus/templates/content/shared/labs.ts +35 -1
  27. package/corpus/templates/slides/actions/get-design-system.ts +4 -2
  28. package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
  29. package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
  30. package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
  31. package/dist/agent/engine/provider-endpoint-validation.js +47 -10
  32. package/dist/agent/engine/registry.js +8 -5
  33. package/dist/automation/index.d.ts +1 -1
  34. package/dist/cli/skills.d.ts +3 -0
  35. package/dist/cli/skills.js +45 -9
  36. package/dist/client/agent-engine-key.d.ts +7 -0
  37. package/dist/client/agent-engine-key.js +37 -0
  38. package/dist/client/agent-provider-catalog.js +1 -1
  39. package/dist/client/settings/ProviderSetupForm.js +80 -7
  40. package/dist/client/settings/SettingsPanel.js +149 -11
  41. package/dist/client/use-chat-models.js +31 -0
  42. package/dist/collab/awareness.d.ts +2 -2
  43. package/dist/collab/routes.d.ts +1 -1
  44. package/dist/labs/actions/set-lab.js +1 -0
  45. package/dist/labs/registry.js +6 -1
  46. package/dist/localization/default-messages.d.ts +7 -1
  47. package/dist/localization/default-messages.js +7 -1
  48. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  49. package/dist/notifications/routes.d.ts +3 -3
  50. package/dist/observability/routes.d.ts +3 -3
  51. package/dist/org/index.d.ts +1 -1
  52. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  53. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  54. package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
  55. package/dist/secrets/routes.d.ts +3 -3
  56. package/dist/server/agent-engine-api-key-route.d.ts +3 -3
  57. package/dist/server/agent-engine-api-key-route.js +8 -5
  58. package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
  59. package/dist/server/agent-engine-ollama-models-route.js +105 -0
  60. package/dist/server/core-routes-plugin.js +5 -0
  61. package/dist/server/credential-provider.d.ts +13 -0
  62. package/dist/server/credential-provider.js +19 -0
  63. package/package.json +3 -3
@@ -4,3 +4,10 @@ export declare const OLLAMA_BASE_URL_ENV_VAR = "OLLAMA_BASE_URL";
4
4
  export declare function isCustomOpenAiBaseUrl(value: string | undefined): boolean;
5
5
  export declare function normalizeProviderBaseUrl(value: string): string;
6
6
  export declare function normalizeOpenAiBaseUrl(value: string): string;
7
+ /**
8
+ * Ollama's own API has no `/v1` path — that suffix only applies to its
9
+ * OpenAI-compatibility shim, which this integration doesn't use. Docs for
10
+ * other tools commonly show `/v1`, so users copy-paste it in; strip it
11
+ * silently rather than making them notice and remove it themselves.
12
+ */
13
+ export declare function stripOllamaV1Suffix(value: string): string;
@@ -28,3 +28,12 @@ export function normalizeProviderBaseUrl(value) {
28
28
  export function normalizeOpenAiBaseUrl(value) {
29
29
  return normalizeProviderBaseUrl(value);
30
30
  }
31
+ /**
32
+ * Ollama's own API has no `/v1` path — that suffix only applies to its
33
+ * OpenAI-compatibility shim, which this integration doesn't use. Docs for
34
+ * other tools commonly show `/v1`, so users copy-paste it in; strip it
35
+ * silently rather than making them notice and remove it themselves.
36
+ */
37
+ export function stripOllamaV1Suffix(value) {
38
+ return value.replace(/\/v1$/i, "");
39
+ }
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * Validate a provider endpoint before a server-side model request can use it.
3
3
  * `allowPrivate` is reserved for operator-owned deployment configuration; it
4
- * must never be enabled for a user- or agent-supplied URL.
4
+ * must never be enabled for a user- or agent-supplied URL. `allowLocalOllama`
5
+ * additionally requires the caller to have already established this is a
6
+ * trusted, self-hosted context (see `isTrustedSelfHostedRuntime`) — it is
7
+ * never safe to grant purely from the endpoint value itself. `isOllama` says
8
+ * this endpoint is for Ollama specifically (independent of trust level), so a
9
+ * copy-pasted trailing `/v1` — meaningless for Ollama's own API — is silently
10
+ * removed instead of left to confuse the address that gets saved and used.
5
11
  */
6
12
  export declare function validateProviderBaseUrl(value: string, options?: {
7
13
  allowPrivate?: boolean;
8
14
  allowLocalOllama?: boolean;
15
+ isOllama?: boolean;
9
16
  }): Promise<string>;
@@ -1,20 +1,57 @@
1
1
  import { isBlockedExtensionUrlWithDns } from "../../extensions/url-safety.js";
2
- import { normalizeProviderBaseUrl } from "./openai-compatible-endpoint.js";
3
- function isLoopbackOllamaEndpoint(value) {
4
- const hostname = new URL(value).hostname.toLowerCase();
5
- return (hostname === "localhost" ||
6
- hostname === "127.0.0.1" ||
7
- hostname === "::1" ||
8
- hostname === "[::1]");
2
+ import { normalizeProviderBaseUrl, stripOllamaV1Suffix, } from "./openai-compatible-endpoint.js";
3
+ function isPrivateIpv4Lan(a, b, c, d) {
4
+ if (![a, b, c, d].every((part) => part >= 0 && part <= 255))
5
+ return false;
6
+ if (a === 127)
7
+ return true; // loopback
8
+ if (a === 10)
9
+ return true; // 10.0.0.0/8
10
+ if (a === 172 && b >= 16 && b <= 31)
11
+ return true; // 172.16.0.0/12
12
+ if (a === 192 && b === 168)
13
+ return true; // 192.168.0.0/16
14
+ return false;
15
+ }
16
+ /**
17
+ * True for hosts an Ollama server commonly runs on: loopback, or an RFC1918
18
+ * LAN address such as `192.168.1.123`. Deliberately narrower than the
19
+ * generic SSRF private-address check in `url-safety.ts` — link-local
20
+ * addresses (169.254.x.x, which also covers the cloud metadata endpoint) and
21
+ * other reserved ranges stay blocked even for Ollama. Only literal IPs and
22
+ * `localhost` are recognized; an arbitrary hostname that merely resolves to
23
+ * a LAN address is not, so this never depends on a DNS lookup.
24
+ */
25
+ function isLocalNetworkOllamaEndpoint(value) {
26
+ const hostname = new URL(value).hostname
27
+ .toLowerCase()
28
+ .replace(/^\[|\]$/g, "");
29
+ if (hostname === "localhost" || hostname === "::1")
30
+ return true;
31
+ const parts = hostname.split(".");
32
+ if (parts.length === 4 && parts.every((p) => /^\d+$/.test(p))) {
33
+ const [a, b, c, d] = parts.map(Number);
34
+ return isPrivateIpv4Lan(a, b, c, d);
35
+ }
36
+ return false;
9
37
  }
10
38
  /**
11
39
  * Validate a provider endpoint before a server-side model request can use it.
12
40
  * `allowPrivate` is reserved for operator-owned deployment configuration; it
13
- * must never be enabled for a user- or agent-supplied URL.
41
+ * must never be enabled for a user- or agent-supplied URL. `allowLocalOllama`
42
+ * additionally requires the caller to have already established this is a
43
+ * trusted, self-hosted context (see `isTrustedSelfHostedRuntime`) — it is
44
+ * never safe to grant purely from the endpoint value itself. `isOllama` says
45
+ * this endpoint is for Ollama specifically (independent of trust level), so a
46
+ * copy-pasted trailing `/v1` — meaningless for Ollama's own API — is silently
47
+ * removed instead of left to confuse the address that gets saved and used.
14
48
  */
15
49
  export async function validateProviderBaseUrl(value, options = {}) {
16
- const normalized = normalizeProviderBaseUrl(value);
17
- const allowLocalOllama = options.allowLocalOllama === true && isLoopbackOllamaEndpoint(normalized);
50
+ let normalized = normalizeProviderBaseUrl(value);
51
+ if (options.isOllama)
52
+ normalized = stripOllamaV1Suffix(normalized);
53
+ const allowLocalOllama = options.allowLocalOllama === true &&
54
+ isLocalNetworkOllamaEndpoint(normalized);
18
55
  if (!options.allowPrivate &&
19
56
  !allowLocalOllama &&
20
57
  (await isBlockedExtensionUrlWithDns(normalized))) {
@@ -12,7 +12,7 @@ import { getAppConfig } from "../../app-config/index.js";
12
12
  import { getUserLabs } from "../../labs/store.js";
13
13
  import { BUILDER_OAUTH_SCOPE, hasBuilderOAuthSession, resolveBuilderOAuthRequestAccess, } from "../../server/builder-oauth.js";
14
14
  import { hasChatGPTSubscriptionCredential } from "../../server/chatgpt-subscription-oauth.js";
15
- import { assertCredentialStoreReadable, canUseDeployCredentialFallbackForRequest, getBuilderCredentialAuthFailure, getProviderCredentialAuthFailure, prefetchSecrets, readDeployCredentialEnv, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveSecret, } from "../../server/credential-provider.js";
15
+ import { assertCredentialStoreReadable, canUseDeployCredentialFallbackForRequest, getBuilderCredentialAuthFailure, getProviderCredentialAuthFailure, isTrustedSelfHostedRuntime, prefetchSecrets, readDeployCredentialEnv, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveSecret, } from "../../server/credential-provider.js";
16
16
  import { getRequestOrgId, getRequestContext, getRequestUserEmail, } from "../../server/request-context.js";
17
17
  import { getSetting } from "../../settings/store.js";
18
18
  import { getAgentAppModelDefaultForCurrentRequest } from "../app-model-defaults.js";
@@ -625,11 +625,13 @@ async function resolveProviderBaseUrl(envVar) {
625
625
  const deployValue = canUseDeployCredentialFallbackForRequest(envVar)
626
626
  ? readDeployCredentialEnv(envVar)
627
627
  : undefined;
628
+ const isOllama = envVar === OLLAMA_BASE_URL_ENV_VAR;
628
629
  if (!raw) {
629
630
  if (!deployValue)
630
631
  return undefined;
631
632
  return validateProviderBaseUrl(deployValue, {
632
633
  allowPrivate: true,
634
+ isOllama,
633
635
  });
634
636
  }
635
637
  return raw
@@ -639,8 +641,8 @@ async function resolveProviderBaseUrl(envVar) {
639
641
  // allowance as the explicit deploy-only branch above without extending
640
642
  // it to user-, org-, or workspace-scoped endpoint values.
641
643
  allowPrivate: deployValue !== undefined && raw === deployValue,
642
- allowLocalOllama: envVar === OLLAMA_BASE_URL_ENV_VAR &&
643
- process.env.NODE_ENV !== "production",
644
+ allowLocalOllama: isOllama && isTrustedSelfHostedRuntime(),
645
+ isOllama,
644
646
  })
645
647
  : undefined;
646
648
  }
@@ -815,9 +817,10 @@ async function engineCreateConfigForEntry(entry, apiKey, extra, credentialResolu
815
817
  }
816
818
  if (entry.name === "ai-sdk:openai" || entry.name === "ai-sdk:ollama") {
817
819
  if (typeof safeExtra.baseURL === "string" && safeExtra.baseUrl == null) {
820
+ const isOllama = entry.name === "ai-sdk:ollama";
818
821
  safeExtra.baseUrl = await validateProviderBaseUrl(safeExtra.baseURL, {
819
- allowLocalOllama: entry.name === "ai-sdk:ollama" &&
820
- process.env.NODE_ENV !== "production",
822
+ allowLocalOllama: isOllama && isTrustedSelfHostedRuntime(),
823
+ isOllama,
821
824
  });
822
825
  }
823
826
  if (safeExtra.baseUrl == null) {
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
186
186
  duplicate?: undefined;
187
187
  eventId?: undefined;
188
188
  } | {
189
- error?: undefined;
190
189
  accepted: boolean;
191
190
  duplicate: boolean;
192
191
  eventId: string;
192
+ error?: undefined;
193
193
  }>;
@@ -189,6 +189,7 @@ export type SkillsCatalogMode = "agent-native" | "all";
189
189
  export interface PublicSkillCatalogEntry {
190
190
  name: string;
191
191
  description?: string;
192
+ installerGroup?: string;
192
193
  }
193
194
  interface ConnectSpinner {
194
195
  start(message?: string): void;
@@ -263,7 +264,9 @@ interface SkillsClientPromptContext {
263
264
  installsMcp: boolean;
264
265
  }
265
266
  interface SkillsTargetPromptContext {
267
+ message?: string;
266
268
  initialTargets: string[];
269
+ required?: boolean;
267
270
  options: Array<{
268
271
  value: string;
269
272
  label: string;
@@ -1808,6 +1808,7 @@ function publicSkillEntries(options) {
1808
1808
  .map((entry) => ({
1809
1809
  name: entry.name.trim().toLowerCase(),
1810
1810
  description: entry.description,
1811
+ installerGroup: entry.installerGroup?.trim().toLowerCase(),
1811
1812
  }))
1812
1813
  .filter((entry) => {
1813
1814
  if (!entry.name || isKnownSkill(entry.name) || seen.has(entry.name)) {
@@ -1822,11 +1823,16 @@ function publicSkillNames(options) {
1822
1823
  return new Set(publicSkillEntries(options).map((entry) => entry.name));
1823
1824
  }
1824
1825
  function publicSkillPromptOptions(options) {
1825
- return publicSkillEntries(options).map((entry) => ({
1826
- value: entry.name,
1827
- label: entry.name,
1828
- hint: entry.description ?? "Public skill from the BuilderIO skills catalog.",
1829
- }));
1826
+ return publicSkillEntries(options).map((entry) => {
1827
+ const description = entry.description ?? "Public skill from the BuilderIO skills catalog.";
1828
+ return {
1829
+ value: entry.name,
1830
+ label: entry.name,
1831
+ hint: entry.installerGroup === entry.name
1832
+ ? `${description} Select to choose the full group or a subset.`
1833
+ : description,
1834
+ };
1835
+ });
1830
1836
  }
1831
1837
  function skillPromptOptions(options = {}) {
1832
1838
  return [
@@ -1999,11 +2005,12 @@ async function promptForUpdateInstructions() {
1999
2005
  async function promptForSkills(context) {
2000
2006
  const clack = await import("@clack/prompts");
2001
2007
  const result = await clack.multiselect({
2002
- message: "Which Agent-Native skills do you want to install?\n" +
2003
- " (space toggles, enter confirms)",
2008
+ message: context.message ??
2009
+ "Which Agent-Native skills do you want to install?\n" +
2010
+ " (space toggles, enter confirms)",
2004
2011
  options: context.options,
2005
2012
  initialValues: context.initialTargets,
2006
- required: true,
2013
+ required: context.required ?? true,
2007
2014
  });
2008
2015
  if (clack.isCancel(result)) {
2009
2016
  clack.cancel("Cancelled.");
@@ -2149,7 +2156,36 @@ async function resolveSkillTargets(parsed, options) {
2149
2156
  });
2150
2157
  if (!selected || selected.length === 0)
2151
2158
  return null;
2152
- return resolveSelectedSkillTargets(selected, options);
2159
+ const publicEntries = publicSkillEntries(options);
2160
+ const selectedTargets = new Set(selected);
2161
+ for (const group of publicEntries.filter((entry) => entry.installerGroup === entry.name && selected.includes(entry.name))) {
2162
+ const members = publicEntries.filter((entry) => entry.installerGroup === group.name && entry.name !== group.name);
2163
+ if (members.length === 0)
2164
+ continue;
2165
+ const groupLabel = group.name.replace(/^./, (character) => character.toUpperCase());
2166
+ const groupSelection = await prompt({
2167
+ message: `Which ${groupLabel} skills do you want to install?\n` +
2168
+ " (all selected; deselect any you want to skip)",
2169
+ initialTargets: members.map((entry) => entry.name),
2170
+ required: false,
2171
+ options: members.map((entry) => ({
2172
+ value: entry.name,
2173
+ label: entry.name,
2174
+ hint: entry.description ??
2175
+ "Public skill from the BuilderIO skills catalog.",
2176
+ })),
2177
+ });
2178
+ if (!groupSelection)
2179
+ return null;
2180
+ const memberNames = new Set(members.map((entry) => entry.name));
2181
+ for (const memberName of memberNames)
2182
+ selectedTargets.delete(memberName);
2183
+ for (const memberName of groupSelection) {
2184
+ if (memberNames.has(memberName))
2185
+ selectedTargets.add(memberName);
2186
+ }
2187
+ }
2188
+ return resolveSelectedSkillTargets([...selectedTargets], options);
2153
2189
  }
2154
2190
  export function parseSkillsArgs(argv) {
2155
2191
  const first = argv[0];
@@ -53,6 +53,13 @@ export declare function saveAgentEngineApiKey({ provider, key, apiKey, scope, }:
53
53
  * endpoint URLs. Values are stored server-side in scoped secrets.
54
54
  */
55
55
  export declare function saveAgentEngineProviderSettings({ provider, key, apiKey, baseUrl, clearBaseUrl, }: SaveAgentEngineProviderSettingsOptions): Promise<void>;
56
+ /**
57
+ * List the models an Ollama server actually has installed, via its native
58
+ * `/api/tags` endpoint. Pass `baseUrl` to check a server before it's saved
59
+ * (e.g. while the user is still typing the endpoint); omit it to use the
60
+ * saved endpoint. Throws a readable Error if the server can't be reached.
61
+ */
62
+ export declare function fetchOllamaModels(baseUrl?: string): Promise<string[]>;
56
63
  /**
57
64
  * Select the provider and model for the next conversation. This is separate
58
65
  * from saving credentials so keyless local providers such as Ollama can use
@@ -201,6 +201,43 @@ export async function saveAgentEngineProviderSettings({ provider, key, apiKey, b
201
201
  }
202
202
  dispatchConfiguredChanged();
203
203
  }
204
+ /**
205
+ * List the models an Ollama server actually has installed, via its native
206
+ * `/api/tags` endpoint. Pass `baseUrl` to check a server before it's saved
207
+ * (e.g. while the user is still typing the endpoint); omit it to use the
208
+ * saved endpoint. Throws a readable Error if the server can't be reached.
209
+ */
210
+ export async function fetchOllamaModels(baseUrl) {
211
+ const trimmed = baseUrl?.trim() ?? "";
212
+ const path = trimmed
213
+ ? `/_agent-native/agent-engine/ollama-models?baseUrl=${encodeURIComponent(trimmed)}`
214
+ : "/_agent-native/agent-engine/ollama-models";
215
+ const response = await fetch(agentNativePath(path), {
216
+ credentials: "include",
217
+ });
218
+ const text = await response.text();
219
+ let payload;
220
+ try {
221
+ payload = text.trim() ? JSON.parse(text) : undefined;
222
+ }
223
+ catch {
224
+ throw new Error(`Could not read the Ollama models response (HTTP ${response.status}).`);
225
+ }
226
+ if (!response.ok) {
227
+ const message = payload &&
228
+ typeof payload === "object" &&
229
+ typeof payload.error === "string"
230
+ ? payload.error
231
+ : `Could not list Ollama models (HTTP ${response.status}).`;
232
+ throw new Error(message);
233
+ }
234
+ const models = payload && typeof payload === "object"
235
+ ? payload.models
236
+ : undefined;
237
+ return Array.isArray(models)
238
+ ? models.filter((model) => typeof model === "string")
239
+ : [];
240
+ }
204
241
  /**
205
242
  * Select the provider and model for the next conversation. This is separate
206
243
  * from saving credentials so keyless local providers such as Ollama can use
@@ -72,7 +72,7 @@ export const AGENT_PROVIDER_CATALOG = PROVIDER_ORDER.map((id) => {
72
72
  ? {
73
73
  supportsEndpoint: true,
74
74
  endpointPlaceholder: isOllama
75
- ? "http://localhost:11434"
75
+ ? "http://localhost:11434 or local network address like http://192.168.1.123:11434"
76
76
  : "https://gateway.example/v1",
77
77
  }
78
78
  : {}),
@@ -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...",