Skip to content

Commit 171307c

Browse files
authored
fix: ensure correct precedence for roleDefinition and customInstructions when generating system prompt (RooCodeInc#3791)
* fix: ensure correct precedence for roleDefinition and customInstructions * Refactors mode selection logic for custom modes Refactors the mode selection logic to prioritize custom modes
1 parent e8b4dda commit 171307c

4 files changed

Lines changed: 265 additions & 13 deletions

File tree

src/core/prompts/system.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as os from "os"
33

44
import type { ModeConfig, PromptComponent, CustomModePrompts } from "@roo-code/types"
55

6-
import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
6+
import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes"
77
import { DiffStrategy } from "../../shared/tools"
88
import { formatLanguage } from "../../shared/language"
99

@@ -51,9 +51,9 @@ async function generatePrompt(
5151
// If diff is disabled, don't pass the diffStrategy
5252
const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
5353

54-
// Get the full mode config to ensure we have the role definition
54+
// Get the full mode config to ensure we have the role definition (used for groups, etc.)
5555
const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0]
56-
const roleDefinition = promptComponent?.roleDefinition || modeConfig.roleDefinition
56+
const { roleDefinition, baseInstructions } = getModeSelection(mode, promptComponent, customModeConfigs)
5757

5858
const [modesSection, mcpServersSection] = await Promise.all([
5959
getModesSection(context),
@@ -97,7 +97,7 @@ ${getSystemInfoSection(cwd)}
9797
9898
${getObjectiveSection()}
9999
100-
${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions })}`
100+
${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", cwd, mode, { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions })}`
101101

102102
return basePrompt
103103
}
@@ -149,9 +149,14 @@ export const SYSTEM_PROMPT = async (
149149

150150
// If a file-based custom system prompt exists, use it
151151
if (fileCustomSystemPrompt) {
152-
const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition
152+
const { roleDefinition, baseInstructions: baseInstructionsForFile } = getModeSelection(
153+
mode,
154+
promptComponent,
155+
customModes,
156+
)
157+
153158
const customInstructions = await addCustomInstructions(
154-
promptComponent?.customInstructions || currentMode.customInstructions || "",
159+
baseInstructionsForFile,
155160
globalCustomInstructions || "",
156161
cwd,
157162
mode,

src/shared/__tests__/modes.test.ts

Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// npx jest src/shared/__tests__/modes.test.ts
22

3-
import type { ModeConfig } from "@roo-code/types"
3+
import type { ModeConfig, PromptComponent } from "@roo-code/types"
44

55
// Mock setup must come before imports
66
jest.mock("vscode")
@@ -11,7 +11,7 @@ jest.mock("../../core/prompts/sections/custom-instructions", () => ({
1111
addCustomInstructions: mockAddCustomInstructions,
1212
}))
1313

14-
import { isToolAllowedForMode, FileRestrictionError, getFullModeDetails, modes } from "../modes"
14+
import { isToolAllowedForMode, FileRestrictionError, getFullModeDetails, modes, getModeSelection } from "../modes"
1515
import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions"
1616

1717
describe("isToolAllowedForMode", () => {
@@ -371,3 +371,209 @@ describe("FileRestrictionError", () => {
371371
expect(error.name).toBe("FileRestrictionError")
372372
})
373373
})
374+
375+
describe("getModeSelection", () => {
376+
const builtInAskMode = modes.find((m) => m.slug === "ask")!
377+
const customModesList: ModeConfig[] = [
378+
{
379+
slug: "code", // Override
380+
name: "Custom Code Mode",
381+
roleDefinition: "Custom Code Role",
382+
customInstructions: "Custom Code Instructions",
383+
groups: ["read"],
384+
},
385+
{
386+
slug: "new-custom",
387+
name: "New Custom Mode",
388+
roleDefinition: "New Custom Role",
389+
customInstructions: "New Custom Instructions",
390+
groups: ["edit"],
391+
},
392+
]
393+
394+
const promptComponentCode: PromptComponent = {
395+
roleDefinition: "Prompt Component Code Role",
396+
customInstructions: "Prompt Component Code Instructions",
397+
}
398+
399+
const promptComponentAsk: PromptComponent = {
400+
roleDefinition: "Prompt Component Ask Role",
401+
customInstructions: "Prompt Component Ask Instructions",
402+
}
403+
404+
test("should return built-in mode details if no overrides", () => {
405+
const selection = getModeSelection("ask")
406+
expect(selection.roleDefinition).toBe(builtInAskMode.roleDefinition)
407+
expect(selection.baseInstructions).toBe(builtInAskMode.customInstructions || "")
408+
})
409+
410+
test("should prioritize promptComponent for built-in mode if no custom mode exists for that slug", () => {
411+
const selection = getModeSelection("ask", promptComponentAsk) // "ask" is not in customModesList
412+
expect(selection.roleDefinition).toBe(promptComponentAsk.roleDefinition)
413+
expect(selection.baseInstructions).toBe(promptComponentAsk.customInstructions)
414+
})
415+
416+
test("should prioritize customMode over built-in mode", () => {
417+
const selection = getModeSelection("code", undefined, customModesList)
418+
const customCode = customModesList.find((m) => m.slug === "code")!
419+
expect(selection.roleDefinition).toBe(customCode.roleDefinition)
420+
expect(selection.baseInstructions).toBe(customCode.customInstructions)
421+
})
422+
423+
test("should prioritize customMode over promptComponent and built-in mode", () => {
424+
const selection = getModeSelection("code", promptComponentCode, customModesList)
425+
const customCode = customModesList.find((m) => m.slug === "code")!
426+
expect(selection.roleDefinition).toBe(customCode.roleDefinition)
427+
expect(selection.baseInstructions).toBe(customCode.customInstructions)
428+
})
429+
430+
test("should return new custom mode details if it exists", () => {
431+
const selection = getModeSelection("new-custom", undefined, customModesList)
432+
const newCustom = customModesList.find((m) => m.slug === "new-custom")!
433+
expect(selection.roleDefinition).toBe(newCustom.roleDefinition)
434+
expect(selection.baseInstructions).toBe(newCustom.customInstructions)
435+
})
436+
437+
test("customMode takes precedence for a new custom mode even if promptComponent is provided", () => {
438+
const promptComponentNew: PromptComponent = {
439+
roleDefinition: "Prompt New Custom Role",
440+
customInstructions: "Prompt New Custom Instructions",
441+
}
442+
const selection = getModeSelection("new-custom", promptComponentNew, customModesList)
443+
const newCustomMode = customModesList.find((m) => m.slug === "new-custom")!
444+
expect(selection.roleDefinition).toBe(newCustomMode.roleDefinition)
445+
expect(selection.baseInstructions).toBe(newCustomMode.customInstructions)
446+
})
447+
448+
test("should return empty strings if slug does not exist in custom, prompt, or built-in modes", () => {
449+
const selection = getModeSelection("non-existent-mode", undefined, customModesList)
450+
expect(selection.roleDefinition).toBe("")
451+
expect(selection.baseInstructions).toBe("")
452+
})
453+
454+
test("customMode's properties are used if customMode exists, ignoring promptComponent's properties", () => {
455+
const selection = getModeSelection(
456+
"code",
457+
{ roleDefinition: "Prompt Role Only", customInstructions: "Prompt Instructions Only" },
458+
customModesList,
459+
)
460+
const customCodeMode = customModesList.find((m) => m.slug === "code")!
461+
expect(selection.roleDefinition).toBe(customCodeMode.roleDefinition) // Takes from customCodeMode
462+
expect(selection.baseInstructions).toBe(customCodeMode.customInstructions) // Takes from customCodeMode
463+
})
464+
465+
test("handles undefined customInstructions in customMode gracefully", () => {
466+
const modesWithoutCustomInstructions: ModeConfig[] = [
467+
{
468+
slug: "no-instr",
469+
name: "No Instructions Mode",
470+
roleDefinition: "Role for no instructions",
471+
groups: ["read"],
472+
// customInstructions is undefined
473+
},
474+
]
475+
const selection = getModeSelection("no-instr", undefined, modesWithoutCustomInstructions)
476+
expect(selection.roleDefinition).toBe("Role for no instructions")
477+
expect(selection.baseInstructions).toBe("") // Defaults to empty string
478+
})
479+
480+
test("handles empty or undefined roleDefinition in customMode gracefully", () => {
481+
const modesWithEmptyRoleDef: ModeConfig[] = [
482+
{
483+
slug: "empty-role",
484+
name: "Empty Role Mode",
485+
roleDefinition: "",
486+
customInstructions: "Instructions for empty role",
487+
groups: ["read"],
488+
},
489+
]
490+
const selection = getModeSelection("empty-role", undefined, modesWithEmptyRoleDef)
491+
expect(selection.roleDefinition).toBe("")
492+
expect(selection.baseInstructions).toBe("Instructions for empty role")
493+
494+
const modesWithUndefinedRoleDef: ModeConfig[] = [
495+
{
496+
slug: "undefined-role",
497+
name: "Undefined Role Mode",
498+
roleDefinition: "", // Test undefined explicitly by using an empty string
499+
customInstructions: "Instructions for undefined role",
500+
groups: ["read"],
501+
},
502+
]
503+
const selection2 = getModeSelection("undefined-role", undefined, modesWithUndefinedRoleDef)
504+
expect(selection2.roleDefinition).toBe("")
505+
expect(selection2.baseInstructions).toBe("Instructions for undefined role")
506+
})
507+
508+
test("customMode's defined properties take precedence, undefined ones in customMode result in ''", () => {
509+
const customModeRoleOnlyList: ModeConfig[] = [
510+
// Renamed for clarity
511+
{
512+
slug: "role-custom",
513+
name: "Role Custom",
514+
roleDefinition: "Custom Role Only",
515+
groups: ["read"] /* customInstructions undefined */,
516+
},
517+
]
518+
const promptComponentInstrOnly: PromptComponent = { customInstructions: "Prompt Instructions Only" }
519+
// "role-custom" exists in customModeRoleOnlyList
520+
const selection = getModeSelection("role-custom", promptComponentInstrOnly, customModeRoleOnlyList)
521+
// customMode is chosen.
522+
expect(selection.roleDefinition).toBe("Custom Role Only") // From customMode
523+
expect(selection.baseInstructions).toBe("") // From customMode (undefined || '' -> '')
524+
})
525+
526+
test("customMode's defined properties take precedence, empty string ones in customMode are used", () => {
527+
const customModeInstrOnlyList: ModeConfig[] = [
528+
// Renamed for clarity
529+
{
530+
slug: "instr-custom",
531+
name: "Instr Custom",
532+
roleDefinition: "", // Explicitly empty
533+
customInstructions: "Custom Instructions Only",
534+
groups: ["read"],
535+
},
536+
]
537+
const promptComponentRoleOnly: PromptComponent = { roleDefinition: "Prompt Role Only" }
538+
// "instr-custom" exists in customModeInstrOnlyList
539+
const selection = getModeSelection("instr-custom", promptComponentRoleOnly, customModeInstrOnlyList)
540+
// customMode is chosen
541+
expect(selection.roleDefinition).toBe("") // From customMode ( "" || '' -> "")
542+
expect(selection.baseInstructions).toBe("Custom Instructions Only") // From customMode
543+
})
544+
545+
test("customMode with empty/undefined fields takes precedence over promptComponent and builtInMode", () => {
546+
const customModeMinimal: ModeConfig[] = [
547+
{ slug: "ask", name: "Custom Ask Minimal", roleDefinition: "", groups: ["read"] }, // roleDef empty, customInstr undefined
548+
]
549+
const promptComponentMinimal: PromptComponent = {
550+
roleDefinition: "Prompt Min Role",
551+
customInstructions: "Prompt Min Instr",
552+
}
553+
// "ask" is in customModeMinimal
554+
const selection = getModeSelection("ask", promptComponentMinimal, customModeMinimal)
555+
// customMode is chosen
556+
expect(selection.roleDefinition).toBe("") // From customModeMinimal
557+
expect(selection.baseInstructions).toBe("") // From customModeMinimal
558+
})
559+
560+
test("promptComponent is used if customMode for slug does not exist, even if customModesList is provided", () => {
561+
// 'ask' is not in customModesList, but 'code' and 'new-custom' are.
562+
const selection = getModeSelection("ask", promptComponentAsk, customModesList)
563+
expect(selection.roleDefinition).toBe(promptComponentAsk.roleDefinition)
564+
expect(selection.baseInstructions).toBe(promptComponentAsk.customInstructions)
565+
})
566+
567+
test("builtInMode is used if customMode for slug does not exist and promptComponent is not provided", () => {
568+
// 'ask' is not in customModesList
569+
const selection = getModeSelection("ask", undefined, customModesList)
570+
expect(selection.roleDefinition).toBe(builtInAskMode.roleDefinition)
571+
expect(selection.baseInstructions).toBe(builtInAskMode.customInstructions || "")
572+
})
573+
574+
test("promptComponent is used if customMode is not provided (undefined customModesList)", () => {
575+
const selection = getModeSelection("ask", promptComponentAsk, undefined)
576+
expect(selection.roleDefinition).toBe(promptComponentAsk.roleDefinition)
577+
expect(selection.baseInstructions).toBe(promptComponentAsk.customInstructions)
578+
})
579+
})

src/shared/modes.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import * as vscode from "vscode"
22

3-
import type { GroupOptions, GroupEntry, ModeConfig, CustomModePrompts, ExperimentId, ToolGroup } from "@roo-code/types"
3+
import type {
4+
GroupOptions,
5+
GroupEntry,
6+
ModeConfig,
7+
CustomModePrompts,
8+
ExperimentId,
9+
ToolGroup,
10+
PromptComponent,
11+
} from "@roo-code/types"
412

513
import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
614

@@ -149,6 +157,34 @@ export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean
149157
return !!customModes?.some((mode) => mode.slug === slug)
150158
}
151159

160+
/**
161+
* Find a mode by its slug, don't fall back to built-in modes
162+
*/
163+
export function findModeBySlug(slug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined {
164+
return modes?.find((mode) => mode.slug === slug)
165+
}
166+
167+
/**
168+
* Get the mode selection based on the provided mode slug, prompt component, and custom modes.
169+
* If a custom mode is found, it takes precedence over the built-in modes.
170+
* If no custom mode is found, the built-in mode is used.
171+
* If neither is found, the default mode is used.
172+
*/
173+
export function getModeSelection(mode: string, promptComponent?: PromptComponent, customModes?: ModeConfig[]) {
174+
const customMode = findModeBySlug(mode, customModes)
175+
const builtInMode = findModeBySlug(mode, modes)
176+
177+
const modeToUse = customMode || promptComponent || builtInMode
178+
179+
const roleDefinition = modeToUse?.roleDefinition || ""
180+
const baseInstructions = modeToUse?.customInstructions || ""
181+
182+
return {
183+
roleDefinition,
184+
baseInstructions,
185+
}
186+
}
187+
152188
// Custom error class for file restrictions
153189
export class FileRestrictionError extends Error {
154190
constructor(mode: string, pattern: string, description: string | undefined, filePath: string) {

webview-ui/src/components/prompts/PromptsView.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import { ChevronsUpDown, X } from "lucide-react"
1111

1212
import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types"
1313

14-
import { Mode, getRoleDefinition, getWhenToUse, getCustomInstructions, getAllModes } from "@roo/modes"
14+
import {
15+
Mode,
16+
getRoleDefinition,
17+
getWhenToUse,
18+
getCustomInstructions,
19+
getAllModes,
20+
findModeBySlug as findCustomModeBySlug,
21+
} from "@roo/modes"
1522
import { supportPrompt, SupportPromptType } from "@roo/support-prompt"
1623
import { TOOL_GROUPS } from "@roo/tools"
1724

@@ -133,9 +140,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
133140
// Helper function to find a mode by slug
134141
const findModeBySlug = useCallback(
135142
(searchSlug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined => {
136-
if (!modes) return undefined
137-
const isModeWithSlug = (mode: ModeConfig): mode is ModeConfig => mode.slug === searchSlug
138-
return modes.find(isModeWithSlug)
143+
return findCustomModeBySlug(searchSlug, modes)
139144
},
140145
[],
141146
)

0 commit comments

Comments
 (0)