mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Add mode-specific custom instructions
This commit is contained in:
@@ -789,7 +789,15 @@ export class Cline {
|
|||||||
browserViewportSize,
|
browserViewportSize,
|
||||||
mode,
|
mode,
|
||||||
customPrompts
|
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 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) {
|
if (previousApiReqIndex >= 0) {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ Detailed commit message with multiple lines
|
|||||||
await openMention("/path/to/file")
|
await openMention("/path/to/file")
|
||||||
expect(mockExecuteCommand).not.toHaveBeenCalled()
|
expect(mockExecuteCommand).not.toHaveBeenCalled()
|
||||||
expect(mockOpenExternal).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")
|
await openMention("problems")
|
||||||
expect(mockExecuteCommand).toHaveBeenCalledWith("workbench.actions.view.problems")
|
expect(mockExecuteCommand).toHaveBeenCalledWith("workbench.actions.view.problems")
|
||||||
|
|||||||
@@ -2185,6 +2185,66 @@ Custom test instructions
|
|||||||
2. Second rule"
|
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`] = `
|
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"
|
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.
|
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:
|
# Rules from .clinerules:
|
||||||
# Test Rules
|
# Test Rules
|
||||||
1. First rule
|
1. First rule
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ClineProvider } from '../../../core/webview/ClineProvider'
|
|||||||
import { SearchReplaceDiffStrategy } from '../../../core/diff/strategies/search-replace'
|
import { SearchReplaceDiffStrategy } from '../../../core/diff/strategies/search-replace'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
|
import { codeMode, askMode, architectMode } from '../modes'
|
||||||
// Import path utils to get access to toPosix string extension
|
// Import path utils to get access to toPosix string extension
|
||||||
import '../../../utils/path'
|
import '../../../utils/path'
|
||||||
|
|
||||||
@@ -18,13 +19,22 @@ jest.mock('default-shell', () => '/bin/bash')
|
|||||||
|
|
||||||
jest.mock('os-name', () => () => 'Linux')
|
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.mock('fs/promises', () => ({
|
||||||
...jest.requireActual('fs/promises'),
|
...jest.requireActual('fs/promises'),
|
||||||
readFile: jest.fn().mockImplementation(async (path: string) => {
|
readFile: jest.fn().mockImplementation(async (path: string) => {
|
||||||
if (path.endsWith('mcpSettings.json')) {
|
if (path.endsWith('mcpSettings.json')) {
|
||||||
return '{"mcpServers": {}}'
|
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')) {
|
if (path.endsWith('.clinerules')) {
|
||||||
return '# Test Rules\n1. First rule\n2. Second rule'
|
return '# Test Rules\n1. First rule\n2. Second rule'
|
||||||
}
|
}
|
||||||
@@ -159,42 +169,149 @@ describe('addCustomInstructions', () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should include preferred language when provided', async () => {
|
it('should prioritize mode-specific rules for code mode', async () => {
|
||||||
const result = await addCustomInstructions(
|
const instructions = await addCustomInstructions(
|
||||||
'',
|
{},
|
||||||
'/test/path',
|
'/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 () => {
|
it('should include custom instructions when provided', async () => {
|
||||||
const result = await addCustomInstructions(
|
const instructions = await addCustomInstructions(
|
||||||
'Custom test instructions',
|
{ customInstructions: 'Custom test instructions' },
|
||||||
'/test/path'
|
'/test/path'
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
|
||||||
|
|
||||||
it('should include rules from .clinerules', async () => {
|
|
||||||
const result = await addCustomInstructions(
|
|
||||||
'',
|
|
||||||
'/test/path'
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result).toMatchSnapshot()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should combine all custom instructions', async () => {
|
it('should combine all custom instructions', async () => {
|
||||||
const result = await addCustomInstructions(
|
const instructions = await addCustomInstructions(
|
||||||
'Custom test instructions',
|
{
|
||||||
|
customInstructions: 'Custom test instructions',
|
||||||
|
preferredLanguage: 'French'
|
||||||
|
},
|
||||||
'/test/path',
|
'/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(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
getRulesSection,
|
getRulesSection,
|
||||||
getSystemInfoSection,
|
getSystemInfoSection,
|
||||||
getObjectiveSection,
|
getObjectiveSection,
|
||||||
addCustomInstructions,
|
|
||||||
getSharedToolUseSection,
|
getSharedToolUseSection,
|
||||||
getMcpServersSection,
|
getMcpServersSection,
|
||||||
getToolUseGuidelinesSection,
|
getToolUseGuidelinesSection,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
getRulesSection,
|
getRulesSection,
|
||||||
getSystemInfoSection,
|
getSystemInfoSection,
|
||||||
getObjectiveSection,
|
getObjectiveSection,
|
||||||
addCustomInstructions,
|
|
||||||
getSharedToolUseSection,
|
getSharedToolUseSection,
|
||||||
getMcpServersSection,
|
getMcpServersSection,
|
||||||
getToolUseGuidelinesSection,
|
getToolUseGuidelinesSection,
|
||||||
|
|||||||
@@ -8,11 +8,26 @@ import { CustomPrompts } from "../../shared/modes"
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
async function loadRuleFiles(cwd: string): Promise<string> {
|
async function loadRuleFiles(cwd: string, mode: Mode): Promise<string> {
|
||||||
const ruleFiles = ['.clinerules', '.cursorrules', '.windsurfrules']
|
|
||||||
let combinedRules = ''
|
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 {
|
try {
|
||||||
const content = await fs.readFile(path.join(cwd, file), 'utf-8')
|
const content = await fs.readFile(path.join(cwd, file), 'utf-8')
|
||||||
if (content.trim()) {
|
if (content.trim()) {
|
||||||
@@ -29,16 +44,30 @@ async function loadRuleFiles(cwd: string): Promise<string> {
|
|||||||
return combinedRules
|
return combinedRules
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addCustomInstructions(customInstructions: string, cwd: string, preferredLanguage?: string): Promise<string> {
|
interface State {
|
||||||
const ruleFileContent = await loadRuleFiles(cwd)
|
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 = []
|
const allInstructions = []
|
||||||
|
|
||||||
if (preferredLanguage) {
|
if (state.preferredLanguage) {
|
||||||
allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
|
allInstructions.push(`You should always speak and think in the ${state.preferredLanguage} language.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customInstructions.trim()) {
|
if (state.customInstructions?.trim()) {
|
||||||
allInstructions.push(customInstructions.trim())
|
allInstructions.push(state.customInstructions.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.customPrompts?.[mode]?.customInstructions?.trim()) {
|
||||||
|
allInstructions.push(state.customPrompts[mode].customInstructions.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ruleFileContent && ruleFileContent.trim()) {
|
if (ruleFileContent && ruleFileContent.trim()) {
|
||||||
@@ -59,11 +88,11 @@ ${joinedInstructions}`
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SYSTEM_PROMPT = async (
|
export const SYSTEM_PROMPT = async (
|
||||||
cwd: string,
|
cwd: string,
|
||||||
supportsComputerUse: boolean,
|
supportsComputerUse: boolean,
|
||||||
mcpHub?: McpHub,
|
mcpHub?: McpHub,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
mode: Mode = codeMode,
|
mode: Mode = codeMode,
|
||||||
customPrompts?: CustomPrompts,
|
customPrompts?: CustomPrompts,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -246,15 +246,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.clearTask()
|
await this.clearTask()
|
||||||
const {
|
const {
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
customPrompts,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
fuzzyMatchThreshold
|
fuzzyMatchThreshold,
|
||||||
|
mode,
|
||||||
|
customInstructions: globalInstructions,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
|
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||||
|
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n')
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
this,
|
this,
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
effectiveInstructions,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
fuzzyMatchThreshold,
|
fuzzyMatchThreshold,
|
||||||
task,
|
task,
|
||||||
@@ -266,15 +273,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.clearTask()
|
await this.clearTask()
|
||||||
const {
|
const {
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
customPrompts,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
fuzzyMatchThreshold
|
fuzzyMatchThreshold,
|
||||||
|
mode,
|
||||||
|
customInstructions: globalInstructions,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
|
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||||
|
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n')
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
this,
|
this,
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
effectiveInstructions,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
fuzzyMatchThreshold,
|
fuzzyMatchThreshold,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -379,6 +393,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
async (message: WebviewMessage) => {
|
async (message: WebviewMessage) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "webviewDidLaunch":
|
case "webviewDidLaunch":
|
||||||
|
|
||||||
this.postStateToWebview()
|
this.postStateToWebview()
|
||||||
this.workspaceTracker?.initializeFilePaths() // don't await
|
this.workspaceTracker?.initializeFilePaths() // don't await
|
||||||
getTheme().then((theme) =>
|
getTheme().then((theme) =>
|
||||||
@@ -572,7 +587,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
openImage(message.text!)
|
openImage(message.text!)
|
||||||
break
|
break
|
||||||
case "openFile":
|
case "openFile":
|
||||||
openFile(message.text!)
|
openFile(message.text!, message.values as { create?: boolean; content?: string })
|
||||||
break
|
break
|
||||||
case "openMention":
|
case "openMention":
|
||||||
openMention(message.text)
|
openMention(message.text)
|
||||||
@@ -732,30 +747,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
case "updateEnhancedPrompt":
|
case "updateEnhancedPrompt":
|
||||||
if (message.text !== undefined) {
|
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
||||||
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
|
||||||
|
|
||||||
const updatedPrompts = {
|
const updatedPrompts = {
|
||||||
...existingPrompts,
|
...existingPrompts,
|
||||||
enhance: message.text
|
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
break
|
||||||
case "updatePrompt":
|
case "updatePrompt":
|
||||||
if (message.promptMode && message.customPrompt !== undefined) {
|
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 { apiConfiguration, customPrompts, customInstructions, preferredLanguage, browserViewportSize, mcpEnabled } = await this.getState()
|
||||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ''
|
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,
|
cwd,
|
||||||
apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false,
|
apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false,
|
||||||
mcpEnabled ? this.mcpHub : undefined,
|
mcpEnabled ? this.mcpHub : undefined,
|
||||||
undefined,
|
undefined,
|
||||||
browserViewportSize ?? "900x600",
|
browserViewportSize ?? "900x600",
|
||||||
message.mode,
|
mode,
|
||||||
customPrompts
|
customPrompts
|
||||||
) + await addCustomInstructions(customInstructions ?? '', cwd, preferredLanguage)
|
)
|
||||||
|
const fullPrompt = instructions ? `${systemPrompt}${instructions}` : systemPrompt
|
||||||
|
|
||||||
await this.postMessageToWebview({
|
await this.postMessageToWebview({
|
||||||
type: "systemPrompt",
|
type: "systemPrompt",
|
||||||
|
|||||||
@@ -130,19 +130,25 @@ jest.mock('../../../integrations/workspace/WorkspaceTracker', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Mock Cline
|
// Mock Cline
|
||||||
jest.mock('../../Cline', () => {
|
jest.mock('../../Cline', () => ({
|
||||||
return {
|
Cline: jest.fn().mockImplementation((
|
||||||
Cline: jest.fn().mockImplementation(() => ({
|
provider,
|
||||||
abortTask: jest.fn(),
|
apiConfiguration,
|
||||||
handleWebviewAskResponse: jest.fn(),
|
customInstructions,
|
||||||
clineMessages: [],
|
diffEnabled,
|
||||||
apiConversationHistory: [],
|
fuzzyMatchThreshold,
|
||||||
overwriteClineMessages: jest.fn(),
|
task,
|
||||||
overwriteApiConversationHistory: jest.fn(),
|
taskId
|
||||||
taskId: 'test-task-id'
|
) => ({
|
||||||
}))
|
abortTask: jest.fn(),
|
||||||
}
|
handleWebviewAskResponse: jest.fn(),
|
||||||
})
|
clineMessages: [],
|
||||||
|
apiConversationHistory: [],
|
||||||
|
overwriteClineMessages: jest.fn(),
|
||||||
|
overwriteApiConversationHistory: jest.fn(),
|
||||||
|
taskId: taskId || 'test-task-id'
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock extract-text
|
// Mock extract-text
|
||||||
jest.mock('../../../integrations/misc/extract-text', () => ({
|
jest.mock('../../../integrations/misc/extract-text', () => ({
|
||||||
@@ -571,6 +577,82 @@ describe('ClineProvider', () => {
|
|||||||
expect(state.customPrompts).toEqual({})
|
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 () => {
|
test('saves mode config when updating API configuration', async () => {
|
||||||
// Setup mock context with mode and config name
|
// Setup mock context with mode and config name
|
||||||
mockContext = {
|
mockContext = {
|
||||||
@@ -848,5 +930,79 @@ describe('ClineProvider', () => {
|
|||||||
|
|
||||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Failed to get system prompt')
|
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) {
|
interface OpenFileOptions {
|
||||||
try {
|
create?: boolean;
|
||||||
const uri = vscode.Uri.file(absolutePath)
|
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 {
|
try {
|
||||||
for (const group of vscode.window.tabGroups.all) {
|
for (const group of vscode.window.tabGroups.all) {
|
||||||
const existingTab = group.tabs.find(
|
const existingTab = group.tabs.find(
|
||||||
@@ -47,6 +77,10 @@ export async function openFile(absolutePath: string) {
|
|||||||
const document = await vscode.workspace.openTextDocument(uri)
|
const document = await vscode.workspace.openTextDocument(uri)
|
||||||
await vscode.window.showTextDocument(document, { preview: false })
|
await vscode.window.showTextDocument(document, { preview: false })
|
||||||
} catch (error) {
|
} 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 = {
|
export type PromptComponent = {
|
||||||
roleDefinition?: string;
|
roleDefinition?: string;
|
||||||
|
customInstructions?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomPrompts = {
|
export type CustomPrompts = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { VSCodeButton, VSCodeTextArea, VSCodeDropdown, VSCodeOption, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton, VSCodeTextArea, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { defaultPrompts, askMode, codeMode, architectMode, Mode, PromptComponent } from "../../../../src/shared/modes"
|
import { defaultPrompts, askMode, codeMode, architectMode, Mode, PromptComponent } from "../../../../src/shared/modes"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
@@ -15,7 +15,14 @@ const AGENT_MODES = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
const PromptsView = ({ onDone }: PromptsViewProps) => {
|
const PromptsView = ({ onDone }: PromptsViewProps) => {
|
||||||
const { customPrompts, listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId, mode } = useExtensionState()
|
const {
|
||||||
|
customPrompts,
|
||||||
|
listApiConfigMeta,
|
||||||
|
enhancementApiConfigId,
|
||||||
|
setEnhancementApiConfigId,
|
||||||
|
mode,
|
||||||
|
customInstructions
|
||||||
|
} = useExtensionState()
|
||||||
const [testPrompt, setTestPrompt] = useState('')
|
const [testPrompt, setTestPrompt] = useState('')
|
||||||
const [isEnhancing, setIsEnhancing] = useState(false)
|
const [isEnhancing, setIsEnhancing] = useState(false)
|
||||||
const [activeTab, setActiveTab] = useState<Mode>(mode)
|
const [activeTab, setActiveTab] = useState<Mode>(mode)
|
||||||
@@ -47,10 +54,20 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
type AgentMode = typeof codeMode | typeof architectMode | typeof askMode
|
type AgentMode = typeof codeMode | typeof architectMode | typeof askMode
|
||||||
|
|
||||||
const updateAgentPrompt = (mode: AgentMode, promptData: PromptComponent) => {
|
const updateAgentPrompt = (mode: AgentMode, promptData: PromptComponent) => {
|
||||||
|
const updatedPrompt = {
|
||||||
|
...customPrompts?.[mode],
|
||||||
|
...promptData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include properties that differ from defaults
|
||||||
|
if (updatedPrompt.roleDefinition === defaultPrompts[mode].roleDefinition) {
|
||||||
|
delete updatedPrompt.roleDefinition
|
||||||
|
}
|
||||||
|
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: "updatePrompt",
|
type: "updatePrompt",
|
||||||
promptMode: mode,
|
promptMode: mode,
|
||||||
customPrompt: promptData
|
customPrompt: updatedPrompt
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +85,17 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
|
|
||||||
const handleEnhancePromptChange = (e: Event | React.FormEvent<HTMLElement>) => {
|
const handleEnhancePromptChange = (e: Event | React.FormEvent<HTMLElement>) => {
|
||||||
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||||
updateEnhancePrompt(value.trim() || undefined)
|
const trimmedValue = value.trim()
|
||||||
|
if (trimmedValue !== defaultPrompts.enhance) {
|
||||||
|
updateEnhancePrompt(trimmedValue || undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAgentReset = (mode: AgentMode) => {
|
const handleAgentReset = (mode: AgentMode) => {
|
||||||
updateAgentPrompt(mode, { roleDefinition: undefined })
|
updateAgentPrompt(mode, {
|
||||||
|
...customPrompts?.[mode],
|
||||||
|
roleDefinition: undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEnhanceReset = () => {
|
const handleEnhanceReset = () => {
|
||||||
@@ -120,71 +143,156 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
|
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
|
||||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 20px 0" }}>Agent Modes</h3>
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Custom Instructions for All Modes</div>
|
||||||
<div
|
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||||
style={{
|
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced by mode-specific instructions below.
|
||||||
color: "var(--vscode-foreground)",
|
|
||||||
fontSize: "13px",
|
|
||||||
marginBottom: "20px",
|
|
||||||
marginTop: "5px",
|
|
||||||
}}>
|
|
||||||
Customize Cline's prompt in each mode. The rest of the system prompt will be automatically appended. Click the button to preview the full prompt. Leave empty or click the reset button to use the default.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: '12px'
|
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
||||||
{AGENT_MODES.map((tab, index) => (
|
|
||||||
<React.Fragment key={tab.id}>
|
|
||||||
<button
|
|
||||||
data-testid={`${tab.id}-tab`}
|
|
||||||
data-active={activeTab === tab.id ? "true" : "false"}
|
|
||||||
onClick={() => setActiveTab(tab.id)}
|
|
||||||
style={{
|
|
||||||
padding: '4px 0',
|
|
||||||
border: 'none',
|
|
||||||
background: 'none',
|
|
||||||
color: activeTab === tab.id ? 'var(--vscode-textLink-foreground)' : 'var(--vscode-foreground)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
opacity: activeTab === tab.id ? 1 : 0.8,
|
|
||||||
borderBottom: activeTab === tab.id ?
|
|
||||||
'1px solid var(--vscode-textLink-foreground)' :
|
|
||||||
'1px solid var(--vscode-foreground)',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
{index < AGENT_MODES.length - 1 && (
|
|
||||||
<span style={{ color: 'var(--vscode-foreground)', opacity: 0.4 }}>|</span>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<VSCodeButton
|
|
||||||
appearance="icon"
|
|
||||||
onClick={() => handleAgentReset(activeTab)}
|
|
||||||
data-testid="reset-prompt-button"
|
|
||||||
title="Revert to default"
|
|
||||||
>
|
|
||||||
<span className="codicon codicon-discard"></span>
|
|
||||||
</VSCodeButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '8px' }}>
|
|
||||||
<VSCodeTextArea
|
<VSCodeTextArea
|
||||||
value={getAgentPromptValue(activeTab)}
|
value={customInstructions ?? ''}
|
||||||
onChange={(e) => handleAgentPromptChange(activeTab, e)}
|
onChange={(e) => {
|
||||||
|
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "customInstructions",
|
||||||
|
text: value.trim() || undefined
|
||||||
|
})
|
||||||
|
}}
|
||||||
rows={4}
|
rows={4}
|
||||||
resize="vertical"
|
resize="vertical"
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
data-testid={`${activeTab}-prompt-textarea`}
|
data-testid="global-custom-instructions-textarea"
|
||||||
/>
|
/>
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", marginTop: "5px" }}>
|
||||||
|
Instructions can also be loaded from <span
|
||||||
|
style={{
|
||||||
|
color: 'var(--vscode-textLink-foreground)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'underline'
|
||||||
|
}}
|
||||||
|
onClick={() => vscode.postMessage({
|
||||||
|
type: "openFile",
|
||||||
|
text: "./.clinerules",
|
||||||
|
values: {
|
||||||
|
create: true,
|
||||||
|
content: "",
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>.clinerules</span> in your workspace.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 20px 0" }}>Mode-Specific Prompts</h3>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '16px',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '12px'
|
||||||
|
}}>
|
||||||
|
{AGENT_MODES.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
data-testid={`${tab.id}-tab`}
|
||||||
|
data-active={activeTab === tab.id ? "true" : "false"}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
border: 'none',
|
||||||
|
background: activeTab === tab.id ? 'var(--vscode-button-background)' : 'none',
|
||||||
|
color: activeTab === tab.id ? 'var(--vscode-button-foreground)' : 'var(--vscode-foreground)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: activeTab === tab.id ? 1 : 0.8,
|
||||||
|
borderRadius: '3px',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: "4px"
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: "bold" }}>Role Definition</div>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
onClick={() => handleAgentReset(activeTab)}
|
||||||
|
data-testid="reset-prompt-button"
|
||||||
|
title="Revert to default"
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-discard"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||||
|
Define Cline's expertise and personality for this mode. This description shapes how Cline presents itself and approaches tasks.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={getAgentPromptValue(activeTab)}
|
||||||
|
onChange={(e) => handleAgentPromptChange(activeTab, e)}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
data-testid={`${activeTab}-prompt-textarea`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Mode-specific Custom Instructions</div>
|
||||||
|
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||||
|
Add behavioral guidelines specific to {activeTab} mode. These instructions enhance the base behaviors defined above.
|
||||||
|
</div>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={customPrompts?.[activeTab]?.customInstructions ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||||
|
updateAgentPrompt(activeTab, {
|
||||||
|
...customPrompts?.[activeTab],
|
||||||
|
customInstructions: value.trim() || undefined
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
data-testid={`${activeTab}-custom-instructions-textarea`}
|
||||||
|
/>
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", marginTop: "5px" }}>
|
||||||
|
Custom instructions specific to {activeTab} mode can also be loaded from <span
|
||||||
|
style={{
|
||||||
|
color: 'var(--vscode-textLink-foreground)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'underline'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
// First create/update the file with current custom instructions
|
||||||
|
const defaultContent = `# ${activeTab} Mode Rules\n\nAdd mode-specific rules and guidelines here.`
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "updatePrompt",
|
||||||
|
promptMode: activeTab,
|
||||||
|
customPrompt: {
|
||||||
|
...customPrompts?.[activeTab],
|
||||||
|
customInstructions: customPrompts?.[activeTab]?.customInstructions || defaultContent
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Then open the file
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "openFile",
|
||||||
|
text: `./.clinerules-${activeTab}`,
|
||||||
|
values: {
|
||||||
|
create: true,
|
||||||
|
content: "",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>.clinerules-{activeTab}</span> in your workspace.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'flex-start' }}>
|
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'flex-start' }}>
|
||||||
<VSCodeButton
|
<VSCodeButton
|
||||||
@@ -203,6 +311,15 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
|
|
||||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "40px 0 20px 0" }}>Prompt Enhancement</h3>
|
<h3 style={{ color: "var(--vscode-foreground)", margin: "40px 0 20px 0" }}>Prompt Enhancement</h3>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
color: "var(--vscode-foreground)",
|
||||||
|
fontSize: "13px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
marginTop: "5px",
|
||||||
|
}}>
|
||||||
|
Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Cline understands your intent and provides the best possible responses.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: "12px" }}>
|
<div style={{ marginBottom: "12px" }}>
|
||||||
@@ -234,12 +351,17 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
</VSCodeDropdown>
|
</VSCodeDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: "8px", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
<div style={{ marginBottom: "8px" }}>
|
||||||
<div style={{ fontWeight: "bold" }}>Enhancement Prompt</div>
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
|
||||||
<div style={{ display: "flex", gap: "8px" }}>
|
<div style={{ fontWeight: "bold" }}>Enhancement Prompt</div>
|
||||||
<VSCodeButton appearance="icon" onClick={handleEnhanceReset} title="Revert to default">
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
<span className="codicon codicon-discard"></span>
|
<VSCodeButton appearance="icon" onClick={handleEnhanceReset} title="Revert to default">
|
||||||
</VSCodeButton>
|
<span className="codicon codicon-discard"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||||
|
This prompt will be used to refine your input when you hit the sparkle icon in chat.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VSCodeTextArea
|
<VSCodeTextArea
|
||||||
@@ -299,37 +421,52 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
boxShadow: '-2px 0 5px rgba(0, 0, 0, 0.2)',
|
boxShadow: '-2px 0 5px rgba(0, 0, 0, 0.2)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: '20px',
|
position: 'relative'
|
||||||
overflowY: 'auto'
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
flex: 1,
|
||||||
justifyContent: 'space-between',
|
padding: '20px',
|
||||||
alignItems: 'center',
|
overflowY: 'auto',
|
||||||
marginBottom: '16px'
|
minHeight: 0
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ margin: 0 }}>{selectedPromptTitle}</h2>
|
<VSCodeButton
|
||||||
<VSCodeButton appearance="icon" onClick={() => setIsDialogOpen(false)}>
|
appearance="icon"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '20px',
|
||||||
|
right: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span className="codicon codicon-close"></span>
|
<span className="codicon codicon-close"></span>
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
|
<h2 style={{ margin: '0 0 16px' }}>{selectedPromptTitle}</h2>
|
||||||
|
<pre style={{
|
||||||
|
padding: '8px',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontFamily: 'var(--vscode-editor-font-family)',
|
||||||
|
fontSize: 'var(--vscode-editor-font-size)',
|
||||||
|
color: 'var(--vscode-editor-foreground)',
|
||||||
|
backgroundColor: 'var(--vscode-editor-background)',
|
||||||
|
border: '1px solid var(--vscode-editor-lineHighlightBorder)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
{selectedPromptContent}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<VSCodeDivider />
|
<div style={{
|
||||||
<pre style={{
|
display: 'flex',
|
||||||
margin: '16px 0',
|
justifyContent: 'flex-end',
|
||||||
padding: '8px',
|
padding: '12px 20px',
|
||||||
whiteSpace: 'pre-wrap',
|
borderTop: '1px solid var(--vscode-editor-lineHighlightBorder)',
|
||||||
wordBreak: 'break-word',
|
backgroundColor: 'var(--vscode-editor-background)'
|
||||||
fontFamily: 'var(--vscode-editor-font-family)',
|
|
||||||
fontSize: 'var(--vscode-editor-font-size)',
|
|
||||||
color: 'var(--vscode-editor-foreground)',
|
|
||||||
backgroundColor: 'var(--vscode-editor-background)',
|
|
||||||
border: '1px solid var(--vscode-editor-lineHighlightBorder)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
flex: 1,
|
|
||||||
overflowY: 'auto'
|
|
||||||
}}>
|
}}>
|
||||||
{selectedPromptContent}
|
<VSCodeButton onClick={() => setIsDialogOpen(false)}>
|
||||||
</pre>
|
Close
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -278,24 +278,26 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VSCodeTextArea
|
<div style={{ marginBottom: 15 }}>
|
||||||
value={customInstructions ?? ""}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
rows={4}
|
|
||||||
placeholder={
|
|
||||||
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
|
|
||||||
}
|
|
||||||
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
|
|
||||||
<span style={{ fontWeight: "500" }}>Custom Instructions</span>
|
<span style={{ fontWeight: "500" }}>Custom Instructions</span>
|
||||||
</VSCodeTextArea>
|
<VSCodeTextArea
|
||||||
<p
|
value={customInstructions ?? ""}
|
||||||
style={{
|
style={{ width: "100%" }}
|
||||||
fontSize: "12px",
|
rows={4}
|
||||||
marginTop: "5px",
|
placeholder={
|
||||||
color: "var(--vscode-descriptionForeground)",
|
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
|
||||||
}}>
|
}
|
||||||
These instructions are added to the end of the system prompt sent with every request. Custom instructions set in .clinerules and .cursorrules in the working directory are also included.
|
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}
|
||||||
</p>
|
/>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "5px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
These instructions are added to the end of the system prompt sent with every request. Custom instructions set in .clinerules in the working directory are also included. For mode-specific instructions, use the <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> Prompts tab in the top menu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<McpEnabledToggle />
|
<McpEnabledToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user