Skip to content
Draft
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
2 changes: 2 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ export type ExtensionState = Pick<
currentTaskItem?: HistoryItem
currentTaskTodos?: TodoItem[] // Initial todos for the current task
apiConfiguration: ProviderSettings
currentConfigScope?: "global" | "workspace" // Current configuration scope
uriScheme?: string
shouldShowAnnouncement: boolean

Expand Down Expand Up @@ -583,6 +584,7 @@ export interface WebviewMessage {
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
disabled?: boolean
context?: string
scope?: "global" | "workspace"
dataUri?: string
askResponse?: ClineAskResponse
apiConfiguration?: ProviderSettings
Expand Down
9 changes: 9 additions & 0 deletions src/core/config/ContextProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,15 @@ export class ContextProxy {
: this.updateGlobalState(key as GlobalStateKey, value)
}

/**
* Update a value only in the in-memory cache without persisting to globalState.
* Use this when a value should be reflected locally (e.g. webview display)
* but should not leak to other VS Code windows via shared globalState.
*/
public setLocalValue<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
this.stateCache[key] = value
}

public getValue<K extends RooCodeSettingsKey>(key: K): RooCodeSettings[K] {
return isSecretStateKey(key)
? (this.getSecret(key as SecretStateKey) as RooCodeSettings[K])
Expand Down
165 changes: 152 additions & 13 deletions src/core/config/ProviderSettingsManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExtensionContext } from "vscode"
import { ExtensionContext, workspace } from "vscode"
import { z, ZodError } from "zod"
import deepEqual from "fast-deep-equal"

Expand All @@ -19,6 +19,8 @@ import { TelemetryService } from "@roo-code/telemetry"
import { Mode, modes } from "../../shared/modes"
import { buildApiHandler } from "../../api"

export type ConfigScope = "global" | "workspace"

