fix(web): gate Claude model interactions during restore

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-08-05 11:16:41 +08:00
co-authored by HAPI
parent 6246a11c26
commit 4c11de8aac
2 changed files with 61 additions and 5 deletions
+55 -2
View File
@@ -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
}) => (
<>
<button type="button" data-testid="model" onClick={() => props.onModelChange('gpt-5.6-terra')}>
<button
type="button"
data-testid="model"
disabled={props.isDisabled || props.isLoading}
onClick={() => props.onModelChange('gpt-5.6-terra')}
>
{props.model}
</button>
<div data-testid="model-options">{props.options?.map((option) => option.label).join(',')}</div>
@@ -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(
<NewSession
api={claudeApi}
machines={[machine]}
initialMachineId="machine-1"
initialDirectory="C:\\repo"
onSuccess={mocks.onSuccess}
onCancel={() => {}}
/>
)
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']
+6 -3
View File
@@ -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