mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
refactor: flatten image generation settings structure (#7536)
This commit is contained in:
parent
666db9a7f4
commit
df556e72f9
17 changed files with 264 additions and 120 deletions
|
|
@ -42,6 +42,10 @@ export const globalSettingsSchema = z.object({
|
|||
customInstructions: z.string().optional(),
|
||||
taskHistory: z.array(historyItemSchema).optional(),
|
||||
|
||||
// Image generation settings (experimental) - flattened for simplicity
|
||||
openRouterImageApiKey: z.string().optional(),
|
||||
openRouterImageGenerationSelectedModel: z.string().optional(),
|
||||
|
||||
condensingApiConfigId: z.string().optional(),
|
||||
customCondensingPrompt: z.string().optional(),
|
||||
|
||||
|
|
@ -201,11 +205,24 @@ export const SECRET_STATE_KEYS = [
|
|||
"featherlessApiKey",
|
||||
"ioIntelligenceApiKey",
|
||||
"vercelAiGatewayApiKey",
|
||||
] as const satisfies readonly (keyof ProviderSettings)[]
|
||||
export type SecretState = Pick<ProviderSettings, (typeof SECRET_STATE_KEYS)[number]>
|
||||
] as const
|
||||
|
||||
// Global secrets that are part of GlobalSettings (not ProviderSettings)
|
||||
export const GLOBAL_SECRET_KEYS = [
|
||||
"openRouterImageApiKey", // For image generation
|
||||
] as const
|
||||
|
||||
// Type for the actual secret storage keys
|
||||
type ProviderSecretKey = (typeof SECRET_STATE_KEYS)[number]
|
||||
type GlobalSecretKey = (typeof GLOBAL_SECRET_KEYS)[number]
|
||||
|
||||
// Type representing all secrets that can be stored
|
||||
export type SecretState = Pick<ProviderSettings, Extract<ProviderSecretKey, keyof ProviderSettings>> & {
|
||||
[K in GlobalSecretKey]?: string
|
||||
}
|
||||
|
||||
export const isSecretStateKey = (key: string): key is Keys<SecretState> =>
|
||||
SECRET_STATE_KEYS.includes(key as Keys<SecretState>)
|
||||
SECRET_STATE_KEYS.includes(key as ProviderSecretKey) || GLOBAL_SECRET_KEYS.includes(key as GlobalSecretKey)
|
||||
|
||||
/**
|
||||
* GlobalState
|
||||
|
|
@ -214,7 +231,7 @@ export const isSecretStateKey = (key: string): key is Keys<SecretState> =>
|
|||
export type GlobalState = Omit<RooCodeSettings, Keys<SecretState>>
|
||||
|
||||
export const GLOBAL_STATE_KEYS = [...GLOBAL_SETTINGS_KEYS, ...PROVIDER_SETTINGS_KEYS].filter(
|
||||
(key: Keys<RooCodeSettings>) => !SECRET_STATE_KEYS.includes(key as Keys<SecretState>),
|
||||
(key: Keys<RooCodeSettings>) => !isSecretStateKey(key),
|
||||
) as Keys<GlobalState>[]
|
||||
|
||||
export const isGlobalStateKey = (key: string): key is Keys<GlobalState> =>
|
||||
|
|
|
|||
|
|
@ -142,13 +142,6 @@ const openRouterSchema = baseProviderSettingsSchema.extend({
|
|||
openRouterBaseUrl: z.string().optional(),
|
||||
openRouterSpecificProvider: z.string().optional(),
|
||||
openRouterUseMiddleOutTransform: z.boolean().optional(),
|
||||
// Image generation settings (experimental)
|
||||
openRouterImageGenerationSettings: z
|
||||
.object({
|
||||
openRouterApiKey: z.string().optional(),
|
||||
selectedModel: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const bedrockSchema = apiModelIdProviderModelSchema.extend({
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
GLOBAL_SETTINGS_KEYS,
|
||||
SECRET_STATE_KEYS,
|
||||
GLOBAL_STATE_KEYS,
|
||||
GLOBAL_SECRET_KEYS,
|
||||
type ProviderSettings,
|
||||
type GlobalSettings,
|
||||
type SecretState,
|
||||
|
|
@ -61,19 +62,77 @@ export class ContextProxy {
|
|||
}
|
||||
}
|
||||
|
||||
const promises = SECRET_STATE_KEYS.map(async (key) => {
|
||||
try {
|
||||
this.secretCache[key] = await this.originalContext.secrets.get(key)
|
||||
} catch (error) {
|
||||
logger.error(`Error loading secret ${key}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
})
|
||||
const promises = [
|
||||
...SECRET_STATE_KEYS.map(async (key) => {
|
||||
try {
|
||||
this.secretCache[key] = await this.originalContext.secrets.get(key)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error loading secret ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
...GLOBAL_SECRET_KEYS.map(async (key) => {
|
||||
try {
|
||||
this.secretCache[key] = await this.originalContext.secrets.get(key)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error loading global secret ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
// Migration: Check for old nested image generation settings and migrate them
|
||||
await this.migrateImageGenerationSettings()
|
||||
|
||||
this._isInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates old nested openRouterImageGenerationSettings to the new flattened structure
|
||||
*/
|
||||
private async migrateImageGenerationSettings() {
|
||||
try {
|
||||
// Check if there's an old nested structure
|
||||
const oldNestedSettings = this.originalContext.globalState.get<any>("openRouterImageGenerationSettings")
|
||||
|
||||
if (oldNestedSettings && typeof oldNestedSettings === "object") {
|
||||
logger.info("Migrating old nested image generation settings to flattened structure")
|
||||
|
||||
// Migrate the API key if it exists and we don't already have one
|
||||
if (oldNestedSettings.openRouterApiKey && !this.secretCache.openRouterImageApiKey) {
|
||||
await this.originalContext.secrets.store(
|
||||
"openRouterImageApiKey",
|
||||
oldNestedSettings.openRouterApiKey,
|
||||
)
|
||||
this.secretCache.openRouterImageApiKey = oldNestedSettings.openRouterApiKey
|
||||
logger.info("Migrated openRouterImageApiKey to secrets")
|
||||
}
|
||||
|
||||
// Migrate the selected model if it exists and we don't already have one
|
||||
if (oldNestedSettings.selectedModel && !this.stateCache.openRouterImageGenerationSelectedModel) {
|
||||
await this.originalContext.globalState.update(
|
||||
"openRouterImageGenerationSelectedModel",
|
||||
oldNestedSettings.selectedModel,
|
||||
)
|
||||
this.stateCache.openRouterImageGenerationSelectedModel = oldNestedSettings.selectedModel
|
||||
logger.info("Migrated openRouterImageGenerationSelectedModel to global state")
|
||||
}
|
||||
|
||||
// Clean up the old nested structure
|
||||
await this.originalContext.globalState.update("openRouterImageGenerationSettings", undefined)
|
||||
logger.info("Removed old nested openRouterImageGenerationSettings")
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error during image generation settings migration: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public get extensionUri() {
|
||||
return this.originalContext.extensionUri
|
||||
}
|
||||
|
|
@ -152,20 +211,34 @@ export class ContextProxy {
|
|||
* This is useful when you need to ensure the cache has the latest values
|
||||
*/
|
||||
async refreshSecrets(): Promise<void> {
|
||||
const promises = SECRET_STATE_KEYS.map(async (key) => {
|
||||
try {
|
||||
this.secretCache[key] = await this.originalContext.secrets.get(key)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error refreshing secret ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
const promises = [
|
||||
...SECRET_STATE_KEYS.map(async (key) => {
|
||||
try {
|
||||
this.secretCache[key] = await this.originalContext.secrets.get(key)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error refreshing secret ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
...GLOBAL_SECRET_KEYS.map(async (key) => {
|
||||
try {
|
||||
this.secretCache[key] = await this.originalContext.secrets.get(key)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error refreshing global secret ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
]
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
private getAllSecretState(): SecretState {
|
||||
return Object.fromEntries(SECRET_STATE_KEYS.map((key) => [key, this.getSecret(key)]))
|
||||
return Object.fromEntries([
|
||||
...SECRET_STATE_KEYS.map((key) => [key, this.getSecret(key as SecretStateKey)]),
|
||||
...GLOBAL_SECRET_KEYS.map((key) => [key, this.getSecret(key as SecretStateKey)]),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -232,18 +305,24 @@ export class ContextProxy {
|
|||
* RooCodeSettings
|
||||
*/
|
||||
|
||||
public setValue<K extends RooCodeSettingsKey>(key: K, value: RooCodeSettings[K]) {
|
||||
return isSecretStateKey(key) ? this.storeSecret(key, value as string) : this.updateGlobalState(key, value)
|
||||
public async setValue<K extends RooCodeSettingsKey>(key: K, value: RooCodeSettings[K]) {
|
||||
return isSecretStateKey(key)
|
||||
? this.storeSecret(key as SecretStateKey, value as string)
|
||||
: this.updateGlobalState(key as GlobalStateKey, value)
|
||||
}
|
||||
|
||||
public getValue<K extends RooCodeSettingsKey>(key: K): RooCodeSettings[K] {
|
||||
return isSecretStateKey(key)
|
||||
? (this.getSecret(key) as RooCodeSettings[K])
|
||||
: (this.getGlobalState(key) as RooCodeSettings[K])
|
||||
? (this.getSecret(key as SecretStateKey) as RooCodeSettings[K])
|
||||
: (this.getGlobalState(key as GlobalStateKey) as RooCodeSettings[K])
|
||||
}
|
||||
|
||||
public getValues(): RooCodeSettings {
|
||||
return { ...this.getAllGlobalState(), ...this.getAllSecretState() }
|
||||
const globalState = this.getAllGlobalState()
|
||||
const secretState = this.getAllSecretState()
|
||||
|
||||
// Simply merge all states - no nested secrets to handle
|
||||
return { ...globalState, ...secretState }
|
||||
}
|
||||
|
||||
public async setValues(values: RooCodeSettings) {
|
||||
|
|
@ -285,6 +364,7 @@ export class ContextProxy {
|
|||
await Promise.all([
|
||||
...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)),
|
||||
...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)),
|
||||
...GLOBAL_SECRET_KEYS.map((key) => this.originalContext.secrets.delete(key)),
|
||||
])
|
||||
|
||||
await this.initialize()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { GLOBAL_STATE_KEYS, SECRET_STATE_KEYS } from "@roo-code/types"
|
||||
import { GLOBAL_STATE_KEYS, SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS } from "@roo-code/types"
|
||||
|
||||
import { ContextProxy } from "../ContextProxy"
|
||||
|
||||
|
|
@ -70,17 +70,23 @@ describe("ContextProxy", () => {
|
|||
|
||||
describe("constructor", () => {
|
||||
it("should initialize state cache with all global state keys", () => {
|
||||
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length)
|
||||
// +1 for the migration check of old nested settings
|
||||
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1)
|
||||
for (const key of GLOBAL_STATE_KEYS) {
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith(key)
|
||||
}
|
||||
// Also check for migration call
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith("openRouterImageGenerationSettings")
|
||||
})
|
||||
|
||||
it("should initialize secret cache with all secret keys", () => {
|
||||
expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length)
|
||||
expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length + GLOBAL_SECRET_KEYS.length)
|
||||
for (const key of SECRET_STATE_KEYS) {
|
||||
expect(mockSecrets.get).toHaveBeenCalledWith(key)
|
||||
}
|
||||
for (const key of GLOBAL_SECRET_KEYS) {
|
||||
expect(mockSecrets.get).toHaveBeenCalledWith(key)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -93,8 +99,8 @@ describe("ContextProxy", () => {
|
|||
const result = proxy.getGlobalState("apiProvider")
|
||||
expect(result).toBe("deepseek")
|
||||
|
||||
// Original context should be called once during updateGlobalState
|
||||
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length) // Only from initialization
|
||||
// Original context should be called once during updateGlobalState (+1 for migration check)
|
||||
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1) // From initialization + migration check
|
||||
})
|
||||
|
||||
it("should handle default values correctly", async () => {
|
||||
|
|
@ -403,9 +409,12 @@ describe("ContextProxy", () => {
|
|||
for (const key of SECRET_STATE_KEYS) {
|
||||
expect(mockSecrets.delete).toHaveBeenCalledWith(key)
|
||||
}
|
||||
for (const key of GLOBAL_SECRET_KEYS) {
|
||||
expect(mockSecrets.delete).toHaveBeenCalledWith(key)
|
||||
}
|
||||
|
||||
// Total calls should equal the number of secret keys
|
||||
expect(mockSecrets.delete).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length)
|
||||
expect(mockSecrets.delete).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length + GLOBAL_SECRET_KEYS.length)
|
||||
})
|
||||
|
||||
it("should reinitialize caches after reset", async () => {
|
||||
|
|
|
|||
|
|
@ -46,12 +46,8 @@ describe("generateImageTool", () => {
|
|||
experiments: {
|
||||
[EXPERIMENT_IDS.IMAGE_GENERATION]: true,
|
||||
},
|
||||
apiConfiguration: {
|
||||
openRouterImageGenerationSettings: {
|
||||
openRouterApiKey: "test-api-key",
|
||||
selectedModel: "google/gemini-2.5-flash-image-preview",
|
||||
},
|
||||
},
|
||||
openRouterImageApiKey: "test-api-key",
|
||||
openRouterImageGenerationSelectedModel: "google/gemini-2.5-flash-image-preview",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -129,10 +129,8 @@ export async function generateImageTool(
|
|||
// Check if file is write-protected
|
||||
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
|
||||
|
||||
// Get OpenRouter API key from experimental settings ONLY (no fallback to profile)
|
||||
const apiConfiguration = state?.apiConfiguration
|
||||
const imageGenerationSettings = apiConfiguration?.openRouterImageGenerationSettings
|
||||
const openRouterApiKey = imageGenerationSettings?.openRouterApiKey
|
||||
// Get OpenRouter API key from global settings (experimental image generation)
|
||||
const openRouterApiKey = state?.openRouterImageApiKey
|
||||
|
||||
if (!openRouterApiKey) {
|
||||
await cline.say(
|
||||
|
|
@ -148,7 +146,7 @@ export async function generateImageTool(
|
|||
}
|
||||
|
||||
// Get selected model from settings or use default
|
||||
const selectedModel = imageGenerationSettings?.selectedModel || IMAGE_GENERATION_MODELS[0]
|
||||
const selectedModel = state?.openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0]
|
||||
|
||||
// Determine if the path is outside the workspace
|
||||
const fullPath = path.resolve(cline.cwd, removeClosingTag("path", relPath))
|
||||
|
|
|
|||
|
|
@ -1820,6 +1820,8 @@ export class ClineProvider
|
|||
maxDiagnosticMessages,
|
||||
includeTaskHistoryInEnhance,
|
||||
remoteControlEnabled,
|
||||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
} = await this.getState()
|
||||
|
||||
const telemetryKey = process.env.POSTHOG_API_KEY
|
||||
|
|
@ -1952,6 +1954,8 @@ export class ClineProvider
|
|||
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
|
||||
remoteControlEnabled,
|
||||
filesChangedEnabled: this.getGlobalState("filesChangedEnabled") ?? true,
|
||||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2151,6 +2155,9 @@ export class ClineProvider
|
|||
return false
|
||||
}
|
||||
})(),
|
||||
// Add image generation settings
|
||||
openRouterImageApiKey: stateValues.openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -576,6 +576,8 @@ describe("ClineProvider", () => {
|
|||
hasOpenedModeSelector: false,
|
||||
diagnosticsEnabled: true,
|
||||
filesChangedEnabled: true,
|
||||
openRouterImageApiKey: undefined,
|
||||
openRouterImageGenerationSelectedModel: undefined,
|
||||
}
|
||||
|
||||
const message: ExtensionMessage = {
|
||||
|
|
|
|||
|
|
@ -1314,6 +1314,14 @@ export const webviewMessageHandler = async (
|
|||
await updateGlobalState("language", message.text as Language)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "openRouterImageApiKey":
|
||||
await provider.contextProxy.setValue("openRouterImageApiKey", message.text)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "openRouterImageGenerationSelectedModel":
|
||||
await provider.contextProxy.setValue("openRouterImageGenerationSelectedModel", message.text)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "showRooIgnoredFiles":
|
||||
await updateGlobalState("showRooIgnoredFiles", message.bool ?? false)
|
||||
await provider.postStateToWebview()
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ export type ExtensionState = Pick<
|
|||
| "includeDiagnosticMessages"
|
||||
| "maxDiagnosticMessages"
|
||||
| "remoteControlEnabled"
|
||||
| "openRouterImageGenerationSelectedModel"
|
||||
> & {
|
||||
version: string
|
||||
clineMessages: ClineMessage[]
|
||||
|
|
@ -340,6 +341,7 @@ export type ExtensionState = Pick<
|
|||
profileThresholds: Record<string, number>
|
||||
hasOpenedModeSelector: boolean
|
||||
filesChangedEnabled: boolean
|
||||
openRouterImageApiKey?: string
|
||||
}
|
||||
|
||||
export interface ClineSayTool {
|
||||
|
|
|
|||
|
|
@ -218,6 +218,9 @@ export interface WebviewMessage {
|
|||
| "filesChangedEnabled"
|
||||
| "filesChangedRequest"
|
||||
| "filesChangedBaselineUpdate"
|
||||
| "imageGenerationSettings"
|
||||
| "openRouterImageApiKey"
|
||||
| "openRouterImageGenerationSelectedModel"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
|
@ -254,6 +257,7 @@ export interface WebviewMessage {
|
|||
messageTs?: number
|
||||
historyPreviewCollapsed?: boolean
|
||||
filters?: { type?: string; search?: string; tags?: string[] }
|
||||
settings?: any
|
||||
url?: string // For openExternal
|
||||
mpItem?: MarketplaceItem
|
||||
mpInstallOptions?: InstallMarketplaceItemOptions
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { SECRET_STATE_KEYS, ProviderSettings } from "@roo-code/types"
|
||||
import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, ProviderSettings } from "@roo-code/types"
|
||||
|
||||
export function checkExistKey(config: ProviderSettings | undefined) {
|
||||
if (!config) {
|
||||
|
|
@ -14,7 +14,9 @@ export function checkExistKey(config: ProviderSettings | undefined) {
|
|||
}
|
||||
|
||||
// Check all secret keys from the centralized SECRET_STATE_KEYS array.
|
||||
const hasSecretKey = SECRET_STATE_KEYS.some((key) => config[key] !== undefined)
|
||||
// Filter out keys that are not part of ProviderSettings (global secrets are stored separately)
|
||||
const providerSecretKeys = SECRET_STATE_KEYS.filter((key) => !GLOBAL_SECRET_KEYS.includes(key as any))
|
||||
const hasSecretKey = providerSecretKeys.some((key) => config[key as keyof ProviderSettings] !== undefined)
|
||||
|
||||
// Check additional non-secret configuration properties
|
||||
const hasOtherConfig = [
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
setCachedStateField?: SetCachedStateField<"filesChangedEnabled">
|
||||
apiConfiguration?: any
|
||||
setApiConfigurationField?: any
|
||||
openRouterImageApiKey?: string
|
||||
openRouterImageGenerationSelectedModel?: string
|
||||
setOpenRouterImageApiKey?: (apiKey: string) => void
|
||||
setImageGenerationSelectedModel?: (model: string) => void
|
||||
}
|
||||
|
||||
export const ExperimentalSettings = ({
|
||||
|
|
@ -32,6 +36,10 @@ export const ExperimentalSettings = ({
|
|||
setCachedStateField,
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
setOpenRouterImageApiKey,
|
||||
setImageGenerationSelectedModel,
|
||||
className,
|
||||
...props
|
||||
}: ExperimentalSettingsProps) => {
|
||||
|
|
@ -80,7 +88,11 @@ export const ExperimentalSettings = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
if (config[0] === "IMAGE_GENERATION" && apiConfiguration && setApiConfigurationField) {
|
||||
if (
|
||||
config[0] === "IMAGE_GENERATION" &&
|
||||
setOpenRouterImageApiKey &&
|
||||
setImageGenerationSelectedModel
|
||||
) {
|
||||
return (
|
||||
<ImageGenerationSettings
|
||||
key={config[0]}
|
||||
|
|
@ -88,8 +100,10 @@ export const ExperimentalSettings = ({
|
|||
onChange={(enabled) =>
|
||||
setExperimentEnabled(EXPERIMENT_IDS.IMAGE_GENERATION, enabled)
|
||||
}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
openRouterImageApiKey={openRouterImageApiKey}
|
||||
openRouterImageGenerationSelectedModel={openRouterImageGenerationSelectedModel}
|
||||
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
|
||||
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { VSCodeCheckbox, VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import type { ProviderSettings } from "@roo-code/types"
|
||||
|
||||
interface ImageGenerationSettingsProps {
|
||||
enabled: boolean
|
||||
onChange: (enabled: boolean) => void
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: <K extends keyof ProviderSettings>(
|
||||
field: K,
|
||||
value: ProviderSettings[K],
|
||||
isUserAction?: boolean,
|
||||
) => void
|
||||
openRouterImageApiKey?: string
|
||||
openRouterImageGenerationSelectedModel?: string
|
||||
setOpenRouterImageApiKey: (apiKey: string) => void
|
||||
setImageGenerationSelectedModel: (model: string) => void
|
||||
}
|
||||
|
||||
// Hardcoded list of image generation models
|
||||
|
|
@ -24,43 +21,34 @@ const IMAGE_GENERATION_MODELS = [
|
|||
export const ImageGenerationSettings = ({
|
||||
enabled,
|
||||
onChange,
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
setOpenRouterImageApiKey,
|
||||
setImageGenerationSelectedModel,
|
||||
}: ImageGenerationSettingsProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
// Get image generation settings from apiConfiguration
|
||||
const imageGenerationSettings = apiConfiguration?.openRouterImageGenerationSettings || {}
|
||||
const [openRouterApiKey, setOpenRouterApiKey] = useState(imageGenerationSettings.openRouterApiKey || "")
|
||||
const [apiKey, setApiKey] = useState(openRouterImageApiKey || "")
|
||||
const [selectedModel, setSelectedModel] = useState(
|
||||
imageGenerationSettings.selectedModel || IMAGE_GENERATION_MODELS[0].value,
|
||||
openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0].value,
|
||||
)
|
||||
|
||||
// Update local state when apiConfiguration changes (e.g., when switching profiles)
|
||||
// Update local state when props change (e.g., when switching profiles)
|
||||
useEffect(() => {
|
||||
setOpenRouterApiKey(imageGenerationSettings.openRouterApiKey || "")
|
||||
setSelectedModel(imageGenerationSettings.selectedModel || IMAGE_GENERATION_MODELS[0].value)
|
||||
}, [imageGenerationSettings.openRouterApiKey, imageGenerationSettings.selectedModel])
|
||||
|
||||
// Helper function to update settings
|
||||
const updateSettings = (newApiKey: string, newModel: string) => {
|
||||
const newSettings = {
|
||||
openRouterApiKey: newApiKey,
|
||||
selectedModel: newModel,
|
||||
}
|
||||
setApiConfigurationField("openRouterImageGenerationSettings", newSettings, true)
|
||||
}
|
||||
setApiKey(openRouterImageApiKey || "")
|
||||
setSelectedModel(openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0].value)
|
||||
}, [openRouterImageApiKey, openRouterImageGenerationSelectedModel])
|
||||
|
||||
// Handle API key changes
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
setOpenRouterApiKey(value)
|
||||
updateSettings(value, selectedModel)
|
||||
setApiKey(value)
|
||||
setOpenRouterImageApiKey(value)
|
||||
}
|
||||
|
||||
// Handle model selection changes
|
||||
const handleModelChange = (value: string) => {
|
||||
setSelectedModel(value)
|
||||
updateSettings(openRouterApiKey, value)
|
||||
setImageGenerationSelectedModel(value)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -84,7 +72,7 @@ export const ImageGenerationSettings = ({
|
|||
{t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyLabel")}
|
||||
</label>
|
||||
<VSCodeTextField
|
||||
value={openRouterApiKey}
|
||||
value={apiKey}
|
||||
onInput={(e: any) => handleApiKeyChange(e.target.value)}
|
||||
placeholder={t("settings:experimental.IMAGE_GENERATION.openRouterApiKeyPlaceholder")}
|
||||
className="w-full"
|
||||
|
|
@ -123,13 +111,13 @@ export const ImageGenerationSettings = ({
|
|||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{enabled && !openRouterApiKey && (
|
||||
{enabled && !apiKey && (
|
||||
<div className="p-2 bg-vscode-editorWarning-background text-vscode-editorWarning-foreground rounded text-sm">
|
||||
{t("settings:experimental.IMAGE_GENERATION.warningMissingKey")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enabled && openRouterApiKey && (
|
||||
{enabled && apiKey && (
|
||||
<div className="p-2 bg-vscode-editorInfo-background text-vscode-editorInfo-foreground rounded text-sm">
|
||||
{t("settings:experimental.IMAGE_GENERATION.successConfigured")}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -183,6 +183,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
maxDiagnosticMessages,
|
||||
includeTaskHistoryInEnhance,
|
||||
filesChangedEnabled,
|
||||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
} = cachedState
|
||||
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
|
@ -262,6 +264,20 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
})
|
||||
}, [])
|
||||
|
||||
const setOpenRouterImageApiKey = useCallback((apiKey: string) => {
|
||||
setCachedState((prevState) => {
|
||||
setChangeDetected(true)
|
||||
return { ...prevState, openRouterImageApiKey: apiKey }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setImageGenerationSelectedModel = useCallback((model: string) => {
|
||||
setCachedState((prevState) => {
|
||||
setChangeDetected(true)
|
||||
return { ...prevState, openRouterImageGenerationSelectedModel: model }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setCustomSupportPromptsField = useCallback((prompts: Record<string, string | undefined>) => {
|
||||
setCachedState((prevState) => {
|
||||
if (JSON.stringify(prevState.customSupportPrompts) === JSON.stringify(prompts)) {
|
||||
|
|
@ -346,6 +362,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
|
||||
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
|
||||
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
|
||||
vscode.postMessage({ type: "openRouterImageApiKey", text: openRouterImageApiKey })
|
||||
vscode.postMessage({
|
||||
type: "openRouterImageGenerationSelectedModel",
|
||||
text: openRouterImageGenerationSelectedModel,
|
||||
})
|
||||
setChangeDetected(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -724,10 +745,16 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
<ExperimentalSettings
|
||||
setExperimentEnabled={setExperimentEnabled}
|
||||
experiments={experiments}
|
||||
filesChangedEnabled={filesChangedEnabled}
|
||||
setCachedStateField={setCachedStateField as SetCachedStateField<"filesChangedEnabled">}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
filesChangedEnabled={filesChangedEnabled}
|
||||
setCachedStateField={setCachedStateField as SetCachedStateField<"filesChangedEnabled">}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
openRouterImageApiKey={openRouterImageApiKey as string | undefined}
|
||||
openRouterImageGenerationSelectedModel={
|
||||
openRouterImageGenerationSelectedModel as string | undefined
|
||||
}
|
||||
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
|
||||
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { render, fireEvent } from "@testing-library/react"
|
||||
|
||||
import type { ProviderSettings } from "@roo-code/types"
|
||||
|
||||
import { ImageGenerationSettings } from "../ImageGenerationSettings"
|
||||
|
||||
// Mock the translation context
|
||||
|
|
@ -12,14 +10,17 @@ vi.mock("@/i18n/TranslationContext", () => ({
|
|||
}))
|
||||
|
||||
describe("ImageGenerationSettings", () => {
|
||||
const mockSetApiConfigurationField = vi.fn()
|
||||
const mockSetOpenRouterImageApiKey = vi.fn()
|
||||
const mockSetImageGenerationSelectedModel = vi.fn()
|
||||
const mockOnChange = vi.fn()
|
||||
|
||||
const defaultProps = {
|
||||
enabled: false,
|
||||
onChange: mockOnChange,
|
||||
apiConfiguration: {} as ProviderSettings,
|
||||
setApiConfigurationField: mockSetApiConfigurationField,
|
||||
openRouterImageApiKey: undefined,
|
||||
openRouterImageGenerationSelectedModel: undefined,
|
||||
setOpenRouterImageApiKey: mockSetOpenRouterImageApiKey,
|
||||
setImageGenerationSelectedModel: mockSetImageGenerationSelectedModel,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -27,30 +28,31 @@ describe("ImageGenerationSettings", () => {
|
|||
})
|
||||
|
||||
describe("Initial Mount Behavior", () => {
|
||||
it("should not call setApiConfigurationField on initial mount with empty configuration", () => {
|
||||
it("should not call setter functions on initial mount with empty configuration", () => {
|
||||
render(<ImageGenerationSettings {...defaultProps} />)
|
||||
|
||||
// Should NOT call setApiConfigurationField on initial mount to prevent dirty state
|
||||
expect(mockSetApiConfigurationField).not.toHaveBeenCalled()
|
||||
// Should NOT call setter functions on initial mount to prevent dirty state
|
||||
expect(mockSetOpenRouterImageApiKey).not.toHaveBeenCalled()
|
||||
expect(mockSetImageGenerationSelectedModel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not call setApiConfigurationField on initial mount with existing configuration", () => {
|
||||
const apiConfiguration = {
|
||||
openRouterImageGenerationSettings: {
|
||||
openRouterApiKey: "existing-key",
|
||||
selectedModel: "google/gemini-2.5-flash-image-preview:free",
|
||||
},
|
||||
} as ProviderSettings
|
||||
it("should not call setter functions on initial mount with existing configuration", () => {
|
||||
render(
|
||||
<ImageGenerationSettings
|
||||
{...defaultProps}
|
||||
openRouterImageApiKey="existing-key"
|
||||
openRouterImageGenerationSelectedModel="google/gemini-2.5-flash-image-preview:free"
|
||||
/>,
|
||||
)
|
||||
|
||||
render(<ImageGenerationSettings {...defaultProps} apiConfiguration={apiConfiguration} />)
|
||||
|
||||
// Should NOT call setApiConfigurationField on initial mount to prevent dirty state
|
||||
expect(mockSetApiConfigurationField).not.toHaveBeenCalled()
|
||||
// Should NOT call setter functions on initial mount to prevent dirty state
|
||||
expect(mockSetOpenRouterImageApiKey).not.toHaveBeenCalled()
|
||||
expect(mockSetImageGenerationSelectedModel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("User Interaction Behavior", () => {
|
||||
it("should call setApiConfigurationField when user changes API key", async () => {
|
||||
it("should call setimageGenerationSettings when user changes API key", async () => {
|
||||
const { getByPlaceholderText } = render(<ImageGenerationSettings {...defaultProps} enabled={true} />)
|
||||
|
||||
const apiKeyInput = getByPlaceholderText(
|
||||
|
|
@ -60,15 +62,8 @@ describe("ImageGenerationSettings", () => {
|
|||
// Simulate user typing
|
||||
fireEvent.input(apiKeyInput, { target: { value: "new-api-key" } })
|
||||
|
||||
// Should call setApiConfigurationField with isUserAction=true
|
||||
expect(mockSetApiConfigurationField).toHaveBeenCalledWith(
|
||||
"openRouterImageGenerationSettings",
|
||||
{
|
||||
openRouterApiKey: "new-api-key",
|
||||
selectedModel: "google/gemini-2.5-flash-image-preview",
|
||||
},
|
||||
true, // This should be true for user actions
|
||||
)
|
||||
// Should call setimageGenerationSettings
|
||||
expect(defaultProps.setOpenRouterImageApiKey).toHaveBeenCalledWith("new-api-key")
|
||||
})
|
||||
|
||||
// Note: Testing VSCode dropdown components is complex due to their custom nature
|
||||
|
|
|
|||
|
|
@ -257,6 +257,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
alwaysAllowUpdateTodoList: true,
|
||||
includeDiagnosticMessages: true,
|
||||
maxDiagnosticMessages: 50,
|
||||
openRouterImageApiKey: "",
|
||||
openRouterImageGenerationSelectedModel: "",
|
||||
})
|
||||
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue