mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: add per-workspace API profile selection
Implements per-workspace API profile selection as requested in #8010. Addresses critical issues from review by @daniel-lxs: 1. Fix: Workspace selection no longer overwrites global config across windows. When scope="workspace", only the local cache is updated via setLocalValue(), avoiding leaking currentApiConfigName to other windows via shared globalState. 2. Fix: Toggling scope back to Global now clears workspace overrides via clearWorkspaceOverrides(), preventing stale workspace state from keeping the scope stuck on "workspace". Backend changes: - Add ConfigScope type and WorkspaceOverrides schema to ProviderSettingsManager - Add workspace override methods (getWorkspaceOverrides, storeWorkspaceOverrides, clearWorkspaceOverrides, getConfigScope, getActiveProfileName) - Modify activateProfile and setModeConfig to accept scope parameter - Modify getModeConfigId to check workspace overrides first - Add setLocalValue to ContextProxy for cache-only state updates - Add scope field to WebviewMessage and currentConfigScope to ExtensionState Frontend changes: - Add scope toggle UI to ApiConfigSelector with [W] indicator - Wire scope through ChatTextArea to pass with config changes - Add i18n translation keys for scope UI elements Testing: - Add comprehensive workspace-scoped tests to ProviderSettingsManager.spec.ts - Update ClineProvider test mocks with getConfigScope method - All tests passing (175 across 5 test files) Fixes #8010
This commit is contained in:
parent
d8cfbfdb05
commit
44c495da2d
11 changed files with 633 additions and 33 deletions
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ExtensionContext } from "vscode"
|
||||
import { ExtensionContext, workspace } from "vscode"
|
||||
import { z, ZodError } from "zod"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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(
|
||||
|
|
@ -418,17 +429,31 @@ 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) {
|
||||
|
|
@ -436,6 +461,33 @@ export class ProviderSettingsManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
@ -476,18 +528,32 @@ 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}`)
|
||||
|
|
@ -495,11 +561,28 @@ export class ProviderSettingsManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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]
|
||||
})
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
// npx vitest src/core/config/__tests__/ProviderSettingsManager.spec.ts
|
||||
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { ExtensionContext, workspace } from "vscode"
|
||||
|
||||
import type { ProviderSettings } from "@roo-code/types"
|
||||
|
||||
import { ProviderSettingsManager, ProviderProfiles, SyncCloudProfilesResult } from "../ProviderSettingsManager"
|
||||
|
||||
// Mock VSCode workspace
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock VSCode ExtensionContext
|
||||
const mockSecrets = {
|
||||
get: vi.fn(),
|
||||
|
|
@ -18,9 +25,15 @@ const mockGlobalState = {
|
|||
update: vi.fn(),
|
||||
}
|
||||
|
||||
const mockWorkspaceState = {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
}
|
||||
|
||||
const mockContext = {
|
||||
secrets: mockSecrets,
|
||||
globalState: mockGlobalState,
|
||||
workspaceState: mockWorkspaceState,
|
||||
} as unknown as ExtensionContext
|
||||
|
||||
describe("ProviderSettingsManager", () => {
|
||||
|
|
@ -1397,4 +1410,331 @@ describe("ProviderSettingsManager", () => {
|
|||
expect(result.activeProfileId).toBe("local-id")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Workspace-scoped functionality", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceState.get.mockReturnValue(undefined)
|
||||
mockWorkspaceState.update.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe("activateProfile with workspace scope", () => {
|
||||
it("should store profile activation in workspace when scope is workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const existingConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: { id: "default-id" },
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
modeApiConfigs: {},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const { name } = await providerSettingsManager.activateProfile({ name: "test" }, "workspace")
|
||||
|
||||
expect(name).toBe("test")
|
||||
|
||||
// Should update workspace state, not global state
|
||||
expect(mockWorkspaceState.update).toHaveBeenCalledWith("roo_workspace_overrides", {
|
||||
currentApiConfigName: "test",
|
||||
})
|
||||
})
|
||||
|
||||
it("should fall back to global when not in a workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = undefined
|
||||
|
||||
const existingConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: { id: "default-id" },
|
||||
test: { apiProvider: "anthropic", id: "test-id" },
|
||||
},
|
||||
modeApiConfigs: {},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
await providerSettingsManager.activateProfile({ name: "test" }, "workspace")
|
||||
|
||||
// Should update global state since no workspace
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe("test")
|
||||
|
||||
// Should NOT update workspace state
|
||||
expect(mockWorkspaceState.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should clear workspace overrides when activating with global scope", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const existingConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: { id: "default-id" },
|
||||
test: { apiProvider: "anthropic", id: "test-id" },
|
||||
},
|
||||
modeApiConfigs: {},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
// Activate with global scope - should clear workspace overrides
|
||||
await providerSettingsManager.activateProfile({ name: "test" }, "global")
|
||||
|
||||
// Should clear workspace overrides
|
||||
expect(mockWorkspaceState.update).toHaveBeenCalledWith("roo_workspace_overrides", undefined)
|
||||
|
||||
// Should update global state
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe("test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getActiveProfileName with workspace overrides", () => {
|
||||
it("should return workspace override when it exists", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: { id: "default-id" },
|
||||
workspace: { id: "workspace-id" },
|
||||
},
|
||||
modeApiConfigs: {},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
mockWorkspaceState.get.mockReturnValue({
|
||||
currentApiConfigName: "workspace",
|
||||
})
|
||||
|
||||
const activeProfile = await providerSettingsManager.getActiveProfileName()
|
||||
expect(activeProfile).toBe("workspace")
|
||||
})
|
||||
|
||||
it("should fall back to global when workspace override profile doesn't exist", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: { id: "default-id" },
|
||||
},
|
||||
modeApiConfigs: {},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
mockWorkspaceState.get.mockReturnValue({
|
||||
currentApiConfigName: "non-existent",
|
||||
})
|
||||
|
||||
const activeProfile = await providerSettingsManager.getActiveProfileName()
|
||||
expect(activeProfile).toBe("default")
|
||||
})
|
||||
|
||||
it("should return global profile when not in workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = undefined
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: { id: "default-id" },
|
||||
},
|
||||
modeApiConfigs: {},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
|
||||
const activeProfile = await providerSettingsManager.getActiveProfileName()
|
||||
expect(activeProfile).toBe("default")
|
||||
})
|
||||
})
|
||||
|
||||
describe("setModeConfig with workspace scope", () => {
|
||||
it("should store mode config in workspace when scope is workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: { id: "default-id" },
|
||||
test: { id: "test-id" },
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: "default-id",
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
|
||||
await providerSettingsManager.setModeConfig("code", "test-id", "workspace")
|
||||
|
||||
// Should update workspace state
|
||||
expect(mockWorkspaceState.update).toHaveBeenCalledWith("roo_workspace_overrides", {
|
||||
modeApiConfigs: { code: "test-id" },
|
||||
})
|
||||
})
|
||||
|
||||
it("should update existing workspace mode configs", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
mockWorkspaceState.get.mockReturnValue({
|
||||
modeApiConfigs: {
|
||||
architect: "architect-id",
|
||||
},
|
||||
})
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {},
|
||||
modeApiConfigs: {},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
|
||||
await providerSettingsManager.setModeConfig("code", "test-id", "workspace")
|
||||
|
||||
expect(mockWorkspaceState.update).toHaveBeenCalledWith("roo_workspace_overrides", {
|
||||
modeApiConfigs: {
|
||||
architect: "architect-id",
|
||||
code: "test-id",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModeConfigId with workspace overrides", () => {
|
||||
it("should return workspace mode config when it exists", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
global: { id: "global-id" },
|
||||
workspace: { id: "workspace-id" },
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: "global-id",
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
mockWorkspaceState.get.mockReturnValue({
|
||||
modeApiConfigs: {
|
||||
code: "workspace-id",
|
||||
},
|
||||
})
|
||||
|
||||
const configId = await providerSettingsManager.getModeConfigId("code")
|
||||
expect(configId).toBe("workspace-id")
|
||||
})
|
||||
|
||||
it("should fall back to global when workspace config doesn't exist", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
global: { id: "global-id" },
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: "global-id",
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
mockWorkspaceState.get.mockReturnValue({
|
||||
modeApiConfigs: {
|
||||
code: "non-existent-id",
|
||||
},
|
||||
})
|
||||
|
||||
const configId = await providerSettingsManager.getModeConfigId("code")
|
||||
expect(configId).toBe("global-id")
|
||||
})
|
||||
|
||||
it("should return global config when not in workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = undefined
|
||||
|
||||
const globalConfig: ProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
global: { id: "global-id" },
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: "global-id",
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(globalConfig))
|
||||
|
||||
const configId = await providerSettingsManager.getModeConfigId("code")
|
||||
expect(configId).toBe("global-id")
|
||||
})
|
||||
})
|
||||
|
||||
describe("clearWorkspaceOverrides", () => {
|
||||
it("should clear workspace overrides when in workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
await providerSettingsManager.clearWorkspaceOverrides()
|
||||
|
||||
expect(mockWorkspaceState.update).toHaveBeenCalledWith("roo_workspace_overrides", undefined)
|
||||
})
|
||||
|
||||
it("should do nothing when not in workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = undefined
|
||||
|
||||
await providerSettingsManager.clearWorkspaceOverrides()
|
||||
|
||||
expect(mockWorkspaceState.update).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getConfigScope", () => {
|
||||
it("should return workspace when workspace overrides exist", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
mockWorkspaceState.get.mockReturnValue({
|
||||
currentApiConfigName: "workspace-profile",
|
||||
})
|
||||
|
||||
const scope = await providerSettingsManager.getConfigScope()
|
||||
expect(scope).toBe("workspace")
|
||||
})
|
||||
|
||||
it("should return workspace when mode configs exist in workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
mockWorkspaceState.get.mockReturnValue({
|
||||
modeApiConfigs: { code: "test-id" },
|
||||
})
|
||||
|
||||
const scope = await providerSettingsManager.getConfigScope()
|
||||
expect(scope).toBe("workspace")
|
||||
})
|
||||
|
||||
it("should return global when no workspace overrides", async () => {
|
||||
;(workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
mockWorkspaceState.get.mockReturnValue(undefined)
|
||||
|
||||
const scope = await providerSettingsManager.getConfigScope()
|
||||
expect(scope).toBe("global")
|
||||
})
|
||||
|
||||
it("should return global when not in workspace", async () => {
|
||||
;(workspace as any).workspaceFolders = undefined
|
||||
|
||||
const scope = await providerSettingsManager.getConfigScope()
|
||||
expect(scope).toBe("global")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1628,24 +1628,35 @@ export class ClineProvider
|
|||
|
||||
async activateProviderProfile(
|
||||
args: { name: string } | { id: string },
|
||||
options?: { persistModeConfig?: boolean; persistTaskHistory?: boolean },
|
||||
options?: { persistModeConfig?: boolean; persistTaskHistory?: boolean; scope?: "global" | "workspace" },
|
||||
) {
|
||||
const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
|
||||
const scope = options?.scope ?? "global"
|
||||
const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args, scope)
|
||||
|
||||
const persistModeConfig = options?.persistModeConfig ?? true
|
||||
const persistTaskHistory = options?.persistTaskHistory ?? true
|
||||
|
||||
// See `upsertProviderProfile` for a description of what this is doing.
|
||||
await Promise.all([
|
||||
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
|
||||
this.contextProxy.setValue("currentApiConfigName", name),
|
||||
this.contextProxy.setProviderSettings(providerSettings),
|
||||
])
|
||||
// When scope is "workspace", only update the local cache for currentApiConfigName
|
||||
// to avoid leaking the selection to other VS Code windows via shared globalState.
|
||||
if (scope === "workspace") {
|
||||
await Promise.all([
|
||||
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
|
||||
this.contextProxy.setProviderSettings(providerSettings),
|
||||
])
|
||||
this.contextProxy.setLocalValue("currentApiConfigName", name)
|
||||
} else {
|
||||
await Promise.all([
|
||||
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
|
||||
this.contextProxy.setValue("currentApiConfigName", name),
|
||||
this.contextProxy.setProviderSettings(providerSettings),
|
||||
])
|
||||
}
|
||||
|
||||
const { mode } = await this.getState()
|
||||
|
||||
if (id && persistModeConfig) {
|
||||
await this.providerSettingsManager.setModeConfig(mode, id)
|
||||
await this.providerSettingsManager.setModeConfig(mode, id, scope)
|
||||
}
|
||||
|
||||
// Change the provider for the current task.
|
||||
|
|
@ -2276,6 +2287,7 @@ export class ClineProvider
|
|||
terminalZdotdir: terminalZdotdir ?? false,
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
currentConfigScope: await this.providerSettingsManager.getConfigScope(),
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
pinnedApiConfigs: pinnedApiConfigs ?? {},
|
||||
mode: mode ?? defaultModeSlug,
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
|
|||
|
||||
// Mock providerSettingsManager
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
saveConfig: vi.fn().mockResolvedValue("test-id"),
|
||||
listConfig: vi
|
||||
.fn()
|
||||
|
|
|
|||
|
|
@ -879,6 +879,7 @@ describe("ClineProvider", () => {
|
|||
const profile: ProviderSettingsEntry = { name: "test-config", id: "test-id", apiProvider: "anthropic" }
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue("test-id"),
|
||||
listConfig: vi.fn().mockResolvedValue([profile]),
|
||||
activateProfile: vi.fn().mockResolvedValue(profile),
|
||||
|
|
@ -891,7 +892,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Should load the saved config for architect mode
|
||||
expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
|
||||
expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "test-config" })
|
||||
expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "test-config" }, "global")
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
|
||||
})
|
||||
|
||||
|
|
@ -900,6 +901,7 @@ describe("ClineProvider", () => {
|
|||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi
|
||||
.fn()
|
||||
|
|
@ -923,6 +925,7 @@ describe("ClineProvider", () => {
|
|||
const profile: ProviderSettingsEntry = { apiProvider: "anthropic", id: "new-id", name: "new-config" }
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
activateProfile: vi.fn().mockResolvedValue(profile),
|
||||
listConfig: vi.fn().mockResolvedValue([profile]),
|
||||
setModeConfig: vi.fn(),
|
||||
|
|
@ -936,7 +939,7 @@ describe("ClineProvider", () => {
|
|||
await messageHandler({ type: "loadApiConfiguration", text: "new-config" })
|
||||
|
||||
// Should save new config as default for architect mode
|
||||
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
|
||||
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id", "global")
|
||||
})
|
||||
|
||||
it("load API configuration by ID works and updates mode config", async () => {
|
||||
|
|
@ -950,6 +953,7 @@ describe("ClineProvider", () => {
|
|||
}
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
activateProfile: vi.fn().mockResolvedValue(profile),
|
||||
listConfig: vi.fn().mockResolvedValue([profile]),
|
||||
setModeConfig: vi.fn(),
|
||||
|
|
@ -963,10 +967,14 @@ describe("ClineProvider", () => {
|
|||
await messageHandler({ type: "loadApiConfigurationById", text: "config-id-123" })
|
||||
|
||||
// Should save new config as default for architect mode
|
||||
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
|
||||
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith(
|
||||
"architect",
|
||||
"config-id-123",
|
||||
"global",
|
||||
)
|
||||
|
||||
// Ensure the `activateProfile` method was called with the correct ID
|
||||
expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ id: "config-id-123" })
|
||||
expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ id: "config-id-123" }, "global")
|
||||
})
|
||||
|
||||
test("handles showRooIgnoredFiles setting", async () => {
|
||||
|
|
@ -1120,6 +1128,7 @@ describe("ClineProvider", () => {
|
|||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
listConfig: vi.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
|
||||
saveConfig: vi.fn().mockResolvedValue("test-id"),
|
||||
setModeConfig: vi.fn(),
|
||||
|
|
@ -1466,6 +1475,7 @@ describe("ClineProvider", () => {
|
|||
}
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue("saved-config-id"),
|
||||
listConfig: vi.fn().mockResolvedValue([profile]),
|
||||
activateProfile: vi.fn().mockResolvedValue(profile),
|
||||
|
|
@ -1481,7 +1491,10 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Verify saved config was loaded
|
||||
expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
|
||||
expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "saved-config" })
|
||||
expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith(
|
||||
{ name: "saved-config" },
|
||||
"global",
|
||||
)
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "saved-config")
|
||||
|
||||
// Verify state was posted to webview
|
||||
|
|
@ -1490,6 +1503,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
test("saves current config when switching to mode without config", async () => {
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi
|
||||
.fn()
|
||||
|
|
@ -1550,6 +1564,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Mock provider settings manager
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi.fn().mockResolvedValue([]),
|
||||
}
|
||||
|
|
@ -1614,6 +1629,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Mock provider settings manager
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue("config-id"),
|
||||
listConfig: vi
|
||||
.fn()
|
||||
|
|
@ -1674,6 +1690,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Mock provider settings manager
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi.fn().mockResolvedValue([]),
|
||||
}
|
||||
|
|
@ -1705,6 +1722,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Mock provider settings manager
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi.fn().mockResolvedValue([]),
|
||||
}
|
||||
|
|
@ -1749,6 +1767,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Mock provider settings manager to throw error
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
getModeConfigId: vi.fn().mockResolvedValue("config-id"),
|
||||
listConfig: vi
|
||||
.fn()
|
||||
|
|
@ -1849,6 +1868,7 @@ describe("ClineProvider", () => {
|
|||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
setModeConfig: vi.fn().mockRejectedValue(new Error("Failed to update mode config")),
|
||||
listConfig: vi
|
||||
.fn()
|
||||
|
|
@ -1880,6 +1900,7 @@ describe("ClineProvider", () => {
|
|||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
setModeConfig: vi.fn(),
|
||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi
|
||||
|
|
@ -1923,6 +1944,7 @@ describe("ClineProvider", () => {
|
|||
throw new Error("API handler error")
|
||||
})
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
setModeConfig: vi.fn(),
|
||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi
|
||||
|
|
@ -1964,6 +1986,7 @@ describe("ClineProvider", () => {
|
|||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getConfigScope: vi.fn().mockResolvedValue("global"),
|
||||
setModeConfig: vi.fn(),
|
||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi
|
||||
|
|
|
|||
|
|
@ -1786,7 +1786,8 @@ export const webviewMessageHandler = async (
|
|||
case "loadApiConfiguration":
|
||||
if (message.text) {
|
||||
try {
|
||||
await provider.activateProviderProfile({ name: message.text })
|
||||
const scope = message.scope || "global"
|
||||
await provider.activateProviderProfile({ name: message.text }, { scope })
|
||||
} catch (error) {
|
||||
provider.log(
|
||||
`Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
|
|
@ -1798,7 +1799,8 @@ export const webviewMessageHandler = async (
|
|||
case "loadApiConfigurationById":
|
||||
if (message.text) {
|
||||
try {
|
||||
await provider.activateProviderProfile({ id: message.text })
|
||||
const scope = message.scope || "global"
|
||||
await provider.activateProviderProfile({ id: message.text }, { scope })
|
||||
} catch (error) {
|
||||
provider.log(
|
||||
`Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ interface ApiConfigSelectorProps {
|
|||
togglePinnedApiConfig: (id: string) => void
|
||||
lockApiConfigAcrossModes: boolean
|
||||
onToggleLockApiConfig: () => void
|
||||
isWorkspaceScoped?: boolean
|
||||
onScopeChange?: (scope: "global" | "workspace") => void
|
||||
hasWorkspace?: boolean
|
||||
}
|
||||
|
||||
export const ApiConfigSelector = ({
|
||||
|
|
@ -36,6 +39,9 @@ export const ApiConfigSelector = ({
|
|||
togglePinnedApiConfig,
|
||||
lockApiConfigAcrossModes,
|
||||
onToggleLockApiConfig,
|
||||
isWorkspaceScoped = false,
|
||||
onScopeChange,
|
||||
hasWorkspace = false,
|
||||
}: ApiConfigSelectorProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
@ -89,6 +95,13 @@ export const ApiConfigSelector = ({
|
|||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
const handleScopeToggle = useCallback(() => {
|
||||
if (onScopeChange) {
|
||||
const newScope = isWorkspaceScoped ? "global" : "workspace"
|
||||
onScopeChange(newScope)
|
||||
}
|
||||
}, [isWorkspaceScoped, onScopeChange])
|
||||
|
||||
const renderConfigItem = useCallback(
|
||||
(config: { id: string; name: string; modelId?: string }, isPinned: boolean) => {
|
||||
const isCurrentConfig = config.id === value
|
||||
|
|
@ -161,6 +174,7 @@ export const ApiConfigSelector = ({
|
|||
triggerClassName,
|
||||
)}>
|
||||
<span className="truncate">{displayName}</span>
|
||||
{hasWorkspace && isWorkspaceScoped && <span className="ml-1 text-[10px] opacity-60">[W]</span>}
|
||||
</PopoverTrigger>
|
||||
</StandardTooltip>
|
||||
<PopoverContent
|
||||
|
|
@ -169,6 +183,37 @@ export const ApiConfigSelector = ({
|
|||
container={portalContainer}
|
||||
className="p-0 overflow-hidden w-[300px]">
|
||||
<div className="flex flex-col w-full">
|
||||
{/* Scope selector for workspace-enabled projects */}
|
||||
{hasWorkspace && onScopeChange && (
|
||||
<div className="p-2 border-b border-vscode-dropdown-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("prompts:apiConfiguration.scope")}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleScopeToggle}
|
||||
className={cn(
|
||||
"px-2 py-0.5 text-xs rounded",
|
||||
"border border-vscode-dropdown-border",
|
||||
"hover:bg-vscode-list-hoverBackground",
|
||||
"transition-colors duration-150",
|
||||
)}>
|
||||
{isWorkspaceScoped ? (
|
||||
<>
|
||||
<span className="codicon codicon-folder text-xs mr-1" />
|
||||
{t("prompts:apiConfiguration.workspace")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="codicon codicon-globe text-xs mr-1" />
|
||||
{t("prompts:apiConfiguration.global")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search input or info blurb */}
|
||||
{listApiConfigMeta.length > 6 ? (
|
||||
<div className="relative p-2 border-b border-vscode-dropdown-border">
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
cloudUserInfo,
|
||||
enterBehavior,
|
||||
lockApiConfigAcrossModes,
|
||||
currentConfigScope,
|
||||
} = useExtensionState()
|
||||
|
||||
// Find the ID and display text for the currently selected API configuration.
|
||||
|
|
@ -937,9 +938,29 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
)
|
||||
|
||||
// Helper function to handle API config change
|
||||
const handleApiConfigChange = useCallback((value: string) => {
|
||||
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
|
||||
}, [])
|
||||
const handleApiConfigChange = useCallback(
|
||||
(value: string) => {
|
||||
const scope = currentConfigScope === "workspace" ? "workspace" : "global"
|
||||
vscode.postMessage({ type: "loadApiConfigurationById", text: value, scope })
|
||||
},
|
||||
[currentConfigScope],
|
||||
)
|
||||
|
||||
// Handle scope change for API configuration
|
||||
const handleApiConfigScopeChange = useCallback(
|
||||
(scope: "global" | "workspace") => {
|
||||
// When user changes scope, re-apply the current config with the new scope
|
||||
if (currentConfigId) {
|
||||
vscode.postMessage({ type: "loadApiConfigurationById", text: currentConfigId, scope })
|
||||
}
|
||||
},
|
||||
[currentConfigId],
|
||||
)
|
||||
|
||||
// Check if we're in a workspace context
|
||||
const hasWorkspace = useMemo(() => {
|
||||
return !!(cwd && cwd.length > 0)
|
||||
}, [cwd])
|
||||
|
||||
const handleToggleLockApiConfig = useCallback(() => {
|
||||
const newValue = !lockApiConfigAcrossModes
|
||||
|
|
@ -1319,6 +1340,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
togglePinnedApiConfig={togglePinnedApiConfig}
|
||||
lockApiConfigAcrossModes={!!lockApiConfigAcrossModes}
|
||||
onToggleLockApiConfig={handleToggleLockApiConfig}
|
||||
isWorkspaceScoped={currentConfigScope === "workspace"}
|
||||
onScopeChange={handleApiConfigScopeChange}
|
||||
hasWorkspace={hasWorkspace}
|
||||
/>
|
||||
<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@
|
|||
},
|
||||
"apiConfiguration": {
|
||||
"title": "API Configuration",
|
||||
"select": "Select which API configuration to use for this mode"
|
||||
"select": "Select which API configuration to use for this mode",
|
||||
"scope": "Scope",
|
||||
"global": "Global",
|
||||
"workspace": "Workspace"
|
||||
},
|
||||
"tools": {
|
||||
"title": "Available Tools",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue