Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ export type ExtensionState = Pick<
profileThresholds: Record<string, number>
hasOpenedModeSelector: boolean
openRouterImageApiKey?: string
hasOpenRouterImageApiKey: boolean
messageQueue?: QueuedMessage[]
lastShownAnnouncementId?: string
apiModelId?: string
Expand Down
3 changes: 2 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2320,7 +2320,7 @@ export class ClineProvider
taskSyncEnabled,
remoteControlEnabled,
imageGenerationProvider,
openRouterImageApiKey,
hasOpenRouterImageApiKey: Boolean(openRouterImageApiKey?.trim()),
openRouterImageGenerationSelectedModel,
featureRoomoteControlEnabled,
openAiCodexIsAuthenticated: await (async () => {
Expand Down Expand Up @@ -2563,6 +2563,7 @@ export class ClineProvider
})(),
imageGenerationProvider: stateValues.imageGenerationProvider,
openRouterImageApiKey: stateValues.openRouterImageApiKey,
hasOpenRouterImageApiKey: Boolean(stateValues.openRouterImageApiKey?.trim()),
openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel,
featureRoomoteControlEnabled: (() => {
try {
Expand Down
18 changes: 17 additions & 1 deletion src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ describe("ClineProvider", () => {
profileThresholds: {},
hasOpenedModeSelector: false,
diagnosticsEnabled: true,
openRouterImageApiKey: undefined,
hasOpenRouterImageApiKey: false,
openRouterImageGenerationSelectedModel: undefined,
remoteControlEnabled: false,
taskSyncEnabled: false,
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions webview-ui/src/components/settings/ExperimentalSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
apiConfiguration?: any
setApiConfigurationField?: any
imageGenerationProvider?: ImageGenerationProvider
openRouterImageApiKey?: string
hasOpenRouterImageApiKey?: boolean
openRouterImageGenerationSelectedModel?: string
setImageGenerationProvider?: (provider: ImageGenerationProvider) => void
setOpenRouterImageApiKey?: (apiKey: string) => void
Expand All @@ -34,7 +34,7 @@ export const ExperimentalSettings = ({
apiConfiguration,
setApiConfigurationField,
imageGenerationProvider,
openRouterImageApiKey,
hasOpenRouterImageApiKey,
openRouterImageGenerationSelectedModel,
setImageGenerationProvider,
setOpenRouterImageApiKey,
Expand Down Expand Up @@ -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}
Expand Down
13 changes: 8 additions & 5 deletions webview-ui/src/components/settings/ImageGenerationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,7 +18,7 @@ export const ImageGenerationSettings = ({
enabled,
onChange,
imageGenerationProvider,
openRouterImageApiKey,
hasOpenRouterImageApiKey,
openRouterImageGenerationSelectedModel,
setImageGenerationProvider,
setOpenRouterImageApiKey,
Expand Down Expand Up @@ -88,7 +88,7 @@ export const ImageGenerationSettings = ({
}

const requiresApiKey = currentProvider === "openrouter"
const isConfigured = !requiresApiKey || (requiresApiKey && openRouterImageApiKey)
const isConfigured = !requiresApiKey || (requiresApiKey && hasOpenRouterImageApiKey)

return (
<div className="space-y-4">
Expand Down Expand Up @@ -133,9 +133,12 @@ export const ImageGenerationSettings = ({
{t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyLabel")}
</label>
<VSCodeTextField
value={openRouterImageApiKey || ""}
onInput={(e: any) => 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"
/>
Expand Down
21 changes: 11 additions & 10 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxDiagnosticMessages,
includeTaskHistoryInEnhance,
imageGenerationProvider,
openRouterImageApiKey,
hasOpenRouterImageApiKey,
openRouterImageGenerationSelectedModel,
reasoningBlockCollapsed,
enterBehavior,
Expand All @@ -225,13 +225,15 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t

setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
prevApiConfigName.current = currentApiConfigName
setPendingImageApiKey(null)
setChangeDetected(false)
}, [currentApiConfigName, extensionState])

// Bust the cache when settings are imported.
useEffect(() => {
if (settingsImportedAt) {
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
setPendingImageApiKey(null)
setChangeDetected(false)
}
}, [settingsImportedAt, extensionState])
Expand Down Expand Up @@ -331,14 +333,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
})
}, [])

const setOpenRouterImageApiKey = useCallback((apiKey: string) => {
setCachedState((prevState) => {
if (prevState.openRouterImageApiKey !== apiKey) {
setChangeDetected(true)
}
const [pendingImageApiKey, setPendingImageApiKey] = useState<string | null>(null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pendingImageApiKey is not reset when cachedState is reset elsewhere in this component. The useEffect hooks on lines 226 and 234 reset cachedState and clear changeDetected when the API config profile changes or settings are imported, but they don't call setPendingImageApiKey(null). If a user types an API key, then switches profiles (triggering the reset), and later saves another unrelated change, the stale pendingImageApiKey will be sent in the updateSettings payload, writing an unintended key.

Suggested change
const [pendingImageApiKey, setPendingImageApiKey] = useState<string | null>(null)
const [pendingImageApiKey, setPendingImageApiKey] = useState<string | null>(null)

The fix itself is straightforward: add setPendingImageApiKey(null) alongside setChangeDetected(false) in both useEffect blocks (lines ~228 and ~235). I didn't include a multi-line suggestion since the affected lines are spread across non-contiguous blocks.

Fix it with Roo Code or mention @roomote and request a fix.


return { ...prevState, openRouterImageApiKey: apiKey }
})
const setOpenRouterImageApiKey = useCallback((apiKey: string) => {
setPendingImageApiKey(apiKey)
setChangeDetected(true)
}, [])

const setImageGenerationSelectedModel = useCallback((model: string) => {
Expand Down Expand Up @@ -433,7 +432,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxGitStatusFiles: maxGitStatusFiles ?? 0,
profileThresholds,
imageGenerationProvider,
openRouterImageApiKey,
...(pendingImageApiKey !== null ? { openRouterImageApiKey: pendingImageApiKey } : {}),
openRouterImageGenerationSelectedModel,
experiments,
customSupportPrompts,
Expand All @@ -446,6 +445,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
vscode.postMessage({ type: "debugSetting", bool: cachedState.debug })

setPendingImageApiKey(null)
setChangeDetected(false)
}
}
Expand All @@ -469,6 +469,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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)
}
Expand Down Expand Up @@ -933,7 +934,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
imageGenerationProvider={imageGenerationProvider}
openRouterImageApiKey={openRouterImageApiKey as string | undefined}
hasOpenRouterImageApiKey={!!hasOpenRouterImageApiKey}
openRouterImageGenerationSelectedModel={
openRouterImageGenerationSelectedModel as string | undefined
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("ImageGenerationSettings", () => {
enabled: false,
onChange: mockOnChange,
imageGenerationProvider: undefined,
openRouterImageApiKey: undefined,
hasOpenRouterImageApiKey: false,
openRouterImageGenerationSelectedModel: undefined,
setImageGenerationProvider: mockSetImageGenerationProvider,
setOpenRouterImageApiKey: mockSetOpenRouterImageApiKey,
Expand All @@ -44,7 +44,7 @@ describe("ImageGenerationSettings", () => {
render(
<ImageGenerationSettings
{...defaultProps}
openRouterImageApiKey="existing-key"
hasOpenRouterImageApiKey={true}
openRouterImageGenerationSelectedModel="google/gemini-2.5-flash-image"
/>,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ describe("SettingsView - Change Detection Fix", () => {
includeDiagnosticMessages: false,
maxDiagnosticMessages: 50,
includeTaskHistoryInEnhance: true,
openRouterImageApiKey: undefined,
hasOpenRouterImageApiKey: false,
openRouterImageGenerationSelectedModel: undefined,
reasoningBlockCollapsed: true,
...overrides,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ describe("SettingsView - Unsaved Changes Detection", () => {
includeDiagnosticMessages: false,
maxDiagnosticMessages: 50,
includeTaskHistoryInEnhance: true,
openRouterImageApiKey: undefined,
hasOpenRouterImageApiKey: false,
openRouterImageGenerationSelectedModel: undefined,
reasoningBlockCollapsed: true,
}
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -285,6 +286,7 @@ describe("mergeExtensionState", () => {
hasOpenedModeSelector: false,
maxImageFileSize: 5,
maxTotalImageSize: 20,
hasOpenRouterImageApiKey: false,
remoteControlEnabled: false,
taskSyncEnabled: false,
featureRoomoteControlEnabled: false,
Expand Down
Loading