Skip to content

Commit 2b59d0d

Browse files
author
chenjiajun
committed
feat: add enabledForSwitching config for mode switching control
- Add enabledForSwitching config to control which modes can be switched to by LLMs - Update system prompts to exclude non-switchable modes from switch_mode tool - Update UI components in PromptsView to include enabledForSwitching checkbox - Update localization files for all supported languages - Update tests to verify behavior with non-switchable modes
1 parent 0374436 commit 2b59d0d

File tree

25 files changed

+317
-46
lines changed

25 files changed

+317
-46
lines changed

src/core/config/__tests__/CustomModesSettings.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,49 @@ describe("CustomModesSettings", () => {
160160
{
161161
...validMode,
162162
customInstructions: "Optional instructions",
163+
enabledForSwitching: false,
163164
},
164165
],
165166
}
166167

167168
// TypeScript compilation will fail if the type is incorrect
168169
expect(settings.customModes[0].customInstructions).toBeDefined()
170+
expect(settings.customModes[0].enabledForSwitching).toBeDefined()
171+
})
172+
173+
test("enabledForSwitching can be set to false", () => {
174+
const settings = {
175+
customModes: [
176+
{
177+
...validMode,
178+
enabledForSwitching: false,
179+
},
180+
],
181+
}
182+
183+
expect(() => {
184+
customModesSettingsSchema.parse(settings)
185+
}).not.toThrow()
186+
expect(settings.customModes[0].enabledForSwitching).toBe(false)
187+
})
188+
189+
test("enabledForSwitching can be undefined (default to enabled)", () => {
190+
const settings = {
191+
customModes: [
192+
{
193+
...validMode,
194+
// enabledForSwitching is not specified
195+
},
196+
],
197+
}
198+
199+
expect(() => {
200+
customModesSettingsSchema.parse(settings)
201+
}).not.toThrow()
202+
// Since validMode doesn't have enabledForSwitching, it should be undefined
203+
// which means it's enabled by default
204+
const parsedSettings = customModesSettingsSchema.parse(settings)
205+
expect(parsedSettings.customModes[0].enabledForSwitching).toBeUndefined()
169206
})
170207
})
171208
})

src/core/prompts/sections/modes.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as path from "path"
22
import * as vscode from "vscode"
33
import { promises as fs } from "fs"
44
import { ModeConfig, getAllModesWithPrompts } from "../../../shared/modes"
5-
import { GlobalFileNames } from "../../../shared/globalFileNames"
65

