Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 10030d7

Browse files
committed
feat(SettingsView): pending image API key state and clear-on-save
1 parent 468cb3f commit 10030d7

3 files changed

Lines changed: 112 additions & 13 deletions

File tree

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
130130
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
131131
const [isChangeDetected, setChangeDetected] = useState(false)
132132
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
133+
const [pendingImageApiKey, setPendingImageApiKey] = useState<string | null>(null)
133134
const [activeTab, setActiveTab] = useState<SectionName>(
134135
targetSection && sectionNames.includes(targetSection as SectionName)
135136
? (targetSection as SectionName)
@@ -196,7 +197,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
196197
maxDiagnosticMessages,
197198
includeTaskHistoryInEnhance,
198199
imageGenerationProvider,
199-
openRouterImageApiKey,
200+
openRouterImageApiKeyConfigured,
200201
openRouterImageGenerationSelectedModel,
201202
reasoningBlockCollapsed,
202203
enterBehavior,
@@ -323,13 +324,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
323324
}, [])
324325

325326
const setOpenRouterImageApiKey = useCallback((apiKey: string) => {
326-
setCachedState((prevState) => {
327-
if (prevState.openRouterImageApiKey !== apiKey) {
328-
setChangeDetected(true)
329-
}
330-
331-
return { ...prevState, openRouterImageApiKey: apiKey }
332-
})
327+
setPendingImageApiKey(apiKey)
328+
setChangeDetected(true)
333329
}, [])
334330

335331
const setImageGenerationSelectedModel = useCallback((model: string) => {
@@ -418,7 +414,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
418414
maxGitStatusFiles: maxGitStatusFiles ?? 0,
419415
profileThresholds,
420416
imageGenerationProvider,
421-
openRouterImageApiKey,
417+
...(pendingImageApiKey !== null ? { openRouterImageApiKey: pendingImageApiKey || undefined } : {}),
422418
openRouterImageGenerationSelectedModel,
423419
experiments,
424420
customSupportPrompts,
@@ -431,6 +427,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
431427
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
432428
vscode.postMessage({ type: "debugSetting", bool: cachedState.debug })
433429

430+
setPendingImageApiKey(null)
434431
setChangeDetected(false)
435432
}
436433
}
@@ -454,6 +451,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
454451
if (confirm) {
455452
// Discard changes: Reset state and flag
456453
setCachedState(extensionState) // Revert to original state
454+
setPendingImageApiKey(null)
457455
setChangeDetected(false) // Reset change flag
458456
confirmDialogHandler.current?.() // Execute the pending action (e.g., tab switch)
459457
}
@@ -904,7 +902,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
904902
apiConfiguration={apiConfiguration}
905903
setApiConfigurationField={setApiConfigurationField}
906904
imageGenerationProvider={imageGenerationProvider}
907-
openRouterImageApiKey={openRouterImageApiKey as string | undefined}
905+
openRouterImageApiKeyConfigured={openRouterImageApiKeyConfigured ?? false}
908906
openRouterImageGenerationSelectedModel={
909907
openRouterImageGenerationSelectedModel as string | undefined
910908
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ describe("SettingsView - Change Detection Fix", () => {
299299
includeDiagnosticMessages: false,
300300
maxDiagnosticMessages: 50,
301301
includeTaskHistoryInEnhance: true,
302-
openRouterImageApiKey: undefined,
302+
openRouterImageApiKeyConfigured: false,
303303
openRouterImageGenerationSelectedModel: undefined,
304304
reasoningBlockCollapsed: true,
305305
...overrides,

webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import React from "react"
55

66
import SettingsView from "../SettingsView"
77

8-
// Mock vscode API
8+
// Mock the vscode utility module (acquireVsCodeApi is only available inside the VS Code webview)
9+
vi.mock("@src/utils/vscode", () => ({
10+
vscode: { postMessage: vi.fn(), getState: vi.fn(), setState: vi.fn() },
11+
}))
12+
13+
// Legacy global mock retained for other test setups that may rely on it
914
const mockPostMessage = vi.fn()
1015
const mockVscode = {
1116
postMessage: mockPostMessage,
@@ -242,6 +247,8 @@ vi.mock("../SettingsSearch", () => ({
242247

243248
import { useExtensionState } from "@src/context/ExtensionStateContext"
244249
import ApiOptions from "../ApiOptions"
250+
import { ExperimentalSettings } from "../ExperimentalSettings"
251+
import { vscode } from "@src/utils/vscode"
245252

246253
describe("SettingsView - Unsaved Changes Detection", () => {
247254
let queryClient: QueryClient
@@ -304,7 +311,7 @@ describe("SettingsView - Unsaved Changes Detection", () => {
304311
includeDiagnosticMessages: false,
305312
maxDiagnosticMessages: 50,
306313
includeTaskHistoryInEnhance: true,
307-
openRouterImageApiKey: undefined,
314+
openRouterImageApiKeyConfigured: false,
308315
openRouterImageGenerationSelectedModel: undefined,
309316
reasoningBlockCollapsed: true,
310317
}
@@ -316,6 +323,8 @@ describe("SettingsView - Unsaved Changes Detection", () => {
316323
// Don't do anything with props, just render a div
317324
return <div data-testid="api-options">ApiOptions</div>
318325
})
326+
// Reset ExperimentalSettings to silent default
327+
vi.mocked(ExperimentalSettings).mockImplementation(() => <div>ExperimentalSettings</div>)
319328
queryClient = new QueryClient({
320329
defaultOptions: {
321330
queries: { retry: false },
@@ -603,4 +612,96 @@ describe("SettingsView - Unsaved Changes Detection", () => {
603612
// No dialog should appear
604613
expect(screen.queryByText("settings:unsavedChangesDialog.title")).not.toBeInTheDocument()
605614
})
615+
616+
describe("pending image API key", () => {
617+
const renderWithApiKeyTrigger = () => {
618+
vi.mocked(ExperimentalSettings).mockImplementation(({ setOpenRouterImageApiKey }: any) => (
619+
<div>
620+
<button data-testid="set-api-key" onClick={() => setOpenRouterImageApiKey?.("sk-or-new-key")}>
621+
Set Key
622+
</button>
623+
<button data-testid="clear-api-key" onClick={() => setOpenRouterImageApiKey?.("")}>
624+
Clear Key
625+
</button>
626+
</div>
627+
))
628+
// Render with the experimental tab active so ExperimentalSettings is mounted
629+
return render(
630+
<QueryClientProvider client={queryClient}>
631+
<SettingsView onDone={vi.fn()} targetSection="experimental" />
632+
</QueryClientProvider>,
633+
)
634+
}
635+
636+
it("typing a new key marks settings as changed and includes key in save payload", async () => {
637+
renderWithApiKeyTrigger()
638+
639+
await waitFor(() => expect(screen.getByTestId("save-button")).toBeInTheDocument())
640+
641+
fireEvent.click(screen.getByTestId("set-api-key"))
642+
643+
// Save button should now be enabled
644+
await waitFor(() => {
645+
expect((screen.getByTestId("save-button") as HTMLButtonElement).disabled).toBe(false)
646+
})
647+
648+
fireEvent.click(screen.getByTestId("save-button"))
649+
650+
await waitFor(() => {
651+
const calls = vi.mocked(vscode.postMessage).mock.calls
652+
const updateCall = (calls.find(([msg]: any) => msg?.type === "updateSettings") as any)?.[0]
653+
expect(updateCall).toBeDefined()
654+
expect(updateCall.updatedSettings.openRouterImageApiKey).toBe("sk-or-new-key")
655+
})
656+
})
657+
658+
it("clearing the key sends undefined (triggers deletion) rather than empty string", async () => {
659+
renderWithApiKeyTrigger()
660+
661+
await waitFor(() => expect(screen.getByTestId("save-button")).toBeInTheDocument())
662+
663+
fireEvent.click(screen.getByTestId("clear-api-key"))
664+
665+
await waitFor(() => {
666+
expect((screen.getByTestId("save-button") as HTMLButtonElement).disabled).toBe(false)
667+
})
668+
669+
fireEvent.click(screen.getByTestId("save-button"))
670+
671+
await waitFor(() => {
672+
const calls = vi.mocked(vscode.postMessage).mock.calls
673+
const updateCall = (calls.find(([msg]: any) => msg?.type === "updateSettings") as any)?.[0]
674+
expect(updateCall).toBeDefined()
675+
// Empty string should become undefined so the host deletes the secret
676+
expect(updateCall.updatedSettings.openRouterImageApiKey).toBeUndefined()
677+
})
678+
})
679+
680+
it("discarding changes resets pending key so save button returns to disabled", async () => {
681+
renderWithApiKeyTrigger()
682+
683+
await waitFor(() => expect(screen.getByTestId("save-button")).toBeInTheDocument())
684+
685+
fireEvent.click(screen.getByTestId("set-api-key"))
686+
687+
await waitFor(() => {
688+
expect((screen.getByTestId("save-button") as HTMLButtonElement).disabled).toBe(false)
689+
})
690+
691+
// Click Done to trigger the unsaved-changes dialog
692+
fireEvent.click(screen.getByText("settings:common.done"))
693+
694+
await waitFor(() => {
695+
expect(screen.getByText("settings:unsavedChangesDialog.title")).toBeInTheDocument()
696+
})
697+
698+
// Confirm discard
699+
fireEvent.click(screen.getByText("settings:unsavedChangesDialog.discardButton"))
700+
701+
// Save button should be disabled again (pending key cleared)
702+
await waitFor(() => {
703+
expect((screen.getByTestId("save-button") as HTMLButtonElement).disabled).toBe(true)
704+
})
705+
})
706+
})
606707
})

0 commit comments

Comments
 (0)