Skip to content

Commit c345dd0

Browse files
committed
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
1 parent 0a0ff43 commit c345dd0

File tree

14 files changed

+428
-68
lines changed

14 files changed

+428
-68
lines changed

src/core/Cline.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { OpenRouterHandler } from "../api/providers/openrouter"
6161
import { McpHub } from "../services/mcp/McpHub"
6262
import crypto from "crypto"
6363
import { insertGroups } from "./diff/insert-groups"
64+
import { EXPERIMENT_IDS } from "../shared/experiments"
6465

6566
const cwd =
6667
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 {
151152
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
152153
// If not provided, get from current state
153154
if (experimentalDiffStrategy === undefined) {
154-
const { experimentalDiffStrategy: stateExperimentalDiffStrategy } =
155-
(await this.providerRef.deref()?.getState()) ?? {}
156-
experimentalDiffStrategy = stateExperimentalDiffStrategy ?? false
155+
const { experiments: stateExperimental } = (await this.providerRef.deref()?.getState()) ?? {}
156+
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
157157
}
158158
this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
159159
}
@@ -810,7 +810,7 @@ export class Cline {
810810
})
811811
}
812812

813-
const { browserViewportSize, mode, customModePrompts, preferredLanguage } =
813+
const { browserViewportSize, mode, customModePrompts, preferredLanguage, experiments } =
814814
(await this.providerRef.deref()?.getState()) ?? {}
815815
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
816816
const systemPrompt = await (async () => {
@@ -831,6 +831,7 @@ export class Cline {
831831
this.customInstructions,
832832
preferredLanguage,
833833
this.diffEnabled,
834+
experiments,
834835
)
835836
})()
836837

src/core/prompts/__tests__/system.test.ts

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { defaultModeSlug, modes } from "../../../shared/modes"
1111
import "../../../utils/path"
1212
import { addCustomInstructions } from "../sections/custom-instructions"
1313
import * as modesSection from "../sections/modes"
14+
import { EXPERIMENT_IDS } from "../../../shared/experiments"
1415

