@marimo-team/islands 0.19.5-dev40 → 0.19.5-dev43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -60790,11 +60790,11 @@ ${r}
60790
60790
  },
60791
60791
  replace: (e, r) => e.endsWith(`.${r}`) ? e : `${Filenames.withoutExtension(e)}.${r}`
60792
60792
  };
60793
- async function downloadHTMLAsImage(e, r) {
60794
- let c = document.getElementById("App"), d = (c == null ? void 0 : c.scrollTop) ?? 0;
60795
- document.body.classList.add("printing");
60793
+ async function downloadHTMLAsImage(e) {
60794
+ let { element: r, filename: c, prepare: d } = e, f = document.getElementById("App"), h = (f == null ? void 0 : f.scrollTop) ?? 0, _;
60795
+ d ? _ = d(r) : document.body.classList.add("printing");
60796
60796
  try {
60797
- downloadByURL(await toPng(e), Filenames.toPNG(r));
60797
+ downloadByURL(await toPng(r), Filenames.toPNG(c));
60798
60798
  } catch {
60799
60799
  toast({
60800
60800
  title: "Error",
@@ -60802,8 +60802,8 @@ ${r}
60802
60802
  variant: "danger"
60803
60803
  });
60804
60804
  } finally {
60805
- document.body.classList.remove("printing"), requestAnimationFrame(() => {
60806
- c == null ? void 0 : c.scrollTo(0, d);
60805
+ _ == null ? void 0 : _(), document.body.classList.contains("printing") && document.body.classList.remove("printing"), requestAnimationFrame(() => {
60806
+ f == null ? void 0 : f.scrollTo(0, h);
60807
60807
  });
60808
60808
  }
60809
60809
  }
@@ -101103,7 +101103,7 @@ Defaulting to \`null\`.`;
101103
101103
  return Logger.warn("Failed to get version from mount config"), null;
101104
101104
  }
101105
101105
  }
101106
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.5-dev40"), showCodeInRunModeAtom = atom(true);
101106
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.5-dev43"), showCodeInRunModeAtom = atom(true);
101107
101107
  atom(null);
101108
101108
  var VIRTUAL_FILE_REGEX = /\/@file\/([^\s"&'/]+)\.([\dA-Za-z]+)/g, VirtualFileTracker = class e {
101109
101109
  constructor() {
@@ -101524,7 +101524,10 @@ Defaulting to \`null\`.`;
101524
101524
  }
101525
101525
  async function _temp3() {
101526
101526
  let e = document.getElementById("App");
101527
- e && await downloadHTMLAsImage(e, document.title);
101527
+ e && await downloadHTMLAsImage({
101528
+ element: e,
101529
+ filename: document.title
101530
+ });
101528
101531
  }
101529
101532
  async function _temp4() {
101530
101533
  document.getElementById("App") && await downloadAsHTML({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.19.5-dev40",
3
+ "version": "0.19.5-dev43",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,276 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { AiModel } from "@marimo-team/llm-info";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type { UserConfig } from "@/core/config/config-schema";
6
+
7
+ // Mock the models.json import
8
+ vi.mock("@marimo-team/llm-info/models.json", () => {
9
+ const models: AiModel[] = [
10
+ {
11
+ name: "GPT-4",
12
+ model: "gpt-4",
13
+ description: "OpenAI GPT-4 model",
14
+ providers: ["openai"],
15
+ roles: ["chat", "edit"],
16
+ thinking: false,
17
+ },
18
+ {
19
+ name: "Claude 3",
20
+ model: "claude-3-sonnet",
21
+ description: "Anthropic Claude 3 Sonnet",
22
+ providers: ["anthropic"],
23
+ roles: ["chat", "edit"],
24
+ thinking: false,
25
+ },
26
+ {
27
+ name: "Gemini Pro",
28
+ model: "gemini-pro",
29
+ description: "Google Gemini Pro model",
30
+ providers: ["google"],
31
+ roles: ["chat", "edit"],
32
+ thinking: false,
33
+ },
34
+ {
35
+ name: "Ollama Model",
36
+ model: "llama2",
37
+ description: "Ollama Llama 2 model",
38
+ providers: ["ollama"],
39
+ roles: ["chat", "edit"],
40
+ thinking: false,
41
+ },
42
+ ];
43
+
44
+ return {
45
+ models: models,
46
+ };
47
+ });
48
+
49
+ // Must import after mock
50
+ import {
51
+ autoPopulateModels,
52
+ getConfiguredProvider,
53
+ getRecommendedModel,
54
+ } from "../ai-utils";
55
+
56
+ describe("ai-utils", () => {
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+ });
60
+
61
+ describe("getConfiguredProvider", () => {
62
+ it("should return undefined when no AI config", () => {
63
+ const config: UserConfig = {} as UserConfig;
64
+ expect(getConfiguredProvider(config)).toBeUndefined();
65
+ });
66
+
67
+ it("should return undefined when AI config has no credentials", () => {
68
+ const config: UserConfig = {
69
+ ai: {},
70
+ } as UserConfig;
71
+ expect(getConfiguredProvider(config)).toBeUndefined();
72
+ });
73
+
74
+ it("should return openai when OpenAI API key is set", () => {
75
+ const config: UserConfig = {
76
+ ai: {
77
+ open_ai: { api_key: "sk-test" },
78
+ },
79
+ } as UserConfig;
80
+ expect(getConfiguredProvider(config)).toBe("openai");
81
+ });
82
+
83
+ it("should return anthropic when Anthropic API key is set", () => {
84
+ const config: UserConfig = {
85
+ ai: {
86
+ anthropic: { api_key: "sk-ant-test" },
87
+ },
88
+ } as UserConfig;
89
+ expect(getConfiguredProvider(config)).toBe("anthropic");
90
+ });
91
+
92
+ it("should return google when Google API key is set", () => {
93
+ const config: UserConfig = {
94
+ ai: {
95
+ google: { api_key: "google-key" },
96
+ },
97
+ } as UserConfig;
98
+ expect(getConfiguredProvider(config)).toBe("google");
99
+ });
100
+
101
+ it("should return ollama when Ollama base URL is set", () => {
102
+ const config: UserConfig = {
103
+ ai: {
104
+ ollama: { base_url: "http://localhost:11434" },
105
+ },
106
+ } as UserConfig;
107
+ expect(getConfiguredProvider(config)).toBe("ollama");
108
+ });
109
+
110
+ it("should return azure only when both API key and base URL are set", () => {
111
+ const config: UserConfig = {
112
+ ai: {
113
+ azure: { api_key: "azure-key", base_url: "https://azure.com" },
114
+ },
115
+ } as UserConfig;
116
+ expect(getConfiguredProvider(config)).toBe("azure");
117
+ });
118
+
119
+ it("should return undefined for azure with only API key", () => {
120
+ const config: UserConfig = {
121
+ ai: {
122
+ azure: { api_key: "azure-key" },
123
+ },
124
+ } as UserConfig;
125
+ expect(getConfiguredProvider(config)).toBeUndefined();
126
+ });
127
+
128
+ it("should return custom provider when configured", () => {
129
+ const config = {
130
+ ai: {
131
+ custom_providers: {
132
+ my_provider: { base_url: "https://my-api.com" },
133
+ },
134
+ },
135
+ } as unknown as UserConfig;
136
+ expect(getConfiguredProvider(config)).toBe("my_provider");
137
+ });
138
+ });
139
+
140
+ describe("getRecommendedModel", () => {
141
+ it("should return undefined when no provider is configured", () => {
142
+ const config: UserConfig = {} as UserConfig;
143
+ expect(getRecommendedModel(config)).toBeUndefined();
144
+ });
145
+
146
+ it("should return openai model when OpenAI is configured", () => {
147
+ const config: UserConfig = {
148
+ ai: {
149
+ open_ai: { api_key: "sk-test" },
150
+ },
151
+ } as UserConfig;
152
+ expect(getRecommendedModel(config)).toBe("openai/gpt-4");
153
+ });
154
+
155
+ it("should return anthropic model when Anthropic is configured", () => {
156
+ const config: UserConfig = {
157
+ ai: {
158
+ anthropic: { api_key: "sk-ant-test" },
159
+ },
160
+ } as UserConfig;
161
+ expect(getRecommendedModel(config)).toBe("anthropic/claude-3-sonnet");
162
+ });
163
+
164
+ it("should return google model when Google is configured", () => {
165
+ const config: UserConfig = {
166
+ ai: {
167
+ google: { api_key: "google-key" },
168
+ },
169
+ } as UserConfig;
170
+ expect(getRecommendedModel(config)).toBe("google/gemini-pro");
171
+ });
172
+
173
+ it("should return ollama model when Ollama is configured", () => {
174
+ const config: UserConfig = {
175
+ ai: {
176
+ ollama: { base_url: "http://localhost:11434" },
177
+ },
178
+ } as UserConfig;
179
+ expect(getRecommendedModel(config)).toBe("ollama/llama2");
180
+ });
181
+ });
182
+
183
+ describe("autoPopulateModels", () => {
184
+ it("should return empty result when both models are already set", () => {
185
+ const values = {
186
+ ai: {
187
+ open_ai: { api_key: "sk-test" },
188
+ models: {
189
+ chat_model: "openai/gpt-4",
190
+ edit_model: "openai/gpt-4",
191
+ custom_models: [],
192
+ displayed_models: [],
193
+ },
194
+ },
195
+ } as unknown as UserConfig;
196
+
197
+ const result = autoPopulateModels(values);
198
+
199
+ expect(result.chatModel).toBeUndefined();
200
+ expect(result.editModel).toBeUndefined();
201
+ });
202
+
203
+ it("should return empty result when no credentials are configured", () => {
204
+ const values: UserConfig = {
205
+ ai: {},
206
+ } as UserConfig;
207
+
208
+ const result = autoPopulateModels(values);
209
+
210
+ expect(result.chatModel).toBeUndefined();
211
+ expect(result.editModel).toBeUndefined();
212
+ });
213
+
214
+ it("should auto-populate both models when neither is set", () => {
215
+ const values: UserConfig = {
216
+ ai: {
217
+ open_ai: { api_key: "sk-test" },
218
+ },
219
+ } as UserConfig;
220
+
221
+ const result = autoPopulateModels(values);
222
+
223
+ expect(result.chatModel).toBe("openai/gpt-4");
224
+ expect(result.editModel).toBe("openai/gpt-4");
225
+ });
226
+
227
+ it("should only auto-populate chat_model when edit_model is set", () => {
228
+ const values = {
229
+ ai: {
230
+ open_ai: { api_key: "sk-test" },
231
+ models: {
232
+ edit_model: "openai/gpt-3.5-turbo",
233
+ custom_models: [],
234
+ displayed_models: [],
235
+ },
236
+ },
237
+ } as unknown as UserConfig;
238
+
239
+ const result = autoPopulateModels(values);
240
+
241
+ expect(result.chatModel).toBe("openai/gpt-4");
242
+ expect(result.editModel).toBeUndefined();
243
+ });
244
+
245
+ it("should only auto-populate edit_model when chat_model is set", () => {
246
+ const values = {
247
+ ai: {
248
+ open_ai: { api_key: "sk-test" },
249
+ models: {
250
+ chat_model: "openai/gpt-3.5-turbo",
251
+ custom_models: [],
252
+ displayed_models: [],
253
+ },
254
+ },
255
+ } as unknown as UserConfig;
256
+
257
+ const result = autoPopulateModels(values);
258
+
259
+ expect(result.chatModel).toBeUndefined();
260
+ expect(result.editModel).toBe("openai/gpt-4");
261
+ });
262
+
263
+ it("should return recommended model for anthropic provider", () => {
264
+ const values: UserConfig = {
265
+ ai: {
266
+ anthropic: { api_key: "sk-ant-test" },
267
+ },
268
+ } as UserConfig;
269
+
270
+ const result = autoPopulateModels(values);
271
+
272
+ expect(result.chatModel).toBe("anthropic/claude-3-sonnet");
273
+ expect(result.editModel).toBe("anthropic/claude-3-sonnet");
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,101 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ KNOWN_PROVIDERS,
5
+ type KnownProviderId,
6
+ type ProviderId,
7
+ } from "@/core/ai/ids/ids";
8
+ import { getKnownModelMaps } from "@/core/ai/model-registry";
9
+ import type { AiConfig, UserConfig } from "@/core/config/config-schema";
10
+
11
+ type CredentialChecker = (ai: AiConfig | undefined) => boolean;
12
+
13
+ /**
14
+ * Credential checkers for each known provider.
15
+ */
16
+ const CREDENTIAL_CHECKERS: Record<KnownProviderId, CredentialChecker> = {
17
+ openai: (ai) => Boolean(ai?.open_ai?.api_key),
18
+ anthropic: (ai) => Boolean(ai?.anthropic?.api_key),
19
+ google: (ai) => Boolean(ai?.google?.api_key),
20
+ github: (ai) => Boolean(ai?.github?.api_key),
21
+ openrouter: (ai) => Boolean(ai?.openrouter?.api_key),
22
+ azure: (ai) => Boolean(ai?.azure?.api_key && ai?.azure?.base_url),
23
+ wandb: (ai) => Boolean(ai?.wandb?.api_key),
24
+ bedrock: (ai) => Boolean(ai?.bedrock?.region_name),
25
+ ollama: (ai) => Boolean(ai?.ollama?.base_url),
26
+ // These providers don't have user-configurable credentials in the UI
27
+ deepseek: () => false,
28
+ marimo: () => false,
29
+ };
30
+
31
+ /**
32
+ * Returns the first configured provider based on credentials.
33
+ */
34
+ export function getConfiguredProvider(
35
+ config: UserConfig,
36
+ ): ProviderId | undefined {
37
+ const ai = config.ai;
38
+
39
+ for (const provider of KNOWN_PROVIDERS) {
40
+ if (CREDENTIAL_CHECKERS[provider](ai)) {
41
+ return provider;
42
+ }
43
+ }
44
+
45
+ // Check custom providers
46
+ const customProviders = ai?.custom_providers;
47
+ if (customProviders) {
48
+ const firstCustomProvider = Object.entries(customProviders).find(
49
+ ([_, providerConfig]) => providerConfig?.base_url,
50
+ );
51
+ if (firstCustomProvider) {
52
+ return firstCustomProvider[0];
53
+ }
54
+ }
55
+ }
56
+
57
+ export function getRecommendedModel(config: UserConfig): string | undefined {
58
+ const provider = getConfiguredProvider(config);
59
+ if (!provider) {
60
+ return undefined;
61
+ }
62
+ return getKnownModelMaps().defaultModelByProvider.get(provider);
63
+ }
64
+
65
+ export interface AutoPopulateResult {
66
+ chatModel: string | undefined;
67
+ editModel: string | undefined;
68
+ }
69
+
70
+ /**
71
+ * Determines which models to auto-populate based on configured credentials.
72
+ * Returns the recommended model for chat/edit if credentials are configured but models aren't set.
73
+ *
74
+ * @param values - The full form values
75
+ */
76
+ export function autoPopulateModels(values: UserConfig): AutoPopulateResult {
77
+ const result: AutoPopulateResult = {
78
+ chatModel: undefined,
79
+ editModel: undefined,
80
+ };
81
+
82
+ const needsChatModel = !values.ai?.models?.chat_model;
83
+ const needsEditModel = !values.ai?.models?.edit_model;
84
+
85
+ if (!needsChatModel && !needsEditModel) {
86
+ return result;
87
+ }
88
+
89
+ const recommendedModel = getRecommendedModel(values);
90
+ if (!recommendedModel) {
91
+ return result;
92
+ }
93
+
94
+ if (needsChatModel) {
95
+ result.chatModel = recommendedModel;
96
+ }
97
+ if (needsEditModel) {
98
+ result.editModel = recommendedModel;
99
+ }
100
+ return result;
101
+ }
@@ -115,6 +115,7 @@ interface ApiKeyProps {
115
115
  placeholder: string;
116
116
  testId: string;
117
117
  description?: React.ReactNode;
118
+ onChange?: (value: string) => void;
118
119
  }
119
120
 
120
121
  export const ApiKey: React.FC<ApiKeyProps> = ({
@@ -124,6 +125,7 @@ export const ApiKey: React.FC<ApiKeyProps> = ({
124
125
  placeholder,
125
126
  testId,
126
127
  description,
128
+ onChange,
127
129
  }) => {
128
130
  return (
129
131
  <FormField
@@ -141,11 +143,12 @@ export const ApiKey: React.FC<ApiKeyProps> = ({
141
143
  placeholder={placeholder}
142
144
  type="password"
143
145
  {...field}
144
- value={asStringOrUndefined(field.value)}
146
+ value={asStringOrEmpty(field.value)}
145
147
  onChange={(e) => {
146
148
  const value = e.target.value;
147
149
  if (!value.includes("*")) {
148
150
  field.onChange(value);
151
+ onChange?.(value);
149
152
  }
150
153
  }}
151
154
  />
@@ -168,12 +171,12 @@ interface BaseUrlProps {
168
171
  testId: string;
169
172
  description?: React.ReactNode;
170
173
  disabled?: boolean;
171
- defaultValue?: string;
174
+ onChange?: (value: string) => void;
172
175
  }
173
176
 
174
- function asStringOrUndefined<T>(value: T): string | undefined {
177
+ function asStringOrEmpty<T>(value: T): string {
175
178
  if (value == null) {
176
- return undefined;
179
+ return "";
177
180
  }
178
181
 
179
182
  if (typeof value === "string") {
@@ -191,13 +194,12 @@ export const BaseUrl: React.FC<BaseUrlProps> = ({
191
194
  testId,
192
195
  description,
193
196
  disabled = false,
194
- defaultValue,
197
+ onChange,
195
198
  }) => {
196
199
  return (
197
200
  <FormField
198
201
  control={form.control}
199
202
  name={name}
200
- disabled={disabled}
201
203
  render={({ field }) => (
202
204
  <div className="flex flex-col space-y-1">
203
205
  <FormItem className={formItemClasses}>
@@ -208,9 +210,13 @@ export const BaseUrl: React.FC<BaseUrlProps> = ({
208
210
  rootClassName="flex-1"
209
211
  className="m-0 inline-flex h-7"
210
212
  placeholder={placeholder}
211
- defaultValue={defaultValue}
212
213
  {...field}
213
- value={asStringOrUndefined(field.value)}
214
+ value={asStringOrEmpty(field.value)}
215
+ disabled={disabled}
216
+ onChange={(e) => {
217
+ field.onChange(e.target.value);
218
+ onChange?.(e.target.value);
219
+ }}
214
220
  />
215
221
  </FormControl>
216
222
  <FormMessage />
@@ -252,9 +258,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
252
258
  <FormField
253
259
  control={form.control}
254
260
  name={name}
255
- disabled={disabled}
256
261
  render={({ field }) => {
257
- const value = asStringOrUndefined(field.value);
262
+ const value = asStringOrEmpty(field.value);
258
263
 
259
264
  const selectModel = (modelId: QualifiedModelId) => {
260
265
  field.onChange(modelId);
@@ -286,7 +291,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
286
291
  className="w-full border-border shadow-none focus-visible:shadow-xs"
287
292
  placeholder={placeholder}
288
293
  {...field}
289
- value={asStringOrUndefined(field.value)}
294
+ value={asStringOrEmpty(field.value)}
295
+ disabled={disabled}
290
296
  onKeyDown={Events.stopPropagation()}
291
297
  />
292
298
  {value && (
@@ -339,7 +345,6 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
339
345
  <FormField
340
346
  control={form.control}
341
347
  name={name}
342
- disabled={disabled}
343
348
  render={({ field }) => (
344
349
  <div className="flex flex-col space-y-1">
345
350
  <FormItem className={formItemClasses}>
@@ -354,14 +359,14 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
354
359
  field.onChange(e.target.value);
355
360
  }
356
361
  }}
357
- value={asStringOrUndefined(
362
+ value={asStringOrEmpty(
358
363
  field.value === true
359
364
  ? "github"
360
365
  : field.value === false
361
366
  ? "none"
362
367
  : field.value,
363
368
  )}
364
- disabled={field.disabled}
369
+ disabled={disabled}
365
370
  className="inline-flex mr-2"
366
371
  >
367
372
  {options.map((option) => (
@@ -715,6 +720,22 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
715
720
  </div>
716
721
  );
717
722
 
723
+ // Update a provider field by updating the entire custom_providers object.
724
+ // As this config will be replaced, it needs to be sent in its entirety.
725
+ const updateProviderField = (opts: {
726
+ providerName: string;
727
+ fieldName: keyof CustomProviderConfig;
728
+ value: string;
729
+ }) => {
730
+ field.onChange({
731
+ ...customProviders,
732
+ [opts.providerName]: {
733
+ ...customProviders[opts.providerName],
734
+ [opts.fieldName]: opts.value || undefined,
735
+ },
736
+ });
737
+ };
738
+
718
739
  const renderAccordionItem = ({
719
740
  providerName,
720
741
  providerConfig,
@@ -744,6 +765,13 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
744
765
  }
745
766
  placeholder="sk-..."
746
767
  testId={`custom-provider-${providerName}-api-key`}
768
+ onChange={(value) =>
769
+ updateProviderField({
770
+ providerName,
771
+ fieldName: "api_key",
772
+ value,
773
+ })
774
+ }
747
775
  />
748
776
  <BaseUrl
749
777
  form={form}
@@ -753,6 +781,13 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
753
781
  }
754
782
  placeholder="https://api.example.com/v1"
755
783
  testId={`custom-provider-${providerName}-base-url`}
784
+ onChange={(value) =>
785
+ updateProviderField({
786
+ providerName,
787
+ fieldName: "base_url",
788
+ value,
789
+ })
790
+ }
756
791
  />
757
792
  <Button
758
793
  variant="destructive"
@@ -918,7 +953,6 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
918
953
  config={config}
919
954
  name="ai.ollama.base_url"
920
955
  placeholder="http://localhost:11434/v1"
921
- defaultValue="http://localhost:11434/v1"
922
956
  testId="ollama-base-url-input"
923
957
  />
924
958
  </AccordionFormItem>
@@ -1038,7 +1072,6 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
1038
1072
  config={config}
1039
1073
  name="ai.azure.base_url"
1040
1074
  placeholder="https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment-name>?api-version=<api-version>"
1041
- defaultValue="https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment-name>?api-version=<api-version>"
1042
1075
  testId="ai-azure-base-url-input"
1043
1076
  />
1044
1077
  </AccordionFormItem>
@@ -1427,6 +1460,13 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
1427
1460
 
1428
1461
  const deleteModel = useEvent((modelId: QualifiedModelId) => {
1429
1462
  const newModels = customModels.filter((id) => id !== modelId);
1463
+ // Remove from displayed models if it's in there
1464
+ const newDisplayedModels = currentDisplayedModels.filter(
1465
+ (id) => id !== modelId,
1466
+ );
1467
+ form.setValue("ai.models.displayed_models", newDisplayedModels, {
1468
+ shouldDirty: true,
1469
+ });
1430
1470
  form.setValue("ai.models.custom_models", newModels, {
1431
1471
  shouldDirty: true,
1432
1472
  });
@@ -50,6 +50,7 @@ import { Banner } from "@/plugins/impl/common/error-banner";
50
50
  import { THEMES } from "@/theme/useTheme";
51
51
  import { arrayToggle } from "@/utils/arrays";
52
52
  import { cn } from "@/utils/cn";
53
+ import { autoPopulateModels } from "../ai/ai-utils";
53
54
  import { keyboardShortcutsAtom } from "../editor/controls/keyboard-shortcuts";
54
55
  import { Badge } from "../ui/badge";
55
56
  import { ExternalLink } from "../ui/links";
@@ -180,6 +181,28 @@ export const UserConfigForm: React.FC = () => {
180
181
  defaultValues: config,
181
182
  });
182
183
 
184
+ const setAiModels = (values: UserConfig, dirtyAiConfig: UserConfig["ai"]) => {
185
+ const { chatModel, editModel } = autoPopulateModels(values);
186
+ if (chatModel || editModel) {
187
+ dirtyAiConfig = {
188
+ ...dirtyAiConfig,
189
+ models: {
190
+ ...dirtyAiConfig?.models,
191
+ ...(chatModel && { chat_model: chatModel }),
192
+ ...(editModel && { edit_model: editModel }),
193
+ },
194
+ } as typeof dirtyAiConfig;
195
+ if (chatModel) {
196
+ form.setValue("ai.models.chat_model", chatModel);
197
+ }
198
+ if (editModel) {
199
+ form.setValue("ai.models.edit_model", editModel);
200
+ }
201
+ }
202
+
203
+ return dirtyAiConfig;
204
+ };
205
+
183
206
  const onSubmitNotDebounced = async (values: UserConfig) => {
184
207
  // Only send values that were actually changed to avoid
185
208
  // overwriting backend values the form doesn't manage
@@ -187,6 +210,12 @@ export const UserConfigForm: React.FC = () => {
187
210
  if (Object.keys(dirtyValues).length === 0) {
188
211
  return; // Nothing changed
189
212
  }
213
+
214
+ // Auto-populate AI models when credentials are set, makes it easier to get started
215
+ if (dirtyValues.ai) {
216
+ dirtyValues.ai = setAiModels(values, dirtyValues.ai);
217
+ }
218
+
190
219
  await saveUserConfig({ config: dirtyValues }).then(() => {
191
220
  // Update local state with form values
192
221
  setConfig((prev) => ({ ...prev, ...values }));