Merge pull request #1113 from RooVetGit/debugger_mode

Debugger mode
This commit is contained in:
Matt Rubens 2025-02-21 19:56:32 -05:00 committed by GitHub
commit 655b3a11a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 267 additions and 10 deletions

View file

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

View file

@ -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 (

View file

@ -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", () => {

View file

@ -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)
})
})
})

View file

@ -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(

View file

@ -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(

View file

@ -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)