// Type-safe model migrations mapping
type ModelMigrations = {
[K in ProviderName]?: Record<string, string>
Expand Down Expand Up @@ -54,8 +56,17 @@ export const providerProfilesSchema = z.object({

export type ProviderProfiles = z.infer<typeof providerProfilesSchema>

// Schema for workspace-specific overrides
export const workspaceOverridesSchema = z.object({
currentApiConfigName: z.string().optional(),
modeApiConfigs: z.record(z.string(), z.string()).optional(),
})

export type WorkspaceOverrides = z.infer<typeof workspaceOverridesSchema>

export class ProviderSettingsManager {
private static readonly SCOPE_PREFIX = "roo_cline_config_"
private static readonly WORKSPACE_KEY = "roo_workspace_overrides"
private readonly defaultConfigId = this.generateId()

private readonly defaultModeApiConfigs: Record<string, string> = Object.fromEntries(
Expand Down Expand Up @@ -418,24 +429,65 @@ export class ProviderSettingsManager {

/**
* Activate a profile by name or ID.
* @param params Profile identifier
* @param scope Whether to apply globally or to workspace only
*/
public async activateProfile(
params: { name: string } | { id: string },
scope: ConfigScope = "global",
): Promise<ProviderSettingsWithId & { name: string }> {
const { name, ...providerSettings } = await this.getProfile(params)

try {
return await this.lock(async () => {
const providerProfiles = await this.load()
providerProfiles.currentApiConfigName = name
await this.store(providerProfiles)
if (scope === "workspace" && workspace.workspaceFolders?.length) {
// Store workspace-specific override only
const overrides = await this.getWorkspaceOverrides()
overrides.currentApiConfigName = name
await this.storeWorkspaceOverrides(overrides)
} else {
// Store globally and clear any workspace overrides
if (scope === "global") {
await this.clearWorkspaceOverrides()
}
const providerProfiles = await this.load()
providerProfiles.currentApiConfigName = name
await this.store(providerProfiles)
}
return { name, ...providerSettings }
})
} catch (error) {
throw new Error(`Failed to activate profile: ${error instanceof Error ? error.message : error}`)
}
}

/**
* Get the currently active profile name, considering workspace overrides.
*/
public async getActiveProfileName(): Promise<string> {
try {
return await this.lock(async () => {
// Check for workspace override first
if (workspace.workspaceFolders?.length) {
const overrides = await this.getWorkspaceOverrides()
if (overrides.currentApiConfigName) {
// Verify the profile still exists
const providerProfiles = await this.load()
if (providerProfiles.apiConfigs[overrides.currentApiConfigName]) {
return overrides.currentApiConfigName
}
}
}

// Fall back to global setting
const providerProfiles = await this.load()
return providerProfiles.currentApiConfigName
})
} catch (error) {
throw new Error(`Failed to get active profile: ${error}`)
}
}

/**
* Delete a config by name.
*/
Expand Down Expand Up @@ -476,30 +528,61 @@ export class ProviderSettingsManager {

/**
* Set the API config for a specific mode.
* @param mode The mode to set config for
* @param configId The config ID to use
* @param scope Whether to apply globally or to workspace only
*/
public async setModeConfig(mode: Mode, configId: string) {
public async setModeConfig(mode: Mode, configId: string, scope: ConfigScope = "global") {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
// Ensure the per-mode config map exists
if (!providerProfiles.modeApiConfigs) {
providerProfiles.modeApiConfigs = {}
if (scope === "workspace" && workspace.workspaceFolders?.length) {
// Store workspace-specific override
const overrides = await this.getWorkspaceOverrides()
if (!overrides.modeApiConfigs) {
overrides.modeApiConfigs = {}
}
overrides.modeApiConfigs[mode] = configId
await this.storeWorkspaceOverrides(overrides)
} else {
// Store globally
const providerProfiles = await this.load()
// Ensure the per-mode config map exists
if (!providerProfiles.modeApiConfigs) {
providerProfiles.modeApiConfigs = {}
}
// Assign the chosen config ID to this mode
providerProfiles.modeApiConfigs[mode] = configId
await this.store(providerProfiles)
}
// Assign the chosen config ID to this mode
providerProfiles.modeApiConfigs[mode] = configId
await this.store(providerProfiles)
})
} catch (error) {
throw new Error(`Failed to set mode config: ${error}`)
}
}

/**
* Get the API config ID for a specific mode.
* Get the API config ID for a specific mode, considering workspace overrides.
*/
public async getModeConfigId(mode: Mode) {
try {
return await this.lock(async () => {
// Check for workspace override first
if (workspace.workspaceFolders?.length) {
const overrides = await this.getWorkspaceOverrides()
if (overrides.modeApiConfigs?.[mode]) {
// Verify the config still exists
const providerProfiles = await this.load()
const configId = overrides.modeApiConfigs[mode]
const configExists = Object.values(providerProfiles.apiConfigs).some(
(config) => config.id === configId,
)
if (configExists) {
return configId
}
}
}

// Fall back to global setting
const { modeApiConfigs } = await this.load()
return modeApiConfigs?.[mode]
})
Expand Down Expand Up @@ -878,4 +961,60 @@ export class ProviderSettingsManager {
throw new Error(`Failed to sync cloud profiles: ${error}`)
}
}

/**
* Get workspace-specific overrides.
*/
private async getWorkspaceOverrides(): Promise<WorkspaceOverrides> {
if (!workspace.workspaceFolders?.length) {
return {}
}

const stored = this.context.workspaceState.get<WorkspaceOverrides>(ProviderSettingsManager.WORKSPACE_KEY)

if (stored) {
try {
return workspaceOverridesSchema.parse(stored)
} catch {
return {}
}
}

return {}
}

/**
* Store workspace-specific overrides.
*/
private async storeWorkspaceOverrides(overrides: WorkspaceOverrides): Promise<void> {
if (!workspace.workspaceFolders?.length) {
return
}

await this.context.workspaceState.update(ProviderSettingsManager.WORKSPACE_KEY, overrides)
}

/**
* Clear workspace-specific overrides.
*/
public async clearWorkspaceOverrides(): Promise<void> {
if (!workspace.workspaceFolders?.length) {
return
}

await this.context.workspaceState.update(ProviderSettingsManager.WORKSPACE_KEY, undefined)
}

/**
* Get the current configuration scope preference.
*/
public async getConfigScope(): Promise<ConfigScope> {
if (workspace.workspaceFolders?.length) {
const overrides = await this.getWorkspaceOverrides()
if (overrides.currentApiConfigName || overrides.modeApiConfigs) {
return "workspace"
}
}
return "global"
}
}
Loading
Loading