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 }),