feat: add individual tool controls to reduce token usage

- Add disabledTools configuration to global settings schema
- Update tool system to filter out disabled tools from system prompt
- Create ToolSettings UI component for toggling tools on/off
- Add tool settings section to settings view with wrench icon
- Update message handlers to persist disabled tools state
- Add comprehensive tests for disabled tools functionality
- Ensure always-available tools cannot be disabled

Fixes #5963
This commit is contained in:
Roo Code 2025-07-20 01:51:51 +00:00
parent 1b12108172
commit 7267fe0299
13 changed files with 400 additions and 2 deletions

View file

@ -72,6 +72,8 @@ export const globalSettingsSchema = z.object({
autoCondenseContextPercent: z.number().optional(),
maxConcurrentFileReads: z.number().optional(),
disabledTools: z.array(z.string()).optional(),
browserToolEnabled: z.boolean().optional(),
browserViewportSize: z.string().optional(),
screenshotQuality: z.number().optional(),

View file

@ -98,6 +98,7 @@ ${getToolDescriptionsForMode(
experiments,
partialReadsEnabled,
settings,
settings?.disabledTools,
)}
${getToolUseGuidelinesSection(codeIndexManager)}

View file

@ -0,0 +1,231 @@
// npx vitest core/prompts/tools/__tests__/index.spec.ts
import { describe, it, expect } from "vitest"
import { getToolDescriptionsForMode } from "../index"
import { defaultModeSlug } from "../../../../shared/modes"
describe("getToolDescriptionsForMode", () => {
const mockCwd = "/test/path"
const supportsComputerUse = false
it("should return tool descriptions for a given mode", () => {
const result = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined, // codeIndexManager
undefined, // diffStrategy
undefined, // browserViewportSize
undefined, // mcpHub
undefined, // customModes
undefined, // experiments
undefined, // partialReadsEnabled
undefined, // settings
undefined, // disabledTools
)
expect(result).toBeTruthy()
expect(result).toContain("# Tools")
// Check that it includes some expected tools from architect mode
expect(result).toContain("read_file")
expect(result).toContain("write_to_file")
expect(result).toContain("list_files")
// Note: execute_command is not in architect mode
})
it("should filter out disabled tools", () => {
const disabledTools = ["read_file", "write_to_file"]
const result = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
disabledTools,
)
// Check that disabled tools are not included
expect(result).not.toContain("## read_file")
expect(result).not.toContain("## write_to_file")
// Check that other tools are still included
expect(result).toContain("## list_files")
expect(result).toContain("## search_files")
})
it("should not filter out always-available tools even if disabled", () => {
const disabledTools = ["ask_followup_question", "attempt_completion"]
const result = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
disabledTools,
)
// These tools should always be available
expect(result).toContain("## ask_followup_question")
expect(result).toContain("## attempt_completion")
})
it("should handle empty disabled tools array", () => {
const disabledTools: string[] = []
const resultWithEmpty = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
disabledTools,
)
const resultWithoutDisabled = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
)
// Should return the same tools
expect(resultWithEmpty).toEqual(resultWithoutDisabled)
})
it("should handle undefined disabled tools", () => {
const resultWithUndefined = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
)
const resultWithoutDisabled = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
)
// Should return the same tools
expect(resultWithUndefined).toEqual(resultWithoutDisabled)
})
it("should filter out multiple disabled tools correctly", () => {
const disabledTools = ["read_file", "write_to_file", "list_files", "search_files"]
const result = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
disabledTools,
)
// Check that all disabled tools are filtered out
disabledTools.forEach((tool) => {
expect(result).not.toContain(`## ${tool}`)
})
// Check that some other tools are still included
expect(result).toContain("# Tools")
// Always available tools should still be there
expect(result).toContain("## ask_followup_question")
})
it("should handle invalid tool names in disabled list", () => {
const disabledTools = ["invalid_tool", "another_invalid", "read_file"]
const result = getToolDescriptionsForMode(
defaultModeSlug,
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
disabledTools,
)
// Should still filter out valid disabled tools
expect(result).not.toContain("## read_file")
// Invalid tools should not affect the result
expect(result).toContain("## write_to_file")
expect(result).toContain("## list_files")
})
it("should work with code mode that has execute_command", () => {
const disabledTools = ["execute_command"]
const result = getToolDescriptionsForMode(
"code", // code mode has the command group
mockCwd,
supportsComputerUse,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
disabledTools,
)
// execute_command should be filtered out
expect(result).not.toContain("## execute_command")
// Other tools should still be included
expect(result).toContain("## read_file")
expect(result).toContain("## write_to_file")
})
})

View file

