From 69f5944e96149e08e2d9537aa82c09d76156fe03 Mon Sep 17 00:00:00 2001 From: Derrick Barra Date: Thu, 9 Apr 2026 21:14:01 -0400 Subject: [PATCH 1/2] Fix Bug 2 default model/effort inheritance display --- .../chat/components/useModelEffort.test.ts | 64 +++++- .../chat/components/useModelEffort.ts | 195 ++++++++++++------ 2 files changed, 195 insertions(+), 64 deletions(-) diff --git a/src/features/chat/components/useModelEffort.test.ts b/src/features/chat/components/useModelEffort.test.ts index 0030831a..775de4e1 100644 --- a/src/features/chat/components/useModelEffort.test.ts +++ b/src/features/chat/components/useModelEffort.test.ts @@ -17,8 +17,8 @@ vi.mock('@/contexts/SessionContext', () => ({ import { buildModelCatalogUiError, buildSelectableModelList, type GatewayModelInfo, useModelEffort } from './useModelEffort'; const CONFIGURED_MODELS: GatewayModelInfo[] = [ - { id: 'zai/glm-4.7', label: 'glm-4.7', provider: 'zai' }, - { id: 'ollama/qwen2.5:7b-instruct-q5_K_M', label: 'qwen-local', provider: 'ollama' }, + { id: 'zai/glm-4.7', label: 'glm-4.7', provider: 'zai', role: 'primary' }, + { id: 'ollama/qwen2.5:7b-instruct-q5_K_M', label: 'qwen-local', provider: 'ollama', role: 'fallback' }, ]; function jsonResponse(data: unknown, init: { ok?: boolean; status?: number } = {}) { @@ -106,11 +106,71 @@ describe('useModelEffort', () => { }); expect(result.current.modelOptions).toEqual([ + { value: 'primary', label: 'primary' }, { value: 'zai/glm-4.7', label: 'glm-4.7' }, { value: 'ollama/qwen2.5:7b-instruct-q5_K_M', label: 'qwen-local' }, { value: 'openrouter/xiaomi/mimo-v2-pro', label: 'xiaomi/mimo-v2-pro' }, ]); }); + + it('surfaces inherited defaults as primary + thinkingDefault for sessions on OpenClaw defaults', async () => { + mockUseGateway.mockReturnValue({ + rpc: vi.fn(), + connectionState: 'connected', + model: 'zai/glm-4.7', + thinking: 'medium', + }); + + mockUseSessionContext.mockReturnValue({ + currentSession: 'agent:main:main', + sessions: [ + { key: 'agent:main:main', model: 'openai-codex/gpt-5.4', thinking: 'medium' }, + ], + updateSession: vi.fn(), + }); + + globalThis.fetch = vi.fn((input: string | URL | Request) => { + const url = String(input); + if (url === '/api/gateway/models') { + return Promise.resolve(jsonResponse({ + models: [ + { id: 'openai/gpt-5.4', label: 'gpt-5.4', provider: 'openai', role: 'primary' }, + { id: 'zai/glm-4.7', label: 'glm-4.7', provider: 'zai', role: 'fallback' }, + ], + error: null, + })); + } + if (url.startsWith('/api/gateway/session-info?sessionKey=')) { + return Promise.resolve(jsonResponse({})); + } + throw new Error(`Unexpected fetch: ${url}`); + }) as typeof globalThis.fetch; + + const { result } = renderHook(() => useModelEffort()); + + await waitFor(() => { + expect(result.current.selectedModel).toBe('primary'); + expect(result.current.selectedEffort).toBe('thinkingDefault'); + }); + }); + + it('preserves explicit effort overrides when present', async () => { + mockUseSessionContext.mockReturnValue({ + currentSession: 'agent:main:subagent:explicit', + sessions: [ + { key: 'agent:main:main', model: 'zai/glm-4.7' }, + { key: 'agent:main:subagent:explicit', model: 'zai/glm-4.7', thinking: 'medium', thinkingLevel: 'high' }, + ], + updateSession: vi.fn(), + }); + + const { result } = renderHook(() => useModelEffort()); + + await waitFor(() => { + expect(result.current.selectedModel).toBe('primary'); + expect(result.current.selectedEffort).toBe('high'); + }); + }); }); describe('buildModelCatalogUiError', () => { diff --git a/src/features/chat/components/useModelEffort.ts b/src/features/chat/components/useModelEffort.ts index ee1782e5..b3e5f5dc 100644 --- a/src/features/chat/components/useModelEffort.ts +++ b/src/features/chat/components/useModelEffort.ts @@ -32,10 +32,19 @@ function getEffortKey(sessionKey?: string | null) { return sessionKey ? `oc-effort-${sessionKey}` : 'oc-effort-default'; } +const INHERITED_MODEL_VALUE = 'primary'; +const INHERITED_EFFORT_VALUE = 'thinkingDefault'; + type EffortLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'; +type EffortSelection = typeof INHERITED_EFFORT_VALUE | EffortLevel; const EFFORT_OPTIONS: EffortLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh']; -export type GatewayModelInfo = { id: string; label: string; provider: string }; +export type GatewayModelInfo = { + id: string; + label: string; + provider: string; + role?: 'primary' | 'fallback' | 'allowed'; +}; type GatewayModelsResponse = { models: GatewayModelInfo[]; @@ -62,6 +71,15 @@ function resolveModelId(raw: string, options: GatewayModelInfo[]): string { return raw; } +function modelRefsMatch(a: string | null | undefined, b: string | null | undefined, options: GatewayModelInfo[]): boolean { + if (!a || !b) return false; + if (a === b) return true; + const resolvedA = resolveModelId(a, options); + const resolvedB = resolveModelId(b, options); + if (resolvedA === resolvedB) return true; + return baseModelName(resolvedA) === baseModelName(resolvedB); +} + export function buildSelectableModelList( gatewayModels: GatewayModelInfo[] | null, currentModel: string | null | undefined, @@ -160,12 +178,18 @@ export function useModelEffort(): UseModelEffortReturn { () => buildSelectableModelList(gatewayModels, rawCurrentSessionModel || model), [gatewayModels, rawCurrentSessionModel, model], ); - const [selectedEffort, setSelectedEffort] = useState(() => { + const primaryModelId = useMemo( + () => gatewayModels?.find((entry) => entry.role === 'primary')?.id || null, + [gatewayModels], + ); + const [selectedEffort, setSelectedEffort] = useState(() => { try { - const saved = localStorage.getItem(getEffortKey(currentSession)) as EffortLevel | null; - return saved && EFFORT_OPTIONS.includes(saved) ? saved : 'low'; + const saved = localStorage.getItem(getEffortKey(currentSession)) as EffortSelection | null; + return saved && (saved === INHERITED_EFFORT_VALUE || EFFORT_OPTIONS.includes(saved as EffortLevel)) + ? saved + : INHERITED_EFFORT_VALUE; } catch { - return 'low'; + return INHERITED_EFFORT_VALUE; } }); const [prevEffortSource, setPrevEffortSource] = useState(null); @@ -173,24 +197,44 @@ export function useModelEffort(): UseModelEffortReturn { // Resolve current session's model. // Priority: resolved cache (from transcript/cron) → session.model from gateway const currentSessionModel = useMemo(() => { - // Check cached resolved model first (accurate for cron/subagent sessions) - const cached = resolvedSessionModels[currentSession]; - if (cached) return resolveModelId(cached, modelOptionsList); + const sessionRecord = sessions.find(sess => getSessionKey(sess) === currentSession); - // Fall back to session.model from sessions.list (correct for main, default for others) - const s = sessions.find(sess => getSessionKey(sess) === currentSession); - const raw = s?.model; + // Check cached resolved model first (accurate for cron/subagent sessions) + const raw = resolvedSessionModels[currentSession] || sessionRecord?.model; if (!raw) return null; + + if (modelRefsMatch(raw, primaryModelId, modelOptionsList)) { + return INHERITED_MODEL_VALUE; + } + return resolveModelId(raw, modelOptionsList); - }, [sessions, currentSession, modelOptionsList, resolvedSessionModels]); + }, [sessions, currentSession, modelOptionsList, resolvedSessionModels, primaryModelId]); - // Resolve current session's thinking level + // Resolve current session's thinking level. + // Prefer explicit overrides. If no explicit override exists, surface + // the inherited default selector state (thinkingDefault). const currentSessionThinking = useMemo(() => { const s = sessions.find(sess => getSessionKey(sess) === currentSession); - const raw = (s?.thinkingLevel || s?.thinking)?.toLowerCase(); - if (raw && EFFORT_OPTIONS.includes(raw as EffortLevel)) return raw as EffortLevel; + + const explicit = s?.thinkingLevel?.toLowerCase(); + if (explicit && EFFORT_OPTIONS.includes(explicit as EffortLevel)) { + return explicit as EffortLevel; + } + + const effective = s?.thinking?.toLowerCase(); + const inheritedDefault = thinking?.toLowerCase(); + if ( + effective + && EFFORT_OPTIONS.includes(effective as EffortLevel) + && inheritedDefault + && EFFORT_OPTIONS.includes(inheritedDefault as EffortLevel) + && effective !== inheritedDefault + ) { + return effective as EffortLevel; + } + return null; - }, [sessions, currentSession]); + }, [sessions, currentSession, thinking]); // Sync model dropdown when switching sessions (setState-during-render pattern) // @@ -200,7 +244,10 @@ export function useModelEffort(): UseModelEffortReturn { // is available). const rawModelSource = currentSessionModel || model || '--'; let modelSource = rawModelSource; - if (modelSource !== '--' && !modelOptionsList.some(m => m.id === modelSource)) { + + if (modelSource !== '--' && modelRefsMatch(modelSource, primaryModelId, modelOptionsList)) { + modelSource = INHERITED_MODEL_VALUE; + } else if (modelSource !== '--' && modelSource !== INHERITED_MODEL_VALUE && !modelOptionsList.some(m => m.id === modelSource)) { const byLabel = modelOptionsList.find(m => m.label === modelSource); const srcBase = baseModelName(modelSource); const byBaseName = modelOptionsList.find(m => baseModelName(m.id) === srcBase); @@ -224,18 +271,16 @@ export function useModelEffort(): UseModelEffortReturn { } // Sync effort dropdown from gateway thinking level (setState-during-render pattern) - const effortSource = `${currentSession}:${currentSessionThinking ?? thinking ?? ''}`; + const effortSource = `${currentSession}:${currentSessionThinking ?? INHERITED_EFFORT_VALUE}`; if (effortSource !== prevEffortSource) { setPrevEffortSource(effortSource); // Fix 1: Respect optimistic lock for effort changes too if (Date.now() <= effortLockUntilRef.current) { // Skip — we're in the grace period after a manual effort change - } else if (currentSessionThinking) { - setSelectedEffort(currentSessionThinking); - try { localStorage.setItem(getEffortKey(currentSession), currentSessionThinking); } catch { /* ignore */ } - } else if (thinking && thinking !== '--' && EFFORT_OPTIONS.includes(thinking as EffortLevel)) { - setSelectedEffort(thinking as EffortLevel); - try { localStorage.setItem(getEffortKey(currentSession), thinking); } catch { /* ignore */ } + } else { + const nextEffortSelection = currentSessionThinking || INHERITED_EFFORT_VALUE; + setSelectedEffort(nextEffortSelection); + try { localStorage.setItem(getEffortKey(currentSession), nextEffortSelection); } catch { /* ignore */ } } } @@ -282,17 +327,8 @@ export function useModelEffort(): UseModelEffortReturn { useEffect(() => { const signal = { cancelled: false }; (async () => { - const sessionInfo = await fetchGatewaySessionInfo(currentSession || undefined); if (signal.cancelled) return; - if (sessionInfo?.thinking && !currentSessionThinking) { - const level = sessionInfo.thinking.toLowerCase() as EffortLevel; - if (EFFORT_OPTIONS.includes(level)) { - setSelectedEffort(level); - try { localStorage.setItem(getEffortKey(currentSession), level); } catch { /* ignore */ } - } - } - // For child sessions, resolve the actual model from cron payload or transcript if (!currentSession) return; const sessionType = getSessionType(currentSession); @@ -339,7 +375,7 @@ export function useModelEffort(): UseModelEffortReturn { console.warn('[useModelEffort] Failed to fetch session info:', err); }); return () => { signal.cancelled = true; }; - }, [currentSession, currentSessionThinking, modelOptionsList]); + }, [currentSession, modelOptionsList]); const controlsDisabled = connectionState !== 'connected' || !currentSession; @@ -366,26 +402,44 @@ export function useModelEffort(): UseModelEffortReturn { confirmTimerRef.current = null; } + const selectingInheritedPrimary = nextInput === INHERITED_MODEL_VALUE; + let patchModel: string | null = selectingInheritedPrimary ? null : next; + try { let wsSucceeded = false; // Attempt 1: WS RPC (fast path) try { - await rpc('sessions.patch', { key: currentSession, model: next }); + await rpc('sessions.patch', { key: currentSession, model: patchModel }); wsSucceeded = true; } catch (patchErr) { - // Attempt 2: Cross-provider fallback via WS - const nextBase = baseModelName(next); - const alt = modelOptionsList.find(m => m.id !== next && baseModelName(m.id) === nextBase); - if (alt) { + // For inherited-default model selection, some gateways may reject null model. + // In that case, fall back to explicitly setting the configured primary model ID. + if (selectingInheritedPrimary && primaryModelId) { try { - await rpc('sessions.patch', { key: currentSession, model: alt.id }); - next = alt.id; - setSelectedModel(next); - try { localStorage.setItem(MODEL_KEY, next); } catch { /* ignore */ } + await rpc('sessions.patch', { key: currentSession, model: primaryModelId }); + patchModel = primaryModelId; wsSucceeded = true; } catch { - // WS completely broken — fall through to HTTP + // Keep failing through to HTTP fallback. + } + } + + // Attempt 2: Cross-provider fallback via WS + if (!wsSucceeded && !selectingInheritedPrimary) { + const nextBase = baseModelName(next); + const alt = modelOptionsList.find(m => m.id !== next && baseModelName(m.id) === nextBase); + if (alt) { + try { + await rpc('sessions.patch', { key: currentSession, model: alt.id }); + next = alt.id; + patchModel = alt.id; + setSelectedModel(next); + try { localStorage.setItem(MODEL_KEY, next); } catch { /* ignore */ } + wsSucceeded = true; + } catch { + // WS completely broken — fall through to HTTP + } } } @@ -396,10 +450,14 @@ export function useModelEffort(): UseModelEffortReturn { // Attempt 3: HTTP fallback (reliable path via session_status tool) if (!wsSucceeded) { + const fallbackModel = selectingInheritedPrimary ? (primaryModelId || model || null) : patchModel; + if (!fallbackModel) { + throw new Error('No primary model is available to apply inherited default model state'); + } const res = await fetch('/api/gateway/session-patch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionKey: currentSession, model: next }), + body: JSON.stringify({ sessionKey: currentSession, model: fallbackModel }), }); if (!res.ok) { const data = await res.json().catch(() => ({})) as { error?: string }; @@ -410,7 +468,10 @@ export function useModelEffort(): UseModelEffortReturn { // Optimistically update the session object so that // SessionContext.refreshSessions() doesn't overwrite with stale data if (currentSession) { - updateSession(currentSession, { model: next }); + const optimisticModel = selectingInheritedPrimary + ? (primaryModelId || patchModel || model || undefined) + : (patchModel || next); + updateSession(currentSession, { model: optimisticModel }); } // Schedule a confirmation poll to verify the change took effect @@ -419,13 +480,17 @@ export function useModelEffort(): UseModelEffortReturn { try { const info = await fetchGatewaySessionInfo(currentSession || undefined); if (info?.model) { - const infoBase = baseModelName(info.model); - const confirmed = modelOptionsList.find(m => - m.id === info.model || m.label === info.model || - baseModelName(m.id) === infoBase || m.id.endsWith('/' + info.model) - ); - if (confirmed) { - setSelectedModel(confirmed.id); + if (modelRefsMatch(info.model, primaryModelId, modelOptionsList)) { + setSelectedModel(INHERITED_MODEL_VALUE); + } else { + const infoBase = baseModelName(info.model); + const confirmed = modelOptionsList.find(m => + m.id === info.model || m.label === info.model || + baseModelName(m.id) === infoBase || m.id.endsWith('/' + info.model) + ); + if (confirmed) { + setSelectedModel(confirmed.id); + } } } } catch { @@ -442,20 +507,21 @@ export function useModelEffort(): UseModelEffortReturn { try { localStorage.setItem(MODEL_KEY, prev); } catch { /* ignore */ } setUiError(`Model: ${errMsg}`); } - }, [controlsDisabled, selectedModel, rpc, currentSession, updateSession, modelOptionsList]); + }, [controlsDisabled, selectedModel, rpc, currentSession, updateSession, modelOptionsList, primaryModelId, model]); const handleEffortChange = useCallback(async (next: string) => { if (controlsDisabled) return; setUiError(null); const prev = selectedEffort; - const nextEffort = next as EffortLevel; + const nextEffort = next as EffortSelection; setSelectedEffort(nextEffort); effortLockUntilRef.current = Date.now() + OPTIMISTIC_LOCK_MS; try { localStorage.setItem(getEffortKey(currentSession), nextEffort); } catch { /* ignore */ } try { - const thinkingValue = nextEffort === 'off' ? null : nextEffort; + const isInheritedDefault = nextEffort === INHERITED_EFFORT_VALUE; + const thinkingValue = isInheritedDefault || nextEffort === 'off' ? null : nextEffort; try { await rpc('sessions.patch', { key: currentSession, thinkingLevel: thinkingValue }); } catch (wsErr) { @@ -466,7 +532,9 @@ export function useModelEffort(): UseModelEffortReturn { await rpc('sessions.patch', { key: currentSession, thinkingLevel: thinkingValue }); } if (currentSession) { - updateSession(currentSession, { thinkingLevel: nextEffort }); + updateSession(currentSession, { + thinkingLevel: isInheritedDefault ? undefined : nextEffort, + }); } setTimeout(() => { effortLockUntilRef.current = 0; }, CONFIRM_POLL_DELAY_MS); } catch (err) { @@ -479,13 +547,16 @@ export function useModelEffort(): UseModelEffortReturn { } }, [controlsDisabled, selectedEffort, rpc, currentSession, updateSession]); - const modelOptions = useMemo( - () => modelOptionsList.map((m) => ({ value: m.id, label: m.label })), - [modelOptionsList], - ); + const modelOptions = useMemo(() => { + const configured = modelOptionsList.map((m) => ({ value: m.id, label: m.label })); + return [{ value: INHERITED_MODEL_VALUE, label: INHERITED_MODEL_VALUE }, ...configured]; + }, [modelOptionsList]); const effortOptions = useMemo( - () => EFFORT_OPTIONS.map((lvl) => ({ value: lvl, label: lvl })), + () => [ + { value: INHERITED_EFFORT_VALUE, label: INHERITED_EFFORT_VALUE }, + ...EFFORT_OPTIONS.map((lvl) => ({ value: lvl, label: lvl })), + ], [], ); From 89fce6d07fb267d6ebbf4aee012fe8c75769e9d4 Mon Sep 17 00:00:00 2001 From: Derrick Barra Date: Thu, 9 Apr 2026 21:48:20 -0400 Subject: [PATCH 2/2] Fix spawn dialog inherited primary model option --- .../sessions/SpawnAgentDialog.test.tsx | 31 ++++++++++++++++--- src/features/sessions/SpawnAgentDialog.tsx | 26 +++++++++++----- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/features/sessions/SpawnAgentDialog.test.tsx b/src/features/sessions/SpawnAgentDialog.test.tsx index 81adef4f..dd444327 100644 --- a/src/features/sessions/SpawnAgentDialog.test.tsx +++ b/src/features/sessions/SpawnAgentDialog.test.tsx @@ -92,7 +92,7 @@ describe('SpawnAgentDialog', () => { }); }); - it('shows only configured models from the gateway catalog', async () => { + it('shows inherited primary plus configured models from the gateway catalog', async () => { globalThis.fetch = vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ @@ -109,10 +109,11 @@ describe('SpawnAgentDialog', () => { fireEvent.click(screen.getByText('New agent')); const modelSelect = await screen.findByRole('button', { name: 'Select model' }); - expect(modelSelect).toHaveTextContent('glm-4.7'); + expect(modelSelect).toHaveTextContent('primary'); fireEvent.click(modelSelect); - expect(await screen.findByRole('option', { name: 'qwen-local' })).toBeInTheDocument(); + expect(await screen.findByRole('option', { name: 'primary' })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'qwen-local' })).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'claude-sonnet-4-5' })).toBeNull(); expect(screen.queryByRole('option', { name: 'gpt-5.4' })).toBeNull(); }); @@ -140,6 +141,28 @@ describe('SpawnAgentDialog', () => { expect(onSpawn).not.toHaveBeenCalled(); }); + it('omits model in spawn payload when inherited primary is selected', async () => { + const onSpawn = vi.fn(async () => {}); + renderDialog(onSpawn); + + fireEvent.click(screen.getByText('New agent')); + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Select model' })).toHaveTextContent('primary'); + }); + fireEvent.change(screen.getByPlaceholderText('e.g. reviewer'), { target: { value: 'research' } }); + fireEvent.change(screen.getByPlaceholderText('What should this new agent start working on?'), { target: { value: 'test task' } }); + fireEvent.click(screen.getByText('Create agent')); + + await waitFor(() => { + expect(onSpawn).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'root', + agentName: 'research', + task: 'test task', + model: undefined, + })); + }); + }); + it('keeps the dialog open when spawning is deferred by a guard', async () => { const onSpawn = vi.fn(async () => false); const onOpenChange = vi.fn(); @@ -147,7 +170,7 @@ describe('SpawnAgentDialog', () => { fireEvent.click(screen.getByText('New agent')); await waitFor(() => { - expect(screen.getByRole('button', { name: 'Select model' })).toHaveTextContent('claude-sonnet-4-5'); + expect(screen.getByRole('button', { name: 'Select model' })).toHaveTextContent('primary'); }); fireEvent.change(screen.getByPlaceholderText('e.g. reviewer'), { target: { value: 'research' } }); fireEvent.change(screen.getByPlaceholderText('What should this new agent start working on?'), { target: { value: 'test task' } }); diff --git a/src/features/sessions/SpawnAgentDialog.tsx b/src/features/sessions/SpawnAgentDialog.tsx index 55783cf9..dcc4690a 100644 --- a/src/features/sessions/SpawnAgentDialog.tsx +++ b/src/features/sessions/SpawnAgentDialog.tsx @@ -29,6 +29,7 @@ const AFTER_RUN_OPTIONS: InlineSelectOption[] = [ { value: 'keep', label: 'Keep' }, { value: 'delete', label: 'Delete' }, ]; +const INHERITED_MODEL_VALUE = 'primary'; type ModelEntry = { id: string; alias?: string }; type ModelCatalogResponse = { @@ -101,10 +102,14 @@ export function SpawnAgentDialog({ open, onOpenChange, onSpawn }: SpawnAgentDial }, [open]); const modelOptions = useMemo(() => { - return fetchedModels.map((entry) => ({ - value: entry.id, - label: entry.alias || deriveAlias(entry.id), - })); + if (fetchedModels.length === 0) return []; + return [ + { value: INHERITED_MODEL_VALUE, label: INHERITED_MODEL_VALUE }, + ...fetchedModels.map((entry) => ({ + value: entry.id, + label: entry.alias || deriveAlias(entry.id), + })), + ]; }, [fetchedModels]); const visibleModelOptions = useMemo(() => { @@ -112,7 +117,10 @@ export function SpawnAgentDialog({ open, onOpenChange, onSpawn }: SpawnAgentDial return [{ value: '', label: 'No configured models' }]; }, [modelOptions]); - const defaultModelId = useMemo(() => fetchedModels[0]?.id || '', [fetchedModels]); + const defaultModelId = useMemo( + () => (fetchedModels.length > 0 ? INHERITED_MODEL_VALUE : ''), + [fetchedModels], + ); useEffect(() => { if (fetchedModels.length === 0) { @@ -120,7 +128,8 @@ export function SpawnAgentDialog({ open, onOpenChange, onSpawn }: SpawnAgentDial return; } - if (!model || !fetchedModels.some((entry) => entry.id === model)) { + const hasValidSelection = model === INHERITED_MODEL_VALUE || fetchedModels.some((entry) => entry.id === model); + if (!hasValidSelection) { setModel(defaultModelId); } }, [defaultModelId, fetchedModels, model]); @@ -160,13 +169,14 @@ export function SpawnAgentDialog({ open, onOpenChange, onSpawn }: SpawnAgentDial setSpawning(true); setSpawnError(''); + const spawnModel = model === INHERITED_MODEL_VALUE ? undefined : model; try { const spawnResult = mode === 'root' ? await onSpawn({ kind: 'root', agentName: agentNameInput.trim(), task: task.trim(), - model, + model: spawnModel, thinking, }) : await onSpawn({ @@ -174,7 +184,7 @@ export function SpawnAgentDialog({ open, onOpenChange, onSpawn }: SpawnAgentDial parentSessionKey: parentRootKey, task: task.trim(), label: label.trim() || undefined, - model, + model: spawnModel, thinking, cleanup, });