fix(desktop): preselect model in onboarding so first chat doesn't 400 (#427)

This commit is contained in:
Andrew Park
2026-05-28 11:19:52 -07:00
committed by GitHub
parent 6a27094658
commit c6448f7cf2
3 changed files with 46 additions and 7 deletions
+8 -3
View File
@@ -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() {
/>
<button
onClick={sendMessage}
disabled={!input.trim() || modelLoading}
disabled={!input.trim() || modelLoading || !selectedModel}
title={selectedModel ? 'Send message' : 'Pick a model first (⌘K)'}
className="p-2 rounded-xl transition-colors shrink-0 cursor-pointer disabled:opacity-30 disabled:cursor-default"
style={{
background: input.trim() ? 'var(--color-accent)' : 'var(--color-bg-tertiary)',
color: input.trim() ? 'white' : 'var(--color-text-tertiary)',
}}
title="Send message"
>
<Send size={16} />
</button>
+32 -3
View File
@@ -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<SetupStatus | null>(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]);
+6 -1
View File
@@ -438,7 +438,12 @@ export const useAppStore = create<AppState>((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 }),