@ -61,6 +61,7 @@ export function getToolDescriptionsForMode(
experiments?: Record<string, boolean>,
partialReadsEnabled?: boolean,
settings?: Record<string, any>,
disabledTools?: string[],
): string {
const config = getModeConfig(mode, customModes)
const args: ToolArgs = {
@ -109,6 +110,16 @@ export function getToolDescriptionsForMode(
tools.delete("codebase_search")
}
// Filter out disabled tools (except always-available tools)
if (disabledTools && disabledTools.length > 0) {
disabledTools.forEach((tool) => {
// Don't filter out always-available tools
if (!ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
tools.delete(tool)
}
})
}
// Map tool descriptions for allowed tools
const descriptions = Array.from(tools).map((toolName) => {
const descriptionFn = toolDescriptionMap[toolName]

View file

@ -1624,6 +1624,7 @@ export class Task extends EventEmitter<ClineEvents> {
language,
maxConcurrentFileReads,
maxReadFileLine,
disabledTools,
} = state ?? {}
return await (async () => {
@ -1652,6 +1653,7 @@ export class Task extends EventEmitter<ClineEvents> {
maxReadFileLine !== -1,
{
maxConcurrentFileReads,
disabledTools,
},
)
})()

View file

@ -1440,6 +1440,7 @@ export class ClineProvider
alwaysAllowFollowupQuestions,
followupAutoApproveTimeoutMs,
diagnosticsEnabled,
disabledTools,
} = await this.getState()
const telemetryKey = process.env.POSTHOG_API_KEY
@ -1561,6 +1562,7 @@ export class ClineProvider
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
diagnosticsEnabled: diagnosticsEnabled ?? true,
disabledTools: disabledTools ?? [],
}
}
@ -1726,6 +1728,7 @@ export class ClineProvider
codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
},
profileThresholds: stateValues.profileThresholds ?? {},
disabledTools: stateValues.disabledTools ?? [],
}
}

View file

@ -24,6 +24,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
language,
maxReadFileLine,
maxConcurrentFileReads,
disabledTools,
} = await provider.getState()
// Check experiment to determine which diff strategy to use
@ -82,6 +83,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
maxReadFileLine !== -1,
{
maxConcurrentFileReads,
disabledTools,
},
)

View file

