mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
feat: implement experimental features system
- Add experiments.ts to manage experimental features - Refactor experimental diff strategy into experiments system - Add UI components for managing experimental features - Add tests for experimental tools - Update system prompts to handle experiments
This commit is contained in:
@@ -61,6 +61,7 @@ import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||
import { McpHub } from "../services/mcp/McpHub"
|
||||
import crypto from "crypto"
|
||||
import { insertGroups } from "./diff/insert-groups"
|
||||
import { EXPERIMENT_IDS } from "../shared/experiments"
|
||||
|
||||
const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
@@ -151,9 +152,8 @@ export class Cline {
|
||||
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
|
||||
// If not provided, get from current state
|
||||
if (experimentalDiffStrategy === undefined) {
|
||||
const { experimentalDiffStrategy: stateExperimentalDiffStrategy } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
experimentalDiffStrategy = stateExperimentalDiffStrategy ?? false
|
||||
const { experiments: stateExperimental } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
|
||||
}
|
||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
|
||||
}
|
||||
@@ -810,7 +810,7 @@ export class Cline {
|
||||
})
|
||||
}
|
||||
|
||||
const { browserViewportSize, mode, customModePrompts, preferredLanguage } =
|
||||
const { browserViewportSize, mode, customModePrompts, preferredLanguage, experiments } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const systemPrompt = await (async () => {
|
||||
@@ -831,6 +831,7 @@ export class Cline {
|
||||
this.customInstructions,
|
||||
preferredLanguage,
|
||||
this.diffEnabled,
|
||||
experiments,
|
||||
)
|
||||
})()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { defaultModeSlug, modes } from "../../../shared/modes"
|
||||
import "../../../utils/path"
|
||||
import { addCustomInstructions } from "../sections/custom-instructions"
|
||||
import * as modesSection from "../sections/modes"
|
||||
import { EXPERIMENT_IDS } from "../../../shared/experiments"
|
||||
|
||||
// Mock the sections
|
||||
jest.mock("../sections/modes", () => ({
|
||||
@@ -121,6 +122,7 @@ const createMockMcpHub = (): McpHub =>
|
||||
|
||||
describe("SYSTEM_PROMPT", () => {
|
||||
let mockMcpHub: McpHub
|
||||
let experiments: Record<string, boolean>
|
||||
|
||||
beforeAll(() => {
|
||||
// Ensure fs mock is properly initialized
|
||||
@@ -140,6 +142,10 @@ describe("SYSTEM_PROMPT", () => {
|
||||
"/mock/mcp/path",
|
||||
]
|
||||
dirs.forEach((dir) => mockFs._mockDirectories.add(dir))
|
||||
experiments = {
|
||||
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
|
||||
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -164,6 +170,10 @@ describe("SYSTEM_PROMPT", () => {
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -179,7 +189,11 @@ describe("SYSTEM_PROMPT", () => {
|
||||
"1280x800", // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -197,7 +211,11 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -213,7 +231,11 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -229,7 +251,11 @@ describe("SYSTEM_PROMPT", () => {
|
||||
"900x600", // different viewport size
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -249,6 +275,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
true, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toContain("apply_diff")
|
||||
@@ -269,6 +296,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
false, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("apply_diff")
|
||||
@@ -289,6 +317,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("apply_diff")
|
||||
@@ -308,6 +337,8 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // customModes
|
||||
undefined, // globalCustomInstructions
|
||||
"Spanish", // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toContain("Language Preference:")
|
||||
@@ -337,6 +368,9 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // customModePrompts
|
||||
customModes, // customModes
|
||||
"Global instructions", // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
// Role definition should be at the top
|
||||
@@ -368,6 +402,10 @@ describe("SYSTEM_PROMPT", () => {
|
||||
defaultModeSlug,
|
||||
customModePrompts,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
)
|
||||
|
||||
// Role definition from promptComponent should be at the top
|
||||
@@ -394,18 +432,101 @@ describe("SYSTEM_PROMPT", () => {
|
||||
defaultModeSlug,
|
||||
customModePrompts,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
)
|
||||
|
||||
// Should use the default mode's role definition
|
||||
expect(prompt.indexOf(modes[0].roleDefinition)).toBeLessThan(prompt.indexOf("TOOL USE"))
|
||||
})
|
||||
|
||||
describe("experimental tools", () => {
|
||||
it("should disable experimental tools by default", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments, // experiments - undefined should disable all experimental tools
|
||||
)
|
||||
|
||||
// Verify experimental tools are not included in the prompt
|
||||
expect(prompt).not.toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
|
||||
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
|
||||
})
|
||||
|
||||
it("should enable experimental tools when explicitly enabled", async () => {
|
||||
const experiments = {
|
||||
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
|
||||
[EXPERIMENT_IDS.INSERT_BLOCK]: true,
|
||||
}
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
// Verify experimental tools are included in the prompt when enabled
|
||||
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
|
||||
expect(prompt).toContain(EXPERIMENT_IDS.INSERT_BLOCK)
|
||||
})
|
||||
|
||||
it("should selectively enable experimental tools", async () => {
|
||||
const experiments = {
|
||||
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
|
||||
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
|
||||
}
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
)
|
||||
|
||||
// Verify only enabled experimental tools are included
|
||||
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
|
||||
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
|
||||
describe("addCustomInstructions", () => {
|
||||
let experiments: Record<string, boolean>
|
||||
beforeAll(() => {
|
||||
// Ensure fs mock is properly initialized
|
||||
const mockFs = jest.requireMock("fs/promises")
|
||||
@@ -417,6 +538,11 @@ describe("addCustomInstructions", () => {
|
||||
}
|
||||
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`)
|
||||
})
|
||||
|
||||
experiments = {
|
||||
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
|
||||
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -434,6 +560,10 @@ describe("addCustomInstructions", () => {
|
||||
"architect", // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -450,6 +580,10 @@ describe("addCustomInstructions", () => {
|
||||
"ask", // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
|
||||
@@ -39,6 +39,7 @@ async function generatePrompt(
|
||||
globalCustomInstructions?: string,
|
||||
preferredLanguage?: string,
|
||||
diffEnabled?: boolean,
|
||||
experiments?: Record<string, boolean>,
|
||||
): Promise<string> {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
@@ -68,6 +69,7 @@ ${getToolDescriptionsForMode(
|
||||
browserViewportSize,
|
||||
mcpHub,
|
||||
customModeConfigs,
|
||||
experiments,
|
||||
)}
|
||||
|
||||
${getToolUseGuidelinesSection()}
|
||||
@@ -102,6 +104,7 @@ export const SYSTEM_PROMPT = async (
|
||||
globalCustomInstructions?: string,
|
||||
preferredLanguage?: string,
|
||||
diffEnabled?: boolean,
|
||||
experiments?: Record<string, boolean>,
|
||||
): Promise<string> => {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
@@ -135,5 +138,6 @@ export const SYSTEM_PROMPT = async (
|
||||
globalCustomInstructions,
|
||||
preferredLanguage,
|
||||
diffEnabled,
|
||||
experiments,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export function getToolDescriptionsForMode(
|
||||
browserViewportSize?: string,
|
||||
mcpHub?: McpHub,
|
||||
customModes?: ModeConfig[],
|
||||
experiments?: Record<string, boolean>,
|
||||
): string {
|
||||
const config = getModeConfig(mode, customModes)
|
||||
const args: ToolArgs = {
|
||||
@@ -64,7 +65,7 @@ export function getToolDescriptionsForMode(
|
||||
const toolGroup = TOOL_GROUPS[groupName]
|
||||
if (toolGroup) {
|
||||
toolGroup.forEach((tool) => {
|
||||
if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [])) {
|
||||
if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [], experiments ?? {})) {
|
||||
tools.add(tool)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -40,6 +40,12 @@ import { singleCompletionHandler } from "../../utils/single-completion-handler"
|
||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||
import { ConfigManager } from "../config/ConfigManager"
|
||||
import { CustomModesManager } from "../config/CustomModesManager"
|
||||
import {
|
||||
EXPERIMENT_IDS,
|
||||
experimentConfigs,
|
||||
experiments as Experiments,
|
||||
experimentDefault,
|
||||
} from "../../shared/experiments"
|
||||
import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
|
||||
|
||||
import { ACTION_NAMES } from "../CodeActionProvider"
|
||||
@@ -118,7 +124,7 @@ type GlobalStateKey =
|
||||
| "customModePrompts"
|
||||
| "customSupportPrompts"
|
||||
| "enhancementApiConfigId"
|
||||
| "experimentalDiffStrategy"
|
||||
| "experiments" // Map of experiment IDs to their enabled state
|
||||
| "autoApprovalEnabled"
|
||||
| "customModes" // Array of custom modes
|
||||
|
||||
@@ -339,7 +345,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy,
|
||||
experiments,
|
||||
} = await this.getState()
|
||||
|
||||
const modePrompt = customModePrompts?.[mode] as PromptComponent
|
||||
@@ -354,7 +360,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
task,
|
||||
images,
|
||||
undefined,
|
||||
experimentalDiffStrategy,
|
||||
Experiments.isEnabled(experiments, EXPERIMENT_IDS.DIFF_STRATEGY),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -367,7 +373,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy,
|
||||
experiments,
|
||||
} = await this.getState()
|
||||
|
||||
const modePrompt = customModePrompts?.[mode] as PromptComponent
|
||||
@@ -382,7 +388,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem,
|
||||
experimentalDiffStrategy,
|
||||
Experiments.isEnabled(experiments, EXPERIMENT_IDS.DIFF_STRATEGY),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1044,14 +1050,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
diffEnabled,
|
||||
mcpEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
experimentalDiffStrategy,
|
||||
experiments,
|
||||
} = await this.getState()
|
||||
|
||||
// Create diffStrategy based on current model and settings
|
||||
const diffStrategy = getDiffStrategy(
|
||||
apiConfiguration.apiModelId || apiConfiguration.openRouterModelId || "",
|
||||
fuzzyMatchThreshold,
|
||||
experimentalDiffStrategy,
|
||||
Experiments.isEnabled(experiments, EXPERIMENT_IDS.DIFF_STRATEGY),
|
||||
)
|
||||
const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
|
||||
@@ -1072,6 +1078,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
customInstructions,
|
||||
preferredLanguage,
|
||||
diffEnabled,
|
||||
experiments,
|
||||
)
|
||||
|
||||
await this.postMessageToWebview({
|
||||
@@ -1207,14 +1214,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
vscode.window.showErrorMessage("Failed to get list api configuration")
|
||||
}
|
||||
break
|
||||
case "experimentalDiffStrategy":
|
||||
await this.updateGlobalState("experimentalDiffStrategy", message.bool ?? false)
|
||||
// Update diffStrategy in current Cline instance if it exists
|
||||
if (this.cline) {
|
||||
await this.cline.updateDiffStrategy(message.bool ?? false)
|
||||
case "updateExperimental": {
|
||||
if (!message.values) {
|
||||
break
|
||||
}
|
||||
|
||||
const updatedExperiments = {
|
||||
...((await this.getGlobalState("experiments")) ?? experimentDefault),
|
||||
...message.values,
|
||||
}
|
||||
|
||||
await this.updateGlobalState("experiments", updatedExperiments)
|
||||
|
||||
// Update diffStrategy in current Cline instance if it exists
|
||||
if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY] !== undefined && this.cline) {
|
||||
await this.cline.updateDiffStrategy(
|
||||
Experiments.isEnabled(updatedExperiments, EXPERIMENT_IDS.DIFF_STRATEGY),
|
||||
)
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
}
|
||||
case "updateMcpTimeout":
|
||||
if (message.serverName && typeof message.timeout === "number") {
|
||||
try {
|
||||
@@ -1873,8 +1894,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
customModePrompts,
|
||||
customSupportPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
experiments,
|
||||
} = await this.getState()
|
||||
|
||||
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
|
||||
@@ -1914,9 +1935,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
customModePrompts: customModePrompts ?? {},
|
||||
customSupportPrompts: customSupportPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||
customModes: await this.customModesManager.getCustomModes(),
|
||||
experiments: experiments ?? experimentDefault,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2039,9 +2060,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
customModePrompts,
|
||||
customSupportPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
customModes,
|
||||
experiments,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||
@@ -2109,9 +2130,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("customModePrompts") as Promise<CustomModePrompts | undefined>,
|
||||
this.getGlobalState("customSupportPrompts") as Promise<CustomSupportPrompts | undefined>,
|
||||
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
||||
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
|
||||
this.customModesManager.getCustomModes(),
|
||||
this.getGlobalState("experiments") as Promise<Record<string, boolean> | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -2225,7 +2246,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
customModePrompts: customModePrompts ?? {},
|
||||
customSupportPrompts: customSupportPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
experiments: experiments ?? experimentDefault,
|
||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||
customModes,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessa
|
||||
import { setSoundEnabled } from "../../../utils/sound"
|
||||
import { defaultModeSlug, modes } from "../../../shared/modes"
|
||||
import { addCustomInstructions } from "../../prompts/sections/custom-instructions"
|
||||
import { experimentDefault, experiments } from "../../../shared/experiments"
|
||||
|
||||
// Mock custom-instructions module
|
||||
const mockAddCustomInstructions = jest.fn()
|
||||
@@ -320,6 +321,7 @@ describe("ClineProvider", () => {
|
||||
requestDelaySeconds: 5,
|
||||
mode: defaultModeSlug,
|
||||
customModes: [],
|
||||
experiments: experimentDefault,
|
||||
}
|
||||
|
||||
const message: ExtensionMessage = {
|
||||
@@ -617,6 +619,7 @@ describe("ClineProvider", () => {
|
||||
mode: "code",
|
||||
diffEnabled: true,
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
||||
// Reset Cline mock
|
||||
@@ -636,7 +639,7 @@ describe("ClineProvider", () => {
|
||||
"Test task",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
)
|
||||
})
|
||||
test("handles mode-specific custom instructions updates", async () => {
|
||||
@@ -887,6 +890,7 @@ describe("ClineProvider", () => {
|
||||
},
|
||||
mcpEnabled: true,
|
||||
mode: "code" as const,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
||||
const handler1 = getMessageHandler()
|
||||
@@ -918,6 +922,7 @@ describe("ClineProvider", () => {
|
||||
},
|
||||
mcpEnabled: false,
|
||||
mode: "code" as const,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
||||
const handler2 = getMessageHandler()
|
||||
@@ -985,6 +990,7 @@ describe("ClineProvider", () => {
|
||||
experimentalDiffStrategy: true,
|
||||
diffEnabled: true,
|
||||
fuzzyMatchThreshold: 0.8,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
||||
// Mock SYSTEM_PROMPT to verify diffStrategy and diffEnabled are passed
|
||||
@@ -1012,6 +1018,7 @@ describe("ClineProvider", () => {
|
||||
undefined, // effectiveInstructions
|
||||
undefined, // preferredLanguage
|
||||
true, // diffEnabled
|
||||
experimentDefault,
|
||||
)
|
||||
|
||||
// Run the test again to verify it's consistent
|
||||
@@ -1034,6 +1041,7 @@ describe("ClineProvider", () => {
|
||||
experimentalDiffStrategy: true,
|
||||
diffEnabled: false,
|
||||
fuzzyMatchThreshold: 0.8,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
||||
// Mock SYSTEM_PROMPT to verify diffEnabled is passed as false
|
||||
@@ -1061,6 +1069,7 @@ describe("ClineProvider", () => {
|
||||
undefined, // effectiveInstructions
|
||||
undefined, // preferredLanguage
|
||||
false, // diffEnabled
|
||||
experimentDefault,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1077,6 +1086,7 @@ describe("ClineProvider", () => {
|
||||
mode: "architect",
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: "900x600",
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
||||
// Mock SYSTEM_PROMPT to call addCustomInstructions
|
||||
|
||||
@@ -108,7 +108,7 @@ export interface ExtensionState {
|
||||
mode: Mode
|
||||
modeApiConfigs?: Record<Mode, string>
|
||||
enhancementApiConfigId?: string
|
||||
experimentalDiffStrategy?: boolean
|
||||
experiments: Record<string, boolean> // Map of experiment IDs to their enabled state
|
||||
autoApprovalEnabled?: boolean
|
||||
customModes: ModeConfig[]
|
||||
toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)
|
||||
|
||||
@@ -75,7 +75,7 @@ export interface WebviewMessage {
|
||||
| "getSystemPrompt"
|
||||
| "systemPrompt"
|
||||
| "enhancementApiConfigId"
|
||||
| "experimentalDiffStrategy"
|
||||
| "updateExperimental"
|
||||
| "autoApprovalEnabled"
|
||||
| "updateCustomMode"
|
||||
| "deleteCustomMode"
|
||||
|
||||
@@ -14,6 +14,12 @@ describe("isToolAllowedForMode", () => {
|
||||
roleDefinition: "You are a CSS editor",
|
||||
groups: ["read", ["edit", { fileRegex: "\\.css$" }], "browser"],
|
||||
},
|
||||
{
|
||||
slug: "test-exp-mode",
|
||||
name: "Test Exp Mode",
|
||||
roleDefinition: "You are an experimental tester",
|
||||
groups: ["read", "edit", "browser"],
|
||||
},
|
||||
]
|
||||
|
||||
it("allows always available tools", () => {
|
||||
@@ -240,6 +246,87 @@ describe("isToolAllowedForMode", () => {
|
||||
|
||||
expect(isToolAllowedForMode("write_to_file", "markdown-editor", customModes, toolRequirements)).toBe(false)
|
||||
})
|
||||
|
||||
describe("experimental tools", () => {
|
||||
it("disables tools when experiment is disabled", () => {
|
||||
const experiments = {
|
||||
search_and_replace: false,
|
||||
insert_code_block: false,
|
||||
}
|
||||
|
||||
expect(
|
||||
isToolAllowedForMode(
|
||||
"search_and_replace",
|
||||
"test-exp-mode",
|
||||
customModes,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
),
|
||||
).toBe(false)
|
||||
|
||||
expect(
|
||||
isToolAllowedForMode(
|
||||
"insert_code_block",
|
||||
"test-exp-mode",
|
||||
customModes,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("allows tools when experiment is enabled", () => {
|
||||
const experiments = {
|
||||
search_and_replace: true,
|
||||
insert_code_block: true,
|
||||
}
|
||||
|
||||
expect(
|
||||
isToolAllowedForMode(
|
||||
"search_and_replace",
|
||||
"test-exp-mode",
|
||||
customModes,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
isToolAllowedForMode(
|
||||
"insert_code_block",
|
||||
"test-exp-mode",
|
||||
customModes,
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("allows non-experimental tools when experiments are disabled", () => {
|
||||
const experiments = {
|
||||
search_and_replace: false,
|
||||
insert_code_block: false,
|
||||
}
|
||||
|
||||
expect(
|
||||
isToolAllowedForMode("read_file", "markdown-editor", customModes, undefined, undefined, experiments),
|
||||
).toBe(true)
|
||||
expect(
|
||||
isToolAllowedForMode(
|
||||
"write_to_file",
|
||||
"markdown-editor",
|
||||
customModes,
|
||||
undefined,
|
||||
{ path: "test.md" },
|
||||
experiments,
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("FileRestrictionError", () => {
|
||||
|
||||
63
src/shared/experiments.ts
Normal file
63
src/shared/experiments.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export interface ExperimentConfig {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export const EXPERIMENT_IDS = {
|
||||
DIFF_STRATEGY: "experimentalDiffStrategy",
|
||||
SEARCH_AND_REPLACE: "search_and_replace",
|
||||
INSERT_BLOCK: "insert_code_block",
|
||||
} as const
|
||||
|
||||
export type ExperimentId = keyof typeof EXPERIMENT_IDS
|
||||
|
||||
export const experimentConfigsMap: Record<ExperimentId, ExperimentConfig> = {
|
||||
DIFF_STRATEGY: {
|
||||
id: EXPERIMENT_IDS.DIFF_STRATEGY,
|
||||
name: "Use experimental unified diff strategy",
|
||||
description:
|
||||
"Enable the experimental unified diff strategy. This strategy might reduce the number of retries caused by model errors but may cause unexpected behavior or incorrect edits. Only enable if you understand the risks and are willing to carefully review all changes.",
|
||||
enabled: false,
|
||||
},
|
||||
SEARCH_AND_REPLACE: {
|
||||
id: EXPERIMENT_IDS.SEARCH_AND_REPLACE,
|
||||
name: "Use experimental search and replace tool",
|
||||
description:
|
||||
"Enable the experimental Search and Replace tool. This tool allows Roo to search and replace term. Can be run multiple search and replace in sequence at once request.",
|
||||
enabled: false,
|
||||
},
|
||||
INSERT_BLOCK: {
|
||||
id: EXPERIMENT_IDS.INSERT_BLOCK,
|
||||
name: "Use experimental insert block tool",
|
||||
|
||||
description:
|
||||
"Enable the experimental insert block tool. This tool allows Roo to insert code blocks into files. Can be insert multiple blocks at once.",
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Keep the array version for backward compatibility
|
||||
export const experimentConfigs = Object.values(experimentConfigsMap)
|
||||
export const experimentDefault = Object.fromEntries(
|
||||
Object.entries(experimentConfigsMap).map(([_, config]) => [config.id, config.enabled]),
|
||||
)
|
||||
|
||||
export const experiments = {
|
||||
get: (id: ExperimentId): ExperimentConfig | undefined => {
|
||||
return experimentConfigsMap[id]
|
||||
},
|
||||
isEnabled: (experimentsConfig: Record<string, boolean>, id: string): boolean => {
|
||||
return experimentsConfig[id] ?? experimentDefault[id]
|
||||
},
|
||||
} as const
|
||||
|
||||
// Expose experiment details for UI - pre-compute from map for better performance
|
||||
export const experimentLabels = Object.fromEntries(
|
||||
Object.values(experimentConfigsMap).map((config) => [config.id, config.name]),
|
||||
) as Record<string, string>
|
||||
|
||||
export const experimentDescriptions = Object.fromEntries(
|
||||
Object.values(experimentConfigsMap).map((config) => [config.id, config.description]),
|
||||
) as Record<string, string>
|
||||
@@ -160,12 +160,19 @@ export function isToolAllowedForMode(
|
||||
customModes: ModeConfig[],
|
||||
toolRequirements?: Record<string, boolean>,
|
||||
toolParams?: Record<string, any>, // All tool parameters
|
||||
experiments?: Record<string, boolean>,
|
||||
): boolean {
|
||||
// Always allow these tools
|
||||
if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (experiments && tool in experiments) {
|
||||
if (!experiments[tool]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check tool requirements if any exist
|
||||
if (toolRequirements && tool in toolRequirements) {
|
||||
if (!toolRequirements[tool]) {
|
||||
|
||||
37
webview-ui/src/components/settings/ExperimentalFeature.tsx
Normal file
37
webview-ui/src/components/settings/ExperimentalFeature.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
interface ExperimentalFeatureProps {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const ExperimentalFeature = ({ id, name, description, enabled, onChange }: ExperimentalFeatureProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
paddingLeft: 10,
|
||||
borderLeft: "2px solid var(--vscode-button-background)",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
|
||||
<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>{name}</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginBottom: 15,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExperimentalFeature
|
||||
@@ -4,6 +4,8 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import ApiOptions from "./ApiOptions"
|
||||
import ExperimentalFeature from "./ExperimentalFeature"
|
||||
import { experimentConfigs, EXPERIMENT_IDS, experimentConfigsMap } from "../../../../src/shared/experiments"
|
||||
import ApiConfigManager from "./ApiConfigManager"
|
||||
|
||||
type SettingsViewProps = {
|
||||
@@ -51,8 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
setRequestDelaySeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
experimentalDiffStrategy,
|
||||
setExperimentalDiffStrategy,
|
||||
experiments,
|
||||
setExperimentEnabled,
|
||||
alwaysAllowModeSwitch,
|
||||
setAlwaysAllowModeSwitch,
|
||||
} = useExtensionState()
|
||||
@@ -94,7 +96,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
text: currentApiConfigName,
|
||||
apiConfiguration,
|
||||
})
|
||||
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
|
||||
|
||||
vscode.postMessage({
|
||||
type: "updateExperimental",
|
||||
values: experiments,
|
||||
})
|
||||
|
||||
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
|
||||
onDone()
|
||||
}
|
||||
@@ -583,7 +590,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
setDiffEnabled(e.target.checked)
|
||||
if (!e.target.checked) {
|
||||
// Reset experimental strategy when diffs are disabled
|
||||
setExperimentalDiffStrategy(false)
|
||||
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
|
||||
}
|
||||
}}>
|
||||
<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
|
||||
@@ -599,35 +606,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</p>
|
||||
|
||||
{diffEnabled && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
paddingLeft: 10,
|
||||
borderLeft: "2px solid var(--vscode-button-background)",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
|
||||
<VSCodeCheckbox
|
||||
checked={experimentalDiffStrategy}
|
||||
onChange={(e: any) => setExperimentalDiffStrategy(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>
|
||||
Use experimental unified diff strategy
|
||||
</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginBottom: 15,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Enable the experimental unified diff strategy. This strategy might reduce the number
|
||||
of retries caused by model errors but may cause unexpected behavior or incorrect
|
||||
edits. Only enable if you understand the risks and are willing to carefully review
|
||||
all changes.
|
||||
</p>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<ExperimentalFeature
|
||||
key={EXPERIMENT_IDS.DIFF_STRATEGY}
|
||||
{...experimentConfigsMap.DIFF_STRATEGY}
|
||||
enabled={experiments[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false}
|
||||
onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px", marginTop: "15px" }}>
|
||||
<span style={{ fontWeight: "500", minWidth: "100px" }}>Match precision</span>
|
||||
<input
|
||||
type="range"
|
||||
@@ -660,6 +646,16 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{experimentConfigs
|
||||
.filter((config) => config.id !== EXPERIMENT_IDS.DIFF_STRATEGY)
|
||||
.map((config) => (
|
||||
<ExperimentalFeature
|
||||
key={config.id}
|
||||
{...config}
|
||||
enabled={experiments[config.id] ?? false}
|
||||
onChange={(enabled) => setExperimentEnabled(config.id, enabled)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { McpServer } from "../../../src/shared/mcp"
|
||||
import { checkExistKey } from "../../../src/shared/checkExistApiConfig"
|
||||
import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes"
|
||||
import { CustomSupportPrompts } from "../../../src/shared/support-prompt"
|
||||
import { EXPERIMENT_IDS, experimentDefault } from "../../../src/shared/experiments"
|
||||
|
||||
export interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
@@ -63,9 +64,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
setCustomSupportPrompts: (value: CustomSupportPrompts) => void
|
||||
enhancementApiConfigId?: string
|
||||
setEnhancementApiConfigId: (value: string) => void
|
||||
experimentalDiffStrategy: boolean
|
||||
setExperimentalDiffStrategy: (value: boolean) => void
|
||||
autoApprovalEnabled?: boolean
|
||||
experiments: Record<string, boolean>
|
||||
setExperimentEnabled: (id: string, enabled: boolean) => void
|
||||
setAutoApprovalEnabled: (value: boolean) => void
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
customModes: ModeConfig[]
|
||||
@@ -98,8 +98,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
mode: defaultModeSlug,
|
||||
customModePrompts: defaultPrompts,
|
||||
customSupportPrompts: {},
|
||||
experiments: experimentDefault,
|
||||
enhancementApiConfigId: "",
|
||||
experimentalDiffStrategy: false,
|
||||
autoApprovalEnabled: false,
|
||||
customModes: [],
|
||||
})
|
||||
@@ -242,7 +242,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
writeDelayMs: state.writeDelayMs,
|
||||
screenshotQuality: state.screenshotQuality,
|
||||
experimentalDiffStrategy: state.experimentalDiffStrategy ?? false,
|
||||
setExperimentEnabled: (id, enabled) =>
|
||||
setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
@@ -279,8 +280,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
|
||||
setEnhancementApiConfigId: (value) =>
|
||||
setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
||||
setExperimentalDiffStrategy: (value) =>
|
||||
setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })),
|
||||
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
||||
handleInputChange,
|
||||
setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
|
||||
|
||||
Reference in New Issue
Block a user