diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 45c4ab2090a..eda131c25c9 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -377,6 +377,7 @@ export type ExtensionState = Pick< profileThresholds: Record hasOpenedModeSelector: boolean openRouterImageApiKey?: string + hasOpenRouterImageApiKey: boolean messageQueue?: QueuedMessage[] lastShownAnnouncementId?: string apiModelId?: string diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 332ae31c8b5..16eb677105e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2320,7 +2320,7 @@ export class ClineProvider taskSyncEnabled, remoteControlEnabled, imageGenerationProvider, - openRouterImageApiKey, + hasOpenRouterImageApiKey: Boolean(openRouterImageApiKey?.trim()), openRouterImageGenerationSelectedModel, featureRoomoteControlEnabled, openAiCodexIsAuthenticated: await (async () => { @@ -2563,6 +2563,7 @@ export class ClineProvider })(), imageGenerationProvider: stateValues.imageGenerationProvider, openRouterImageApiKey: stateValues.openRouterImageApiKey, + hasOpenRouterImageApiKey: Boolean(stateValues.openRouterImageApiKey?.trim()), openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel, featureRoomoteControlEnabled: (() => { try { diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 5a57fa96788..01efb3a49d6 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -586,7 +586,7 @@ describe("ClineProvider", () => { profileThresholds: {}, hasOpenedModeSelector: false, diagnosticsEnabled: true, - openRouterImageApiKey: undefined, + hasOpenRouterImageApiKey: false, openRouterImageGenerationSelectedModel: undefined, remoteControlEnabled: false, taskSyncEnabled: false, @@ -809,6 +809,22 @@ describe("ClineProvider", () => { expect(state).toHaveProperty("writeDelayMs") }) + test("getStateToPostToWebview does not expose openRouterImageApiKey", async () => { + // Store a sentinel API key value via contextProxy (which caches secrets internally) + const sentinelKey = "sk-or-v1-SENTINEL-KEY-VALUE" + // @ts-ignore - Access private property for testing + await provider.contextProxy.storeSecret("openRouterImageApiKey", sentinelKey) + + const state = await provider.getStateToPostToWebview() + + // Must expose only a boolean flag, not the raw key + expect(state).toHaveProperty("hasOpenRouterImageApiKey", true) + // The raw key value must never appear in the serialized webview state + expect(JSON.stringify(state)).not.toContain(sentinelKey) + // The property name "openRouterImageApiKey" must not be a direct key in the state + expect(state).not.toHaveProperty("openRouterImageApiKey") + }) + test("language is set to VSCode language", async () => { // Mock VSCode language as Spanish ;(vscode.env as any).language = "pt-BR" diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 23786ce0b98..c71b0717d3c 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -21,7 +21,7 @@ type ExperimentalSettingsProps = HTMLAttributes & { apiConfiguration?: any setApiConfigurationField?: any imageGenerationProvider?: ImageGenerationProvider - openRouterImageApiKey?: string + hasOpenRouterImageApiKey?: boolean openRouterImageGenerationSelectedModel?: string setImageGenerationProvider?: (provider: ImageGenerationProvider) => void setOpenRouterImageApiKey?: (apiKey: string) => void @@ -34,7 +34,7 @@ export const ExperimentalSettings = ({ apiConfiguration, setApiConfigurationField, imageGenerationProvider, - openRouterImageApiKey, + hasOpenRouterImageApiKey, openRouterImageGenerationSelectedModel, setImageGenerationProvider, setOpenRouterImageApiKey, @@ -74,7 +74,7 @@ export const ExperimentalSettings = ({ setExperimentEnabled(EXPERIMENT_IDS.IMAGE_GENERATION, enabled) } imageGenerationProvider={imageGenerationProvider} - openRouterImageApiKey={openRouterImageApiKey} + hasOpenRouterImageApiKey={hasOpenRouterImageApiKey} openRouterImageGenerationSelectedModel={openRouterImageGenerationSelectedModel} setImageGenerationProvider={setImageGenerationProvider} setOpenRouterImageApiKey={setOpenRouterImageApiKey} diff --git a/webview-ui/src/components/settings/ImageGenerationSettings.tsx b/webview-ui/src/components/settings/ImageGenerationSettings.tsx index ccbd0a3fff9..4f5c11d5d10 100644 --- a/webview-ui/src/components/settings/ImageGenerationSettings.tsx +++ b/webview-ui/src/components/settings/ImageGenerationSettings.tsx @@ -7,7 +7,7 @@ interface ImageGenerationSettingsProps { enabled: boolean onChange: (enabled: boolean) => void imageGenerationProvider?: ImageGenerationProvider - openRouterImageApiKey?: string + hasOpenRouterImageApiKey?: boolean openRouterImageGenerationSelectedModel?: string setImageGenerationProvider: (provider: ImageGenerationProvider) => void setOpenRouterImageApiKey: (apiKey: string) => void @@ -18,7 +18,7 @@ export const ImageGenerationSettings = ({ enabled, onChange, imageGenerationProvider, - openRouterImageApiKey, + hasOpenRouterImageApiKey, openRouterImageGenerationSelectedModel, setImageGenerationProvider, setOpenRouterImageApiKey, @@ -88,7 +88,7 @@ export const ImageGenerationSettings = ({ } const requiresApiKey = currentProvider === "openrouter" - const isConfigured = !requiresApiKey || (requiresApiKey && openRouterImageApiKey) + const isConfigured = !requiresApiKey || (requiresApiKey && hasOpenRouterImageApiKey) return (
@@ -133,9 +133,12 @@ export const ImageGenerationSettings = ({ {t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyLabel")} handleApiKeyChange(e.target.value)} - placeholder={t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder")} + placeholder={ + hasOpenRouterImageApiKey + ? t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyConfigured", { defaultValue: "Key configured (enter new value to replace)" }) + : t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder") + } className="w-full" type="password" /> diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index eb57b4e0009..bc13ae82941 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -205,7 +205,7 @@ const SettingsView = forwardRef(({ onDone, t maxDiagnosticMessages, includeTaskHistoryInEnhance, imageGenerationProvider, - openRouterImageApiKey, + hasOpenRouterImageApiKey, openRouterImageGenerationSelectedModel, reasoningBlockCollapsed, enterBehavior, @@ -225,6 +225,7 @@ const SettingsView = forwardRef(({ onDone, t setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) prevApiConfigName.current = currentApiConfigName + setPendingImageApiKey(null) setChangeDetected(false) }, [currentApiConfigName, extensionState]) @@ -232,6 +233,7 @@ const SettingsView = forwardRef(({ onDone, t useEffect(() => { if (settingsImportedAt) { setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) + setPendingImageApiKey(null) setChangeDetected(false) } }, [settingsImportedAt, extensionState]) @@ -331,14 +333,11 @@ const SettingsView = forwardRef(({ onDone, t }) }, []) - const setOpenRouterImageApiKey = useCallback((apiKey: string) => { - setCachedState((prevState) => { - if (prevState.openRouterImageApiKey !== apiKey) { - setChangeDetected(true) - } + const [pendingImageApiKey, setPendingImageApiKey] = useState(null) - return { ...prevState, openRouterImageApiKey: apiKey } - }) + const setOpenRouterImageApiKey = useCallback((apiKey: string) => { + setPendingImageApiKey(apiKey) + setChangeDetected(true) }, []) const setImageGenerationSelectedModel = useCallback((model: string) => { @@ -433,7 +432,7 @@ const SettingsView = forwardRef(({ onDone, t maxGitStatusFiles: maxGitStatusFiles ?? 0, profileThresholds, imageGenerationProvider, - openRouterImageApiKey, + ...(pendingImageApiKey !== null ? { openRouterImageApiKey: pendingImageApiKey } : {}), openRouterImageGenerationSelectedModel, experiments, customSupportPrompts, @@ -446,6 +445,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting }) vscode.postMessage({ type: "debugSetting", bool: cachedState.debug }) + setPendingImageApiKey(null) setChangeDetected(false) } } @@ -469,6 +469,7 @@ const SettingsView = forwardRef(({ onDone, t if (confirm) { // Discard changes: Reset state and flag setCachedState(extensionState) // Revert to original state + setPendingImageApiKey(null) setChangeDetected(false) // Reset change flag confirmDialogHandler.current?.() // Execute the pending action (e.g., tab switch) } @@ -933,7 +934,7 @@ const SettingsView = forwardRef(({ onDone, t apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} imageGenerationProvider={imageGenerationProvider} - openRouterImageApiKey={openRouterImageApiKey as string | undefined} + hasOpenRouterImageApiKey={!!hasOpenRouterImageApiKey} openRouterImageGenerationSelectedModel={ openRouterImageGenerationSelectedModel as string | undefined } diff --git a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx index 12ad1af591e..49b61175ef1 100644 --- a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx @@ -19,7 +19,7 @@ describe("ImageGenerationSettings", () => { enabled: false, onChange: mockOnChange, imageGenerationProvider: undefined, - openRouterImageApiKey: undefined, + hasOpenRouterImageApiKey: false, openRouterImageGenerationSelectedModel: undefined, setImageGenerationProvider: mockSetImageGenerationProvider, setOpenRouterImageApiKey: mockSetOpenRouterImageApiKey, @@ -44,7 +44,7 @@ describe("ImageGenerationSettings", () => { render( , ) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx index be725ea6a15..8b90ec44a98 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx @@ -257,7 +257,7 @@ describe("SettingsView - Change Detection Fix", () => { includeDiagnosticMessages: false, maxDiagnosticMessages: 50, includeTaskHistoryInEnhance: true, - openRouterImageApiKey: undefined, + hasOpenRouterImageApiKey: false, openRouterImageGenerationSelectedModel: undefined, reasoningBlockCollapsed: true, ...overrides, diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index 437404e0e7d..377fe5736df 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -262,7 +262,7 @@ describe("SettingsView - Unsaved Changes Detection", () => { includeDiagnosticMessages: false, maxDiagnosticMessages: 50, includeTaskHistoryInEnhance: true, - openRouterImageApiKey: undefined, + hasOpenRouterImageApiKey: false, openRouterImageGenerationSelectedModel: undefined, reasoningBlockCollapsed: true, } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 85a750065ff..4933111968b 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -274,7 +274,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode codebaseIndexModels: { ollama: {}, openai: {} }, includeDiagnosticMessages: true, maxDiagnosticMessages: 50, - openRouterImageApiKey: "", + hasOpenRouterImageApiKey: false, openRouterImageGenerationSelectedModel: "", includeCurrentTime: true, includeCurrentCost: true, diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 41dabd7f0c2..4982ee92a01 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -214,6 +214,7 @@ describe("mergeExtensionState", () => { hasOpenedModeSelector: false, // Add the new required property maxImageFileSize: 5, maxTotalImageSize: 20, + hasOpenRouterImageApiKey: false, remoteControlEnabled: false, taskSyncEnabled: false, featureRoomoteControlEnabled: false, @@ -285,6 +286,7 @@ describe("mergeExtensionState", () => { hasOpenedModeSelector: false, maxImageFileSize: 5, maxTotalImageSize: 20, + hasOpenRouterImageApiKey: false, remoteControlEnabled: false, taskSyncEnabled: false, featureRoomoteControlEnabled: false,