diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a30550dce1..accf90973d 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -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(), diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index bfc12930a0..c8ebf91138 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -98,6 +98,7 @@ ${getToolDescriptionsForMode( experiments, partialReadsEnabled, settings, + settings?.disabledTools, )} ${getToolUseGuidelinesSection(codeIndexManager)} diff --git a/src/core/prompts/tools/__tests__/index.spec.ts b/src/core/prompts/tools/__tests__/index.spec.ts new file mode 100644 index 0000000000..3b303d80ca --- /dev/null +++ b/src/core/prompts/tools/__tests__/index.spec.ts @@ -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") + }) +}) diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 3fd5a636a4..b1fc4018e0 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -61,6 +61,7 @@ export function getToolDescriptionsForMode( experiments?: Record, partialReadsEnabled?: boolean, settings?: Record, + 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] diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 53b8ef5b87..1d9fdf51bc 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1624,6 +1624,7 @@ export class Task extends EventEmitter { language, maxConcurrentFileReads, maxReadFileLine, + disabledTools, } = state ?? {} return await (async () => { @@ -1652,6 +1653,7 @@ export class Task extends EventEmitter { maxReadFileLine !== -1, { maxConcurrentFileReads, + disabledTools, }, ) })() diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6231f08167..76ee836b8d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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 ?? [], } } diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts index 2c88b98d2e..248b28bc45 100644 --- a/src/core/webview/generateSystemPrompt.ts +++ b/src/core/webview/generateSystemPrompt.ts @@ -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, }, ) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c5a8573319..a6e3c815d2 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 4f2aa2da15..7ddcdb1f0e 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -189,6 +189,7 @@ export type ExtensionState = Pick< | "allowedCommands" | "deniedCommands" | "allowedMaxRequests" + | "disabledTools" | "browserToolEnabled" | "browserViewportSize" | "screenshotQuality" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 1f56829f7b..7925152450 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -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 diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 882da54ab4..89c899b150 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -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(({ onDone, t alwaysAllowFollowupQuestions, alwaysAllowUpdateTodoList, followupAutoApproveTimeoutMs, + disabledTools, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -333,6 +337,7 @@ const SettingsView = forwardRef(({ 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(({ 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(({ onDone, t /> )} + {/* Tools Section */} + {activeTab === "tools" && ( + + )} + {/* Experimental Section */} {activeTab === "experimental" && ( diff --git a/webview-ui/src/components/settings/ToolSettings.tsx b/webview-ui/src/components/settings/ToolSettings.tsx new file mode 100644 index 0000000000..8899b0c2e6 --- /dev/null +++ b/webview-ui/src/components/settings/ToolSettings.tsx @@ -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 & { + disabledTools?: string[] + setCachedStateField: SetCachedStateField<"disabledTools"> +} + +// Tool display names mapping +const TOOL_DISPLAY_NAMES: Record = { + 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 ( +
+ +
+ +
{t("settings:sections.tools")}
+
+
+ +
+
{t("settings:tools.description")}
+ +
+ {toolGroups.map(({ name, tools }) => ( +
+

{name}

+
+ {tools.map((tool) => ( + handleToolToggle(tool, e.target.checked)}> + {TOOL_DISPLAY_NAMES[tool] || tool} + + ))} +
+
+ ))} +
+ +
{t("settings:tools.note")}
+
+
+ ) +} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 7e3c2e3fcc..84ca8e79e1 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -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)." + } }