mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: gate workspace profile overrides behind experimental flag and show conflict state
- Add WORKSPACE_PROFILE_OVERRIDES experiment to types, schema, and config - Gate workspace override logic in ClineProvider behind experiment flag - Gate workspace override logic in webviewMessageHandler behind experiment flag - Gate workspace pin button in ApiConfigSelector behind experiment flag - Show warning icon when current mode is already pinned to a different profile - Add "reassignWorkspaceProfile" translation key for conflict state - Add "Project-specific profile usage" experimental feature setting - Add test for experiment-disabled scenario
This commit is contained in:
parent
b4b2463461
commit
abafe2d4f5
10 changed files with 116 additions and 42 deletions
|
|
@ -6,7 +6,13 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
|
|||
* ExperimentId
|
||||
*/
|
||||
|
||||
export const experimentIds = ["preventFocusDisruption", "imageGeneration", "runSlashCommand", "customTools"] as const
|
||||
export const experimentIds = [
|
||||
"preventFocusDisruption",
|
||||
"imageGeneration",
|
||||
"runSlashCommand",
|
||||
"customTools",
|
||||
"workspaceProfileOverrides",
|
||||
] as const
|
||||
|
||||
export const experimentIdsSchema = z.enum(experimentIds)
|
||||
|
||||
|
|
@ -21,6 +27,7 @@ export const experimentsSchema = z.object({
|
|||
imageGeneration: z.boolean().optional(),
|
||||
runSlashCommand: z.boolean().optional(),
|
||||
customTools: z.boolean().optional(),
|
||||
workspaceProfileOverrides: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type Experiments = z.infer<typeof experimentsSchema>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import { findLast } from "../../shared/array"
|
|||
import { supportPrompt } from "../../shared/support-prompt"
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
|
||||
import { experimentDefault } from "../../shared/experiments"
|
||||
import { experimentDefault, experiments as experimentsUtil, EXPERIMENT_IDS } from "../../shared/experiments"
|
||||
import { formatLanguage } from "../../shared/language"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
|
||||
|
|
@ -999,9 +999,15 @@ export class ClineProvider
|
|||
const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false)
|
||||
|
||||
if (!historyItem.apiConfigName && !lockApiConfigAcrossModes && !skipProfileRestoreFromHistory) {
|
||||
// Check workspace-level override first, then fall back to global mode config.
|
||||
const workspaceModeApiConfigs =
|
||||
this.context.workspaceState.get<Record<string, string>>("workspaceModeApiConfigs") ?? {}
|
||||
// Check workspace-level override first (if experiment enabled), then fall back to global mode config.
|
||||
const { experiments: experimentsState } = await this.getState()
|
||||
const workspaceOverridesEnabled = experimentsUtil.isEnabled(
|
||||
experimentsState ?? experimentDefault,
|
||||
EXPERIMENT_IDS.WORKSPACE_PROFILE_OVERRIDES,
|
||||
)
|
||||
const workspaceModeApiConfigs = workspaceOverridesEnabled
|
||||
? (this.context.workspaceState.get<Record<string, string>>("workspaceModeApiConfigs") ?? {})
|
||||
: {}
|
||||
const workspaceConfigId = workspaceModeApiConfigs[historyItem.mode]
|
||||
const savedConfigId =
|
||||
workspaceConfigId ?? (await this.providerSettingsManager.getModeConfigId(historyItem.mode))
|
||||
|
|
@ -1438,9 +1444,15 @@ 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") ?? {}
|
||||
// Check for workspace-level mode-to-profile override first (if experiment enabled), then fall back to global.
|
||||
const { experiments: experimentsState } = await this.getState()
|
||||
const workspaceOverridesEnabled = experimentsUtil.isEnabled(
|
||||
experimentsState ?? experimentDefault,
|
||||
EXPERIMENT_IDS.WORKSPACE_PROFILE_OVERRIDES,
|
||||
)
|
||||
const workspaceModeApiConfigs = workspaceOverridesEnabled
|
||||
? (this.context.workspaceState.get<Record<string, string>>("workspaceModeApiConfigs") ?? {})
|
||||
: {}
|
||||
const workspaceConfigId = workspaceModeApiConfigs[newMode]
|
||||
|
||||
// Load the saved API config for the new mode if it exists.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ describe("webviewMessageHandler - setWorkspaceModeApiConfig", () => {
|
|||
currentApiConfigName: "test-config",
|
||||
listApiConfigMeta: [{ name: "test-config", id: "config-123" }],
|
||||
customModes: [],
|
||||
experiments: { workspaceProfileOverrides: true },
|
||||
}),
|
||||
postStateToWebview: vi.fn(),
|
||||
providerSettingsManager: {
|
||||
|
|
@ -53,6 +54,23 @@ describe("webviewMessageHandler - setWorkspaceModeApiConfig", () => {
|
|||
}
|
||||
})
|
||||
|
||||
it("does nothing when experiment is disabled", async () => {
|
||||
mockProvider.getState.mockResolvedValueOnce({
|
||||
currentApiConfigName: "test-config",
|
||||
listApiConfigMeta: [{ name: "test-config", id: "config-123" }],
|
||||
customModes: [],
|
||||
experiments: { workspaceProfileOverrides: false },
|
||||
})
|
||||
|
||||
await webviewMessageHandler(mockProvider as unknown as ClineProvider, {
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
mode: "code",
|
||||
text: "config-123",
|
||||
})
|
||||
|
||||
expect(mockProvider.context.workspaceState.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("sets a workspace mode API config for a specific mode", async () => {
|
||||
await webviewMessageHandler(mockProvider as unknown as ClineProvider, {
|
||||
type: "setWorkspaceModeApiConfig",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import { MessageEnhancer } from "./messageEnhancer"
|
|||
|
||||
import { CodeIndexManager } from "../../services/code-index/manager"
|
||||
import { checkExistKey } from "../../shared/checkExistApiConfig"
|
||||
import { experimentDefault } from "../../shared/experiments"
|
||||
import { experimentDefault, experiments as experimentsUtil, EXPERIMENT_IDS } from "../../shared/experiments"
|
||||
import { Terminal } from "../../integrations/terminal/Terminal"
|
||||
import { openFile } from "../../integrations/misc/open-file"
|
||||
import { openImage, saveImage } from "../../integrations/misc/image-handler"
|
||||
|
|
@ -1653,6 +1653,16 @@ export const webviewMessageHandler = async (
|
|||
case "setWorkspaceModeApiConfig": {
|
||||
// Set a workspace-level mode-to-profile override.
|
||||
// message.mode contains the mode slug, message.text contains the profile config ID.
|
||||
// Only proceed if the workspace profile overrides experiment is enabled.
|
||||
const { experiments: expState } = await provider.getState()
|
||||
const wsOverridesEnabled = experimentsUtil.isEnabled(
|
||||
expState ?? experimentDefault,
|
||||
EXPERIMENT_IDS.WORKSPACE_PROFILE_OVERRIDES,
|
||||
)
|
||||
if (!wsOverridesEnabled) {
|
||||
break
|
||||
}
|
||||
|
||||
const modeSlug = message.mode
|
||||
const configId = message.text
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ describe("experiments", () => {
|
|||
imageGeneration: false,
|
||||
runSlashCommand: false,
|
||||
customTools: false,
|
||||
workspaceProfileOverrides: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false)
|
||||
})
|
||||
|
|
@ -31,6 +32,7 @@ describe("experiments", () => {
|
|||
imageGeneration: false,
|
||||
runSlashCommand: false,
|
||||
customTools: false,
|
||||
workspaceProfileOverrides: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(true)
|
||||
})
|
||||
|
|
@ -41,6 +43,7 @@ describe("experiments", () => {
|
|||
imageGeneration: false,
|
||||
runSlashCommand: false,
|
||||
customTools: false,
|
||||
workspaceProfileOverrides: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const EXPERIMENT_IDS = {
|
|||
IMAGE_GENERATION: "imageGeneration",
|
||||
RUN_SLASH_COMMAND: "runSlashCommand",
|
||||
CUSTOM_TOOLS: "customTools",
|
||||
WORKSPACE_PROFILE_OVERRIDES: "workspaceProfileOverrides",
|
||||
} as const satisfies Record<string, ExperimentId>
|
||||
|
||||
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
|
||||
|
|
@ -20,6 +21,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
|
|||
IMAGE_GENERATION: { enabled: false },
|
||||
RUN_SLASH_COMMAND: { enabled: false },
|
||||
CUSTOM_TOOLS: { enabled: false },
|
||||
WORKSPACE_PROFILE_OVERRIDES: { enabled: false },
|
||||
}
|
||||
|
||||
export const experimentDefault = Object.fromEntries(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ interface ApiConfigSelectorProps {
|
|||
onToggleLockApiConfig: () => void
|
||||
currentMode?: string
|
||||
workspaceModeApiConfigs?: Record<string, string>
|
||||
enableWorkspaceOverrides?: boolean
|
||||
}
|
||||
|
||||
export const ApiConfigSelector = ({
|
||||
|
|
@ -40,6 +41,7 @@ export const ApiConfigSelector = ({
|
|||
onToggleLockApiConfig,
|
||||
currentMode,
|
||||
workspaceModeApiConfigs,
|
||||
enableWorkspaceOverrides,
|
||||
}: ApiConfigSelectorProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
@ -246,39 +248,52 @@ 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,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentMode &&
|
||||
enableWorkspaceOverrides &&
|
||||
(() => {
|
||||
const pinnedConfigId = workspaceModeApiConfigs?.[currentMode]
|
||||
const isPinnedToThis = pinnedConfigId === value
|
||||
const isPinnedToOther = !!pinnedConfigId && pinnedConfigId !== value
|
||||
return (
|
||||
<IconButton
|
||||
iconClass={
|
||||
isPinnedToThis
|
||||
? "codicon-root-folder-opened"
|
||||
: isPinnedToOther
|
||||
? "codicon-warning"
|
||||
: "codicon-root-folder"
|
||||
}
|
||||
title={
|
||||
isPinnedToThis
|
||||
? t("chat:clearWorkspaceProfile")
|
||||
: isPinnedToOther
|
||||
? t("chat:reassignWorkspaceProfile")
|
||||
: t("chat:setWorkspaceProfile")
|
||||
}
|
||||
className={
|
||||
isPinnedToThis
|
||||
? "text-vscode-focusBorder"
|
||||
: isPinnedToOther
|
||||
? "text-vscode-editorWarning-foreground opacity-80"
|
||||
: "opacity-60"
|
||||
}
|
||||
onClick={() => {
|
||||
if (isPinnedToThis) {
|
||||
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 */}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
lockApiConfigAcrossModes,
|
||||
workspaceModeApiConfigs,
|
||||
mode: currentMode,
|
||||
experiments,
|
||||
} = useExtensionState()
|
||||
|
||||
// Find the ID and display text for the currently selected API configuration.
|
||||
|
|
@ -1323,6 +1324,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
onToggleLockApiConfig={handleToggleLockApiConfig}
|
||||
currentMode={currentMode}
|
||||
workspaceModeApiConfigs={workspaceModeApiConfigs}
|
||||
enableWorkspaceOverrides={!!experiments?.workspaceProfileOverrides}
|
||||
/>
|
||||
<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@
|
|||
"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)",
|
||||
"reassignWorkspaceProfile": "This mode is already pinned to a different profile in this workspace (click to reassign)",
|
||||
"enhancePrompt": "Enhance prompt with additional context",
|
||||
"modeSelector": {
|
||||
"title": "Modes",
|
||||
|
|
|
|||
|
|
@ -886,6 +886,10 @@
|
|||
"refreshSuccess": "Tools refreshed successfully",
|
||||
"refreshError": "Failed to refresh tools",
|
||||
"toolParameters": "Parameters"
|
||||
},
|
||||
"WORKSPACE_PROFILE_OVERRIDES": {
|
||||
"name": "Project-specific profile usage",
|
||||
"description": "When enabled, you can pin provider profiles to specific modes on a per-workspace basis. Workspace overrides take priority over global mode-to-profile mappings."
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue