Skip to content

Commit 655b3a1

Browse files
authored
Merge pull request RooCodeInc#1113 from RooVetGit/debugger_mode
Debugger mode
2 parents ba3c29f + c009095 commit 655b3a1

File tree

7 files changed

+267
-10
lines changed

7 files changed

+267
-10
lines changed

.changeset/flat-snails-unite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Debugger mode

src/core/Cline.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import { parseMentions } from "./mentions"
5454
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
5555
import { formatResponse } from "./prompts/responses"
5656
import { SYSTEM_PROMPT } from "./prompts/system"
57-
import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
57+
import { modes, defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
5858
import { truncateConversationIfNeeded } from "./sliding-window"
5959
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
6060
import { detectCodeOmission } from "../integrations/editor/detect-omission"
@@ -63,7 +63,7 @@ import { OpenRouterHandler } from "../api/providers/openrouter"
6363
import { McpHub } from "../services/mcp/McpHub"
6464
import crypto from "crypto"
6565
import { insertGroups } from "./diff/insert-groups"
66-
import { EXPERIMENT_IDS, experiments as Experiments } from "../shared/experiments"
66+
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
6767

6868
const cwd =
6969
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 {
32353235
details += `\n\n# Current Context Size (Tokens)\n${contextTokens ? `${contextTokens.toLocaleString()} (${contextPercentage}%)` : "(Not available)"}`
32363236

32373237
// Add current mode and any mode-specific warnings
3238-
const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {}
3238+
const {
3239+
mode,
3240+
customModes,
3241+
customModePrompts,
3242+
experiments = {} as Record<ExperimentId, boolean>,
3243+
customInstructions: globalCustomInstructions,
3244+
preferredLanguage,
3245+
} = (await this.providerRef.deref()?.getState()) ?? {}
32393246
const currentMode = mode ?? defaultModeSlug
3240-
details += `\n\n# Current Mode\n${currentMode}`
3247+
const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
3248+
cwd,
3249+
globalCustomInstructions,
3250+
preferredLanguage,
3251+
})
3252+
details += `\n\n# Current Mode\n`
3253+
details += `<slug>${currentMode}</slug>\n`
3254+
details += `<name>${modeDetails.name}</name>\n`
3255+
if (Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)) {
3256+
details += `<role>${modeDetails.roleDefinition}</role>\n`
3257+
if (modeDetails.customInstructions) {
3258+
details += `<custom_instructions>${modeDetails.customInstructions}</custom_instructions>\n`
3259+
}
3260+
}
32413261

