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

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

View File

@@ -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'
)
})
})
})