mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 04:41:16 -05:00
Add mode-specific custom instructions
This commit is contained in:
@@ -789,7 +789,15 @@ export class Cline {
|
||||
browserViewportSize,
|
||||
mode,
|
||||
customPrompts
|
||||
) + await addCustomInstructions(this.customInstructions ?? '', cwd, preferredLanguage)
|
||||
) + await addCustomInstructions(
|
||||
{
|
||||
customInstructions: this.customInstructions,
|
||||
customPrompts,
|
||||
preferredLanguage
|
||||
},
|
||||
cwd,
|
||||
mode
|
||||
)
|
||||
|
||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||
if (previousApiReqIndex >= 0) {
|
||||
|
||||
@@ -131,7 +131,7 @@ Detailed commit message with multiple lines
|
||||
await openMention("/path/to/file")
|
||||
expect(mockExecuteCommand).not.toHaveBeenCalled()
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled()
|
||||
expect(mockShowErrorMessage).toHaveBeenCalledWith("Could not open file!")
|
||||
expect(mockShowErrorMessage).toHaveBeenCalledWith("Could not open file: File does not exist")
|
||||
|
||||
await openMention("problems")
|
||||
expect(mockExecuteCommand).toHaveBeenCalledWith("workbench.actions.view.problems")
|
||||
|
||||
@@ -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,
|
||||
) => {
|
||||
|
||||
@@ -246,15 +246,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
customPrompts,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
} = await this.getState()
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
effectiveInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
@@ -266,15 +273,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
customPrompts,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
} = await this.getState()
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
effectiveInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
undefined,
|
||||
@@ -379,6 +393,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
async (message: WebviewMessage) => {
|
||||
switch (message.type) {
|
||||
case "webviewDidLaunch":
|
||||
|
||||
this.postStateToWebview()
|
||||
this.workspaceTracker?.initializeFilePaths() // don't await
|
||||
getTheme().then((theme) =>
|
||||
@@ -572,7 +587,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
openImage(message.text!)
|
||||
break
|
||||
case "openFile":
|
||||
openFile(message.text!)
|
||||
openFile(message.text!, message.values as { create?: boolean; content?: string })
|
||||
break
|
||||
case "openMention":
|
||||
openMention(message.text)
|
||||
@@ -732,30 +747,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "updateEnhancedPrompt":
|
||||
if (message.text !== undefined) {
|
||||
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
enhance: message.text
|
||||
}
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
const currentState = await this.getState()
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts
|
||||
}
|
||||
|
||||
// Post state with prompts
|
||||
this.view?.webview.postMessage({
|
||||
type: "state",
|
||||
state: stateWithPrompts
|
||||
})
|
||||
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
enhance: message.text
|
||||
}
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
const currentState = await this.getState()
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts
|
||||
}
|
||||
|
||||
// Post state with prompts
|
||||
this.view?.webview.postMessage({
|
||||
type: "state",
|
||||
state: stateWithPrompts
|
||||
})
|
||||
break
|
||||
case "updatePrompt":
|
||||
if (message.promptMode && message.customPrompt !== undefined) {
|
||||
@@ -893,15 +906,23 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
const { apiConfiguration, customPrompts, customInstructions, preferredLanguage, browserViewportSize, mcpEnabled } = await this.getState()
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ''
|
||||
|
||||
const fullPrompt = await SYSTEM_PROMPT(
|
||||
const mode = message.mode ?? codeMode
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions, customPrompts, preferredLanguage },
|
||||
cwd,
|
||||
mode
|
||||
)
|
||||
|
||||
const systemPrompt = await SYSTEM_PROMPT(
|
||||
cwd,
|
||||
apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false,
|
||||
mcpEnabled ? this.mcpHub : undefined,
|
||||
undefined,
|
||||
browserViewportSize ?? "900x600",
|
||||
message.mode,
|
||||
mode,
|
||||
customPrompts
|
||||
) + await addCustomInstructions(customInstructions ?? '', cwd, preferredLanguage)
|
||||
)
|
||||
const fullPrompt = instructions ? `${systemPrompt}${instructions}` : systemPrompt
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "systemPrompt",
|
||||
|
||||
@@ -130,19 +130,25 @@ jest.mock('../../../integrations/workspace/WorkspaceTracker', () => {
|
||||
})
|
||||
|
||||
// Mock Cline
|
||||
jest.mock('../../Cline', () => {
|
||||
return {
|
||||
Cline: jest.fn().mockImplementation(() => ({
|
||||
abortTask: jest.fn(),
|
||||
handleWebviewAskResponse: jest.fn(),
|
||||
clineMessages: [],
|
||||
apiConversationHistory: [],
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: 'test-task-id'
|
||||
}))
|
||||
}
|
||||
})
|
||||
jest.mock('../../Cline', () => ({
|
||||
Cline: jest.fn().mockImplementation((
|
||||
provider,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
taskId
|
||||
) => ({
|
||||
abortTask: jest.fn(),
|
||||
handleWebviewAskResponse: jest.fn(),
|
||||
clineMessages: [],
|
||||
apiConversationHistory: [],
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: taskId || 'test-task-id'
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock extract-text
|
||||
jest.mock('../../../integrations/misc/extract-text', () => ({
|
||||
@@ -571,6 +577,82 @@ describe('ClineProvider', () => {
|
||||
expect(state.customPrompts).toEqual({})
|
||||
})
|
||||
|
||||
test('uses mode-specific custom instructions in Cline initialization', async () => {
|
||||
// Setup mock state
|
||||
const modeCustomInstructions = 'Code mode instructions';
|
||||
const mockApiConfig = {
|
||||
apiProvider: 'openrouter',
|
||||
openRouterModelInfo: { supportsComputerUse: true }
|
||||
};
|
||||
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: mockApiConfig,
|
||||
customPrompts: {
|
||||
code: { customInstructions: modeCustomInstructions }
|
||||
},
|
||||
mode: 'code',
|
||||
diffEnabled: true,
|
||||
fuzzyMatchThreshold: 1.0
|
||||
} as any);
|
||||
|
||||
// Reset Cline mock
|
||||
const { Cline } = require('../../Cline');
|
||||
(Cline as jest.Mock).mockClear();
|
||||
|
||||
// Initialize Cline with a task
|
||||
await provider.initClineWithTask('Test task');
|
||||
|
||||
// Verify Cline was initialized with mode-specific instructions
|
||||
expect(Cline).toHaveBeenCalledWith(
|
||||
provider,
|
||||
mockApiConfig,
|
||||
modeCustomInstructions,
|
||||
true,
|
||||
1.0,
|
||||
'Test task',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
test('handles mode-specific custom instructions updates', async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock existing prompts
|
||||
const existingPrompts = {
|
||||
code: {
|
||||
roleDefinition: 'Code role',
|
||||
customInstructions: 'Old instructions'
|
||||
}
|
||||
}
|
||||
mockContext.globalState.get = jest.fn((key: string) => {
|
||||
if (key === 'customPrompts') {
|
||||
return existingPrompts
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Update custom instructions for code mode
|
||||
await messageHandler({
|
||||
type: 'updatePrompt',
|
||||
promptMode: 'code',
|
||||
customPrompt: {
|
||||
roleDefinition: 'Code role',
|
||||
customInstructions: 'New instructions'
|
||||
}
|
||||
})
|
||||
|
||||
// Verify state was updated correctly
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith(
|
||||
'customPrompts',
|
||||
{
|
||||
code: {
|
||||
roleDefinition: 'Code role',
|
||||
customInstructions: 'New instructions'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('saves mode config when updating API configuration', async () => {
|
||||
// Setup mock context with mode and config name
|
||||
mockContext = {
|
||||
@@ -848,5 +930,79 @@ describe('ClineProvider', () => {
|
||||
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Failed to get system prompt')
|
||||
})
|
||||
|
||||
test('uses mode-specific custom instructions in system prompt', async () => {
|
||||
const systemPrompt = require('../../prompts/system')
|
||||
const { addCustomInstructions } = systemPrompt
|
||||
|
||||
// Mock getState to return mode-specific custom instructions
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
openRouterModelInfo: { supportsComputerUse: true }
|
||||
},
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode specific instructions' }
|
||||
},
|
||||
mode: 'code',
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: '900x600'
|
||||
} as any)
|
||||
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
await messageHandler({ type: 'getSystemPrompt', mode: 'code' })
|
||||
|
||||
// Verify addCustomInstructions was called with mode-specific instructions
|
||||
expect(addCustomInstructions).toHaveBeenCalledWith(
|
||||
{
|
||||
customInstructions: undefined,
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode specific instructions' }
|
||||
},
|
||||
preferredLanguage: undefined
|
||||
},
|
||||
expect.any(String),
|
||||
'code'
|
||||
)
|
||||
})
|
||||
|
||||
test('uses correct mode-specific instructions when mode is specified', async () => {
|
||||
const systemPrompt = require('../../prompts/system')
|
||||
const { addCustomInstructions } = systemPrompt
|
||||
|
||||
// Mock getState to return instructions for multiple modes
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
openRouterModelInfo: { supportsComputerUse: true }
|
||||
},
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode instructions' },
|
||||
architect: { customInstructions: 'Architect mode instructions' }
|
||||
},
|
||||
mode: 'code',
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: '900x600'
|
||||
} as any)
|
||||
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Request architect mode prompt
|
||||
await messageHandler({ type: 'getSystemPrompt', mode: 'architect' })
|
||||
|
||||
// Verify architect mode instructions were used
|
||||
expect(addCustomInstructions).toHaveBeenCalledWith(
|
||||
{
|
||||
customInstructions: undefined,
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode instructions' },
|
||||
architect: { customInstructions: 'Architect mode instructions' }
|
||||
},
|
||||
preferredLanguage: undefined
|
||||
},
|
||||
expect.any(String),
|
||||
'architect'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,11 +20,41 @@ export async function openImage(dataUri: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function openFile(absolutePath: string) {
|
||||
try {
|
||||
const uri = vscode.Uri.file(absolutePath)
|
||||
interface OpenFileOptions {
|
||||
create?: boolean;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
// Check if the document is already open in a tab group that's not in the active editor's column. If it is, then close it (if not dirty) so that we don't duplicate tabs
|
||||
export async function openFile(filePath: string, options: OpenFileOptions = {}) {
|
||||
try {
|
||||
// Get workspace root
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
throw new Error('No workspace root found')
|
||||
}
|
||||
|
||||
// If path starts with ./, resolve it relative to workspace root
|
||||
const fullPath = filePath.startsWith('./') ?
|
||||
path.join(workspaceRoot, filePath.slice(2)) :
|
||||
filePath
|
||||
|
||||
const uri = vscode.Uri.file(fullPath)
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await vscode.workspace.fs.stat(uri)
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
if (!options.create) {
|
||||
throw new Error('File does not exist')
|
||||
}
|
||||
|
||||
// Create with provided content or empty string
|
||||
const content = options.content || ''
|
||||
await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8'))
|
||||
}
|
||||
|
||||
// Check if the document is already open in a tab group that's not in the active editor's column
|
||||
try {
|
||||
for (const group of vscode.window.tabGroups.all) {
|
||||
const existingTab = group.tabs.find(
|
||||
@@ -47,6 +77,10 @@ export async function openFile(absolutePath: string) {
|
||||
const document = await vscode.workspace.openTextDocument(uri)
|
||||
await vscode.window.showTextDocument(document, { preview: false })
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Could not open file!`)
|
||||
if (error instanceof Error) {
|
||||
vscode.window.showErrorMessage(`Could not open file: ${error.message}`)
|
||||
} else {
|
||||
vscode.window.showErrorMessage(`Could not open file!`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export type Mode = typeof codeMode | typeof architectMode | typeof askMode;
|
||||
|
||||
export type PromptComponent = {
|
||||
roleDefinition?: string;
|
||||
customInstructions?: string;
|
||||
}
|
||||
|
||||
export type CustomPrompts = {
|
||||
|
||||
Reference in New Issue
Block a user