32423262
// Add warning if not in code mode
32433263
if (

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,41 @@ import { setSoundEnabled } from "../../../utils/sound"
99
import { defaultModeSlug } from "../../../shared/modes"
1010
import { experimentDefault } from "../../../shared/experiments"
1111

12-
// Mock custom-instructions module
13-
const mockAddCustomInstructions = jest.fn()
12+
// Mock setup must come before imports
13+
jest.mock("../../prompts/sections/custom-instructions")
1414

15-
jest.mock("../../prompts/sections/custom-instructions", () => ({
16-
addCustomInstructions: mockAddCustomInstructions,
17-
}))
15+
// Mock dependencies
16+
jest.mock("vscode")
17+
jest.mock("delay")
18+
jest.mock(
19+
"@modelcontextprotocol/sdk/types.js",
20+
() => ({
21+
CallToolResultSchema: {},
22+
ListResourcesResultSchema: {},
23+
ListResourceTemplatesResultSchema: {},
24+
ListToolsResultSchema: {},
25+
ReadResourceResultSchema: {},
26+
ErrorCode: {
27+
InvalidRequest: "InvalidRequest",
28+
MethodNotFound: "MethodNotFound",
29+
InternalError: "InternalError",
30+
},
31+
McpError: class McpError extends Error {
32+
code: string
33+
constructor(code: string, message: string) {
34+
super(message)
35+
this.code = code
36+
this.name = "McpError"
37+
}
38+
},
39+
}),
40+
{ virtual: true },
41+
)
42+
43+
// Initialize mocks
44+
const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
45+
;(jest.requireMock("../../prompts/sections/custom-instructions") as any).addCustomInstructions =
46+
mockAddCustomInstructions
1847

1948
// Mock delay module
2049
jest.mock("delay", () => {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { EXPERIMENT_IDS, experimentConfigsMap, experiments as Experiments, ExperimentId } from "../experiments"
2+
3+
describe("experiments", () => {
4+
describe("POWER_STEERING", () => {
5+
it("is configured correctly", () => {
6+
expect(EXPERIMENT_IDS.POWER_STEERING).toBe("powerSteering")
7+
expect(experimentConfigsMap.POWER_STEERING).toMatchObject({
8+
name: 'Use experimental "power steering" mode',
9+
description:
10+
"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.",
11+
enabled: false,
12+
})
13+
})
14+
})
15+
16+
describe("isEnabled", () => {
17+
it("returns false when experiment is not enabled", () => {
18+
const experiments: Record<ExperimentId, boolean> = {
19+
powerSteering: false,
20+
experimentalDiffStrategy: false,
21+
search_and_replace: false,
22+
insert_content: false,
23+
}
24+
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
25+
})
26+
27+
it("returns true when experiment is enabled", () => {
28+
const experiments: Record<ExperimentId, boolean> = {
29+
powerSteering: true,
30+
experimentalDiffStrategy: false,
31+
search_and_replace: false,
32+
insert_content: false,
33+
}
34+
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
35+
})
36+
37+
it("returns false when experiment is not present", () => {
38+
const experiments: Record<ExperimentId, boolean> = {
39+
experimentalDiffStrategy: false,
40+
search_and_replace: false,
41+
insert_content: false,
42+
powerSteering: false,
43+
}
44+
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
45+
})
46+
})
47+
})

src/shared/__tests__/modes.test.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
1+
// Mock setup must come before imports
2+
jest.mock("vscode")
3+
const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
4+
jest.mock("../../core/prompts/sections/custom-instructions", () => ({
5+
addCustomInstructions: mockAddCustomInstructions,
6+
}))
7+
8+
import { isToolAllowedForMode, FileRestrictionError, ModeConfig, getFullModeDetails, modes } from "../modes"
9+
import * as vscode from "vscode"
10+
import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions"
211

312
describe("isToolAllowedForMode", () => {
413
const customModes: ModeConfig[] = [
@@ -324,6 +333,96 @@ describe("FileRestrictionError", () => {
324333
expect(error.name).toBe("FileRestrictionError")
325334
})
326335

336+
describe("debug mode", () => {
337+
it("is configured correctly", () => {
338+
const debugMode = modes.find((mode) => mode.slug === "debug")
339+
expect(debugMode).toBeDefined()
340+
expect(debugMode).toMatchObject({
341+
slug: "debug",
342+
name: "Debug",
343+
roleDefinition:
344+
"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
345+
groups: ["read", "edit", "browser", "command", "mcp"],
346+
})
347+
expect(debugMode?.customInstructions).toContain("Reflect on 5-7 different possible sources of the problem")
348+
})
349+
})
350+
351+
describe("getFullModeDetails", () => {
352+
beforeEach(() => {
353+
jest.clearAllMocks()
354+
;(addCustomInstructions as jest.Mock).mockResolvedValue("Combined instructions")
355+
})
356+
357+
it("returns base mode when no overrides exist", async () => {
358+
const result = await getFullModeDetails("debug")
359+
expect(result).toMatchObject({
360+
slug: "debug",
361+
name: "Debug",
362+
roleDefinition:
363+
"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
364+
})
365+
})
366+
367+
it("applies custom mode overrides", async () => {
368+
const customModes = [
369+
{
370+
slug: "debug",
371+
name: "Custom Debug",
372+
roleDefinition: "Custom debug role",
373+
groups: ["read"],
374+
},
375+
]
376+
377+
const result = await getFullModeDetails("debug", customModes)
378+
expect(result).toMatchObject({
379+
slug: "debug",
380+
name: "Custom Debug",
381+
roleDefinition: "Custom debug role",
382+
groups: ["read"],
383+
})
384+
})
385+
386+
it("applies prompt component overrides", async () => {
387+
const customModePrompts = {
388+
debug: {
389+
roleDefinition: "Overridden role",
390+
customInstructions: "Overridden instructions",
391+
},
392+
}
393+
394+
const result = await getFullModeDetails("debug", undefined, customModePrompts)
395+
expect(result.roleDefinition).toBe("Overridden role")
396+
expect(result.customInstructions).toBe("Overridden instructions")
397+
})
398+
399+
it("combines custom instructions when cwd provided", async () => {
400+
const options = {
401+
cwd: "/test/path",
402+
globalCustomInstructions: "Global instructions",
403+
preferredLanguage: "en",
404+
}
405+
406+
await getFullModeDetails("debug", undefined, undefined, options)
407+
408+
expect(addCustomInstructions).toHaveBeenCalledWith(
409+
expect.any(String),
410+
"Global instructions",
411+
"/test/path",
412+
"debug",
413+
{ preferredLanguage: "en" },
414+
)
415+
})
416+
417+
it("falls back to first mode for non-existent mode", async () => {
418+
const result = await getFullModeDetails("non-existent")
419+
expect(result).toMatchObject({
420+
...modes[0],
421+
customInstructions: "",
422+
})
423+
})
424+
})
425+
327426
it("formats error message with description when provided", () => {
328427
const error = new FileRestrictionError("Markdown Editor", "\\.md$", "Markdown files only", "test.js")
329428
expect(error.message).toBe(

src/shared/experiments.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const EXPERIMENT_IDS = {
22
DIFF_STRATEGY: "experimentalDiffStrategy",
33
SEARCH_AND_REPLACE: "search_and_replace",
44
INSERT_BLOCK: "insert_content",
5+
POWER_STEERING: "powerSteering",
56
} as const
67

78
export type ExperimentKey = keyof typeof EXPERIMENT_IDS
@@ -35,6 +36,12 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
3536
"Enable the experimental insert content tool, allowing Roo to insert content at specific line numbers without needing to create a diff.",
3637
enabled: false,
3738
},
39+
POWER_STEERING: {
40+
name: 'Use experimental "power steering" mode',
41+
description:
42+
"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.",
43+
enabled: false,
44+
},
3845
}
3946

4047
export const experimentDefault = Object.fromEntries(

src/shared/modes.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from "vscode"
22
import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
3+
import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
34

45
// Mode types
56
export type Mode = string
@@ -98,6 +99,15 @@ export const modes: readonly ModeConfig[] = [
9899
customInstructions:
99100
"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.",
100101
},
102+
{
103+
slug: "debug",
104+
name: "Debug",
105+
roleDefinition:
106+
"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
107+
groups: ["read", "edit", "browser", "command", "mcp"],
108+
customInstructions:
109+
"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.",
110+
},
101111
] as const
102112

103113
// Export the default mode slug
@@ -253,6 +263,46 @@ export async function getAllModesWithPrompts(context: vscode.ExtensionContext):
253263
}))
254264
}
255265

266+
// Helper function to get complete mode details with all overrides
267+
export async function getFullModeDetails(
268+
modeSlug: string,
269+
customModes?: ModeConfig[],
270+
customModePrompts?: CustomModePrompts,
271+
options?: {
272+
cwd?: string
273+
globalCustomInstructions?: string
274+
preferredLanguage?: string
275+
},
276+
): Promise<ModeConfig> {
277+
// First get the base mode config from custom modes or built-in modes
278+
const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0]
279+
280+
// Check for any prompt component overrides
281+
const promptComponent = customModePrompts?.[modeSlug]
282+
283+
// Get the base custom instructions
284+
const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || ""
285+
286+
// If we have cwd, load and combine all custom instructions
287+
let fullCustomInstructions = baseCustomInstructions
288+
if (options?.cwd) {
289+
fullCustomInstructions = await addCustomInstructions(
290+
baseCustomInstructions,
291+
options.globalCustomInstructions || "",
292+
options.cwd,
293+
modeSlug,
294+
{ preferredLanguage: options.preferredLanguage },
295+
)
296+
}
297+
298+
// Return mode with any overrides applied
299+
return {
300+
...baseMode,
301+
roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition,
302+
customInstructions: fullCustomInstructions,
303+
}
304+
}
305+
256306
// Helper function to safely get role definition
257307
export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string {
258308
const mode = getModeBySlug(modeSlug, customModes)

0 commit comments

Comments
 (0)