diff --git a/src/core/prompts/__tests__/system-native-tools.spec.ts b/src/core/prompts/__tests__/system-native-tools.spec.ts new file mode 100644 index 0000000000..5ba620f665 --- /dev/null +++ b/src/core/prompts/__tests__/system-native-tools.spec.ts @@ -0,0 +1,218 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { SYSTEM_PROMPT } from "../system" +import { CodeIndexManager } from "../../../services/code-index/manager" +import type { SystemPromptSettings } from "../types" + +vi.mock("../../../services/code-index/manager") +vi.mock("../../../utils/storage", () => ({ + getSettingsDirectoryPath: vi.fn().mockResolvedValue("/test/settings"), +})) +vi.mock("../../../utils/globalContext", () => ({ + ensureSettingsDirectoryExists: vi.fn().mockResolvedValue("/test/settings"), +})) + +describe("SYSTEM_PROMPT with native tools", () => { + const mockContext = { + extensionUri: { fsPath: "/test/path" }, + globalStorageUri: { fsPath: "/test/global-storage" }, + globalState: { + get: vi.fn(), + update: vi.fn(), + }, + workspaceState: { + get: vi.fn(), + update: vi.fn(), + }, + } as any + + const defaultSettings: SystemPromptSettings = { + maxConcurrentFileReads: 5, + todoListEnabled: true, + useAgentRules: true, + newTaskRequireTodos: true, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should filter out update_todo_list when todoListEnabled is false", async () => { + const mockCodeIndexManager = { + isFeatureEnabled: true, + isFeatureConfigured: true, + isInitialized: true, + } + vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any) + + const result = await SYSTEM_PROMPT( + mockContext, + "/test/cwd", + false, + undefined, + undefined, + undefined, + "code", + undefined, + undefined, + undefined, + true, + undefined, + false, + undefined, + undefined, + false, + { ...defaultSettings, todoListEnabled: false }, + undefined, + undefined, + true, // useNativeTools + ) + + expect(result.tools).toBeDefined() + const toolNames = result.tools?.map((t) => t.name) || [] + expect(toolNames).not.toContain("update_todo_list") + }) + + it("should include update_todo_list when todoListEnabled is true", async () => { + const mockCodeIndexManager = { + isFeatureEnabled: true, + isFeatureConfigured: true, + isInitialized: true, + } + vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any) + + const result = await SYSTEM_PROMPT( + mockContext, + "/test/cwd", + false, + undefined, + undefined, + undefined, + "code", + undefined, + undefined, + undefined, + true, + undefined, + false, + undefined, + undefined, + false, + { ...defaultSettings, todoListEnabled: true }, + undefined, + undefined, + true, // useNativeTools + ) + + expect(result.tools).toBeDefined() + const toolNames = result.tools?.map((t) => t.name) || [] + expect(toolNames).toContain("update_todo_list") + }) + + it("should filter out codebase_search when feature is not configured", async () => { + const mockCodeIndexManager = { + isFeatureEnabled: false, + isFeatureConfigured: false, + isInitialized: false, + } + vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any) + + const result = await SYSTEM_PROMPT( + mockContext, + "/test/cwd", + false, + undefined, + undefined, + undefined, + "code", + undefined, + undefined, + undefined, + true, + undefined, + false, + undefined, + undefined, + false, + undefined, + undefined, + undefined, + true, // useNativeTools + ) + + expect(result.tools).toBeDefined() + const toolNames = result.tools?.map((t) => t.name) || [] + expect(toolNames).not.toContain("codebase_search") + }) + + it("should filter out generate_image when experiment is disabled", async () => { + const mockCodeIndexManager = { + isFeatureEnabled: true, + isFeatureConfigured: true, + isInitialized: true, + } + vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any) + + const result = await SYSTEM_PROMPT( + mockContext, + "/test/cwd", + false, + undefined, + undefined, + undefined, + "code", + undefined, + undefined, + undefined, + true, + { imageGeneration: false }, + false, + undefined, + undefined, + false, + undefined, + undefined, + undefined, + true, // useNativeTools + ) + + expect(result.tools).toBeDefined() + const toolNames = result.tools?.map((t) => t.name) || [] + expect(toolNames).not.toContain("generate_image") + }) + + it("should filter out run_slash_command when experiment is disabled", async () => { + const mockCodeIndexManager = { + isFeatureEnabled: true, + isFeatureConfigured: true, + isInitialized: true, + } + vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any) + + const result = await SYSTEM_PROMPT( + mockContext, + "/test/cwd", + false, + undefined, + undefined, + undefined, + "code", + undefined, + undefined, + undefined, + true, + { runSlashCommand: false }, + false, + undefined, + undefined, + false, + undefined, + undefined, + undefined, + true, // useNativeTools + ) + + expect(result.tools).toBeDefined() + const toolNames = result.tools?.map((t) => t.name) || [] + expect(toolNames).not.toContain("run_slash_command") + }) +}) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index f017273f4a..7be90932d7 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -17,7 +17,7 @@ import { getToolSpecs } from "./tool-specs" import { PromptVariables, loadSystemPromptFile } from "./sections/custom-system-prompt" -import { getToolDescriptionsForMode } from "./tools" +import { getToolDescriptionsForMode, filterToolsByAvailability } from "./tools" import { getRulesSection, getSystemInfoSection, @@ -252,8 +252,12 @@ ${customInstructions}`, // Remove duplicates const uniqueToolNames = [...new Set(toolNames)] + // Apply centralized filtering to ensure consistency with XML tool mode + const codeIndexManager = CodeIndexManager.getInstance(context, cwd) + const filteredTools = filterToolsByAvailability(uniqueToolNames, codeIndexManager, settings, experiments) + // Get tool specifications - const tools = getToolSpecs(uniqueToolNames) + const tools = getToolSpecs(filteredTools) return { systemPrompt: basePrompt, diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index c212b18a3d..ae4b57580b 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -3,6 +3,7 @@ import type { ToolName, ModeConfig } from "@roo-code/types" import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy } from "../../../shared/tools" import { McpHub } from "../../../services/mcp/McpHub" import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes" +import { CodeIndexManager } from "../../../services/code-index/manager" import { ToolArgs } from "./types" import { getExecuteCommandDescription } from "./execute-command" @@ -27,7 +28,50 @@ import { getCodebaseSearchDescription } from "./codebase-search" import { getUpdateTodoListDescription } from "./update-todo-list" import { getRunSlashCommandDescription } from "./run-slash-command" import { getGenerateImageDescription } from "./generate-image" -import { CodeIndexManager } from "../../../services/code-index/manager" + +/** + * Filters a list of tool names based on feature flags, settings, and experiments. + * This ensures consistent tool availability across XML and native tool modes. + */ +export function filterToolsByAvailability( + tools: ToolName[], + codeIndexManager: CodeIndexManager | undefined, + settings?: Record, + experiments?: Record, +): ToolName[] { + return tools.filter((tool) => { + // Conditionally exclude codebase_search if feature is disabled or not configured + if (tool === "codebase_search") { + if ( + !codeIndexManager || + !( + codeIndexManager.isFeatureEnabled && + codeIndexManager.isFeatureConfigured && + codeIndexManager.isInitialized + ) + ) { + return false + } + } + + // Conditionally exclude update_todo_list if disabled in settings + if (tool === "update_todo_list" && settings?.todoListEnabled === false) { + return false + } + + // Conditionally exclude generate_image if experiment is not enabled + if (tool === "generate_image" && !experiments?.imageGeneration) { + return false + } + + // Conditionally exclude run_slash_command if experiment is not enabled + if (tool === "run_slash_command" && !experiments?.runSlashCommand) { + return false + } + + return true + }) +} // Map of tool names to their description functions const toolDescriptionMap: Record string | undefined> = { @@ -120,31 +164,17 @@ export function getToolDescriptionsForMode( // Add always available tools ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool)) - // Conditionally exclude codebase_search if feature is disabled or not configured - if ( - !codeIndexManager || - !(codeIndexManager.isFeatureEnabled && codeIndexManager.isFeatureConfigured && codeIndexManager.isInitialized) - ) { - tools.delete("codebase_search") - } - - // Conditionally exclude update_todo_list if disabled in settings - if (settings?.todoListEnabled === false) { - tools.delete("update_todo_list") - } - - // Conditionally exclude generate_image if experiment is not enabled - if (!experiments?.imageGeneration) { - tools.delete("generate_image") - } - - // Conditionally exclude run_slash_command if experiment is not enabled - if (!experiments?.runSlashCommand) { - tools.delete("run_slash_command") - } + // Apply consistent filtering across all tool modes + const filteredTools = filterToolsByAvailability( + Array.from(tools) as ToolName[], + codeIndexManager, + settings, + experiments, + ) + const filteredToolsSet = new Set(filteredTools) // Map tool descriptions for allowed tools - const descriptions = Array.from(tools).map((toolName) => { + const descriptions = Array.from(filteredToolsSet).map((toolName) => { const descriptionFn = toolDescriptionMap[toolName] if (!descriptionFn) { return undefined