diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 6b4d5611..9e6d76b4 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -1532,12 +1532,17 @@ function ChatInputInner({ - + {modelGroups.length === 0 && !standaloneDefault ? ( - Connect a provider in Settings +
+ Connect a provider in Settings +
) : ( <> -
+ {/* Fixed search header — lives OUTSIDE the scroll area (the + inner div below scrolls), so it's flush at the very top + and always visible without any scroll. */} +
+
No models match
)} +
)}
diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index c13d8cbc..7ef1f813 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -414,8 +414,11 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b } return saved })() + // Gate Connect on credentials only, NOT on resolvedModel — that derives + // from the live fetch, which hasn't settled the instant a key is pasted, so + // gating on it made the first click a no-op (the model is resolved, fetching + // on demand if needed, inside handleTestAndSave). const canTest = - resolvedModel.length > 0 && (!requiresApiKey || activeConfig.apiKey.trim().length > 0) && (!requiresBaseURL || activeConfig.baseURL.trim().length > 0) @@ -567,10 +570,35 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b if (!canTest) return setTestState({ status: "testing" }) try { + // Normally providerModels has loaded by click time and resolvedModel is + // set. But a Connect click right after pasting a key can beat the + // debounced key-change fetch — so when nothing is resolved yet, fetch + // the list on demand (one fetch, then save) instead of forcing a second + // click. Same silent precedence as resolvedModel, minus the customModel + // branch (that path only runs when resolvedModel is already empty). + let model = resolvedModel + if (!model) { + const listRes = await window.ipc.invoke("models:listForProvider", { + provider: { + flavor: provider, + apiKey: activeConfig.apiKey.trim() || undefined, + baseURL: activeConfig.baseURL.trim() || undefined, + }, + }) + if (listRes.success && listRes.models && listRes.models.length > 0) { + const preferred = preferredDefaults[provider] + model = preferred && listRes.models.includes(preferred) ? preferred : listRes.models[0] + } + } + if (!model) { + setTestState({ status: "error", error: "Enter a model to connect" }) + toast.error("Enter a model to connect") + return + } // The silently resolved model takes the primary slot; the rest of the // saved list is preserved (same semantics setPrimaryModel had). const existing = activeConfig.models.map(m => m.trim()) - const allModels = [resolvedModel, ...existing.slice(1).filter(m => m && m !== resolvedModel)] + const allModels = [model, ...existing.slice(1).filter(m => m && m !== model)] const providerConfig = { provider: { flavor: provider,