diff --git a/web/src/components/NewSession/index.test.tsx b/web/src/components/NewSession/index.test.tsx index 699a8388..b91ef77f 100644 --- a/web/src/components/NewSession/index.test.tsx +++ b/web/src/components/NewSession/index.test.tsx @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import type { ApiClient } from '@/api/client' import type { Machine } from '@/types/api' import { saveNewSessionFormDraft } from './newSessionFormDraft' @@ -156,10 +156,17 @@ vi.mock('./ModelSelector', () => ({ ModelSelector: (props: { model: string options?: Array<{ value: string; label: string }> + isDisabled: boolean + isLoading?: boolean onModelChange: (model: string) => void }) => ( <> -
{props.options?.map((option) => option.label).join(',')}
@@ -309,6 +316,52 @@ describe('NewSession launch preferences', () => { }) }) + it('blocks model changes and creation until custom Claude models load', async () => { + savePreferredAgent('claude') + savePreferredLaunchSettings('machine-1', 'claude', { + model: 'deepseek-v4-flash[1m]', + cursorSelectedBase: 'auto', + effort: 'high', + modelReasoningEffort: 'default' + }) + let resolveModels!: (value: { models: string[] }) => void + const claudeApi = { + getClaudeCustomModels: vi.fn().mockReturnValue(new Promise((resolve) => { + resolveModels = resolve + })) + } as unknown as ApiClient + + render( + {}} + /> + ) + + const model = screen.getByTestId('model') + const create = screen.getByTestId('create') + expect(model).toBeDisabled() + expect(create).toBeDisabled() + fireEvent.click(model) + fireEvent.click(create) + expect(model).toHaveTextContent('auto') + expect(mocks.spawnSession).not.toHaveBeenCalled() + + await act(async () => { + resolveModels({ models: ['deepseek-v4-flash[1m]'] }) + }) + + await waitFor(() => { + expect(model).toHaveTextContent('deepseek-v4-flash[1m]') + expect(model).toBeEnabled() + expect(create).toBeEnabled() + }) + }) + it.each([ ['model', 'gpt-5.6-sol', 'default'], ['reasoning effort', 'auto', 'xhigh'] diff --git a/web/src/components/NewSession/index.tsx b/web/src/components/NewSession/index.tsx index ecee9998..b32ba264 100644 --- a/web/src/components/NewSession/index.tsx +++ b/web/src/components/NewSession/index.tsx @@ -308,7 +308,8 @@ export function NewSession(props: { () => agent === 'claude' ? claudeModelOptions.map((option) => option.value) : null, [agent, claudeModelOptions] ) - const preferredModelCatalogReady = agent !== 'claude' || claudeModelsLoaded + const claudeModelsLoading = agent === 'claude' && !claudeModelsLoaded + const preferredModelCatalogReady = !claudeModelsLoading const runnerSpawnError = useMemo( () => formatRunnerSpawnError(selectedMachine), [selectedMachine] @@ -1342,7 +1343,8 @@ export function NewSession(props: { } const isLaunchPreferenceValidationPending = - (agent === 'codex' + claudeModelsLoading + || (agent === 'codex' && (model !== 'auto' || modelReasoningEffort !== 'default') && codexModelsState.isLoading) || (agent === 'agy' @@ -1522,7 +1524,8 @@ export function NewSession(props: { || (agent === 'grok' && Boolean(grokModelsState.error)) || (agent === 'copilot' && Boolean(copilotModelsState.error)) } - isLoading={(agent === 'codex' && codexModelsState.isLoading) + isLoading={claudeModelsLoading + || (agent === 'codex' && codexModelsState.isLoading) || (agent === 'grok' && grokModelsState.isLoading) || (agent === 'copilot' && copilotModelsState.isLoading)} error={agent === 'codex' && codexModelsState.error