mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add workspace-scoped mode-to-profile overrides
Adds the ability to pin a provider profile to a specific mode on a per-workspace basis. When switching modes, the system checks workspace- level overrides first and falls back to global mode-to-profile mappings if no workspace override exists. Changes: - Add workspaceModeApiConfigs to ExtensionState type - Add setWorkspaceModeApiConfig/clearWorkspaceModeApiConfig message types - Update ClineProvider.handleModeSwitch to check workspace overrides - Update ClineProvider.getState to include workspace configs - Add webview message handler for workspace profile operations - Add workspace profile pin button to ApiConfigSelector UI - Add translation keys for new UI elements - Add tests for workspace mode API config message handling Closes #12227
This commit is contained in:
parent
ad25634905
commit
b4b2463461
8 changed files with 254 additions and 2 deletions
|
|
@ -308,6 +308,7 @@ export type ExtensionState = Pick<
|
|||
| "disabledTools"
|
||||
> & {
|
||||
lockApiConfigAcrossModes?: boolean
|
||||
workspaceModeApiConfigs?: Record<string, string>
|
||||
version: string
|
||||
clineMessages: ClineMessage[]
|
||||
currentTaskId?: string
|
||||
|
|
@ -499,6 +500,8 @@ export interface WebviewMessage {
|
|||
| "toggleApiConfigPin"
|
||||
| "hasOpenedModeSelector"
|
||||
| "lockApiConfigAcrossModes"
|
||||
| "setWorkspaceModeApiConfig"
|
||||
| "clearWorkspaceModeApiConfig"
|
||||
| "clearCloudAuthSkipModel"
|
||||
| "cloudButtonClicked"
|
||||
| "rooCloudSignIn"
|
||||
|
|
|
|||
|
|
@ -999,7 +999,12 @@ export class ClineProvider
|
|||
const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false)
|
||||
|
||||
if (!historyItem.apiConfigName && !lockApiConfigAcrossModes && !skipProfileRestoreFromHistory) {
|
||||
const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode)
|
||||
// Check workspace-level override first, then fall back to global mode config.
|
||||
const workspaceModeApiConfigs =
|
||||
this.context.workspaceState.get<Record<string, string>>("workspaceModeApiConfigs") ?? {}
|
||||
const workspaceConfigId = workspaceModeApiConfigs[historyItem.mode]
|
||||
const savedConfigId =
|
||||
workspaceConfigId ?? (await this.providerSettingsManager.getModeConfigId(historyItem.mode))
|
||||
const listApiConfig = await this.providerSettingsManager.listConfig()
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data.
|
||||
|
|
@ -1433,8 +1438,13 @@ export class ClineProvider
|
|||
return
|
||||
}
|
||||
|
||||
// Check for workspace-level mode-to-profile override first, then fall back to global.
|
||||
const workspaceModeApiConfigs =
|
||||
this.context.workspaceState.get<Record<string, string>>("workspaceModeApiConfigs") ?? {}
|
||||
const workspaceConfigId = workspaceModeApiConfigs[newMode]
|
||||
|
||||
// Load the saved API config for the new mode if it exists.
|
||||
const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
|
||||
const savedConfigId = workspaceConfigId ?? (await this.providerSettingsManager.getModeConfigId(newMode))
|
||||
const listApiConfig = await this.providerSettingsManager.listConfig()
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data.
|
||||
|
|
@ -2563,6 +2573,10 @@ export class ClineProvider
|
|||
},
|
||||
profileThresholds: stateValues.profileThresholds ?? {},
|
||||
lockApiConfigAcrossModes: this.context.workspaceState.get("lockApiConfigAcrossModes", false),
|
||||
workspaceModeApiConfigs: this.context.workspaceState.get<Record<string, string>>(
|
||||
"workspaceModeApiConfigs",
|
||||
{},
|
||||
),
|
||||
includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
|
||||
maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
|
||||
includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
// npx vitest run core/webview/__tests__/webviewMessageHandler.workspaceModeApiConfig.spec.ts
|
||||
|
||||
import { webviewMessageHandler } from "../webviewMessageHandler"
|
||||
import type { ClineProvider } from "../ClineProvider"
|
||||
|
||||
describe("webviewMessageHandler - setWorkspaceModeApiConfig", () => {
|
||||
let mockProvider: {
|
||||
context: {
|
||||
workspaceState: {
|
||||
get: ReturnType<typeof vi.fn>
|
||||
update: ReturnType<typeof vi.fn>
|
||||
}
|
||||
}
|
||||
getState: ReturnType<typeof vi.fn>
|
||||
postStateToWebview: ReturnType<typeof vi.fn>
|
||||
providerSettingsManager: {
|
||||
setModeConfig: ReturnType<typeof vi.fn>
|
||||
}
|
||||
postMessageToWebview: ReturnType<typeof vi.fn>
|
||||
getCurrentTask: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
let workspaceStateStore: Record<string, unknown>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
workspaceStateStore = {}
|
||||
|
||||
mockProvider = {
|
||||
context: {
|
||||
workspaceState: {
|
||||
get: vi.fn().mockImplementation((key: string, defaultValue?: unknown) => {
|
||||
return key in workspaceStateStore ? workspaceStateStore[key] : defaultValue
|
||||
}),
|
||||
update: vi.fn().mockImplementation((key: string, value: unknown) => {
|
||||
workspaceStateStore[key] = value
|
||||
return Promise.resolve()
|
||||
}),
|
||||
},
|
||||
},
|
||||
getState: vi.fn().mockResolvedValue({
|
||||
currentApiConfigName: "test-config",
|
||||
listApiConfigMeta: [{ name: "test-config", id: "config-123" }],
|
||||
customModes: [],
|
||||
}),
|
||||
postStateToWebview: vi.fn(),
|
||||
providerSettingsManager: {
|
||||
setModeConfig: vi.fn(),
|
||||
},
|
||||
postMessageToWebview: vi.fn(),
|
||||
getCurrentTask: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
it("sets a workspace mode API config for a specific mode", async () => {
|
||||
await webviewMessageHandler(mockProvider as unknown as ClineProvider, {
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
mode: "code",
|
||||
text: "config-123",
|
||||
})
|
||||
|
||||
expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("workspaceModeApiConfigs", {
|
||||
code: "config-123",
|
||||
})
|
||||
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("clears a workspace mode API config when text is undefined", async () => {
|
||||
// Pre-populate with an existing mapping
|
||||
workspaceStateStore["workspaceModeApiConfigs"] = { code: "config-123", architect: "config-456" }
|
||||
|
||||
await webviewMessageHandler(mockProvider as unknown as ClineProvider, {
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
mode: "code",
|
||||
// text is undefined - clears the override
|
||||
})
|
||||
|
||||
expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("workspaceModeApiConfigs", {
|
||||
architect: "config-456",
|
||||
})
|
||||
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("preserves existing workspace configs when adding a new one", async () => {
|
||||
workspaceStateStore["workspaceModeApiConfigs"] = { architect: "config-456" }
|
||||
|
||||
await webviewMessageHandler(mockProvider as unknown as ClineProvider, {
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
mode: "code",
|
||||
text: "config-789",
|
||||
})
|
||||
|
||||
expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("workspaceModeApiConfigs", {
|
||||
architect: "config-456",
|
||||
code: "config-789",
|
||||
})
|
||||
})
|
||||
|
||||
it("does nothing if mode is not provided", async () => {
|
||||
await webviewMessageHandler(mockProvider as unknown as ClineProvider, {
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
// mode is undefined
|
||||
text: "config-123",
|
||||
})
|
||||
|
||||
expect(mockProvider.context.workspaceState.update).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("webviewMessageHandler - clearWorkspaceModeApiConfig", () => {
|
||||
let mockProvider: {
|
||||
context: {
|
||||
workspaceState: {
|
||||
get: ReturnType<typeof vi.fn>
|
||||
update: ReturnType<typeof vi.fn>
|
||||
}
|
||||
}
|
||||
getState: ReturnType<typeof vi.fn>
|
||||
postStateToWebview: ReturnType<typeof vi.fn>
|
||||
providerSettingsManager: {
|
||||
setModeConfig: ReturnType<typeof vi.fn>
|
||||
}
|
||||
postMessageToWebview: ReturnType<typeof vi.fn>
|
||||
getCurrentTask: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockProvider = {
|
||||
context: {
|
||||
workspaceState: {
|
||||
get: vi.fn(),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
getState: vi.fn().mockResolvedValue({
|
||||
currentApiConfigName: "test-config",
|
||||
listApiConfigMeta: [{ name: "test-config", id: "config-123" }],
|
||||
customModes: [],
|
||||
}),
|
||||
postStateToWebview: vi.fn(),
|
||||
providerSettingsManager: {
|
||||
setModeConfig: vi.fn(),
|
||||
},
|
||||
postMessageToWebview: vi.fn(),
|
||||
getCurrentTask: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
it("clears all workspace mode API configs", async () => {
|
||||
await webviewMessageHandler(mockProvider as unknown as ClineProvider, {
|
||||
type: "clearWorkspaceModeApiConfig",
|
||||
})
|
||||
|
||||
expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("workspaceModeApiConfigs", {})
|
||||
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -1650,6 +1650,37 @@ export const webviewMessageHandler = async (
|
|||
break
|
||||
}
|
||||
|
||||
case "setWorkspaceModeApiConfig": {
|
||||
// Set a workspace-level mode-to-profile override.
|
||||
// message.mode contains the mode slug, message.text contains the profile config ID.
|
||||
const modeSlug = message.mode
|
||||
const configId = message.text
|
||||
|
||||
if (modeSlug) {
|
||||
const workspaceModeApiConfigs = provider.context.workspaceState.get<Record<string, string>>(
|
||||
"workspaceModeApiConfigs",
|
||||
{},
|
||||
)
|
||||
|
||||
if (configId) {
|
||||
workspaceModeApiConfigs[modeSlug] = configId
|
||||
} else {
|
||||
delete workspaceModeApiConfigs[modeSlug]
|
||||
}
|
||||
|
||||
await provider.context.workspaceState.update("workspaceModeApiConfigs", workspaceModeApiConfigs)
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "clearWorkspaceModeApiConfig": {
|
||||
// Clear all workspace-level mode-to-profile overrides.
|
||||
await provider.context.workspaceState.update("workspaceModeApiConfigs", {})
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
}
|
||||
|
||||
case "toggleApiConfigPin":
|
||||
if (message.text) {
|
||||
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ interface ApiConfigSelectorProps {
|
|||
togglePinnedApiConfig: (id: string) => void
|
||||
lockApiConfigAcrossModes: boolean
|
||||
onToggleLockApiConfig: () => void
|
||||
currentMode?: string
|
||||
workspaceModeApiConfigs?: Record<string, string>
|
||||
}
|
||||
|
||||
export const ApiConfigSelector = ({
|
||||
|
|
@ -36,6 +38,8 @@ export const ApiConfigSelector = ({
|
|||
togglePinnedApiConfig,
|
||||
lockApiConfigAcrossModes,
|
||||
onToggleLockApiConfig,
|
||||
currentMode,
|
||||
workspaceModeApiConfigs,
|
||||
}: ApiConfigSelectorProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
@ -242,6 +246,39 @@ export const ApiConfigSelector = ({
|
|||
className={lockApiConfigAcrossModes ? "text-vscode-focusBorder" : "opacity-60"}
|
||||
onClick={onToggleLockApiConfig}
|
||||
/>
|
||||
{currentMode && (
|
||||
<IconButton
|
||||
iconClass={
|
||||
workspaceModeApiConfigs?.[currentMode]
|
||||
? "codicon-root-folder-opened"
|
||||
: "codicon-root-folder"
|
||||
}
|
||||
title={
|
||||
workspaceModeApiConfigs?.[currentMode]
|
||||
? t("chat:clearWorkspaceProfile")
|
||||
: t("chat:setWorkspaceProfile")
|
||||
}
|
||||
className={
|
||||
workspaceModeApiConfigs?.[currentMode]
|
||||
? "text-vscode-focusBorder"
|
||||
: "opacity-60"
|
||||
}
|
||||
onClick={() => {
|
||||
if (workspaceModeApiConfigs?.[currentMode]) {
|
||||
vscode.postMessage({
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
mode: currentMode,
|
||||
})
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
mode: currentMode,
|
||||
text: value,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info icon and title on the right with matching spacing */}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
cloudUserInfo,
|
||||
enterBehavior,
|
||||
lockApiConfigAcrossModes,
|
||||
workspaceModeApiConfigs,
|
||||
mode: currentMode,
|
||||
} = useExtensionState()
|
||||
|
||||
// Find the ID and display text for the currently selected API configuration.
|
||||
|
|
@ -1319,6 +1321,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
togglePinnedApiConfig={togglePinnedApiConfig}
|
||||
lockApiConfigAcrossModes={!!lockApiConfigAcrossModes}
|
||||
onToggleLockApiConfig={handleToggleLockApiConfig}
|
||||
currentMode={currentMode}
|
||||
workspaceModeApiConfigs={workspaceModeApiConfigs}
|
||||
/>
|
||||
<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
includeCurrentTime: true,
|
||||
includeCurrentCost: true,
|
||||
lockApiConfigAcrossModes: false,
|
||||
workspaceModeApiConfigs: {},
|
||||
})
|
||||
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@
|
|||
"selectApiConfig": "Select API configuration",
|
||||
"lockApiConfigAcrossModes": "Lock API configuration across all modes in this workspace",
|
||||
"unlockApiConfigAcrossModes": "API configuration is locked across all modes in this workspace (click to unlock)",
|
||||
"setWorkspaceProfile": "Pin this profile to the current mode for this workspace",
|
||||
"clearWorkspaceProfile": "This profile is pinned to the current mode for this workspace (click to unpin)",
|
||||
"enhancePrompt": "Enhance prompt with additional context",
|
||||
"modeSelector": {
|
||||
"title": "Modes",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue