mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 21:01:06 -05:00
Add mode-specific custom instructions
This commit is contained in:
@@ -2185,6 +2185,66 @@ Custom test instructions
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should combine global and mode-specific instructions 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
Global instructions
|
||||
|
||||
Mode-specific instructions
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should fall back to generic rules when mode-specific rules not found 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should handle empty mode-specific instructions 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should handle undefined mode-specific instructions 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should include custom instructions when provided 1`] = `
|
||||
"
|
||||
====
|
||||
@@ -2217,7 +2277,7 @@ You should always speak and think in the Spanish language.
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should include rules from .clinerules 1`] = `
|
||||
exports[`addCustomInstructions should prioritize mode-specific instructions after global ones 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
@@ -2225,6 +2285,80 @@ 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.
|
||||
|
||||
First instruction
|
||||
|
||||
Second instruction
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should prioritize mode-specific rules for architect mode 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
# Rules from .clinerules-architect:
|
||||
# Architect Mode Rules
|
||||
1. Architect specific rule
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should prioritize mode-specific rules for ask mode 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
# Rules from .clinerules-ask:
|
||||
# Ask Mode Rules
|
||||
1. Ask specific rule
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should prioritize mode-specific rules for code mode 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
# Rules from .clinerules-code:
|
||||
# Code Mode Rules
|
||||
1. Code specific rule
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
2. Second rule"
|
||||
`;
|
||||
|
||||
exports[`addCustomInstructions should trim mode-specific instructions 1`] = `
|
||||
"
|
||||
====
|
||||
|
||||
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.
|
||||
|
||||
Custom mode instructions
|
||||
|
||||
# Rules from .clinerules:
|
||||
# Test Rules
|
||||
1. First rule
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ClineProvider } from '../../../core/webview/ClineProvider'
|
||||
import { SearchReplaceDiffStrategy } from '../../../core/diff/strategies/search-replace'
|
||||
import fs from 'fs/promises'
|
||||
import os from 'os'
|
||||
import { codeMode, askMode, architectMode } from '../modes'
|
||||
// Import path utils to get access to toPosix string extension
|
||||
import '../../../utils/path'
|
||||
|
||||
@@ -18,13 +19,22 @@ jest.mock('default-shell', () => '/bin/bash')
|
||||
|
||||
jest.mock('os-name', () => () => 'Linux')
|
||||
|
||||
// Mock fs.readFile to return empty mcpServers config and mock .clinerules
|
||||
// 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'
|
||||
}
|
||||
@@ -159,42 +169,149 @@ describe('addCustomInstructions', () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should include preferred language when provided', async () => {
|
||||
const result = await addCustomInstructions(
|
||||
'',
|
||||
it('should prioritize mode-specific rules for code mode', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path',
|
||||
'Spanish'
|
||||
codeMode
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should prioritize mode-specific rules for ask mode', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path',
|
||||
askMode
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should prioritize mode-specific rules for architect mode', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path',
|
||||
architectMode
|
||||
)
|
||||
|
||||
expect(result).toMatchSnapshot()
|
||||
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')) {
|
||||
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',
|
||||
codeMode
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include preferred language when provided', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ preferredLanguage: 'Spanish' },
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include custom instructions when provided', async () => {
|
||||
const result = await addCustomInstructions(
|
||||
'Custom test instructions',
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: 'Custom test instructions' },
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include rules from .clinerules', async () => {
|
||||
const result = await addCustomInstructions(
|
||||
'',
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(result).toMatchSnapshot()
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should combine all custom instructions', async () => {
|
||||
const result = await addCustomInstructions(
|
||||
'Custom test instructions',
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: 'Custom test instructions',
|
||||
preferredLanguage: 'French'
|
||||
},
|
||||
'/test/path',
|
||||
'French'
|
||||
codeMode
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle undefined mode-specific instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(result).toMatchSnapshot()
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should trim mode-specific instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: ' Custom mode instructions ' },
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle empty mode-specific instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: '' },
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
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' }
|
||||
}
|
||||
},
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
|
||||
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' }
|
||||
}
|
||||
},
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
|
||||
const instructionParts = instructions.split('\n\n')
|
||||
const globalIndex = instructionParts.findIndex(part => part.includes('First instruction'))
|
||||
const modeSpecificIndex = instructionParts.findIndex(part => part.includes('Second instruction'))
|
||||
|
||||
expect(globalIndex).toBeLessThan(modeSpecificIndex)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
getObjectiveSection,
|
||||
addCustomInstructions,
|
||||
getSharedToolUseSection,
|
||||
getMcpServersSection,
|
||||
getToolUseGuidelinesSection,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
getObjectiveSection,
|
||||
addCustomInstructions,
|
||||
getSharedToolUseSection,
|
||||
getMcpServersSection,
|
||||
getToolUseGuidelinesSection,
|
||||
|
||||
@@ -8,11 +8,26 @@ import { CustomPrompts } from "../../shared/modes"
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
async function loadRuleFiles(cwd: string): Promise<string> {
|
||||
const ruleFiles = ['.clinerules', '.cursorrules', '.windsurfrules']
|
||||
async function loadRuleFiles(cwd: string, mode: Mode): Promise<string> {
|
||||
let combinedRules = ''
|
||||
|
||||
for (const file of ruleFiles) {
|
||||
// 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()) {
|
||||
@@ -29,16 +44,30 @@ async function loadRuleFiles(cwd: string): Promise<string> {
|
||||
return combinedRules
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(customInstructions: string, cwd: string, preferredLanguage?: string): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd)
|
||||
interface State {
|
||||
customInstructions?: string;
|
||||
customPrompts?: CustomPrompts;
|
||||
preferredLanguage?: string;
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(
|
||||
state: State,
|
||||
cwd: string,
|
||||
mode: Mode = codeMode
|
||||
): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd, mode)
|
||||
const allInstructions = []
|
||||
|
||||
if (preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
|
||||
if (state.preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${state.preferredLanguage} language.`)
|
||||
}
|
||||
|
||||
if (customInstructions.trim()) {
|
||||
allInstructions.push(customInstructions.trim())
|
||||
|
||||
if (state.customInstructions?.trim()) {
|
||||
allInstructions.push(state.customInstructions.trim())
|
||||
}
|
||||
|
||||
if (state.customPrompts?.[mode]?.customInstructions?.trim()) {
|
||||
allInstructions.push(state.customPrompts[mode].customInstructions.trim())
|
||||
}
|
||||
|
||||
if (ruleFileContent && ruleFileContent.trim()) {
|
||||
@@ -59,11 +88,11 @@ ${joinedInstructions}`
|
||||
}
|
||||
|
||||
export const SYSTEM_PROMPT = async (
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
mode: Mode = codeMode,
|
||||
customPrompts?: CustomPrompts,
|
||||
) => {
|
||||
|
||||
Reference in New Issue
Block a user