1516
// Mock the sections
1617
jest.mock("../sections/modes", () => ({
@@ -121,6 +122,7 @@ const createMockMcpHub = (): McpHub =>
121122

122123
describe("SYSTEM_PROMPT", () => {
123124
let mockMcpHub: McpHub
125+
let experiments: Record<string, boolean>
124126

125127
beforeAll(() => {
126128
// Ensure fs mock is properly initialized
@@ -140,6 +142,10 @@ describe("SYSTEM_PROMPT", () => {
140142
"/mock/mcp/path",
141143
]
142144
dirs.forEach((dir) => mockFs._mockDirectories.add(dir))
145+
experiments = {
146+
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
147+
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
148+
}
143149
})
144150

145151
beforeEach(() => {
@@ -164,6 +170,10 @@ describe("SYSTEM_PROMPT", () => {
164170
defaultModeSlug, // mode
165171
undefined, // customModePrompts
166172
undefined, // customModes
173+
undefined, // globalCustomInstructions
174+
undefined, // preferredLanguage
175+
undefined, // diffEnabled
176+
experiments,
167177
)
168178

169179
expect(prompt).toMatchSnapshot()
@@ -179,7 +189,11 @@ describe("SYSTEM_PROMPT", () => {
179189
"1280x800", // browserViewportSize
180190
defaultModeSlug, // mode
181191
undefined, // customModePrompts
182-
undefined, // customModes
192+
undefined, // customModes,
193+
undefined, // globalCustomInstructions
194+
undefined, // preferredLanguage
195+
undefined, // diffEnabled
196+
experiments,
183197
)
184198

185199
expect(prompt).toMatchSnapshot()
@@ -197,7 +211,11 @@ describe("SYSTEM_PROMPT", () => {
197211
undefined, // browserViewportSize
198212
defaultModeSlug, // mode
199213
undefined, // customModePrompts
200-
undefined, // customModes
214+
undefined, // customModes,
215+
undefined, // globalCustomInstructions
216+
undefined, // preferredLanguage
217+
undefined, // diffEnabled
218+
experiments,
201219
)
202220

203221
expect(prompt).toMatchSnapshot()
@@ -213,7 +231,11 @@ describe("SYSTEM_PROMPT", () => {
213231
undefined, // browserViewportSize
214232
defaultModeSlug, // mode
215233
undefined, // customModePrompts
216-
undefined, // customModes
234+
undefined, // customModes,
235+
undefined, // globalCustomInstructions
236+
undefined, // preferredLanguage
237+
undefined, // diffEnabled
238+
experiments,
217239
)
218240

219241
expect(prompt).toMatchSnapshot()
@@ -229,7 +251,11 @@ describe("SYSTEM_PROMPT", () => {
229251
"900x600", // different viewport size
230252
defaultModeSlug, // mode
231253
undefined, // customModePrompts
232-
undefined, // customModes
254+
undefined, // customModes,
255+
undefined, // globalCustomInstructions
256+
undefined, // preferredLanguage
257+
undefined, // diffEnabled
258+
experiments,
233259
)
234260

235261
expect(prompt).toMatchSnapshot()
@@ -249,6 +275,7 @@ describe("SYSTEM_PROMPT", () => {
249275
undefined, // globalCustomInstructions
250276
undefined, // preferredLanguage
251277
true, // diffEnabled
278+
experiments,
252279
)
253280

254281
expect(prompt).toContain("apply_diff")
@@ -269,6 +296,7 @@ describe("SYSTEM_PROMPT", () => {
269296
undefined, // globalCustomInstructions
270297
undefined, // preferredLanguage
271298
false, // diffEnabled
299+
experiments,
272300
)
273301

274302
expect(prompt).not.toContain("apply_diff")
@@ -289,6 +317,7 @@ describe("SYSTEM_PROMPT", () => {
289317
undefined, // globalCustomInstructions
290318
undefined, // preferredLanguage
291319
undefined, // diffEnabled
320+
experiments,
292321
)
293322

294323
expect(prompt).not.toContain("apply_diff")
@@ -308,6 +337,8 @@ describe("SYSTEM_PROMPT", () => {
308337
undefined, // customModes
309338
undefined, // globalCustomInstructions
310339
"Spanish", // preferredLanguage
340+
undefined, // diffEnabled
341+
experiments,
311342
)
312343

313344
expect(prompt).toContain("Language Preference:")
@@ -337,6 +368,9 @@ describe("SYSTEM_PROMPT", () => {
337368
undefined, // customModePrompts
338369
customModes, // customModes
339370
"Global instructions", // globalCustomInstructions
371+
undefined, // preferredLanguage
372+
undefined, // diffEnabled
373+
experiments,
340374
)
341375

342376
// Role definition should be at the top
@@ -368,6 +402,10 @@ describe("SYSTEM_PROMPT", () => {
368402
defaultModeSlug,
369403
customModePrompts,
370404
undefined,
405+
undefined,
406+
undefined,
407+
undefined,
408+
experiments,
371409
)
372410

373411
// Role definition from promptComponent should be at the top
@@ -394,18 +432,101 @@ describe("SYSTEM_PROMPT", () => {
394432
defaultModeSlug,
395433
customModePrompts,
396434
undefined,
435+
undefined,
436+
undefined,
437+
undefined,
438+
experiments,
397439
)
398440

399441
// Should use the default mode's role definition
400442
expect(prompt.indexOf(modes[0].roleDefinition)).toBeLessThan(prompt.indexOf("TOOL USE"))
401443
})
402444

445+
describe("experimental tools", () => {
446+
it("should disable experimental tools by default", async () => {
447+
const prompt = await SYSTEM_PROMPT(
448+
mockContext,
449+
"/test/path",
450+
false, // supportsComputerUse
451+
undefined, // mcpHub
452+
undefined, // diffStrategy
453+
undefined, // browserViewportSize
454+
defaultModeSlug, // mode
455+
undefined, // customModePrompts
456+
undefined, // customModes
457+
undefined, // globalCustomInstructions
458+
undefined, // preferredLanguage
459+
undefined, // diffEnabled
460+
experiments, // experiments - undefined should disable all experimental tools
461+
)
462+
463+
// Verify experimental tools are not included in the prompt
464+
expect(prompt).not.toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
465+
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
466+
})
467+
468+
it("should enable experimental tools when explicitly enabled", async () => {
469+
const experiments = {
470+
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
471+
[EXPERIMENT_IDS.INSERT_BLOCK]: true,
472+
}
473+
474+
const prompt = await SYSTEM_PROMPT(
475+
mockContext,
476+
"/test/path",
477+
false, // supportsComputerUse
478+
undefined, // mcpHub
479+
undefined, // diffStrategy
480+
undefined, // browserViewportSize
481+
defaultModeSlug, // mode
482+
undefined, // customModePrompts
483+
undefined, // customModes
484+
undefined, // globalCustomInstructions
485+
undefined, // preferredLanguage
486+
undefined, // diffEnabled
487+
experiments,
488+
)
489+
490+
// Verify experimental tools are included in the prompt when enabled
491+
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
492+
expect(prompt).toContain(EXPERIMENT_IDS.INSERT_BLOCK)
493+
})
494+
495+
it("should selectively enable experimental tools", async () => {
496+
const experiments = {
497+
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
498+
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
499+
}
500+
501+
const prompt = await SYSTEM_PROMPT(
502+
mockContext,
503+
"/test/path",
504+
false, // supportsComputerUse
505+
undefined, // mcpHub
506+
undefined, // diffStrategy
507+
undefined, // browserViewportSize
508+
defaultModeSlug, // mode
509+
undefined, // customModePrompts
510+
undefined, // customModes
511+
undefined, // globalCustomInstructions
512+
undefined, // preferredLanguage
513+
undefined, // diffEnabled
514+
experiments,
515+
)
516+
517+
// Verify only enabled experimental tools are included
518+
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
519+
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
520+
})
521+
})
522+
403523
afterAll(() => {
404524
jest.restoreAllMocks()
405525
})
406526
})
407527

408528
describe("addCustomInstructions", () => {
529+
let experiments: Record<string, boolean>
409530
beforeAll(() => {
410531
// Ensure fs mock is properly initialized
411532
const mockFs = jest.requireMock("fs/promises")
@@ -417,6 +538,11 @@ describe("addCustomInstructions", () => {
417538
}
418539
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`)
419540
})
541+
542+
experiments = {
543+
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
544+
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
545+
}
420546
})
421547

422548
beforeEach(() => {
@@ -434,6 +560,10 @@ describe("addCustomInstructions", () => {
434560
"architect", // mode
435561
undefined, // customModePrompts
436562
undefined, // customModes
563+
undefined,
564+
undefined,
565+
undefined,
566+
experiments,
437567
)
438568

439569
expect(prompt).toMatchSnapshot()
@@ -450,6 +580,10 @@ describe("addCustomInstructions", () => {
450580
"ask", // mode
451581
undefined, // customModePrompts
452582
undefined, // customModes
583+
undefined,
584+
undefined,
585+
undefined,
586+
experiments,
453587
)
454588

455589
expect(prompt).toMatchSnapshot()

src/core/prompts/system.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ async function generatePrompt(
3939
globalCustomInstructions?: string,
4040
preferredLanguage?: string,
4141
diffEnabled?: boolean,
42+
experiments?: Record<string, boolean>,
4243
): Promise<string> {
4344
if (!context) {
4445
throw new Error("Extension context is required for generating system prompt")
@@ -68,6 +69,7 @@ ${getToolDescriptionsForMode(
6869
browserViewportSize,
6970
mcpHub,
7071
customModeConfigs,
72+
experiments,
7173
)}
7274
7375
${getToolUseGuidelinesSection()}
@@ -102,6 +104,7 @@ export const SYSTEM_PROMPT = async (
102104
globalCustomInstructions?: string,
103105
preferredLanguage?: string,
104106
diffEnabled?: boolean,
107+
experiments?: Record<string, boolean>,
105108
): Promise<string> => {
106109
if (!context) {
107110
throw new Error("Extension context is required for generating system prompt")
@@ -135,5 +138,6 @@ export const SYSTEM_PROMPT = async (
135138
globalCustomInstructions,
136139
preferredLanguage,
137140
diffEnabled,
141+
experiments,
138142
)
139143
}

src/core/prompts/tools/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function getToolDescriptionsForMode(
4646
browserViewportSize?: string,
4747
mcpHub?: McpHub,
4848
customModes?: ModeConfig[],
49+
experiments?: Record<string, boolean>,
4950
): string {
5051
const config = getModeConfig(mode, customModes)
5152
const args: ToolArgs = {
@@ -64,7 +65,7 @@ export function getToolDescriptionsForMode(
6465
const toolGroup = TOOL_GROUPS[groupName]
6566
if (toolGroup) {
6667
toolGroup.forEach((tool) => {
67-
if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [])) {
68+
if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [], experiments ?? {})) {
6869
tools.add(tool)
6970
}
7071
})

0 commit comments

Comments
 (0)