Skip to content

Commit 2698f78

Browse files
committed
fix: ensure correct precedence for roleDefinition and customInstructions
1 parent dfacdb3 commit 2698f78

File tree

3 files changed

+204
-7
lines changed

3 files changed

+204
-7
lines changed

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: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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,178 @@ describe("FileRestrictionError", () => {
371371
expect(error.name).toBe("FileRestrictionError")
372372
})
373373
})
374+
375+
describe("getModeSelection", () => {
376+
const builtInAskMode = modes.find((m) => m.slug === "ask")!
377+
378+
const customModesList: ModeConfig[] = [
379+
{
380+
slug: "code", // Override
381+
name: "Custom Code Mode",
382+
roleDefinition: "Custom Code Role",
383+
customInstructions: "Custom Code Instructions",
384+
groups: ["read"],
385+
},
386+
{
387+
slug: "new-custom",
388+
name: "New Custom Mode",
389+
roleDefinition: "New Custom Role",
390+
customInstructions: "New Custom Instructions",
391+
groups: ["edit"],
392+
},
393+
]
394+
395+
const promptComponentCode: PromptComponent = {
396+
roleDefinition: "Prompt Component Code Role",
397+
customInstructions: "Prompt Component Code Instructions",
398+
}
399+
400+
const promptComponentAsk: PromptComponent = {
401+
roleDefinition: "Prompt Component Ask Role",
402+
customInstructions: "Prompt Component Ask Instructions",
403+
}
404+
405+
test("should return built-in mode details if no overrides", () => {
406+
const selection = getModeSelection("ask")
407+
expect(selection.roleDefinition).toBe(builtInAskMode.roleDefinition)
408+
expect(selection.baseInstructions).toBe(builtInAskMode.customInstructions || "")
409+
})
410+
411+
test("should prioritize promptComponent for built-in mode", () => {
412+
const selection = getModeSelection("ask", promptComponentAsk)
413+
expect(selection.roleDefinition).toBe(promptComponentAsk.roleDefinition)
414+
expect(selection.baseInstructions).toBe(promptComponentAsk.customInstructions)
415+
})
416+
417+
test("should prioritize customMode over built-in mode", () => {
418+
const selection = getModeSelection("code", undefined, customModesList)
419+
const customCode = customModesList.find((m) => m.slug === "code")!
420+
expect(selection.roleDefinition).toBe(customCode.roleDefinition)
421+
expect(selection.baseInstructions).toBe(customCode.customInstructions)
422+
})
423+
424+
test("should prioritize customMode over promptComponent and built-in mode when all define properties", () => {
425+
const selection = getModeSelection("code", promptComponentCode, customModesList)
426+
const customCode = customModesList.find((m) => m.slug === "code")!
427+
expect(selection.roleDefinition).toBe(customCode.roleDefinition)
428+
expect(selection.baseInstructions).toBe(customCode.customInstructions)
429+
})
430+
431+
test("should return new custom mode details", () => {
432+
const selection = getModeSelection("new-custom", undefined, customModesList)
433+
const newCustom = customModesList.find((m) => m.slug === "new-custom")!
434+
expect(selection.roleDefinition).toBe(newCustom.roleDefinition)
435+
expect(selection.baseInstructions).toBe(newCustom.customInstructions)
436+
})
437+
438+
test("customMode properties take precedence for new custom mode even with promptComponent", () => {
439+
const promptComponentNew: PromptComponent = {
440+
roleDefinition: "Prompt New Custom Role",
441+
customInstructions: "Prompt New Custom Instructions",
442+
}
443+
const selection = getModeSelection("new-custom", promptComponentNew, customModesList)
444+
const newCustomMode = customModesList.find((m) => m.slug === "new-custom")!
445+
expect(selection.roleDefinition).toBe(newCustomMode.roleDefinition)
446+
expect(selection.baseInstructions).toBe(newCustomMode.customInstructions)
447+
})
448+
449+
test("should fallback to default (first) mode if slug does not exist", () => {
450+
const selection = getModeSelection("non-existent-mode", undefined, customModesList)
451+
expect(selection.roleDefinition).toBe(modes[0].roleDefinition)
452+
expect(selection.baseInstructions).toBe(modes[0].customInstructions || "")
453+
})
454+
455+
test("customMode.roleDefinition is used if defined, ignoring promptComponent's; customMode.customInstructions also from customMode", () => {
456+
const selection = getModeSelection("code", { roleDefinition: "Prompt Role Only" }, customModesList)
457+
const customCodeMode = customModesList.find((m) => m.slug === "code")!
458+
expect(selection.roleDefinition).toBe(customCodeMode.roleDefinition)
459+
expect(selection.baseInstructions).toBe(customCodeMode.customInstructions)
460+
})
461+
462+
test("customMode.customInstructions is used if defined, ignoring promptComponent's; customMode.roleDefinition also from customMode", () => {
463+
const selection = getModeSelection("code", { customInstructions: "Prompt Instructions Only" }, customModesList)
464+
const customCodeMode = customModesList.find((m) => m.slug === "code")!
465+
expect(selection.roleDefinition).toBe(customCodeMode.roleDefinition)
466+
expect(selection.baseInstructions).toBe(customCodeMode.customInstructions)
467+
})
468+
469+
test("customMode takes precedence over built-in when no promptComponent", () => {
470+
const selection = getModeSelection("code", undefined, customModesList)
471+
expect(selection.roleDefinition).toBe(customModesList.find((m) => m.slug === "code")!.roleDefinition)
472+
expect(selection.baseInstructions).toBe(customModesList.find((m) => m.slug === "code")!.customInstructions)
473+
})
474+
475+
test("handles undefined customInstructions in modes gracefully", () => {
476+
const modesWithoutCustomInstructions: ModeConfig[] = [
477+
{
478+
slug: "no-instr",
479+
name: "No Instructions Mode",
480+
roleDefinition: "Role for no instructions",
481+
groups: ["read"],
482+
// customInstructions is undefined
483+
},
484+
]
485+
const selection = getModeSelection("no-instr", undefined, modesWithoutCustomInstructions)
486+
expect(selection.roleDefinition).toBe("Role for no instructions")
487+
expect(selection.baseInstructions).toBe("") // Should default to empty string
488+
})
489+
490+
test("handles undefined roleDefinition in modes gracefully by falling back", () => {
491+
const modesWithoutRoleDef: ModeConfig[] = [
492+
{
493+
slug: "no-role",
494+
name: "No Role Mode",
495+
roleDefinition: "", // Ensure roleDefinition is present
496+
customInstructions: "Instructions for no role",
497+
groups: ["read"],
498+
},
499+
]
500+
// Since 'no-role' is a custom mode not overriding a built-in, and it lacks a role,
501+
// the logic in getModeSelection might fall back if not handled.
502+
// The current getModeBySlug logic in getModeSelection will fetch this mode.
503+
// Then, isCustom will be true.
504+
// roleDefinition = modeConfig?.roleDefinition (undefined) || promptComponent?.roleDefinition (undefined) || ""
505+
// So it should become ""
506+
const selection = getModeSelection("no-role", undefined, modesWithoutRoleDef)
507+
expect(selection.roleDefinition).toBe("")
508+
expect(selection.baseInstructions).toBe("Instructions for no role")
509+
})
510+
511+
test("promptComponent fills customInstructions if customMode's is undefined", () => {
512+
const customModeRoleOnly: ModeConfig[] = [
513+
{ slug: "role-custom", name: "Role Custom", roleDefinition: "Custom Role Only", groups: ["read"] },
514+
]
515+
const promptComponentInstrOnly: PromptComponent = { customInstructions: "Prompt Instructions Only" }
516+
const selection = getModeSelection("role-custom", promptComponentInstrOnly, customModeRoleOnly)
517+
expect(selection.roleDefinition).toBe("Custom Role Only")
518+
expect(selection.baseInstructions).toBe("Prompt Instructions Only")
519+
})
520+
521+
test("promptComponent fills roleDefinition if customMode's is undefined", () => {
522+
const customModeInstrOnly: ModeConfig[] = [
523+
{
524+
slug: "instr-custom",
525+
name: "Instr Custom",
526+
roleDefinition: "", // Added to satisfy ModeConfig type
527+
customInstructions: "Custom Instructions Only",
528+
groups: ["read"],
529+
},
530+
]
531+
const promptComponentRoleOnly: PromptComponent = { roleDefinition: "Prompt Role Only" }
532+
const selection = getModeSelection("instr-custom", promptComponentRoleOnly, customModeInstrOnly)
533+
expect(selection.roleDefinition).toBe("Prompt Role Only")
534+
expect(selection.baseInstructions).toBe("Custom Instructions Only")
535+
})
536+
537+
test("builtInMode fills properties if customMode and promptComponent's are undefined", () => {
538+
const customModeMinimal: ModeConfig[] = [
539+
{ slug: "ask", name: "Custom Ask Minimal", roleDefinition: "", groups: ["read"] }, // No roleDef or customInstr
540+
]
541+
const promptComponentMinimal: PromptComponent = {} // No roleDef or customInstr
542+
const selection = getModeSelection("ask", promptComponentMinimal, customModeMinimal)
543+
544+
// According to original logic, if customMode provides "", that takes precedence.
545+
expect(selection.roleDefinition).toBe("") // Was builtInAskMode.roleDefinition
546+
expect(selection.baseInstructions).toBe("") // Was builtInAskMode.customInstructions || ""
547+
})
548+
})

src/shared/modes.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,23 @@ export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean
149149
return !!customModes?.some((mode) => mode.slug === slug)
150150
}
151151

152+
export function getModeSelection(mode: string, promptComponent?: PromptComponent, customModes?: ModeConfig[]) {
153+
const modeConfig = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
154+
const isCustom = isCustomMode(mode, customModes)
155+
const roleDefinition = isCustom
156+
? modeConfig?.roleDefinition || promptComponent?.roleDefinition || ""
157+
: promptComponent?.roleDefinition || modeConfig.roleDefinition || ""
158+
159+
const baseInstructions = isCustom
160+
? modeConfig?.customInstructions || promptComponent?.customInstructions || ""
161+
: promptComponent?.customInstructions || modeConfig.customInstructions || ""
162+
163+
return {
164+
roleDefinition,
165+
baseInstructions,
166+
}
167+
}
168+
152169
// Custom error class for file restrictions
153170
export class FileRestrictionError extends Error {
154171
constructor(mode: string, pattern: string, description: string | undefined, filePath: string) {

0 commit comments

Comments
 (0)