Browse Source

Merge pull request #1113 from RooVetGit/debugger_mode

Debugger mode
Matt Rubens 1 year ago
parent
commit
655b3a11a0

+ 5 - 0
.changeset/flat-snails-unite.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Debugger mode

+ 24 - 4
src/core/Cline.ts

@@ -54,7 +54,7 @@ import { parseMentions } from "./mentions"
 import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
 import { formatResponse } from "./prompts/responses"
 import { SYSTEM_PROMPT } from "./prompts/system"
-import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
+import { modes, defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
 import { truncateConversationIfNeeded } from "./sliding-window"
 import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
 import { detectCodeOmission } from "../integrations/editor/detect-omission"
@@ -63,7 +63,7 @@ import { OpenRouterHandler } from "../api/providers/openrouter"
 import { McpHub } from "../services/mcp/McpHub"
 import crypto from "crypto"
 import { insertGroups } from "./diff/insert-groups"
-import { EXPERIMENT_IDS, experiments as Experiments } from "../shared/experiments"
+import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
 
 const cwd =
 	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
@@ -3235,9 +3235,29 @@ export class Cline {
 		details += `\n\n# Current Context Size (Tokens)\n${contextTokens ? `${contextTokens.toLocaleString()} (${contextPercentage}%)` : "(Not available)"}`
 
 		// Add current mode and any mode-specific warnings
-		const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {}
+		const {
+			mode,
+			customModes,
+			customModePrompts,
+			experiments = {} as Record<ExperimentId, boolean>,
+			customInstructions: globalCustomInstructions,
+			preferredLanguage,
+		} = (await this.providerRef.deref()?.getState()) ?? {}
 		const currentMode = mode ?? defaultModeSlug
-		details += `\n\n# Current Mode\n${currentMode}`
+		const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
+			cwd,
+			globalCustomInstructions,
+			preferredLanguage,
+		})
+		details += `\n\n# Current Mode\n`
+		details += `<slug>${currentMode}</slug>\n`
+		details += `<name>${modeDetails.name}</name>\n`
+		if (Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)) {
+			details += `<role>${modeDetails.roleDefinition}</role>\n`
+			if (modeDetails.customInstructions) {
+				details += `<custom_instructions>${modeDetails.customInstructions}</custom_instructions>\n`
+			}
+		}
 
 		// Add warning if not in code mode
 		if (

+ 34 - 5
src/core/webview/__tests__/ClineProvider.test.ts

@@ -9,12 +9,41 @@ import { setSoundEnabled } from "../../../utils/sound"
 import { defaultModeSlug } from "../../../shared/modes"
 import { experimentDefault } from "../../../shared/experiments"
 
-// Mock custom-instructions module
-const mockAddCustomInstructions = jest.fn()
+// Mock setup must come before imports
+jest.mock("../../prompts/sections/custom-instructions")
 
-jest.mock("../../prompts/sections/custom-instructions", () => ({
-	addCustomInstructions: mockAddCustomInstructions,
-}))
+// Mock dependencies
+jest.mock("vscode")
+jest.mock("delay")
+jest.mock(
+	"@modelcontextprotocol/sdk/types.js",
+	() => ({
+		CallToolResultSchema: {},
+		ListResourcesResultSchema: {},
+		ListResourceTemplatesResultSchema: {},
+		ListToolsResultSchema: {},
+		ReadResourceResultSchema: {},
+		ErrorCode: {
+			InvalidRequest: "InvalidRequest",
+			MethodNotFound: "MethodNotFound",
+			InternalError: "InternalError",
+		},
+		McpError: class McpError extends Error {
+			code: string
+			constructor(code: string, message: string) {
+				super(message)
+				this.code = code
+				this.name = "McpError"
+			}
+		},
+	}),
+	{ virtual: true },
+)
+
+// Initialize mocks
+const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
+;(jest.requireMock("../../prompts/sections/custom-instructions") as any).addCustomInstructions =
+	mockAddCustomInstructions
 
 // Mock delay module
 jest.mock("delay", () => {

+ 47 - 0
src/shared/__tests__/experiments.test.ts

@@ -0,0 +1,47 @@
+import { EXPERIMENT_IDS, experimentConfigsMap, experiments as Experiments, ExperimentId } from "../experiments"
+
+describe("experiments", () => {
+	describe("POWER_STEERING", () => {
+		it("is configured correctly", () => {
+			expect(EXPERIMENT_IDS.POWER_STEERING).toBe("powerSteering")
+			expect(experimentConfigsMap.POWER_STEERING).toMatchObject({
+				name: 'Use experimental "power steering" mode',
+				description:
+					"When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.",
+				enabled: false,
+			})
+		})
+	})
+
+	describe("isEnabled", () => {
+		it("returns false when experiment is not enabled", () => {
+			const experiments: Record<ExperimentId, boolean> = {
+				powerSteering: false,
+				experimentalDiffStrategy: false,
+				search_and_replace: false,
+				insert_content: false,
+			}
+			expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
+		})
+
+		it("returns true when experiment is enabled", () => {
+			const experiments: Record<ExperimentId, boolean> = {
+				powerSteering: true,
+				experimentalDiffStrategy: false,
+				search_and_replace: false,
+				insert_content: false,
+			}
+			expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
+		})
+
+		it("returns false when experiment is not present", () => {
+			const experiments: Record<ExperimentId, boolean> = {
+				experimentalDiffStrategy: false,
+				search_and_replace: false,
+				insert_content: false,
+				powerSteering: false,
+			}
+			expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
+		})
+	})
+})

+ 100 - 1
src/shared/__tests__/modes.test.ts

@@ -1,4 +1,13 @@
-import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
+// Mock setup must come before imports
+jest.mock("vscode")
+const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
+jest.mock("../../core/prompts/sections/custom-instructions", () => ({
+	addCustomInstructions: mockAddCustomInstructions,
+}))
+
+import { isToolAllowedForMode, FileRestrictionError, ModeConfig, getFullModeDetails, modes } from "../modes"
+import * as vscode from "vscode"
+import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions"
 
 describe("isToolAllowedForMode", () => {
 	const customModes: ModeConfig[] = [
@@ -324,6 +333,96 @@ describe("FileRestrictionError", () => {
 		expect(error.name).toBe("FileRestrictionError")
 	})
 
+	describe("debug mode", () => {
+		it("is configured correctly", () => {
+			const debugMode = modes.find((mode) => mode.slug === "debug")
+			expect(debugMode).toBeDefined()
+			expect(debugMode).toMatchObject({
+				slug: "debug",
+				name: "Debug",
+				roleDefinition:
+					"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
+				groups: ["read", "edit", "browser", "command", "mcp"],
+			})
+			expect(debugMode?.customInstructions).toContain("Reflect on 5-7 different possible sources of the problem")
+		})
+	})
+
+	describe("getFullModeDetails", () => {
+		beforeEach(() => {
+			jest.clearAllMocks()
+			;(addCustomInstructions as jest.Mock).mockResolvedValue("Combined instructions")
+		})
+
+		it("returns base mode when no overrides exist", async () => {
+			const result = await getFullModeDetails("debug")
+			expect(result).toMatchObject({
+				slug: "debug",
+				name: "Debug",
+				roleDefinition:
+					"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
+			})
+		})
+
+		it("applies custom mode overrides", async () => {
+			const customModes = [
+				{
+					slug: "debug",
+					name: "Custom Debug",
+					roleDefinition: "Custom debug role",
+					groups: ["read"],
+				},
+			]
+
+			const result = await getFullModeDetails("debug", customModes)
+			expect(result).toMatchObject({
+				slug: "debug",
+				name: "Custom Debug",
+				roleDefinition: "Custom debug role",
+				groups: ["read"],
+			})
+		})
+
+		it("applies prompt component overrides", async () => {
+			const customModePrompts = {
+				debug: {
+					roleDefinition: "Overridden role",
+					customInstructions: "Overridden instructions",
+				},
+			}
+
+			const result = await getFullModeDetails("debug", undefined, customModePrompts)
+			expect(result.roleDefinition).toBe("Overridden role")
+			expect(result.customInstructions).toBe("Overridden instructions")
+		})
+
+		it("combines custom instructions when cwd provided", async () => {
+			const options = {
+				cwd: "/test/path",
+				globalCustomInstructions: "Global instructions",
+				preferredLanguage: "en",
+			}
+
+			await getFullModeDetails("debug", undefined, undefined, options)
+
+			expect(addCustomInstructions).toHaveBeenCalledWith(
+				expect.any(String),
+				"Global instructions",
+				"/test/path",
+				"debug",
+				{ preferredLanguage: "en" },
+			)
+		})
+
+		it("falls back to first mode for non-existent mode", async () => {
+			const result = await getFullModeDetails("non-existent")
+			expect(result).toMatchObject({
+				...modes[0],
+				customInstructions: "",
+			})
+		})
+	})
+
 	it("formats error message with description when provided", () => {
 		const error = new FileRestrictionError("Markdown Editor", "\\.md$", "Markdown files only", "test.js")
 		expect(error.message).toBe(

+ 7 - 0
src/shared/experiments.ts

@@ -2,6 +2,7 @@ export const EXPERIMENT_IDS = {
 	DIFF_STRATEGY: "experimentalDiffStrategy",
 	SEARCH_AND_REPLACE: "search_and_replace",
 	INSERT_BLOCK: "insert_content",
+	POWER_STEERING: "powerSteering",
 } as const
 
 export type ExperimentKey = keyof typeof EXPERIMENT_IDS
@@ -35,6 +36,12 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
 			"Enable the experimental insert content tool, allowing Roo to insert content at specific line numbers without needing to create a diff.",
 		enabled: false,
 	},
+	POWER_STEERING: {
+		name: 'Use experimental "power steering" mode',
+		description:
+			"When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.",
+		enabled: false,
+	},
 }
 
 export const experimentDefault = Object.fromEntries(

+ 50 - 0
src/shared/modes.ts

@@ -1,5 +1,6 @@
 import * as vscode from "vscode"
 import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
+import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
 
 // Mode types
 export type Mode = string
@@ -98,6 +99,15 @@ export const modes: readonly ModeConfig[] = [
 		customInstructions:
 			"You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code.",
 	},
+	{
+		slug: "debug",
+		name: "Debug",
+		roleDefinition:
+			"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
+		groups: ["read", "edit", "browser", "command", "mcp"],
+		customInstructions:
+			"Reflect on 5-7 different possible sources of the problem, prioritizing them based on likelihood, impact on functionality, and frequency in similar issues. Only consider sources that align with the error logs, recent code changes, and system constraints. Ignore external dependencies unless logs indicate otherwise.\n\nOnce you've narrowed it down to the 1-2 most likely sources, cross-check them against previous error logs, relevant system state, and expected behaviors. If inconsistencies arise, refine your hypothesis.\n\nWhen adding logs, ensure they are strategically placed to confirm or eliminate multiple causes. If the logs do not support your assumptions, suggest an alternative debugging strategy before proceeding.\n\nBefore implementing a fix, summarize the issue, validated assumptions, and expected log outputs that would confirm the problem is solved.",
+	},
 ] as const
 
 // Export the default mode slug
@@ -253,6 +263,46 @@ export async function getAllModesWithPrompts(context: vscode.ExtensionContext):
 	}))
 }
 
+// Helper function to get complete mode details with all overrides
+export async function getFullModeDetails(
+	modeSlug: string,
+	customModes?: ModeConfig[],
+	customModePrompts?: CustomModePrompts,
+	options?: {
+		cwd?: string
+		globalCustomInstructions?: string
+		preferredLanguage?: string
+	},
+): Promise<ModeConfig> {
+	// First get the base mode config from custom modes or built-in modes
+	const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0]
+
+	// Check for any prompt component overrides
+	const promptComponent = customModePrompts?.[modeSlug]
+
+	// Get the base custom instructions
+	const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || ""
+
+	// If we have cwd, load and combine all custom instructions
+	let fullCustomInstructions = baseCustomInstructions
+	if (options?.cwd) {
+		fullCustomInstructions = await addCustomInstructions(
+			baseCustomInstructions,
+			options.globalCustomInstructions || "",
+			options.cwd,
+			modeSlug,
+			{ preferredLanguage: options.preferredLanguage },
+		)
+	}
+
+	// Return mode with any overrides applied
+	return {
+		...baseMode,
+		roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition,
+		customInstructions: fullCustomInstructions,
+	}
+}
+
 // Helper function to safely get role definition
 export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string {
 	const mode = getModeBySlug(modeSlug, customModes)