From c6448f7cf23d4a3efd1ddc9bbcf829bb2b31cdf7 Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Thu, 28 May 2026 11:19:52 -0700 Subject: [PATCH] fix(desktop): preselect model in onboarding so first chat doesn't 400 (#427) --- frontend/src/components/Chat/InputArea.tsx | 11 +++++-- frontend/src/components/SetupScreen.tsx | 35 ++++++++++++++++++++-- frontend/src/lib/store.ts | 7 ++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Chat/InputArea.tsx b/frontend/src/components/Chat/InputArea.tsx index dd841b25..dacb805c 100644 --- a/frontend/src/components/Chat/InputArea.tsx +++ b/frontend/src/components/Chat/InputArea.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { Send, Square, Paperclip, Search } from 'lucide-react'; +import { toast } from 'sonner'; import { useAppStore, generateId } from '../../lib/store'; import { streamChat, streamResearch } from '../../lib/sse'; import { fetchSavings, getBase } from '../../lib/api'; @@ -155,6 +156,10 @@ export function InputArea() { const sendMessage = useCallback(async () => { const content = input.trim(); if (!content || streamState.isStreaming) return; + if (!selectedModel) { + toast.error('Pick a model first (⌘K)'); + return; + } setInput(''); @@ -555,7 +560,7 @@ export function InputArea() { value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} - placeholder="Message OpenJarvis..." + placeholder={selectedModel ? 'Message OpenJarvis...' : 'Pick a model first (⌘K)...'} rows={1} className="flex-1 bg-transparent outline-none resize-none text-sm leading-relaxed" style={{ color: 'var(--color-text)', maxHeight: '200px' }} @@ -580,13 +585,13 @@ export function InputArea() { /> diff --git a/frontend/src/components/SetupScreen.tsx b/frontend/src/components/SetupScreen.tsx index afaf512e..15312c6f 100644 --- a/frontend/src/components/SetupScreen.tsx +++ b/frontend/src/components/SetupScreen.tsx @@ -1,6 +1,12 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { Loader2, CheckCircle2, XCircle, Cpu, Server, Database } from 'lucide-react'; -import { getSetupStatus, type SetupStatus } from '../lib/api'; +import { + getSetupStatus, + fetchModels, + fetchRecommendedModel, + type SetupStatus, +} from '../lib/api'; +import { useAppStore } from '../lib/store'; const STEPS = [ { key: 'ollama_ready', label: 'Inference Engine', icon: Cpu, detail: 'Starting Ollama...' }, @@ -70,10 +76,33 @@ function StepRow({ export function SetupScreen({ onReady }: { onReady: () => void }) { const [status, setStatus] = useState(null); + const handedOffRef = useRef(false); const poll = useCallback(async () => { const s = await getSetupStatus(); if (s) setStatus(s); - if (s?.phase === 'ready') { + if (s?.phase === 'ready' && !handedOffRef.current) { + handedOffRef.current = true; + // Pre-select a model BEFORE handing off so the chat is usable on + // first send. Without this, the main app's post-mount fetch can + // lose a race to a fast first message and Ollama 400s. + try { + const [models, rec] = await Promise.all([ + fetchModels().catch(() => []), + fetchRecommendedModel().catch(() => ({ model: '', reason: '' })), + ]); + const store = useAppStore.getState(); + store.setModels(models); + store.setModelsLoading(false); + const recommended = rec.model && models.some((m) => m.id === rec.model) + ? rec.model + : models[0]?.id || ''; + if (recommended && !store.selectedModel) { + store.setSelectedModel(recommended); + } + } catch { + // Non-fatal: store.setModels auto-selects on later fetch, and + // the InputArea guards the empty-model case with a toast. + } setTimeout(() => onReady(), 600); } }, [onReady]); diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index 37a7d84c..af7d5a1e 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -438,7 +438,12 @@ export const useAppStore = create((set, get) => { // ── Models & server ──────────────────────────────────────────── - setModels: (models: ModelInfo[]) => set({ models }), + setModels: (models: ModelInfo[]) => + set((state) => + !state.selectedModel && models.length > 0 + ? { models, selectedModel: models[0].id } + : { models }, + ), setModelsLoading: (loading: boolean) => set({ modelsLoading: loading }), setSelectedModel: (model: string) => set({ selectedModel: model }), setServerInfo: (info: ServerInfo | null) => set({ serverInfo: info }),