@ -1232,6 +1232,10 @@ export const webviewMessageHandler = async (
await updateGlobalState("browserToolEnabled", message.bool ?? true)
await provider.postStateToWebview()
break
case "disabledTools":
await updateGlobalState("disabledTools", message.disabledTools ?? [])
await provider.postStateToWebview()
break
case "language":
changeLanguage(message.text ?? "en")
await updateGlobalState("language", message.text as Language)

View file

@ -189,6 +189,7 @@ export type ExtensionState = Pick<
| "allowedCommands"
| "deniedCommands"
| "allowedMaxRequests"
| "disabledTools"
| "browserToolEnabled"
| "browserViewportSize"
| "screenshotQuality"

View file

@ -153,6 +153,7 @@ export interface WebviewMessage {
| "humanRelayResponse"
| "humanRelayCancel"
| "browserToolEnabled"
| "disabledTools"
| "codebaseIndexEnabled"
| "telemetrySetting"
| "showRooIgnoredFiles"
@ -241,6 +242,7 @@ export interface WebviewMessage {
visibility?: ShareVisibility // For share visibility
hasContent?: boolean // For checkRulesDirectoryResult
checkOnly?: boolean // For deleteCustomMode check
disabledTools?: string[] // For disabling specific tools
codeIndexSettings?: {
// Global state settings
codebaseIndexEnabled: boolean

View file

@ -23,6 +23,7 @@ import {
Info,
MessageSquare,
LucideIcon,
Wrench,
} from "lucide-react"
import type { ProviderSettings, ExperimentId } from "@roo-code/types"
@ -65,6 +66,7 @@ import { LanguageSettings } from "./LanguageSettings"
import { About } from "./About"
import { Section } from "./Section"
import PromptsSettings from "./PromptsSettings"
import { ToolSettings } from "./ToolSettings"
import { cn } from "@/lib/utils"
export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
@ -87,6 +89,7 @@ const sectionNames = [
"contextManagement",
"terminal",
"prompts",
"tools",
"experimental",
"language",
"about",
@ -177,6 +180,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
alwaysAllowFollowupQuestions,
alwaysAllowUpdateTodoList,
followupAutoApproveTimeoutMs,
disabledTools,
} = cachedState
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@ -333,6 +337,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
vscode.postMessage({ type: "disabledTools", values: disabledTools })
setChangeDetected(false)
}
}
@ -410,6 +415,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
{ id: "contextManagement", icon: Database },
{ id: "terminal", icon: SquareTerminal },
{ id: "prompts", icon: MessageSquare },
{ id: "tools", icon: Wrench },
{ id: "experimental", icon: FlaskConical },
{ id: "language", icon: Globe },
{ id: "about", icon: Info },
@ -698,6 +704,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
/>
)}
{/* Tools Section */}
{activeTab === "tools" && (
<ToolSettings disabledTools={disabledTools} setCachedStateField={setCachedStateField} />
)}
{/* Experimental Section */}
{activeTab === "experimental" && (
<ExperimentalSettings setExperimentEnabled={setExperimentEnabled} experiments={experiments} />

View file

@ -0,0 +1,123 @@
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { Wrench } from "lucide-react"
import { HTMLAttributes, useMemo } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Section } from "./Section"
import { SectionHeader } from "./SectionHeader"
import { SetCachedStateField } from "./types"
type ToolSettingsProps = HTMLAttributes<HTMLDivElement> & {
disabledTools?: string[]
setCachedStateField: SetCachedStateField<"disabledTools">
}
// Tool display names mapping
const TOOL_DISPLAY_NAMES: Record<string, string> = {
execute_command: "Run commands",
read_file: "Read files",
fetch_instructions: "Fetch instructions",
write_to_file: "Write files",
apply_diff: "Apply changes",
search_files: "Search files",
list_files: "List files",
list_code_definition_names: "List definitions",
browser_action: "Use a browser",
use_mcp_tool: "Use MCP tools",
access_mcp_resource: "Access MCP resources",
ask_followup_question: "Ask questions",
attempt_completion: "Complete tasks",
switch_mode: "Switch modes",
new_task: "Create new task",
insert_content: "Insert content",
search_and_replace: "Search and replace",
codebase_search: "Codebase search",
update_todo_list: "Update todo list",
}
// Tools that are always available and cannot be disabled
const ALWAYS_AVAILABLE_TOOLS = [
"ask_followup_question",
"attempt_completion",
"switch_mode",
"new_task",
"update_todo_list",
]
// Tool groups for better organization
const TOOL_GROUPS = {
"File Operations": [
"read_file",
"write_to_file",
"search_files",
"list_files",
"list_code_definition_names",
"codebase_search",
],
"Code Editing": ["apply_diff", "insert_content", "search_and_replace"],
System: ["execute_command", "browser_action"],
MCP: ["use_mcp_tool", "access_mcp_resource"],
Other: ["fetch_instructions"],
}
export const ToolSettings = ({ disabledTools = [], setCachedStateField, ...props }: ToolSettingsProps) => {
const { t } = useAppTranslation()
const handleToolToggle = (toolName: string, enabled: boolean) => {
if (enabled) {
// Remove from disabled tools
setCachedStateField(
"disabledTools",
disabledTools.filter((tool) => tool !== toolName),
)
} else {
// Add to disabled tools
setCachedStateField("disabledTools", [...disabledTools, toolName])
}
}
const isToolEnabled = (toolName: string) => !disabledTools.includes(toolName)
const toolGroups = useMemo(() => {
return Object.entries(TOOL_GROUPS).map(([groupName, tools]) => ({
name: groupName,
tools: tools.filter((tool) => !ALWAYS_AVAILABLE_TOOLS.includes(tool)),
}))
}, [])
return (
<div {...props}>
<SectionHeader>
<div className="flex items-center gap-2">
<Wrench className="w-4" />
<div>{t("settings:sections.tools")}</div>
</div>
</SectionHeader>
<Section>
<div className="text-vscode-descriptionForeground text-sm mb-3">{t("settings:tools.description")}</div>
<div className="space-y-4">
{toolGroups.map(({ name, tools }) => (
<div key={name}>
<h4 className="font-medium mb-2">{name}</h4>
<div className="space-y-1 pl-3">
{tools.map((tool) => (
<VSCodeCheckbox
key={tool}
checked={isToolEnabled(tool)}
onChange={(e: any) => handleToolToggle(tool, e.target.checked)}>
<span className="text-sm">{TOOL_DISPLAY_NAMES[tool] || tool}</span>
</VSCodeCheckbox>
))}
</div>
</div>
))}
</div>
<div className="text-vscode-descriptionForeground text-xs mt-3">{t("settings:tools.note")}</div>
</Section>
</div>
)
}

View file

@ -31,7 +31,8 @@
"prompts": "Prompts",
"experimental": "Experimental",
"language": "Language",
"about": "About Roo Code"
"about": "About Roo Code",
"tools": "Tools"
},
"prompts": {
"description": "Configure support prompts that are used for quick actions like enhancing prompts, explaining code, and fixing issues. These prompts help Roo provide better assistance for common development tasks."
@ -718,5 +719,9 @@
"useCustomArn": "Use custom ARN..."
},
"includeMaxOutputTokens": "Include max output tokens",
"includeMaxOutputTokensDescription": "Send max output tokens parameter in API requests. Some providers may not support this."
"includeMaxOutputTokensDescription": "Send max output tokens parameter in API requests. Some providers may not support this.",
"tools": {
"description": "Disable specific tools to reduce token usage. Disabled tools won't be included in the system prompt.",
"note": "Note: Some tools are always available and cannot be disabled (ask questions, complete tasks, switch modes, create new task, update todo list)."
}
}