mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-22 05:11:06 -05:00
Custom modes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,62 @@
|
||||
import { SYSTEM_PROMPT, addCustomInstructions } from "../system"
|
||||
import { SYSTEM_PROMPT } from "../system"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
import { McpServer } from "../../../shared/mcp"
|
||||
import { ClineProvider } from "../../../core/webview/ClineProvider"
|
||||
import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-replace"
|
||||
import * as vscode from "vscode"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import { defaultModeSlug, modes } from "../../../shared/modes"
|
||||
// Import path utils to get access to toPosix string extension
|
||||
import "../../../utils/path"
|
||||
import { addCustomInstructions } from "../sections/custom-instructions"
|
||||
import * as modesSection from "../sections/modes"
|
||||
|
||||
// Mock the sections
|
||||
jest.mock("../sections/modes", () => ({
|
||||
getModesSection: jest.fn().mockImplementation(async () => `====\n\nMODES\n\n- Test modes section`),
|
||||
}))
|
||||
|
||||
jest.mock("../sections/custom-instructions", () => ({
|
||||
addCustomInstructions: jest
|
||||
.fn()
|
||||
.mockImplementation(async (modeCustomInstructions, globalCustomInstructions, cwd, mode, options) => {
|
||||
const sections = []
|
||||
|
||||
// Add language preference if provided
|
||||
if (options?.preferredLanguage) {
|
||||
sections.push(
|
||||
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Add global instructions first
|
||||
if (globalCustomInstructions?.trim()) {
|
||||
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
|
||||
}
|
||||
|
||||
// Add mode-specific instructions after
|
||||
if (modeCustomInstructions?.trim()) {
|
||||
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions}`)
|
||||
}
|
||||
|
||||
// Add rules
|
||||
const rules = []
|
||||
if (mode) {
|
||||
rules.push(`# Rules from .clinerules-${mode}:\nMock mode-specific rules`)
|
||||
}
|
||||
rules.push(`# Rules from .clinerules:\nMock generic rules`)
|
||||
|
||||
if (rules.length > 0) {
|
||||
sections.push(`Rules:\n${rules.join("\n")}`)
|
||||
}
|
||||
|
||||
const joinedSections = sections.join("\n\n")
|
||||
return joinedSections
|
||||
? `\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.\n\n${joinedSections}`
|
||||
: ""
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock environment-specific values for consistent tests
|
||||
jest.mock("os", () => ({
|
||||
@@ -19,42 +68,38 @@ jest.mock("default-shell", () => "/bin/bash")
|
||||
|
||||
jest.mock("os-name", () => () => "Linux")
|
||||
|
||||
// Mock fs.readFile to return empty mcpServers config and mock rules files
|
||||
jest.mock("fs/promises", () => ({
|
||||
...jest.requireActual("fs/promises"),
|
||||
readFile: jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith("mcpSettings.json")) {
|
||||
return '{"mcpServers": {}}'
|
||||
}
|
||||
if (path.endsWith(".clinerules-code")) {
|
||||
return "# Code Mode Rules\n1. Code specific rule"
|
||||
}
|
||||
if (path.endsWith(".clinerules-ask")) {
|
||||
return "# Ask Mode Rules\n1. Ask specific rule"
|
||||
}
|
||||
if (path.endsWith(".clinerules-architect")) {
|
||||
return "# Architect Mode Rules\n1. Architect specific rule"
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
}),
|
||||
writeFile: jest.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
// Create a mock ExtensionContext
|
||||
const mockContext = {
|
||||
extensionPath: "/mock/extension/path",
|
||||
globalStoragePath: "/mock/storage/path",
|
||||
storagePath: "/mock/storage/path",
|
||||
logPath: "/mock/log/path",
|
||||
subscriptions: [],
|
||||
workspaceState: {
|
||||
get: () => undefined,
|
||||
update: () => Promise.resolve(),
|
||||
},
|
||||
globalState: {
|
||||
get: () => undefined,
|
||||
update: () => Promise.resolve(),
|
||||
setKeysForSync: () => {},
|
||||
},
|
||||
extensionUri: { fsPath: "/mock/extension/path" },
|
||||
globalStorageUri: { fsPath: "/mock/settings/path" },
|
||||
asAbsolutePath: (relativePath: string) => `/mock/extension/path/${relativePath}`,
|
||||
extension: {
|
||||
packageJSON: {
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
// Create a minimal mock of ClineProvider
|
||||
const mockProvider = {
|
||||
ensureMcpServersDirectoryExists: async () => "/mock/mcp/path",
|
||||
ensureSettingsDirectoryExists: async () => "/mock/settings/path",
|
||||
postMessageToWebview: async () => {},
|
||||
context: {
|
||||
extension: {
|
||||
packageJSON: {
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
context: mockContext,
|
||||
} as unknown as ClineProvider
|
||||
|
||||
// Instead of extending McpHub, create a mock that implements just what we need
|
||||
@@ -77,6 +122,26 @@ const createMockMcpHub = (): McpHub =>
|
||||
describe("SYSTEM_PROMPT", () => {
|
||||
let mockMcpHub: McpHub
|
||||
|
||||
beforeAll(() => {
|
||||
// Ensure fs mock is properly initialized
|
||||
const mockFs = jest.requireMock("fs/promises")
|
||||
mockFs._setInitialMockData()
|
||||
|
||||
// Initialize all required directories
|
||||
const dirs = [
|
||||
"/mock",
|
||||
"/mock/extension",
|
||||
"/mock/extension/path",
|
||||
"/mock/storage",
|
||||
"/mock/storage/path",
|
||||
"/mock/settings",
|
||||
"/mock/settings/path",
|
||||
"/mock/mcp",
|
||||
"/mock/mcp/path",
|
||||
]
|
||||
dirs.forEach((dir) => mockFs._mockDirectories.add(dir))
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
@@ -90,18 +155,32 @@ describe("SYSTEM_PROMPT", () => {
|
||||
|
||||
it("should maintain consistent system prompt", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include browser actions when supportsComputerUse is true", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", true, undefined, undefined, "1280x800")
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
true, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
"1280x800", // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
@@ -109,18 +188,32 @@ describe("SYSTEM_PROMPT", () => {
|
||||
it("should include MCP server info when mcpHub is provided", async () => {
|
||||
mockMcpHub = createMockMcpHub()
|
||||
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, mockMcpHub)
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
mockMcpHub, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should explicitly handle undefined mcpHub", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false,
|
||||
false, // supportsComputerUse
|
||||
undefined, // explicitly undefined mcpHub
|
||||
undefined,
|
||||
undefined,
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -128,11 +221,15 @@ describe("SYSTEM_PROMPT", () => {
|
||||
|
||||
it("should handle different browser viewport sizes", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
true, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
"900x600", // different viewport size
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -140,187 +237,198 @@ describe("SYSTEM_PROMPT", () => {
|
||||
|
||||
it("should include diff strategy tool description", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false,
|
||||
undefined,
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
|
||||
undefined,
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include custom mode role definition at top and instructions at bottom", async () => {
|
||||
const modeCustomInstructions = "Custom mode instructions"
|
||||
const customModes = [
|
||||
{
|
||||
slug: "custom-mode",
|
||||
name: "Custom Mode",
|
||||
roleDefinition: "Custom role definition",
|
||||
customInstructions: modeCustomInstructions,
|
||||
groups: ["read"] as const,
|
||||
},
|
||||
]
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
"custom-mode", // mode
|
||||
undefined, // customPrompts
|
||||
customModes, // customModes
|
||||
"Global instructions", // globalCustomInstructions
|
||||
)
|
||||
|
||||
// Role definition should be at the top
|
||||
expect(prompt.indexOf("Custom role definition")).toBeLessThan(prompt.indexOf("TOOL USE"))
|
||||
|
||||
// Custom instructions should be at the bottom
|
||||
const customInstructionsIndex = prompt.indexOf("Custom mode instructions")
|
||||
const userInstructionsHeader = prompt.indexOf("USER'S CUSTOM INSTRUCTIONS")
|
||||
expect(customInstructionsIndex).toBeGreaterThan(-1)
|
||||
expect(userInstructionsHeader).toBeGreaterThan(-1)
|
||||
expect(customInstructionsIndex).toBeGreaterThan(userInstructionsHeader)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
|
||||
describe("addCustomInstructions", () => {
|
||||
beforeAll(() => {
|
||||
// Ensure fs mock is properly initialized
|
||||
const mockFs = jest.requireMock("fs/promises")
|
||||
mockFs._setInitialMockData()
|
||||
mockFs.mkdir.mockImplementation(async (path: string) => {
|
||||
if (path.startsWith("/test")) {
|
||||
mockFs._mockDirectories.add(path)
|
||||
return Promise.resolve()
|
||||
}
|
||||
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`)
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should generate correct prompt for architect mode", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "architect")
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
"architect", // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct prompt for ask mode", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "ask")
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
"ask", // mode
|
||||
undefined, // customPrompts
|
||||
undefined, // customModes
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific rules for code mode", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path", defaultModeSlug)
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific rules for ask mode", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path", modes[2].slug)
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", modes[2].slug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific rules for architect mode", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path", modes[1].slug)
|
||||
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", modes[1].slug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific rules for test engineer mode", async () => {
|
||||
// Mock readFile to include test engineer rules
|
||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith(".clinerules-test")) {
|
||||
return "# Test Engineer Rules\n1. Always write tests first\n2. Get approval before modifying non-test code"
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
||||
|
||||
const instructions = await addCustomInstructions({}, "/test/path", "test")
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", "test")
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific rules for code reviewer mode", async () => {
|
||||
// Mock readFile to include code reviewer rules
|
||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith(".clinerules-review")) {
|
||||
return "# Code Reviewer Rules\n1. Provide specific examples in feedback\n2. Focus on maintainability and best practices"
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
||||
|
||||
const instructions = await addCustomInstructions({}, "/test/path", "review")
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", "review")
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should fall back to generic rules when mode-specific rules not found", async () => {
|
||||
// Mock readFile to return ENOENT for mode-specific file
|
||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
||||
if (
|
||||
path.endsWith(".clinerules-code") ||
|
||||
path.endsWith(".clinerules-test") ||
|
||||
path.endsWith(".clinerules-review")
|
||||
) {
|
||||
const error = new Error("ENOENT") as NodeJS.ErrnoException
|
||||
error.code = "ENOENT"
|
||||
throw error
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
||||
|
||||
const instructions = await addCustomInstructions({}, "/test/path", defaultModeSlug)
|
||||
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include preferred language when provided", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ preferredLanguage: "Spanish" },
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, {
|
||||
preferredLanguage: "Spanish",
|
||||
})
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include custom instructions when provided", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: "Custom test instructions" },
|
||||
"/test/path",
|
||||
)
|
||||
|
||||
const instructions = await addCustomInstructions("Custom test instructions", "", "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should combine all custom instructions", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: "Custom test instructions",
|
||||
preferredLanguage: "French",
|
||||
},
|
||||
"Custom test instructions",
|
||||
"",
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
{ preferredLanguage: "French" },
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should handle undefined mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path")
|
||||
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should trim mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: " Custom mode instructions " },
|
||||
" Custom mode instructions ",
|
||||
"",
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should handle empty mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions({ customInstructions: "" }, "/test/path")
|
||||
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should combine global and mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: "Global instructions",
|
||||
customPrompts: {
|
||||
code: { customInstructions: "Mode-specific instructions" },
|
||||
},
|
||||
},
|
||||
"Mode-specific instructions",
|
||||
"Global instructions",
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific instructions after global ones", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: "First instruction",
|
||||
customPrompts: {
|
||||
code: { customInstructions: "Second instruction" },
|
||||
},
|
||||
},
|
||||
"Second instruction",
|
||||
"First instruction",
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
|
||||
@@ -23,28 +23,70 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(
|
||||
customInstructions: string,
|
||||
modeCustomInstructions: string,
|
||||
globalCustomInstructions: string,
|
||||
cwd: string,
|
||||
preferredLanguage?: string,
|
||||
mode: string,
|
||||
options: { preferredLanguage?: string } = {},
|
||||
): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd)
|
||||
const allInstructions = []
|
||||
const sections = []
|
||||
|
||||
if (preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
|
||||
// Load mode-specific rules if mode is provided
|
||||
let modeRuleContent = ""
|
||||
if (mode) {
|
||||
try {
|
||||
const modeRuleFile = `.clinerules-${mode}`
|
||||
const content = await fs.readFile(path.join(cwd, modeRuleFile), "utf-8")
|
||||
if (content.trim()) {
|
||||
modeRuleContent = content.trim()
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (customInstructions.trim()) {
|
||||
allInstructions.push(customInstructions.trim())
|
||||
// Add language preference if provided
|
||||
if (options.preferredLanguage) {
|
||||
sections.push(
|
||||
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (ruleFileContent && ruleFileContent.trim()) {
|
||||
allInstructions.push(ruleFileContent.trim())
|
||||
// Add global instructions first
|
||||
if (typeof globalCustomInstructions === "string" && globalCustomInstructions.trim()) {
|
||||
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
|
||||
}
|
||||
|
||||
const joinedInstructions = allInstructions.join("\n\n")
|
||||
// Add mode-specific instructions after
|
||||
if (typeof modeCustomInstructions === "string" && modeCustomInstructions.trim()) {
|
||||
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions.trim()}`)
|
||||
}
|
||||
|
||||
return joinedInstructions
|
||||
// Add rules - include both mode-specific and generic rules if they exist
|
||||
const rules = []
|
||||
|
||||
// Add mode-specific rules first if they exist
|
||||
if (modeRuleContent && modeRuleContent.trim()) {
|
||||
const modeRuleFile = `.clinerules-${mode}`
|
||||
rules.push(`# Rules from ${modeRuleFile}:\n${modeRuleContent}`)
|
||||
}
|
||||
|
||||
// Add generic rules
|
||||
const genericRuleContent = await loadRuleFiles(cwd)
|
||||
if (genericRuleContent && genericRuleContent.trim()) {
|
||||
rules.push(genericRuleContent.trim())
|
||||
}
|
||||
|
||||
if (rules.length > 0) {
|
||||
sections.push(`Rules:\n\n${rules.join("\n\n")}`)
|
||||
}
|
||||
|
||||
const joinedSections = sections.join("\n\n")
|
||||
|
||||
return joinedSections
|
||||
? `
|
||||
====
|
||||
|
||||
@@ -52,6 +94,6 @@ USER'S CUSTOM INSTRUCTIONS
|
||||
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||
|
||||
${joinedInstructions}`
|
||||
${joinedSections}`
|
||||
: ""
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export { getSharedToolUseSection } from "./tool-use"
|
||||
export { getMcpServersSection } from "./mcp-servers"
|
||||
export { getToolUseGuidelinesSection } from "./tool-use-guidelines"
|
||||
export { getCapabilitiesSection } from "./capabilities"
|
||||
export { getModesSection } from "./modes"
|
||||
|
||||
45
src/core/prompts/sections/modes.ts
Normal file
45
src/core/prompts/sections/modes.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { promises as fs } from "fs"
|
||||
import { modes, ModeConfig } from "../../../shared/modes"
|
||||
|
||||
export async function getModesSection(context: vscode.ExtensionContext): Promise<string> {
|
||||
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
|
||||
await fs.mkdir(settingsDir, { recursive: true })
|
||||
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
|
||||
|
||||
return `====
|
||||
|
||||
MODES
|
||||
|
||||
- When referring to modes, always use their display names. The built-in modes are:
|
||||
${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
|
||||
Custom modes will be referred to by their configured name property.
|
||||
|
||||
- Custom modes can be configured by creating or editing the custom modes file at '${customModesPath}'. The following fields are required and must not be empty:
|
||||
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
|
||||
* name: The display name for the mode
|
||||
* roleDefinition: A detailed description of the mode's role and capabilities
|
||||
* groups: Array of allowed tool groups (can be empty)
|
||||
|
||||
The customInstructions field is optional.
|
||||
|
||||
The file should follow this structure:
|
||||
{
|
||||
"customModes": [
|
||||
{
|
||||
"slug": "designer", // Required: unique slug with lowercase letters, numbers, and hyphens
|
||||
"name": "Designer", // Required: mode display name
|
||||
"roleDefinition": "You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:\n- Creating and maintaining design systems\n- Implementing responsive and accessible web interfaces\n- Working with CSS, HTML, and modern frontend frameworks\n- Ensuring consistent user experiences across platforms", // Required: non-empty
|
||||
"groups": [ // Required: array of tool groups (can be empty)
|
||||
"read", // Read files group (read_file, search_files, list_files, list_code_definition_names)
|
||||
"edit", // Edit files group (write_to_file, apply_diff)
|
||||
"browser", // Browser group (browser_action)
|
||||
"command", // Command group (execute_command)
|
||||
"mcp" // MCP group (use_mcp_tool, access_mcp_resource)
|
||||
],
|
||||
"customInstructions": "Additional instructions for the Designer mode" // Optional
|
||||
}
|
||||
]
|
||||
}`
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { modes, ModeConfig } from "../../../shared/modes"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
|
||||
export function getRulesSection(cwd: string, supportsComputerUse: boolean, diffStrategy?: DiffStrategy): string {
|
||||
export function getRulesSection(
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
diffStrategy?: DiffStrategy,
|
||||
context?: vscode.ExtensionContext,
|
||||
): string {
|
||||
const settingsDir = context ? path.join(context.globalStorageUri.fsPath, "settings") : "<settings directory>"
|
||||
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
|
||||
return `====
|
||||
|
||||
RULES
|
||||
@@ -11,7 +21,11 @@ RULES
|
||||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
${diffStrategy ? "- You should use apply_diff instead of write_to_file when making changes to existing files since it is much faster and easier to apply a diff than to write the entire file again. Only use write_to_file to edit files when apply_diff has failed repeatedly to apply the diff." : "- When you want to modify a file, use the write_to_file tool directly with the desired content. You do not need to display the content before using the tool."}
|
||||
${
|
||||
diffStrategy
|
||||
? "- You should use apply_diff instead of write_to_file when making changes to existing files since it is much faster and easier to apply a diff than to write the entire file again. Only use write_to_file to edit files when apply_diff has failed repeatedly to apply the diff."
|
||||
: "- When you want to modify a file, use the write_to_file tool directly with the desired content. You do not need to display the content before using the tool."
|
||||
}
|
||||
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
|
||||
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import defaultShell from "default-shell"
|
||||
import os from "os"
|
||||
import osName from "os-name"
|
||||
import { Mode, ModeConfig, getModeBySlug, defaultModeSlug, isToolAllowedForMode } from "../../../shared/modes"
|
||||
|
||||
export function getSystemInfoSection(cwd: string): string {
|
||||
return `====
|
||||
export function getSystemInfoSection(cwd: string, currentMode: Mode, customModes?: ModeConfig[]): string {
|
||||
const findModeBySlug = (slug: string, modes?: ModeConfig[]) => modes?.find((m) => m.slug === slug)
|
||||
|
||||
const currentModeName = findModeBySlug(currentMode, customModes)?.name || currentMode
|
||||
const codeModeName = findModeBySlug(defaultModeSlug, customModes)?.name || "Code"
|
||||
|
||||
let details = `====
|
||||
|
||||
SYSTEM INFORMATION
|
||||
|
||||
@@ -13,4 +19,6 @@ Home Directory: ${os.homedir().toPosix()}
|
||||
Current Working Directory: ${cwd.toPosix()}
|
||||
|
||||
When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current working directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.`
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { Mode, modes, CustomPrompts, PromptComponent, getRoleDefinition, defaultModeSlug } from "../../shared/modes"
|
||||
import {
|
||||
Mode,
|
||||
modes,
|
||||
CustomPrompts,
|
||||
PromptComponent,
|
||||
getRoleDefinition,
|
||||
defaultModeSlug,
|
||||
ModeConfig,
|
||||
getModeBySlug,
|
||||
} from "../../shared/modes"
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
import * as vscode from "vscode"
|
||||
import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
@@ -10,88 +20,14 @@ import {
|
||||
getMcpServersSection,
|
||||
getToolUseGuidelinesSection,
|
||||
getCapabilitiesSection,
|
||||
getModesSection,
|
||||
addCustomInstructions,
|
||||
} from "./sections"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
async function loadRuleFiles(cwd: string, mode: Mode): Promise<string> {
|
||||
let combinedRules = ""
|
||||
|
||||
// First try mode-specific rules
|
||||
const modeSpecificFile = `.clinerules-${mode}`
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, modeSpecificFile), "utf-8")
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${modeSpecificFile}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Then try generic rules files
|
||||
const genericRuleFiles = [".clinerules"]
|
||||
for (const file of genericRuleFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, file), "utf-8")
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return combinedRules
|
||||
}
|
||||
|
||||
interface State {
|
||||
customInstructions?: string
|
||||
customPrompts?: CustomPrompts
|
||||
preferredLanguage?: string
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(state: State, cwd: string, mode: Mode = defaultModeSlug): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd, mode)
|
||||
const allInstructions = []
|
||||
|
||||
if (state.preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${state.preferredLanguage} language.`)
|
||||
}
|
||||
|
||||
if (state.customInstructions?.trim()) {
|
||||
allInstructions.push(state.customInstructions.trim())
|
||||
}
|
||||
|
||||
const customPrompt = state.customPrompts?.[mode]
|
||||
if (typeof customPrompt === "object" && customPrompt?.customInstructions?.trim()) {
|
||||
allInstructions.push(customPrompt.customInstructions.trim())
|
||||
}
|
||||
|
||||
if (ruleFileContent && ruleFileContent.trim()) {
|
||||
allInstructions.push(ruleFileContent.trim())
|
||||
}
|
||||
|
||||
const joinedInstructions = allInstructions.join("\n\n")
|
||||
|
||||
return joinedInstructions
|
||||
? `
|
||||
====
|
||||
|
||||
USER'S CUSTOM INSTRUCTIONS
|
||||
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||
|
||||
${joinedInstructions}`
|
||||
: ""
|
||||
}
|
||||
|
||||
async function generatePrompt(
|
||||
context: vscode.ExtensionContext,
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mode: Mode,
|
||||
@@ -99,29 +35,57 @@ async function generatePrompt(
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
promptComponent?: PromptComponent,
|
||||
customModeConfigs?: ModeConfig[],
|
||||
globalCustomInstructions?: string,
|
||||
): Promise<string> {
|
||||
const basePrompt = `${promptComponent?.roleDefinition || getRoleDefinition(mode)}
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
}
|
||||
|
||||
const [mcpServersSection, modesSection] = await Promise.all([
|
||||
getMcpServersSection(mcpHub, diffStrategy),
|
||||
getModesSection(context),
|
||||
])
|
||||
|
||||
// Get the full mode config to ensure we have the role definition
|
||||
const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0]
|
||||
const roleDefinition = modeConfig.roleDefinition
|
||||
|
||||
const basePrompt = `${roleDefinition}
|
||||
|
||||
${getSharedToolUseSection()}
|
||||
|
||||
${getToolDescriptionsForMode(mode, cwd, supportsComputerUse, diffStrategy, browserViewportSize, mcpHub)}
|
||||
${getToolDescriptionsForMode(
|
||||
mode,
|
||||
cwd,
|
||||
supportsComputerUse,
|
||||
diffStrategy,
|
||||
browserViewportSize,
|
||||
mcpHub,
|
||||
customModeConfigs,
|
||||
)}
|
||||
|
||||
${getToolUseGuidelinesSection()}
|
||||
|
||||
${await getMcpServersSection(mcpHub, diffStrategy)}
|
||||
${mcpServersSection}
|
||||
|
||||
${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)}
|
||||
|
||||
${getRulesSection(cwd, supportsComputerUse, diffStrategy)}
|
||||
${modesSection}
|
||||
|
||||
${getSystemInfoSection(cwd)}
|
||||
${getRulesSection(cwd, supportsComputerUse, diffStrategy, context)}
|
||||
|
||||
${getObjectiveSection()}`
|
||||
${getSystemInfoSection(cwd, mode, customModeConfigs)}
|
||||
|
||||
${getObjectiveSection()}
|
||||
|
||||
${await addCustomInstructions(modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, {})}`
|
||||
|
||||
return basePrompt
|
||||
}
|
||||
|
||||
export const SYSTEM_PROMPT = async (
|
||||
context: vscode.ExtensionContext,
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
@@ -129,7 +93,13 @@ export const SYSTEM_PROMPT = async (
|
||||
browserViewportSize?: string,
|
||||
mode: Mode = defaultModeSlug,
|
||||
customPrompts?: CustomPrompts,
|
||||
) => {
|
||||
customModes?: ModeConfig[],
|
||||
globalCustomInstructions?: string,
|
||||
): Promise<string> => {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
}
|
||||
|
||||
const getPromptComponent = (value: unknown) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return value as PromptComponent
|
||||
@@ -137,11 +107,13 @@ export const SYSTEM_PROMPT = async (
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Use default mode if not found
|
||||
const currentMode = modes.find((m) => m.slug === mode) || modes[0]
|
||||
const promptComponent = getPromptComponent(customPrompts?.[currentMode.slug])
|
||||
// Check if it's a custom mode
|
||||
const promptComponent = getPromptComponent(customPrompts?.[mode])
|
||||
// Get full mode config from custom modes or fall back to built-in modes
|
||||
const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
|
||||
|
||||
return generatePrompt(
|
||||
context,
|
||||
cwd,
|
||||
supportsComputerUse,
|
||||
currentMode.slug,
|
||||
@@ -149,5 +121,7 @@ export const SYSTEM_PROMPT = async (
|
||||
diffStrategy,
|
||||
browserViewportSize,
|
||||
promptComponent,
|
||||
customModes,
|
||||
globalCustomInstructions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import { getUseMcpToolDescription } from "./use-mcp-tool"
|
||||
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
import { Mode, ToolName, getModeConfig, isToolAllowedForMode } from "../../../shared/modes"
|
||||
import { Mode, ModeConfig, getModeConfig, isToolAllowedForMode } from "../../../shared/modes"
|
||||
import { ToolName, getToolName, getToolOptions, TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tool-groups"
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
// Map of tool names to their description functions
|
||||
@@ -38,8 +39,9 @@ export function getToolDescriptionsForMode(
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
mcpHub?: McpHub,
|
||||
customModes?: ModeConfig[],
|
||||
): string {
|
||||
const config = getModeConfig(mode)
|
||||
const config = getModeConfig(mode, customModes)
|
||||
const args: ToolArgs = {
|
||||
cwd,
|
||||
supportsComputerUse,
|
||||
@@ -48,16 +50,27 @@ export function getToolDescriptionsForMode(
|
||||
mcpHub,
|
||||
}
|
||||
|
||||
// Map tool descriptions in the exact order specified in the mode's tools array
|
||||
const descriptions = config.tools.map(([toolName, toolOptions]) => {
|
||||
// Get all tools from the mode's groups and always available tools
|
||||
const tools = new Set<string>()
|
||||
|
||||
// Add tools from mode's groups
|
||||
config.groups.forEach((group) => {
|
||||
TOOL_GROUPS[group].forEach((tool) => tools.add(tool))
|
||||
})
|
||||
|
||||
// Add always available tools
|
||||
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
|
||||
|
||||
// Map tool descriptions for all allowed tools
|
||||
const descriptions = Array.from(tools).map((toolName) => {
|
||||
const descriptionFn = toolDescriptionMap[toolName]
|
||||
if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode)) {
|
||||
if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode, customModes ?? [])) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return descriptionFn({
|
||||
...args,
|
||||
toolOptions,
|
||||
toolOptions: undefined, // No tool options in group-based approach
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Mode } from "../../shared/modes"
|
||||
|
||||
export type { Mode }
|
||||
|
||||
export type ToolName =
|
||||
| "execute_command"
|
||||
| "read_file"
|
||||
| "write_to_file"
|
||||
| "apply_diff"
|
||||
| "search_files"
|
||||
| "list_files"
|
||||
| "list_code_definition_names"
|
||||
| "browser_action"
|
||||
| "use_mcp_tool"
|
||||
| "access_mcp_resource"
|
||||
| "ask_followup_question"
|
||||
| "attempt_completion"
|
||||
|
||||
export const CODE_TOOLS: ToolName[] = [
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
]
|
||||
|
||||
export const ARCHITECT_TOOLS: ToolName[] = [
|
||||
"read_file",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
]
|
||||
|
||||
export const ASK_TOOLS: ToolName[] = [
|
||||
"read_file",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
]
|
||||
Reference in New Issue
Block a user