76
export async function getModesSection(context: vscode.ExtensionContext): Promise<string> {
87
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
@@ -11,12 +10,25 @@ export async function getModesSection(context: vscode.ExtensionContext): Promise
1110
// Get all modes with their overrides from extension state
1211
const allModes = await getAllModesWithPrompts(context)
1312

14-
let modesContent = `====
13+
// Filter out modes that are disabled for switching
14+
const enabledModes = allModes.filter((mode) => {
15+
// If enabledForSwitching is explicitly set to false, filter out the mode
16+
// Otherwise, include the mode (undefined or true means enabled)
17+
return mode.enabledForSwitching !== false
18+
})
19+
20+
let modesContent = enabledModes.length
21+
? `====
1522
1623
MODES
1724
1825
- These are the currently available modes:
1926
${allModes.map((mode: ModeConfig) => ` * "${mode.name}" mode (${mode.slug}) - ${mode.roleDefinition.split(".")[0]}`).join("\n")}`
27+
: `====
28+
29+
MODES
30+
31+
`
2032

2133
modesContent += `
2234
If the user asks you to create or edit a new mode for this project, you should read the instructions by using the fetch_instructions tool, like this:

src/core/prompts/system.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ async function generatePrompt(
6666
6767
${getSharedToolUseSection()}
6868
69-
${getToolDescriptionsForMode(
69+
${await getToolDescriptionsForMode(
7070
mode,
7171
cwd,
7272
supportsComputerUse,
73+
context,
7374
effectiveDiffStrategy,
7475
browserViewportSize,
7576
mcpHub,

src/core/prompts/tools/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@ import { getSwitchModeDescription } from "./switch-mode"
1616
import { getNewTaskDescription } from "./new-task"
1717
import { DiffStrategy } from "../../diff/DiffStrategy"
1818
import { McpHub } from "../../../services/mcp/McpHub"
19-
import { Mode, ModeConfig, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
19+
import {
20+
Mode,
21+
ModeConfig,
22+
getModeConfig,
23+
isToolAllowedForMode,
24+
getGroupName,
25+
hasAnySwitchableModes,
26+
} from "../../../shared/modes"
2027
import { ToolName, TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tool-groups"
2128
import { ToolArgs } from "./types"
29+
import { ExtensionContext } from "vscode"
2230

2331
// Map of tool names to their description functions
2432
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
@@ -42,16 +50,17 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
4250
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
4351
}
4452

45-
export function getToolDescriptionsForMode(
53+
export async function getToolDescriptionsForMode(
4654
mode: Mode,
4755
cwd: string,
4856
supportsComputerUse: boolean,
57+
context: ExtensionContext,
4958
diffStrategy?: DiffStrategy,
5059
browserViewportSize?: string,
5160
mcpHub?: McpHub,
5261
customModes?: ModeConfig[],
5362
experiments?: Record<string, boolean>,
54-
): string {
63+
): Promise<string> {
5564
const config = getModeConfig(mode, customModes)
5665
const args: ToolArgs = {
5766
cwd,
@@ -76,8 +85,14 @@ export function getToolDescriptionsForMode(
7685
}
7786
})
7887

79-
// Add always available tools
80-
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
88+
// Add always available tools except switch_mode if no modes are switchable
89+
ALWAYS_AVAILABLE_TOOLS.forEach(async (tool) => {
90+
// Skip adding switch_mode tool if no modes are switchable
91+
if (tool === "switch_mode" && !(await hasAnySwitchableModes(context))) {
92+
return
93+
}
94+
tools.add(tool)
95+
})
8196

8297
// Map tool descriptions for allowed tools
8398
const descriptions = Array.from(tools).map((toolName) => {

src/core/tools/switchModeTool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export async function switchModeTool(
3535

3636
// Verify the mode exists
3737
const targetMode = getModeBySlug(mode_slug, (await cline.providerRef.deref()?.getState())?.customModes)
38-
if (!targetMode) {
38+
if (!targetMode || targetMode.enabledForSwitching === false) {
3939
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode_slug}`))
4040
return
4141
}

src/exports/roo-code.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ type GlobalSettings = {
334334
]
335335
)[]
336336
source?: ("global" | "project") | undefined
337+
enabledForSwitching?: boolean | undefined
337338
}[]
338339
| undefined
339340
customModePrompts?:
@@ -342,6 +343,7 @@ type GlobalSettings = {
342343
| {
343344
roleDefinition?: string | undefined
344345
customInstructions?: string | undefined
346+
enabledForSwitching?: boolean | undefined
345347
}
346348
| undefined
347349
}

src/exports/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ type GlobalSettings = {
337337
]
338338
)[]
339339
source?: ("global" | "project") | undefined
340+
enabledForSwitching?: boolean | undefined
340341
}[]
341342
| undefined
342343
customModePrompts?:
@@ -345,6 +346,7 @@ type GlobalSettings = {
345346
| {
346347
roleDefinition?: string | undefined
347348
customInstructions?: string | undefined
349+
enabledForSwitching?: boolean | undefined
348350
}
349351
| undefined
350352
}

src/schemas/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export const modeConfigSchema = z.object({
215215
customInstructions: z.string().optional(),
216216
groups: groupEntryArraySchema,
217217
source: z.enum(["global", "project"]).optional(),
218+
enabledForSwitching: z.boolean().optional(),
218219
})
219220

220221
export type ModeConfig = z.infer<typeof modeConfigSchema>
@@ -252,6 +253,7 @@ export type CustomModesSettings = z.infer<typeof customModesSettingsSchema>
252253
export const promptComponentSchema = z.object({
253254
roleDefinition: z.string().optional(),
254255
customInstructions: z.string().optional(),
256+
enabledForSwitching: z.boolean().optional(),
255257
})
256258

257259
export type PromptComponent = z.infer<typeof promptComponentSchema>

src/shared/modes.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export async function getAllModesWithPrompts(context: vscode.ExtensionContext):
241241
...mode,
242242
roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition,
243243
customInstructions: customModePrompts[mode.slug]?.customInstructions ?? mode.customInstructions,
244+
enabledForSwitching: customModePrompts[mode.slug]?.enabledForSwitching,
244245
}))
245246
}
246247

@@ -281,6 +282,7 @@ export async function getFullModeDetails(
281282
...baseMode,
282283
roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition,
283284
customInstructions: fullCustomInstructions,
285+
enabledForSwitching: promptComponent?.enabledForSwitching,
284286
}
285287
}
286288

@@ -303,3 +305,21 @@ export function getCustomInstructions(modeSlug: string, customModes?: ModeConfig
303305
}
304306
return mode.customInstructions ?? ""
305307
}
308+
309+
export function getEnabledForSwitching(modeSlug: string, customModes?: ModeConfig[]): boolean {
310+
const mode = getModeBySlug(modeSlug, customModes)
311+
if (!mode) {
312+
console.warn(`No mode found for slug: ${modeSlug}`)
313+
return true
314+
}
315+
return mode.enabledForSwitching ?? true
316+
}
317+
318+
// Check if any modes are enabled for switching
319+
export async function hasAnySwitchableModes(context: vscode.ExtensionContext): Promise<boolean> {
320+
// Get all modes (built-in and custom)
321+
const allModes = await getAllModesWithPrompts(context)
322+
323+
// Check if any mode is enabled for switching
324+
return allModes.some((mode) => mode.enabledForSwitching !== false)
325+
}

0 commit comments

Comments
 (0)