From 0c67a8303b7453564ccf0df18fb08f1fed099240 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 22 Jan 2026 16:44:35 -0500 Subject: [PATCH] feat: add systemPrompt property to ModeConfig for custom system prompt override --- packages/types/src/mode.ts | 6 + .../prompts/__tests__/system-prompt.spec.ts | 171 ++++++++++++++++++ src/core/prompts/system.ts | 28 +++ 3 files changed, 205 insertions(+) diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index c02c47c1345..8ce6faa31a6 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -70,6 +70,12 @@ export const modeConfigSchema = z.object({ customInstructions: z.string().optional(), groups: groupEntryArraySchema, source: z.enum(["global", "project"]).optional(), + /** + * When provided, replaces the middle sections of the generated system prompt + * (CAPABILITIES, TOOL USE, OBJECTIVE, etc.) with this custom content. + * The final prompt will be: roleDefinition + systemPrompt + customInstructions + */ + systemPrompt: z.string().optional(), }) export type ModeConfig = z.infer diff --git a/src/core/prompts/__tests__/system-prompt.spec.ts b/src/core/prompts/__tests__/system-prompt.spec.ts index 79641029296..34ef5787ace 100644 --- a/src/core/prompts/__tests__/system-prompt.spec.ts +++ b/src/core/prompts/__tests__/system-prompt.spec.ts @@ -747,6 +747,177 @@ describe("SYSTEM_PROMPT", () => { expect(prompt).toContain("OBJECTIVE") }) + describe("systemPrompt override", () => { + it("should use systemPrompt from mode config when provided", async () => { + const customSystemPrompt = "This is a custom system prompt that replaces the default sections." + + const customModes: ModeConfig[] = [ + { + slug: "minimal-mode", + name: "Minimal Mode", + roleDefinition: "You are a minimal assistant.", + customInstructions: "Be concise.", + groups: ["mcp"] as const, + systemPrompt: customSystemPrompt, + }, + ] + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + "minimal-mode", // mode + undefined, // customModePrompts + customModes, // customModes + "Global instructions", // globalCustomInstructions + undefined, // diffEnabled + experiments, + true, // enableMcpServerCreation + undefined, // language + undefined, // rooIgnoreInstructions + undefined, // partialReadsEnabled + ) + + // Should contain the custom systemPrompt + expect(prompt).toContain(customSystemPrompt) + + // Should contain role definition at the top + expect(prompt).toContain("You are a minimal assistant.") + expect(prompt.indexOf("You are a minimal assistant.")).toBeLessThan(prompt.indexOf(customSystemPrompt)) + + // Should NOT contain the default sections (TOOL USE, CAPABILITIES, OBJECTIVE, etc.) + expect(prompt).not.toContain("TOOL USE") + expect(prompt).not.toContain("CAPABILITIES") + expect(prompt).not.toContain("OBJECTIVE") + expect(prompt).not.toContain("MARKDOWN RULES") + }) + + it("should include customInstructions when using systemPrompt override", async () => { + const customSystemPrompt = "Custom system prompt content." + const modeCustomInstructions = "Be concise." + + const customModes: ModeConfig[] = [ + { + slug: "minimal-mode", + name: "Minimal Mode", + roleDefinition: "You are a minimal assistant.", + customInstructions: modeCustomInstructions, + groups: ["mcp"] as const, + systemPrompt: customSystemPrompt, + }, + ] + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + "minimal-mode", // mode + undefined, // customModePrompts + customModes, // customModes + "Global instructions", // globalCustomInstructions + undefined, // diffEnabled + experiments, + true, // enableMcpServerCreation + undefined, // language + undefined, // rooIgnoreInstructions + undefined, // partialReadsEnabled + ) + + // Should contain the customInstructions + expect(prompt).toContain(modeCustomInstructions) + + // Custom instructions should come after the systemPrompt + expect(prompt.indexOf(customSystemPrompt)).toBeLessThan(prompt.indexOf(modeCustomInstructions)) + }) + + it("should use default sections when systemPrompt is not provided", async () => { + const customModes: ModeConfig[] = [ + { + slug: "regular-mode", + name: "Regular Mode", + roleDefinition: "You are a regular assistant.", + customInstructions: "Follow all guidelines.", + groups: ["read"] as const, + // No systemPrompt provided + }, + ] + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + "regular-mode", // mode + undefined, // customModePrompts + customModes, // customModes + undefined, // globalCustomInstructions + undefined, // diffEnabled + experiments, + true, // enableMcpServerCreation + undefined, // language + undefined, // rooIgnoreInstructions + undefined, // partialReadsEnabled + ) + + // Should contain the default sections + expect(prompt).toContain("TOOL USE") + expect(prompt).toContain("CAPABILITIES") + expect(prompt).toContain("OBJECTIVE") + }) + + it("should prioritize file-based system prompt over config systemPrompt", async () => { + // Mock fs to return a file-based system prompt + const fsMock = vi.mocked(await import("fs/promises")) + fsMock.readFile.mockResolvedValueOnce("File-based system prompt content.") + + const customSystemPrompt = "Config-based system prompt content." + + const customModes: ModeConfig[] = [ + { + slug: "priority-mode", + name: "Priority Mode", + roleDefinition: "You are a priority assistant.", + customInstructions: "Test instructions.", + groups: ["read"] as const, + systemPrompt: customSystemPrompt, + }, + ] + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + "priority-mode", // mode + undefined, // customModePrompts + customModes, // customModes + undefined, // globalCustomInstructions + undefined, // diffEnabled + experiments, + true, // enableMcpServerCreation + undefined, // language + undefined, // rooIgnoreInstructions + undefined, // partialReadsEnabled + ) + + // Should contain the file-based system prompt + expect(prompt).toContain("File-based system prompt content.") + + // Should NOT contain the config-based systemPrompt + expect(prompt).not.toContain(customSystemPrompt) + }) + }) + afterAll(() => { vi.restoreAllMocks() }) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 01f99570fbd..e743e946aa3 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -189,6 +189,34 @@ export const SYSTEM_PROMPT = async ( ${fileCustomSystemPrompt} +${customInstructions}` + } + + // If the mode config has a systemPrompt defined, use it as custom middle content + if (currentMode.systemPrompt) { + const { roleDefinition, baseInstructions: baseInstructionsForConfig } = getModeSelection( + mode, + promptComponent, + customModes, + ) + + const customInstructions = await addCustomInstructions( + baseInstructionsForConfig, + globalCustomInstructions || "", + cwd, + mode, + { + language: language ?? formatLanguage(vscode.env.language), + rooIgnoreInstructions, + settings, + }, + ) + + // For config-based systemPrompt, use the simplified structure + return `${roleDefinition} + +${currentMode.systemPrompt} + ${customInstructions}` }