Skip to content

Commit 26dea4e

Browse files
fix: provider cache reset after settings import
SettingsView keeps a local cachedState buffer so settings edits do not write through to the live extension state before Save. After importing settings, the import timestamp was used to bust that cache, but the effect could run again for the same import timestamp whenever extensionState changed identity. That allowed a later live state update to overwrite an in-progress provider edit with the previously saved provider, causing the provider UI to briefly switch and then revert. Track the handled settingsImportedAt value and only reset cachedState once per import event. Add a regression test for the imported-settings flow where Baseten is saved with an API key, the same import timestamp is replayed, and a subsequent DeepSeek provider edit is preserved and saved.
1 parent 34898d2 commit 26dea4e

2 files changed

Lines changed: 102 additions & 5 deletions

File tree

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
148148
const contentRef = useRef<HTMLDivElement | null>(null)
149149

150150
const prevApiConfigName = useRef(currentApiConfigName)
151+
const handledSettingsImportedAt = useRef<number | undefined>(undefined)
151152
const confirmDialogHandler = useRef<() => void>()
152153

153154
const [cachedState, setCachedState] = useState(() => extensionState)
@@ -233,10 +234,13 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
233234

234235
// Bust the cache when settings are imported.
235236
useEffect(() => {
236-
if (settingsImportedAt) {
237-
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
238-
setChangeDetected(false)
237+
if (!settingsImportedAt || handledSettingsImportedAt.current === settingsImportedAt) {
238+
return
239239
}
240+
241+
handledSettingsImportedAt.current = settingsImportedAt
242+
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
243+
setChangeDetected(false)
240244
}, [settingsImportedAt, extensionState])
241245

242246
const setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType> = useCallback((field, value) => {

webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
44
import React from "react"
55

66
// Mock vscode API
7-
const mockPostMessage = vi.fn()
7+
const mockPostMessage = vi.hoisted(() => vi.fn())
88
const mockVscode = {
99
postMessage: mockPostMessage,
1010
}
1111
;(global as any).acquireVsCodeApi = () => mockVscode
1212

13+
vi.mock("@src/utils/vscode", () => ({
14+
vscode: {
15+
postMessage: mockPostMessage,
16+
},
17+
}))
18+
1319
// Import the actual component
1420
import SettingsView from "../SettingsView"
1521
import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -187,7 +193,24 @@ vi.mock("../ApiConfigManager", () => ({
187193
}))
188194

189195
vi.mock("../ApiOptions", () => ({
190-
default: () => null,
196+
default: ({ apiConfiguration, setApiConfigurationField }: any) => (
197+
<div>
198+
<span data-testid="provider-value">{apiConfiguration.apiProvider}</span>
199+
<input
200+
data-testid="baseten-api-key"
201+
value={apiConfiguration.basetenApiKey ?? ""}
202+
onChange={(event) => setApiConfigurationField("basetenApiKey", event.target.value)}
203+
/>
204+
{["openrouter", "baseten", "deepseek"].map((provider) => (
205+
<button
206+
key={provider}
207+
data-testid={`set-provider-${provider}`}
208+
onClick={() => setApiConfigurationField("apiProvider", provider)}>
209+
{provider}
210+
</button>
211+
))}
212+
</div>
213+
),
191214
}))
192215

193216
vi.mock("../AutoApproveSettings", () => ({
@@ -369,4 +392,74 @@ describe("SettingsView - Change Detection Fix", () => {
369392

370393
expect(true).toBe(true) // Placeholder - the real test is the running system
371394
})
395+
396+
it("preserves a DeepSeek provider edit after saving Baseten when the same import timestamp replays", async () => {
397+
const onDone = vi.fn()
398+
let extensionState = createExtensionState({
399+
settingsImportedAt: 123,
400+
apiConfiguration: {
401+
apiProvider: "openai",
402+
apiModelId: "gpt-4.1",
403+
},
404+
})
405+
406+
;(useExtensionState as any).mockImplementation(() => extensionState)
407+
408+
const { rerender } = render(
409+
<QueryClientProvider client={queryClient}>
410+
<SettingsView onDone={onDone} />
411+
</QueryClientProvider>,
412+
)
413+
414+
await waitFor(() => {
415+
expect(screen.getByTestId("provider-value")).toHaveTextContent("openai")
416+
})
417+
418+
fireEvent.click(screen.getByTestId("set-provider-baseten"))
419+
fireEvent.change(screen.getByTestId("baseten-api-key"), { target: { value: "test-baseten-key" } })
420+
expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten")
421+
422+
mockPostMessage.mockClear()
423+
fireEvent.click(screen.getByTestId("save-button"))
424+
expect(mockPostMessage).toHaveBeenCalledWith({
425+
type: "upsertApiConfiguration",
426+
text: "default",
427+
apiConfiguration: expect.objectContaining({
428+
apiProvider: "baseten",
429+
basetenApiKey: "test-baseten-key",
430+
}),
431+
})
432+
433+
fireEvent.click(screen.getByTestId("set-provider-deepseek"))
434+
expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek")
435+
436+
extensionState = createExtensionState({
437+
settingsImportedAt: 123,
438+
soundEnabled: true,
439+
apiConfiguration: {
440+
apiProvider: "baseten",
441+
apiModelId: "zai-org/GLM-4.6",
442+
basetenApiKey: "test-baseten-key",
443+
},
444+
})
445+
446+
rerender(
447+
<QueryClientProvider client={queryClient}>
448+
<SettingsView onDone={onDone} />
449+
</QueryClientProvider>,
450+
)
451+
452+
expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek")
453+
454+
mockPostMessage.mockClear()
455+
fireEvent.click(screen.getByTestId("save-button"))
456+
457+
expect(mockPostMessage).toHaveBeenCalledWith({
458+
type: "upsertApiConfiguration",
459+
text: "default",
460+
apiConfiguration: expect.objectContaining({
461+
apiProvider: "deepseek",
462+
}),
463+
})
464+
})
372465
})

0 commit comments

Comments
 (0)