Add mode-specific custom instructions

This commit is contained in:
Matt Rubens
2025-01-14 10:51:59 -05:00
parent 092a121a37
commit 365f4acf63
13 changed files with 841 additions and 204 deletions

View File

@@ -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

View File

@@ -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(() => {

View File

@@ -4,7 +4,6 @@ import {
getRulesSection,
getSystemInfoSection,
getObjectiveSection,
addCustomInstructions,
getSharedToolUseSection,
getMcpServersSection,
getToolUseGuidelinesSection,

View File

@@ -4,7 +4,6 @@ import {
getRulesSection,
getSystemInfoSection,
getObjectiveSection,
addCustomInstructions,
getSharedToolUseSection,
getMcpServersSection,
getToolUseGuidelinesSection,

View File

@@ -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,
) => {