From 7c875f1fea6e0e4406aefd75aa4c569cccbdf316 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:00 +0700 Subject: [PATCH 01/12] feat: add code action prompt handlers for explain, fix and improve code --- .../prompts/__tests__/code-actions.test.ts | 63 +++++++++++++++++++ src/core/prompts/code-actions.ts | 50 +++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 src/core/prompts/__tests__/code-actions.test.ts create mode 100644 src/core/prompts/code-actions.ts diff --git a/src/core/prompts/__tests__/code-actions.test.ts b/src/core/prompts/__tests__/code-actions.test.ts new file mode 100644 index 0000000..ad4c281 --- /dev/null +++ b/src/core/prompts/__tests__/code-actions.test.ts @@ -0,0 +1,63 @@ +import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from '../code-actions'; + +describe('Code Action Prompts', () => { + const testFilePath = 'test/file.ts'; + const testCode = 'function test() { return true; }'; + + describe('explainCodePrompt', () => { + it('should format explain prompt correctly', () => { + const prompt = explainCodePrompt(testFilePath, testCode); + + expect(prompt).toContain(`@/${testFilePath}`); + expect(prompt).toContain(testCode); + expect(prompt).toContain('purpose and functionality'); + expect(prompt).toContain('Key components'); + expect(prompt).toContain('Important patterns'); + }); + }); + + describe('fixCodePrompt', () => { + it('should format fix prompt without diagnostics', () => { + const prompt = fixCodePrompt(testFilePath, testCode); + + expect(prompt).toContain(`@/${testFilePath}`); + expect(prompt).toContain(testCode); + expect(prompt).toContain('Address all detected problems'); + expect(prompt).not.toContain('Current problems detected'); + }); + + it('should format fix prompt with diagnostics', () => { + const diagnostics = [ + { + source: 'eslint', + message: 'Missing semicolon', + code: 'semi' + }, + { + message: 'Unused variable', + severity: 1 + } + ]; + + const prompt = fixCodePrompt(testFilePath, testCode, diagnostics); + + expect(prompt).toContain('Current problems detected:'); + expect(prompt).toContain('[eslint] Missing semicolon (semi)'); + expect(prompt).toContain('[Error] Unused variable'); + expect(prompt).toContain(testCode); + }); + }); + + describe('improveCodePrompt', () => { + it('should format improve prompt correctly', () => { + const prompt = improveCodePrompt(testFilePath, testCode); + + expect(prompt).toContain(`@/${testFilePath}`); + expect(prompt).toContain(testCode); + expect(prompt).toContain('Code readability'); + expect(prompt).toContain('Performance optimization'); + expect(prompt).toContain('Best practices'); + expect(prompt).toContain('Error handling'); + }); + }); +}); \ No newline at end of file diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts new file mode 100644 index 0000000..3fd03ae --- /dev/null +++ b/src/core/prompts/code-actions.ts @@ -0,0 +1,50 @@ +export const explainCodePrompt = (filePath: string, selectedText: string) => ` +Explain the following code from file path @/${filePath}: + +\`\`\` +${selectedText} +\`\`\` + +Please provide a clear and concise explanation of what this code does, including: +1. The purpose and functionality +2. Key components and their interactions +3. Important patterns or techniques used +`; + +export const fixCodePrompt = (filePath: string, selectedText: string, diagnostics?: any[]) => { + const diagnosticText = diagnostics && diagnostics.length > 0 + ? `\nCurrent problems detected: +${diagnostics.map(d => `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}`).join('\n')}` + : ''; + + return ` +Fix any issues in the following code from file path @/${filePath} +${diagnosticText} + +\`\`\` +${selectedText} +\`\`\` + +Please: +1. Address all detected problems listed above (if any) +2. Identify any other potential bugs or issues +3. Provide corrected code +4. Explain what was fixed and why +`; +}; + +export const improveCodePrompt = (filePath: string, selectedText: string) => ` +Improve the following code from file path @/${filePath}: + +\`\`\` +${selectedText} +\`\`\` + +Please suggest improvements for: +1. Code readability and maintainability +2. Performance optimization +3. Best practices and patterns +4. Error handling and edge cases + +Provide the improved code along with explanations for each enhancement. +`; \ No newline at end of file From 86b051df3509f906e9bbf51f45da061e2c0cbeb2 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:15 +0700 Subject: [PATCH 02/12] feat: implement code action provider for VS Code integration --- src/core/CodeActionProvider.ts | 181 ++++++++++++++++++ src/core/__tests__/CodeActionProvider.test.ts | 145 ++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 src/core/CodeActionProvider.ts create mode 100644 src/core/__tests__/CodeActionProvider.test.ts diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts new file mode 100644 index 0000000..11bafdb --- /dev/null +++ b/src/core/CodeActionProvider.ts @@ -0,0 +1,181 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +const ACTION_NAMES = { + EXPLAIN: 'Roo Cline: Explain Code', + FIX: 'Roo Cline: Fix Code', + IMPROVE: 'Roo Cline: Improve Code' +} as const; + +const COMMAND_IDS = { + EXPLAIN: 'roo-cline.explainCode', + FIX: 'roo-cline.fixCode', + IMPROVE: 'roo-cline.improveCode' +} as const; + +interface DiagnosticData { + message: string; + severity: vscode.DiagnosticSeverity; + code?: string | number | { value: string | number; target: vscode.Uri }; + source?: string; + range: vscode.Range; +} + +interface EffectiveRange { + range: vscode.Range; + text: string; +} + +export class CodeActionProvider implements vscode.CodeActionProvider { + public static readonly providedCodeActionKinds = [ + vscode.CodeActionKind.QuickFix, + vscode.CodeActionKind.RefactorRewrite, + ]; + + // Cache file paths for performance + private readonly filePathCache = new WeakMap(); + + private getEffectiveRange( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection + ): EffectiveRange | null { + try { + const selectedText = document.getText(range); + if (selectedText) { + return { range, text: selectedText }; + } + + const currentLine = document.lineAt(range.start.line); + if (!currentLine.text.trim()) { + return null; + } + + // Optimize range creation by checking bounds first + const startLine = Math.max(0, currentLine.lineNumber - 1); + const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1); + + // Only create new positions if needed + const effectiveRange = new vscode.Range( + startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), + endLine === currentLine.lineNumber ? range.end : new vscode.Position(endLine, document.lineAt(endLine).text.length) + ); + + return { + range: effectiveRange, + text: document.getText(effectiveRange) + }; + } catch (error) { + console.error('Error getting effective range:', error); + return null; + } + } + + private getFilePath(document: vscode.TextDocument): string { + // Check cache first + let filePath = this.filePathCache.get(document); + if (filePath) { + return filePath; + } + + try { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder) { + filePath = document.uri.fsPath; + } else { + const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath); + filePath = (!relativePath || relativePath.startsWith('..')) ? document.uri.fsPath : relativePath; + } + + // Cache the result + this.filePathCache.set(document, filePath); + return filePath; + } catch (error) { + console.error('Error getting file path:', error); + return document.uri.fsPath; + } + } + + private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { + return { + message: diagnostic.message, + severity: diagnostic.severity, + code: diagnostic.code, + source: diagnostic.source, + range: diagnostic.range // Reuse the range object + }; + } + + private createAction( + title: string, + kind: vscode.CodeActionKind, + command: string, + args: any[] + ): vscode.CodeAction { + const action = new vscode.CodeAction(title, kind); + action.command = { command, title, arguments: args }; + return action; + } + + private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { + // Optimize range intersection check + return !( + range2.end.line < range1.start.line || + range2.start.line > range1.end.line || + (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || + (range2.start.line === range1.end.line && range2.start.character > range1.end.character) + ); + } + + public provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext + ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + try { + const effectiveRange = this.getEffectiveRange(document, range); + if (!effectiveRange) { + return []; + } + + const filePath = this.getFilePath(document); + const actions: vscode.CodeAction[] = []; + + // Create actions using helper method + actions.push(this.createAction( + ACTION_NAMES.EXPLAIN, + vscode.CodeActionKind.QuickFix, + COMMAND_IDS.EXPLAIN, + [filePath, effectiveRange.text] + )); + + // Only process diagnostics if they exist + if (context.diagnostics.length > 0) { + const relevantDiagnostics = context.diagnostics.filter(d => + this.hasIntersectingRange(effectiveRange.range, d.range) + ); + + if (relevantDiagnostics.length > 0) { + const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData); + actions.push(this.createAction( + ACTION_NAMES.FIX, + vscode.CodeActionKind.QuickFix, + COMMAND_IDS.FIX, + [filePath, effectiveRange.text, diagnosticMessages] + )); + } + } + + actions.push(this.createAction( + ACTION_NAMES.IMPROVE, + vscode.CodeActionKind.RefactorRewrite, + COMMAND_IDS.IMPROVE, + [filePath, effectiveRange.text] + )); + + return actions; + } catch (error) { + console.error('Error providing code actions:', error); + return []; + } + } +} \ No newline at end of file diff --git a/src/core/__tests__/CodeActionProvider.test.ts b/src/core/__tests__/CodeActionProvider.test.ts new file mode 100644 index 0000000..cdc1acf --- /dev/null +++ b/src/core/__tests__/CodeActionProvider.test.ts @@ -0,0 +1,145 @@ +import * as vscode from 'vscode'; +import { CodeActionProvider } from '../CodeActionProvider'; + +// Mock VSCode API +jest.mock('vscode', () => ({ + CodeAction: jest.fn().mockImplementation((title, kind) => ({ + title, + kind, + command: undefined + })), + CodeActionKind: { + QuickFix: { value: 'quickfix' }, + RefactorRewrite: { value: 'refactor.rewrite' } + }, + Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar } + })), + Position: jest.fn().mockImplementation((line, character) => ({ + line, + character + })), + workspace: { + getWorkspaceFolder: jest.fn() + }, + DiagnosticSeverity: { + Error: 0, + Warning: 1, + Information: 2, + Hint: 3 + } +})); + +describe('CodeActionProvider', () => { + let provider: CodeActionProvider; + let mockDocument: any; + let mockRange: any; + let mockContext: any; + + beforeEach(() => { + provider = new CodeActionProvider(); + + // Mock document + mockDocument = { + getText: jest.fn(), + lineAt: jest.fn(), + lineCount: 10, + uri: { fsPath: '/test/file.ts' } + }; + + // Mock range + mockRange = new vscode.Range(0, 0, 0, 10); + + // Mock context + mockContext = { + diagnostics: [] + }; + }); + + describe('getEffectiveRange', () => { + it('should return selected text when available', () => { + mockDocument.getText.mockReturnValue('selected text'); + + const result = (provider as any).getEffectiveRange(mockDocument, mockRange); + + expect(result).toEqual({ + range: mockRange, + text: 'selected text' + }); + }); + + it('should return null for empty line', () => { + mockDocument.getText.mockReturnValue(''); + mockDocument.lineAt.mockReturnValue({ text: '', lineNumber: 0 }); + + const result = (provider as any).getEffectiveRange(mockDocument, mockRange); + + expect(result).toBeNull(); + }); + }); + + describe('getFilePath', () => { + it('should return relative path when in workspace', () => { + const mockWorkspaceFolder = { + uri: { fsPath: '/test' } + }; + (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder); + + const result = (provider as any).getFilePath(mockDocument); + + expect(result).toBe('file.ts'); + }); + + it('should return absolute path when not in workspace', () => { + (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null); + + const result = (provider as any).getFilePath(mockDocument); + + expect(result).toBe('/test/file.ts'); + }); + }); + + describe('provideCodeActions', () => { + beforeEach(() => { + mockDocument.getText.mockReturnValue('test code'); + mockDocument.lineAt.mockReturnValue({ text: 'test code', lineNumber: 0 }); + }); + + it('should provide explain and improve actions by default', () => { + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); + + expect(actions).toHaveLength(2); + expect((actions as any)[0].title).toBe('Roo Cline: Explain Code'); + expect((actions as any)[1].title).toBe('Roo Cline: Improve Code'); + }); + + it('should provide fix action when diagnostics exist', () => { + mockContext.diagnostics = [{ + message: 'test error', + severity: vscode.DiagnosticSeverity.Error, + range: mockRange + }]; + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); + + expect(actions).toHaveLength(3); + expect((actions as any).some((a: any) => a.title === 'Roo Cline: Fix Code')).toBe(true); + }); + + it('should handle errors gracefully', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockDocument.getText.mockImplementation(() => { + throw new Error('Test error'); + }); + mockDocument.lineAt.mockReturnValue({ text: 'test', lineNumber: 0 }); + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); + + expect(actions).toEqual([]); + expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting effective range:', expect.any(Error)); + + consoleErrorSpy.mockRestore(); + }); + }); +}); \ No newline at end of file From 273bfc410bb4b6fcc009cb4f5b5cd3c090b33496 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:31 +0700 Subject: [PATCH 03/12] feat: register new code action commands in package manifest --- package.json | 156 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 94 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index 6ae6c86..7715001 100644 --- a/package.json +++ b/package.json @@ -72,70 +72,102 @@ "title": "New Task", "icon": "$(add)" }, - { - "command": "roo-cline.mcpButtonClicked", - "title": "MCP Servers", - "icon": "$(server)" - }, - { - "command": "roo-cline.promptsButtonClicked", - "title": "Prompts", - "icon": "$(notebook)" - }, - { - "command": "roo-cline.historyButtonClicked", - "title": "History", - "icon": "$(history)" - }, - { - "command": "roo-cline.popoutButtonClicked", - "title": "Open in Editor", - "icon": "$(link-external)" - }, - { - "command": "roo-cline.settingsButtonClicked", - "title": "Settings", - "icon": "$(settings-gear)" - }, - { - "command": "roo-cline.openInNewTab", - "title": "Open In New Tab", - "category": "Roo Code" - } + { + "command": "roo-cline.mcpButtonClicked", + "title": "MCP Servers", + "icon": "$(server)" + }, + { + "command": "roo-cline.promptsButtonClicked", + "title": "Prompts", + "icon": "$(notebook)" + }, + { + "command": "roo-cline.historyButtonClicked", + "title": "History", + "icon": "$(history)" + }, + { + "command": "roo-cline.popoutButtonClicked", + "title": "Open in Editor", + "icon": "$(link-external)" + }, + { + "command": "roo-cline.settingsButtonClicked", + "title": "Settings", + "icon": "$(settings-gear)" + }, + { + "command": "roo-cline.openInNewTab", + "title": "Open In New Tab", + "category": "Roo Code" + }, + { + "command": "roo-cline.explainCode", + "title": "Explain Code", + "category": "Roo Cline" + }, + { + "command": "roo-cline.fixCode", + "title": "Fix Code", + "category": "Roo Cline" + }, + { + "command": "roo-cline.improveCode", + "title": "Improve Code", + "category": "Roo Cline" + } ], "menus": { - "view/title": [ - { - "command": "roo-cline.plusButtonClicked", - "group": "navigation@1", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.promptsButtonClicked", - "group": "navigation@2", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.mcpButtonClicked", - "group": "navigation@3", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.historyButtonClicked", - "group": "navigation@4", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.popoutButtonClicked", - "group": "navigation@5", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.settingsButtonClicked", - "group": "navigation@6", - "when": "view == roo-cline.SidebarProvider" - } - ] + "editor/context": [ + { + "command": "roo-cline.explainCode", + "when": "editorHasSelection", + "group": "Roo Cline@1" + }, + { + "command": "roo-cline.fixCode", + "when": "editorHasSelection", + "group": "Roo Cline@2" + }, + { + "command": "roo-cline.improveCode", + "when": "editorHasSelection", + "group": "Roo Cline@3" + } + ], + "view/title": [ + { + "command": "roo-cline.plusButtonClicked", + "group": "navigation@1", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.promptsButtonClicked", + "group": "navigation@2", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.mcpButtonClicked", + "group": "navigation@3", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.historyButtonClicked", + "group": "navigation@4", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "navigation@5", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.settingsButtonClicked", + "group": "navigation@6", + "when": "view == roo-cline.SidebarProvider" + } + ] }, "configuration": { "title": "Roo Code", From 02a8eb96f12359bdc32f471bfb96ada489d99cd3 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:46 +0700 Subject: [PATCH 04/12] feat: integrate code actions into extension activation --- src/extension.ts | 47 ++++++++++++++++++++++++++++++++++++++ src/test/extension.test.ts | 17 ++++++++------ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 0cf3053..e6cfde7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,8 @@ import * as vscode from "vscode" import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix +import { CodeActionProvider } from "./core/CodeActionProvider" +import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from "./core/prompts/code-actions" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" /* @@ -158,6 +160,51 @@ export function activate(context: vscode.ExtensionContext) { } context.subscriptions.push(vscode.window.registerUriHandler({ handleUri })) + // Register code actions provider + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + { pattern: "**/*" }, + new CodeActionProvider(), + { + providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds + } + ) + ); + + // Register code action commands + context.subscriptions.push( + vscode.commands.registerCommand("roo-cline.explainCode", async (filePath: string, selectedText: string) => { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + const prompt = explainCodePrompt(filePath, selectedText) + await visibleProvider.initClineWithTask(prompt) + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand("roo-cline.fixCode", async (filePath: string, selectedText: string, diagnostics?: any[]) => { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + const prompt = fixCodePrompt(filePath, selectedText, diagnostics) + await visibleProvider.initClineWithTask(prompt) + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand("roo-cline.improveCode", async (filePath: string, selectedText: string) => { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + const prompt = improveCodePrompt(filePath, selectedText) + await visibleProvider.initClineWithTask(prompt) + }) + ); + return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index c67b3db..a3237e2 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -117,13 +117,16 @@ suite("Roo Cline Extension Test Suite", () => { // Test core commands are registered const expectedCommands = [ - "roo-cline.plusButtonClicked", - "roo-cline.mcpButtonClicked", - "roo-cline.historyButtonClicked", - "roo-cline.popoutButtonClicked", - "roo-cline.settingsButtonClicked", - "roo-cline.openInNewTab", - ] + 'roo-cline.plusButtonClicked', + 'roo-cline.mcpButtonClicked', + 'roo-cline.historyButtonClicked', + 'roo-cline.popoutButtonClicked', + 'roo-cline.settingsButtonClicked', + 'roo-cline.openInNewTab', + 'roo-cline.explainCode', + 'roo-cline.fixCode', + 'roo-cline.improveCode' + ]; for (const cmd of expectedCommands) { assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`) From 1b26f91ea7378d11062aa5299d48686ea197b4bc Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 21:20:02 +0700 Subject: [PATCH 05/12] refactor(code-actions): implement parameter object pattern for prompt generation - Extract prompt templates into constants - Add createPrompt utility for template string handling - Consolidate code action handling in ClineProvider - Update tests to use new parameter object pattern --- .../prompts/__tests__/code-actions.test.ts | 21 +++++-- src/core/prompts/code-actions.ts | 59 +++++++++++++------ src/core/webview/ClineProvider.ts | 14 +++++ src/extension.ts | 25 +++----- 4 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/core/prompts/__tests__/code-actions.test.ts b/src/core/prompts/__tests__/code-actions.test.ts index ad4c281..0ddb6bd 100644 --- a/src/core/prompts/__tests__/code-actions.test.ts +++ b/src/core/prompts/__tests__/code-actions.test.ts @@ -6,7 +6,10 @@ describe('Code Action Prompts', () => { describe('explainCodePrompt', () => { it('should format explain prompt correctly', () => { - const prompt = explainCodePrompt(testFilePath, testCode); + const prompt = explainCodePrompt({ + filePath: testFilePath, + selectedText: testCode + }); expect(prompt).toContain(`@/${testFilePath}`); expect(prompt).toContain(testCode); @@ -18,7 +21,10 @@ describe('Code Action Prompts', () => { describe('fixCodePrompt', () => { it('should format fix prompt without diagnostics', () => { - const prompt = fixCodePrompt(testFilePath, testCode); + const prompt = fixCodePrompt({ + filePath: testFilePath, + selectedText: testCode + }); expect(prompt).toContain(`@/${testFilePath}`); expect(prompt).toContain(testCode); @@ -39,7 +45,11 @@ describe('Code Action Prompts', () => { } ]; - const prompt = fixCodePrompt(testFilePath, testCode, diagnostics); + const prompt = fixCodePrompt({ + filePath: testFilePath, + selectedText: testCode, + diagnostics + }); expect(prompt).toContain('Current problems detected:'); expect(prompt).toContain('[eslint] Missing semicolon (semi)'); @@ -50,7 +60,10 @@ describe('Code Action Prompts', () => { describe('improveCodePrompt', () => { it('should format improve prompt correctly', () => { - const prompt = improveCodePrompt(testFilePath, testCode); + const prompt = improveCodePrompt({ + filePath: testFilePath, + selectedText: testCode + }); expect(prompt).toContain(`@/${testFilePath}`); expect(prompt).toContain(testCode); diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts index 3fd03ae..2b9445d 100644 --- a/src/core/prompts/code-actions.ts +++ b/src/core/prompts/code-actions.ts @@ -1,8 +1,29 @@ -export const explainCodePrompt = (filePath: string, selectedText: string) => ` -Explain the following code from file path @/${filePath}: +type PromptParams = Record; + +const generateDiagnosticText = (diagnostics?: any[]) => { + if (!diagnostics?.length) return ''; + return `\nCurrent problems detected:\n${diagnostics.map(d => + `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}` + ).join('\n')}`; +}; + +const createPrompt = (template: string, params: PromptParams): string => { + let result = template; + for (const [key, value] of Object.entries(params)) { + if (key === 'diagnostics') { + result = result.replaceAll('${diagnosticText}', generateDiagnosticText(value as any[])); + } else { + result = result.replaceAll(`\${${key}}`, value as string); + } + } + return result; +}; + +const EXPLAIN_TEMPLATE = ` +Explain the following code from file path @/\${filePath}: \`\`\` -${selectedText} +\${selectedText} \`\`\` Please provide a clear and concise explanation of what this code does, including: @@ -11,18 +32,12 @@ Please provide a clear and concise explanation of what this code does, including 3. Important patterns or techniques used `; -export const fixCodePrompt = (filePath: string, selectedText: string, diagnostics?: any[]) => { - const diagnosticText = diagnostics && diagnostics.length > 0 - ? `\nCurrent problems detected: -${diagnostics.map(d => `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}`).join('\n')}` - : ''; - - return ` -Fix any issues in the following code from file path @/${filePath} -${diagnosticText} +const FIX_TEMPLATE = ` +Fix any issues in the following code from file path @/\${filePath} +\${diagnosticText} \`\`\` -${selectedText} +\${selectedText} \`\`\` Please: @@ -31,13 +46,12 @@ Please: 3. Provide corrected code 4. Explain what was fixed and why `; -}; -export const improveCodePrompt = (filePath: string, selectedText: string) => ` -Improve the following code from file path @/${filePath}: +const IMPROVE_TEMPLATE = ` +Improve the following code from file path @/\${filePath}: \`\`\` -${selectedText} +\${selectedText} \`\`\` Please suggest improvements for: @@ -47,4 +61,13 @@ Please suggest improvements for: 4. Error handling and edge cases Provide the improved code along with explanations for each enhancement. -`; \ No newline at end of file +`; + +export const explainCodePrompt = (params: PromptParams) => + createPrompt(EXPLAIN_TEMPLATE, params); + +export const fixCodePrompt = (params: PromptParams) => + createPrompt(FIX_TEMPLATE, params); + +export const improveCodePrompt = (params: PromptParams) => + createPrompt(IMPROVE_TEMPLATE, params); \ No newline at end of file diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b3e5235..17289f5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -181,6 +181,20 @@ export class ClineProvider implements vscode.WebviewViewProvider { return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } + public static async handleCodeAction( + promptGenerator: (params: Record) => string, + params: Record + ): Promise { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + + const prompt = promptGenerator(params) + + await visibleProvider.initClineWithTask(prompt) + } + resolveWebviewView( webviewView: vscode.WebviewView | vscode.WebviewPanel, //context: vscode.WebviewViewResolveContext, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden diff --git a/src/extension.ts b/src/extension.ts index e6cfde7..6c8c9e7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -174,34 +174,23 @@ export function activate(context: vscode.ExtensionContext) { // Register code action commands context.subscriptions.push( vscode.commands.registerCommand("roo-cline.explainCode", async (filePath: string, selectedText: string) => { - const visibleProvider = ClineProvider.getVisibleInstance() - if (!visibleProvider) { - return - } - const prompt = explainCodePrompt(filePath, selectedText) - await visibleProvider.initClineWithTask(prompt) + await ClineProvider.handleCodeAction(explainCodePrompt, { filePath, selectedText }) }) ); context.subscriptions.push( vscode.commands.registerCommand("roo-cline.fixCode", async (filePath: string, selectedText: string, diagnostics?: any[]) => { - const visibleProvider = ClineProvider.getVisibleInstance() - if (!visibleProvider) { - return - } - const prompt = fixCodePrompt(filePath, selectedText, diagnostics) - await visibleProvider.initClineWithTask(prompt) + await ClineProvider.handleCodeAction(fixCodePrompt, { + filePath, + selectedText, + ...(diagnostics ? { diagnostics } : {}) + }) }) ); context.subscriptions.push( vscode.commands.registerCommand("roo-cline.improveCode", async (filePath: string, selectedText: string) => { - const visibleProvider = ClineProvider.getVisibleInstance() - if (!visibleProvider) { - return - } - const prompt = improveCodePrompt(filePath, selectedText) - await visibleProvider.initClineWithTask(prompt) + await ClineProvider.handleCodeAction(improveCodePrompt, { filePath, selectedText }) }) ); From 78457917207a90616a6f24b9eb4fbbe1dbe104a4 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Mon, 13 Jan 2025 02:48:52 +0700 Subject: [PATCH 06/12] feat(code-actions): add user input and customizable templates Add ability to provide custom input when using code actions Make code action templates customizable and resettable Refactor code action handling for better maintainability Add state management for utility prompts --- src/core/CodeActionProvider.ts | 2 +- src/core/prompts/code-actions.ts | 32 ++++++++++----- src/core/webview/ClineProvider.ts | 12 +++++- src/extension.ts | 65 ++++++++++++++++++++++--------- 4 files changed, 81 insertions(+), 30 deletions(-) diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index 11bafdb..cac2bdc 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; -const ACTION_NAMES = { +export const ACTION_NAMES = { EXPLAIN: 'Roo Cline: Explain Code', FIX: 'Roo Cline: Fix Code', IMPROVE: 'Roo Cline: Improve Code' diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts index 2b9445d..0811c48 100644 --- a/src/core/prompts/code-actions.ts +++ b/src/core/prompts/code-actions.ts @@ -2,12 +2,12 @@ type PromptParams = Record; const generateDiagnosticText = (diagnostics?: any[]) => { if (!diagnostics?.length) return ''; - return `\nCurrent problems detected:\n${diagnostics.map(d => + return `\nCurrent problems detected:\n${diagnostics.map(d => `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}` ).join('\n')}`; }; -const createPrompt = (template: string, params: PromptParams): string => { +export const createPrompt = (template: string, params: PromptParams): string => { let result = template; for (const [key, value] of Object.entries(params)) { if (key === 'diagnostics') { @@ -16,11 +16,16 @@ const createPrompt = (template: string, params: PromptParams): string => { result = result.replaceAll(`\${${key}}`, value as string); } } + + // Replace any remaining user_input placeholders with empty string + result = result.replaceAll('${userInput}', ''); + return result; }; -const EXPLAIN_TEMPLATE = ` +export const EXPLAIN_TEMPLATE = ` Explain the following code from file path @/\${filePath}: +\${userInput} \`\`\` \${selectedText} @@ -32,9 +37,10 @@ Please provide a clear and concise explanation of what this code does, including 3. Important patterns or techniques used `; -const FIX_TEMPLATE = ` +export const FIX_TEMPLATE = ` Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} +\${userInput} \`\`\` \${selectedText} @@ -47,8 +53,9 @@ Please: 4. Explain what was fixed and why `; -const IMPROVE_TEMPLATE = ` +export const IMPROVE_TEMPLATE = ` Improve the following code from file path @/\${filePath}: +\${userInput} \`\`\` \${selectedText} @@ -63,11 +70,18 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement. `; -export const explainCodePrompt = (params: PromptParams) => +export const explainCodePrompt = (params: PromptParams) => createPrompt(EXPLAIN_TEMPLATE, params); -export const fixCodePrompt = (params: PromptParams) => +export const fixCodePrompt = (params: PromptParams) => createPrompt(FIX_TEMPLATE, params); -export const improveCodePrompt = (params: PromptParams) => - createPrompt(IMPROVE_TEMPLATE, params); \ No newline at end of file +export const improveCodePrompt = (params: PromptParams) => + createPrompt(IMPROVE_TEMPLATE, params); + +// Get template based on prompt type +export const defaultTemplates = { + 'EXPLAIN': EXPLAIN_TEMPLATE, + 'FIX': FIX_TEMPLATE, + 'IMPROVE': IMPROVE_TEMPLATE +} \ No newline at end of file diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 17289f5..d897838 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -40,6 +40,12 @@ import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" +import { + defaultTemplates, + createPrompt +} from "../prompts/code-actions" + +import { ACTION_NAMES } from "../CodeActionProvider" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -182,7 +188,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } public static async handleCodeAction( - promptGenerator: (params: Record) => string, + promptType: keyof typeof ACTION_NAMES, params: Record ): Promise { const visibleProvider = ClineProvider.getVisibleInstance() @@ -190,8 +196,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const prompt = promptGenerator(params) + const { utilPrompt } = await visibleProvider.getState() + const template = utilPrompt?.[promptType] ?? defaultTemplates[promptType] + const prompt = createPrompt(template, params) await visibleProvider.initClineWithTask(prompt) } diff --git a/src/extension.ts b/src/extension.ts index 6c8c9e7..cf910fc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode" import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix -import { CodeActionProvider } from "./core/CodeActionProvider" +import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider" import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from "./core/prompts/code-actions" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" @@ -171,27 +171,56 @@ export function activate(context: vscode.ExtensionContext) { ) ); - // Register code action commands - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.explainCode", async (filePath: string, selectedText: string) => { - await ClineProvider.handleCodeAction(explainCodePrompt, { filePath, selectedText }) - }) - ); + // Helper function to handle code actions + const registerCodeAction = ( + context: vscode.ExtensionContext, + command: string, + promptType: keyof typeof ACTION_NAMES, + inputPrompt: string, + inputPlaceholder: string + ) => { + context.subscriptions.push( + vscode.commands.registerCommand(command, async (filePath: string, selectedText: string, diagnostics?: any[]) => { + const userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder + }); - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.fixCode", async (filePath: string, selectedText: string, diagnostics?: any[]) => { - await ClineProvider.handleCodeAction(fixCodePrompt, { - filePath, - selectedText, - ...(diagnostics ? { diagnostics } : {}) + const params = { + filePath, + selectedText, + ...(diagnostics ? { diagnostics } : {}), + ...(userInput ? { userInput } : {}) + }; + + await ClineProvider.handleCodeAction(promptType, params); }) - }) + ); + }; + + // Register code action commands + registerCodeAction( + context, + "roo-cline.explainCode", + 'EXPLAIN', + "Any specific questions about this code?", + "E.g. How does the error handling work?" ); - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.improveCode", async (filePath: string, selectedText: string) => { - await ClineProvider.handleCodeAction(improveCodePrompt, { filePath, selectedText }) - }) + registerCodeAction( + context, + "roo-cline.fixCode", + 'FIX', + "Any specific concerns about fixing this code?", + "E.g. Maintain backward compatibility" + ); + + registerCodeAction( + context, + "roo-cline.improveCode", + 'IMPROVE', + "Any specific aspects you want to improve?", + "E.g. Focus on performance optimization" ); return createClineAPI(outputChannel, sidebarProvider) From 22907a05787b089e83b48c27a2ac987b4e82ea88 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 23 Jan 2025 00:31:43 +0700 Subject: [PATCH 07/12] refactor: consolidate prompt functionality into support-prompt module - Move code action prompts from core/prompts to shared/support-prompt - Migrate enhance prompt functionality from modes to support-prompt - Add UI for managing code action prompts in PromptsView - Update types and interfaces for better prompt management --- .../prompts/__tests__/code-actions.test.ts | 76 ---------- src/core/prompts/code-actions.ts | 87 ----------- src/core/webview/ClineProvider.ts | 85 +++++++---- src/extension.ts | 66 ++++----- src/shared/WebviewMessage.ts | 3 +- src/shared/__tests__/support-prompts.test.ts | 138 ++++++++++++++++++ src/shared/modes.ts | 37 ++--- src/shared/support-prompt.ts | 118 +++++++++++++++ .../src/components/prompts/PromptsView.tsx | 125 ++++++++++++++-- 9 files changed, 467 insertions(+), 268 deletions(-) delete mode 100644 src/core/prompts/__tests__/code-actions.test.ts delete mode 100644 src/core/prompts/code-actions.ts create mode 100644 src/shared/__tests__/support-prompts.test.ts create mode 100644 src/shared/support-prompt.ts diff --git a/src/core/prompts/__tests__/code-actions.test.ts b/src/core/prompts/__tests__/code-actions.test.ts deleted file mode 100644 index 0ddb6bd..0000000 --- a/src/core/prompts/__tests__/code-actions.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from '../code-actions'; - -describe('Code Action Prompts', () => { - const testFilePath = 'test/file.ts'; - const testCode = 'function test() { return true; }'; - - describe('explainCodePrompt', () => { - it('should format explain prompt correctly', () => { - const prompt = explainCodePrompt({ - filePath: testFilePath, - selectedText: testCode - }); - - expect(prompt).toContain(`@/${testFilePath}`); - expect(prompt).toContain(testCode); - expect(prompt).toContain('purpose and functionality'); - expect(prompt).toContain('Key components'); - expect(prompt).toContain('Important patterns'); - }); - }); - - describe('fixCodePrompt', () => { - it('should format fix prompt without diagnostics', () => { - const prompt = fixCodePrompt({ - filePath: testFilePath, - selectedText: testCode - }); - - expect(prompt).toContain(`@/${testFilePath}`); - expect(prompt).toContain(testCode); - expect(prompt).toContain('Address all detected problems'); - expect(prompt).not.toContain('Current problems detected'); - }); - - it('should format fix prompt with diagnostics', () => { - const diagnostics = [ - { - source: 'eslint', - message: 'Missing semicolon', - code: 'semi' - }, - { - message: 'Unused variable', - severity: 1 - } - ]; - - const prompt = fixCodePrompt({ - filePath: testFilePath, - selectedText: testCode, - diagnostics - }); - - expect(prompt).toContain('Current problems detected:'); - expect(prompt).toContain('[eslint] Missing semicolon (semi)'); - expect(prompt).toContain('[Error] Unused variable'); - expect(prompt).toContain(testCode); - }); - }); - - describe('improveCodePrompt', () => { - it('should format improve prompt correctly', () => { - const prompt = improveCodePrompt({ - filePath: testFilePath, - selectedText: testCode - }); - - expect(prompt).toContain(`@/${testFilePath}`); - expect(prompt).toContain(testCode); - expect(prompt).toContain('Code readability'); - expect(prompt).toContain('Performance optimization'); - expect(prompt).toContain('Best practices'); - expect(prompt).toContain('Error handling'); - }); - }); -}); \ No newline at end of file diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts deleted file mode 100644 index 0811c48..0000000 --- a/src/core/prompts/code-actions.ts +++ /dev/null @@ -1,87 +0,0 @@ -type PromptParams = Record; - -const generateDiagnosticText = (diagnostics?: any[]) => { - if (!diagnostics?.length) return ''; - return `\nCurrent problems detected:\n${diagnostics.map(d => - `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}` - ).join('\n')}`; -}; - -export const createPrompt = (template: string, params: PromptParams): string => { - let result = template; - for (const [key, value] of Object.entries(params)) { - if (key === 'diagnostics') { - result = result.replaceAll('${diagnosticText}', generateDiagnosticText(value as any[])); - } else { - result = result.replaceAll(`\${${key}}`, value as string); - } - } - - // Replace any remaining user_input placeholders with empty string - result = result.replaceAll('${userInput}', ''); - - return result; -}; - -export const EXPLAIN_TEMPLATE = ` -Explain the following code from file path @/\${filePath}: -\${userInput} - -\`\`\` -\${selectedText} -\`\`\` - -Please provide a clear and concise explanation of what this code does, including: -1. The purpose and functionality -2. Key components and their interactions -3. Important patterns or techniques used -`; - -export const FIX_TEMPLATE = ` -Fix any issues in the following code from file path @/\${filePath} -\${diagnosticText} -\${userInput} - -\`\`\` -\${selectedText} -\`\`\` - -Please: -1. Address all detected problems listed above (if any) -2. Identify any other potential bugs or issues -3. Provide corrected code -4. Explain what was fixed and why -`; - -export const IMPROVE_TEMPLATE = ` -Improve the following code from file path @/\${filePath}: -\${userInput} - -\`\`\` -\${selectedText} -\`\`\` - -Please suggest improvements for: -1. Code readability and maintainability -2. Performance optimization -3. Best practices and patterns -4. Error handling and edge cases - -Provide the improved code along with explanations for each enhancement. -`; - -export const explainCodePrompt = (params: PromptParams) => - createPrompt(EXPLAIN_TEMPLATE, params); - -export const fixCodePrompt = (params: PromptParams) => - createPrompt(FIX_TEMPLATE, params); - -export const improveCodePrompt = (params: PromptParams) => - createPrompt(IMPROVE_TEMPLATE, params); - -// Get template based on prompt type -export const defaultTemplates = { - 'EXPLAIN': EXPLAIN_TEMPLATE, - 'FIX': FIX_TEMPLATE, - 'IMPROVE': IMPROVE_TEMPLATE -} \ No newline at end of file diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d897838..f919df6 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1,4 +1,5 @@ import { Anthropic } from "@anthropic-ai/sdk" +import delay from "delay" import axios from "axios" import fs from "fs/promises" import os from "os" @@ -23,7 +24,6 @@ import { modes, CustomPrompts, PromptComponent, - enhance, ModeConfig, defaultModeSlug, getModeBySlug, @@ -40,10 +40,7 @@ import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" -import { - defaultTemplates, - createPrompt -} from "../prompts/code-actions" +import { enhance, codeActionPrompt } from "../../shared/support-prompt" import { ACTION_NAMES } from "../CodeActionProvider" @@ -189,17 +186,27 @@ export class ClineProvider implements vscode.WebviewViewProvider { public static async handleCodeAction( promptType: keyof typeof ACTION_NAMES, - params: Record + params: Record, ): Promise { - const visibleProvider = ClineProvider.getVisibleInstance() + let visibleProvider = ClineProvider.getVisibleInstance() + + // If no visible provider, try to show the sidebar view + if (!visibleProvider) { + await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus") + // Wait briefly for the view to become visible + await delay(100) + visibleProvider = ClineProvider.getVisibleInstance() + } + + // If still no visible provider, return if (!visibleProvider) { return } - const { utilPrompt } = await visibleProvider.getState() + const { customPrompts } = await visibleProvider.getState() + + const prompt = codeActionPrompt.create(promptType, params, customPrompts) - const template = utilPrompt?.[promptType] ?? defaultTemplates[promptType] - const prompt = createPrompt(template, params) await visibleProvider.initClineWithTask(prompt) } @@ -297,7 +304,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] + const modePrompt = customPrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -325,7 +332,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] + const modePrompt = customPrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -804,29 +811,49 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() break - case "updateEnhancedPrompt": - const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + case "updateSupportPrompt": + try { + if (Object.keys(message?.values ?? {}).length === 0) { + return + } - const updatedPrompts = { - ...existingPrompts, - enhance: message.text, + const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + + const updatedPrompts = { + ...existingPrompts, + ...message.values, + } + + await this.updateGlobalState("customPrompts", updatedPrompts) + await this.postStateToWebview() + } catch (error) { + console.error("Error update support prompt:", error) + vscode.window.showErrorMessage("Failed to update support prompt") } + break + case "resetSupportPrompt": + try { + if (!message?.text) { + return + } - await this.updateGlobalState("customPrompts", updatedPrompts) + const existingPrompts = ((await this.getGlobalState("customPrompts")) || {}) as Record< + string, + any + > - // Get current state and explicitly include customPrompts - const currentState = await this.getState() + const updatedPrompts = { + ...existingPrompts, + } - const stateWithPrompts = { - ...currentState, - customPrompts: updatedPrompts, + updatedPrompts[message.text] = undefined + + await this.updateGlobalState("customPrompts", updatedPrompts) + await this.postStateToWebview() + } catch (error) { + console.error("Error reset support prompt:", error) + vscode.window.showErrorMessage("Failed to reset support prompt") } - - // Post state with prompts - this.view?.webview.postMessage({ - type: "state", - state: stateWithPrompts, - }) break case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { diff --git a/src/extension.ts b/src/extension.ts index cf910fc..db011e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,7 +6,6 @@ import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider" -import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from "./core/prompts/code-actions" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" /* @@ -162,14 +161,10 @@ export function activate(context: vscode.ExtensionContext) { // Register code actions provider context.subscriptions.push( - vscode.languages.registerCodeActionsProvider( - { pattern: "**/*" }, - new CodeActionProvider(), - { - providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds - } - ) - ); + vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), { + providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds, + }), + ) // Helper function to handle code actions const registerCodeAction = ( @@ -177,51 +172,54 @@ export function activate(context: vscode.ExtensionContext) { command: string, promptType: keyof typeof ACTION_NAMES, inputPrompt: string, - inputPlaceholder: string + inputPlaceholder: string, ) => { context.subscriptions.push( - vscode.commands.registerCommand(command, async (filePath: string, selectedText: string, diagnostics?: any[]) => { - const userInput = await vscode.window.showInputBox({ - prompt: inputPrompt, - placeHolder: inputPlaceholder - }); + vscode.commands.registerCommand( + command, + async (filePath: string, selectedText: string, diagnostics?: any[]) => { + const userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder, + }) - const params = { - filePath, - selectedText, - ...(diagnostics ? { diagnostics } : {}), - ...(userInput ? { userInput } : {}) - }; + const params = { + filePath, + selectedText, + ...(diagnostics ? { diagnostics } : {}), + ...(userInput ? { userInput } : {}), + } - await ClineProvider.handleCodeAction(promptType, params); - }) - ); - }; + await ClineProvider.handleCodeAction(promptType, params) + }, + ), + ) + } // Register code action commands registerCodeAction( context, "roo-cline.explainCode", - 'EXPLAIN', + "EXPLAIN", "Any specific questions about this code?", - "E.g. How does the error handling work?" - ); + "E.g. How does the error handling work?", + ) registerCodeAction( context, "roo-cline.fixCode", - 'FIX', + "FIX", "Any specific concerns about fixing this code?", - "E.g. Maintain backward compatibility" - ); + "E.g. Maintain backward compatibility", + ) registerCodeAction( context, "roo-cline.improveCode", - 'IMPROVE', + "IMPROVE", "Any specific aspects you want to improve?", - "E.g. Focus on performance optimization" - ); + "E.g. Focus on performance optimization", + ) return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index b63b898..e2830bf 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -68,7 +68,8 @@ export interface WebviewMessage { | "requestVsCodeLmModels" | "mode" | "updatePrompt" - | "updateEnhancedPrompt" + | "updateSupportPrompt" + | "resetSupportPrompt" | "getSystemPrompt" | "systemPrompt" | "enhancementApiConfigId" diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts new file mode 100644 index 0000000..92d2342 --- /dev/null +++ b/src/shared/__tests__/support-prompts.test.ts @@ -0,0 +1,138 @@ +import { codeActionPrompt, type CodeActionType } from "../support-prompt" + +describe("Code Action Prompts", () => { + const testFilePath = "test/file.ts" + const testCode = "function test() { return true; }" + + describe("EXPLAIN action", () => { + it("should format explain prompt correctly", () => { + const prompt = codeActionPrompt.create("EXPLAIN", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toContain(`@/${testFilePath}`) + expect(prompt).toContain(testCode) + expect(prompt).toContain("purpose and functionality") + expect(prompt).toContain("Key components") + expect(prompt).toContain("Important patterns") + }) + }) + + describe("FIX action", () => { + it("should format fix prompt without diagnostics", () => { + const prompt = codeActionPrompt.create("FIX", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toContain(`@/${testFilePath}`) + expect(prompt).toContain(testCode) + expect(prompt).toContain("Address all detected problems") + expect(prompt).not.toContain("Current problems detected") + }) + + it("should format fix prompt with diagnostics", () => { + const diagnostics = [ + { + source: "eslint", + message: "Missing semicolon", + code: "semi", + }, + { + message: "Unused variable", + severity: 1, + }, + ] + + const prompt = codeActionPrompt.create("FIX", { + filePath: testFilePath, + selectedText: testCode, + diagnostics, + }) + + expect(prompt).toContain("Current problems detected:") + expect(prompt).toContain("[eslint] Missing semicolon (semi)") + expect(prompt).toContain("[Error] Unused variable") + expect(prompt).toContain(testCode) + }) + }) + + describe("IMPROVE action", () => { + it("should format improve prompt correctly", () => { + const prompt = codeActionPrompt.create("IMPROVE", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toContain(`@/${testFilePath}`) + expect(prompt).toContain(testCode) + expect(prompt).toContain("Code readability") + expect(prompt).toContain("Performance optimization") + expect(prompt).toContain("Best practices") + expect(prompt).toContain("Error handling") + }) + }) + + describe("get template", () => { + it("should return default template when no custom prompts provided", () => { + const template = codeActionPrompt.get(undefined, "EXPLAIN") + expect(template).toBe(codeActionPrompt.default.EXPLAIN) + }) + + it("should return custom template when provided", () => { + const customTemplate = "Custom template for explaining code" + const customPrompts = { + EXPLAIN: customTemplate, + } + const template = codeActionPrompt.get(customPrompts, "EXPLAIN") + expect(template).toBe(customTemplate) + }) + + it("should return default template when custom prompts does not include type", () => { + const customPrompts = { + SOMETHING_ELSE: "Other template", + } + const template = codeActionPrompt.get(customPrompts, "EXPLAIN") + expect(template).toBe(codeActionPrompt.default.EXPLAIN) + }) + }) + + describe("create with custom prompts", () => { + it("should use custom template when provided", () => { + const customTemplate = "Custom template for ${filePath}" + const customPrompts = { + EXPLAIN: customTemplate, + } + + const prompt = codeActionPrompt.create( + "EXPLAIN", + { + filePath: testFilePath, + selectedText: testCode, + }, + customPrompts, + ) + + expect(prompt).toContain(`Custom template for ${testFilePath}`) + expect(prompt).not.toContain("purpose and functionality") + }) + + it("should use default template when custom prompts does not include type", () => { + const customPrompts = { + EXPLAIN: "Other template", + } + + const prompt = codeActionPrompt.create( + "EXPLAIN", + { + filePath: testFilePath, + selectedText: testCode, + }, + customPrompts, + ) + + expect(prompt).toContain("Other template") + }) + }) +}) diff --git a/src/shared/modes.ts b/src/shared/modes.ts index c6ea89a..451e661 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -12,6 +12,16 @@ export type ModeConfig = { groups: readonly ToolGroup[] // Now uses groups instead of tools array } +// Mode-specific prompts only +export type PromptComponent = { + roleDefinition?: string + customInstructions?: string +} + +export type CustomPrompts = { + [key: string]: PromptComponent | undefined | string +} + // Helper to get all tools for a mode export function getToolsForMode(groups: readonly ToolGroup[]): string[] { const tools = new Set() @@ -130,33 +140,6 @@ export function isToolAllowedForMode( return mode.groups.some((group) => TOOL_GROUPS[group].includes(tool as string)) } -export type PromptComponent = { - roleDefinition?: string - customInstructions?: string -} - -// Mode-specific prompts only -export type CustomPrompts = { - [key: string]: PromptComponent | undefined -} - -// Separate enhance prompt type and definition -export type EnhanceConfig = { - prompt: string -} - -export const enhance: EnhanceConfig = { - prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", -} as const - -// Completely separate enhance prompt handling -export const enhancePrompt = { - default: enhance.prompt, - get: (customPrompts: Record | undefined): string => { - return customPrompts?.enhance ?? enhance.prompt - }, -} as const - // Create the mode-specific default prompts export const defaultPrompts: Readonly = Object.freeze( Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])), diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts new file mode 100644 index 0000000..4ce9f93 --- /dev/null +++ b/src/shared/support-prompt.ts @@ -0,0 +1,118 @@ +// Separate enhance prompt type and definition +export type EnhanceConfig = { + prompt: string +} + +export const enhance: EnhanceConfig = { + prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", +} as const + +// Completely separate enhance prompt handling +export const enhancePrompt = { + default: enhance.prompt, + get: (customPrompts: Record | undefined): string => { + return customPrompts?.enhance ?? enhance.prompt + }, +} as const + +// Code action prompts +type PromptParams = Record + +const generateDiagnosticText = (diagnostics?: any[]) => { + if (!diagnostics?.length) return "" + return `\nCurrent problems detected:\n${diagnostics + .map((d) => `- [${d.source || "Error"}] ${d.message}${d.code ? ` (${d.code})` : ""}`) + .join("\n")}` +} + +export const createPrompt = (template: string, params: PromptParams): string => { + let result = template + for (const [key, value] of Object.entries(params)) { + if (key === "diagnostics") { + result = result.replaceAll("${diagnosticText}", generateDiagnosticText(value as any[])) + } else { + result = result.replaceAll(`\${${key}}`, value as string) + } + } + + // Replace any remaining user_input placeholders with empty string + result = result.replaceAll("${userInput}", "") + + return result +} + +const EXPLAIN_TEMPLATE = ` +Explain the following code from file path @/\${filePath}: +\${userInput} + +\`\`\` +\${selectedText} +\`\`\` + +Please provide a clear and concise explanation of what this code does, including: +1. The purpose and functionality +2. Key components and their interactions +3. Important patterns or techniques used +` + +const FIX_TEMPLATE = ` +Fix any issues in the following code from file path @/\${filePath} +\${diagnosticText} +\${userInput} + +\`\`\` +\${selectedText} +\`\`\` + +Please: +1. Address all detected problems listed above (if any) +2. Identify any other potential bugs or issues +3. Provide corrected code +4. Explain what was fixed and why +` + +const IMPROVE_TEMPLATE = ` +Improve the following code from file path @/\${filePath}: +\${userInput} + +\`\`\` +\${selectedText} +\`\`\` + +Please suggest improvements for: +1. Code readability and maintainability +2. Performance optimization +3. Best practices and patterns +4. Error handling and edge cases + +Provide the improved code along with explanations for each enhancement. +` + +// Get template based on prompt type +const defaultTemplates = { + EXPLAIN: EXPLAIN_TEMPLATE, + FIX: FIX_TEMPLATE, + IMPROVE: IMPROVE_TEMPLATE, +} as const + +type CodeActionType = keyof typeof defaultTemplates + +export const codeActionPrompt = { + default: defaultTemplates, + get: (customPrompts: Record | undefined, type: CodeActionType): string => { + return customPrompts?.[type] ?? defaultTemplates[type] + }, + create: (type: CodeActionType, params: PromptParams, customPrompts?: Record): string => { + const template = codeActionPrompt.get(customPrompts, type) + return createPrompt(template, params) + }, +} as const + +export type { CodeActionType } + +// User-friendly labels for code action types +export const codeActionLabels: Record = { + FIX: "Fix Issues", + EXPLAIN: "Explain Code", + IMPROVE: "Improve Code", +} as const diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index ebd4d0f..a767fd4 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -8,14 +8,14 @@ import { VSCodeCheckbox, } from "@vscode/webview-ui-toolkit/react" import { useExtensionState } from "../../context/ExtensionStateContext" +import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes" import { - Mode, - PromptComponent, - getRoleDefinition, - getAllModes, - ModeConfig, enhancePrompt, -} from "../../../../src/shared/modes" + codeActionPrompt, + CodeActionType, + codeActionLabels, +} from "../../../../src/shared/support-prompt" + import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" @@ -50,11 +50,12 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) + const [activeCodeActionTab, setActiveCodeActionTab] = useState("FIX") // Direct update functions const updateAgentPrompt = useCallback( (mode: Mode, promptData: PromptComponent) => { - const existingPrompt = customPrompts?.[mode] + const existingPrompt = customPrompts?.[mode] as PromptComponent const updatedPrompt = { ...existingPrompt, ...promptData } // Only include properties that differ from defaults @@ -256,8 +257,19 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const updateEnhancePrompt = (value: string | undefined) => { vscode.postMessage({ - type: "updateEnhancedPrompt", - text: value, + type: "updateSupportPrompt", + values: { + enhance: value, + }, + }) + } + + const updateCodeActionPrompt = (type: CodeActionType, value: string | undefined) => { + vscode.postMessage({ + type: "updateSupportPrompt", + values: { + [type]: value, + }, }) } @@ -271,7 +283,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const handleAgentReset = (modeSlug: string) => { // Only reset role definition for built-in modes - const existingPrompt = customPrompts?.[modeSlug] + const existingPrompt = customPrompts?.[modeSlug] as PromptComponent updateAgentPrompt(modeSlug, { ...existingPrompt, roleDefinition: undefined, @@ -279,13 +291,27 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { } const handleEnhanceReset = () => { - updateEnhancePrompt(undefined) + vscode.postMessage({ + type: "resetSupportPrompt", + text: "enhance", + }) + } + + const handleCodeActionReset = (type: CodeActionType) => { + vscode.postMessage({ + type: "resetSupportPrompt", + text: type, + }) } const getEnhancePromptValue = (): string => { return enhancePrompt.get(customPrompts) } + const getCodeActionPromptValue = (type: CodeActionType): string => { + return codeActionPrompt.get(customPrompts, type) + } + const handleTestEnhancement = () => { if (!testPrompt.trim()) return @@ -563,7 +589,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] + const prompt = customPrompts?.[mode] as PromptComponent return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode) })()} onChange={(e) => { @@ -680,7 +706,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] + const prompt = customPrompts?.[mode] as PromptComponent return customMode?.customInstructions ?? prompt?.customInstructions ?? "" })()} onChange={(e) => { @@ -696,7 +722,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } else { // For built-in modes, update the prompts - const existingPrompt = customPrompts?.[mode] + const existingPrompt = customPrompts?.[mode] as PromptComponent updateAgentPrompt(mode, { ...existingPrompt, customInstructions: value.trim() || undefined, @@ -759,6 +785,77 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { +
+
Code Action Prompts
+
+ {Object.keys(codeActionPrompt.default).map((type) => ( + + ))} +
+ + {/* Show active tab content */} +
+
+
{activeCodeActionTab} Prompt
+ handleCodeActionReset(activeCodeActionTab)} + title={`Reset ${activeCodeActionTab} prompt to default`}> + + +
+ { + const value = + (e as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + const trimmedValue = value.trim() + updateCodeActionPrompt(activeCodeActionTab, trimmedValue || undefined) + }} + rows={4} + resize="vertical" + style={{ width: "100%" }} + /> +
+
+

Prompt Enhancement

Date: Thu, 23 Jan 2025 10:46:04 +0700 Subject: [PATCH 08/12] refactor: consolidate code action and enhance prompts into unified support prompts system - Rename codeActionPrompt to supportPrompt for better clarity - Move enhance prompt functionality into support prompts system - Add ENHANCE tab alongside other support prompt types - Update UI to show enhancement configuration in ENHANCE tab - Update tests to reflect new unified structure This change simplifies the prompt system by treating enhancement as another type of support prompt rather than a separate system. --- src/core/webview/ClineProvider.ts | 12 +- src/shared/__tests__/support-prompts.test.ts | 40 ++- src/shared/support-prompt.ts | 49 ++-- .../src/components/prompts/PromptsView.tsx | 248 +++++++----------- .../prompts/__tests__/PromptsView.test.tsx | 4 + 5 files changed, 140 insertions(+), 213 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index f919df6..b28bad6 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -40,7 +40,7 @@ import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" -import { enhance, codeActionPrompt } from "../../shared/support-prompt" +import { supportPrompt } from "../../shared/support-prompt" import { ACTION_NAMES } from "../CodeActionProvider" @@ -205,7 +205,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { const { customPrompts } = await visibleProvider.getState() - const prompt = codeActionPrompt.create(promptType, params, customPrompts) + const prompt = supportPrompt.create(promptType, params, customPrompts) await visibleProvider.initClineWithTask(prompt) } @@ -996,16 +996,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } - const getEnhancePrompt = (value: string | PromptComponent | undefined): string => { - if (typeof value === "string") { - return value - } - return enhance.prompt // Use the constant from modes.ts which we know is a string - } const enhancedPrompt = await enhancePrompt( configToUse, message.text, - getEnhancePrompt(customPrompts?.enhance), + supportPrompt.get(customPrompts, "ENHANCE"), ) await this.postMessageToWebview({ type: "enhancedPrompt", diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts index 92d2342..ee7253e 100644 --- a/src/shared/__tests__/support-prompts.test.ts +++ b/src/shared/__tests__/support-prompts.test.ts @@ -1,4 +1,4 @@ -import { codeActionPrompt, type CodeActionType } from "../support-prompt" +import { supportPrompt } from "../support-prompt" describe("Code Action Prompts", () => { const testFilePath = "test/file.ts" @@ -6,7 +6,7 @@ describe("Code Action Prompts", () => { describe("EXPLAIN action", () => { it("should format explain prompt correctly", () => { - const prompt = codeActionPrompt.create("EXPLAIN", { + const prompt = supportPrompt.create("EXPLAIN", { filePath: testFilePath, selectedText: testCode, }) @@ -21,7 +21,7 @@ describe("Code Action Prompts", () => { describe("FIX action", () => { it("should format fix prompt without diagnostics", () => { - const prompt = codeActionPrompt.create("FIX", { + const prompt = supportPrompt.create("FIX", { filePath: testFilePath, selectedText: testCode, }) @@ -45,7 +45,7 @@ describe("Code Action Prompts", () => { }, ] - const prompt = codeActionPrompt.create("FIX", { + const prompt = supportPrompt.create("FIX", { filePath: testFilePath, selectedText: testCode, diagnostics, @@ -60,7 +60,7 @@ describe("Code Action Prompts", () => { describe("IMPROVE action", () => { it("should format improve prompt correctly", () => { - const prompt = codeActionPrompt.create("IMPROVE", { + const prompt = supportPrompt.create("IMPROVE", { filePath: testFilePath, selectedText: testCode, }) @@ -74,10 +74,26 @@ describe("Code Action Prompts", () => { }) }) + describe("ENHANCE action", () => { + it("should format enhance prompt correctly", () => { + const prompt = supportPrompt.create("ENHANCE", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toBe( + "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", + ) + // Verify it ignores parameters since ENHANCE template doesn't use any + expect(prompt).not.toContain(testFilePath) + expect(prompt).not.toContain(testCode) + }) + }) + describe("get template", () => { it("should return default template when no custom prompts provided", () => { - const template = codeActionPrompt.get(undefined, "EXPLAIN") - expect(template).toBe(codeActionPrompt.default.EXPLAIN) + const template = supportPrompt.get(undefined, "EXPLAIN") + expect(template).toBe(supportPrompt.default.EXPLAIN) }) it("should return custom template when provided", () => { @@ -85,7 +101,7 @@ describe("Code Action Prompts", () => { const customPrompts = { EXPLAIN: customTemplate, } - const template = codeActionPrompt.get(customPrompts, "EXPLAIN") + const template = supportPrompt.get(customPrompts, "EXPLAIN") expect(template).toBe(customTemplate) }) @@ -93,8 +109,8 @@ describe("Code Action Prompts", () => { const customPrompts = { SOMETHING_ELSE: "Other template", } - const template = codeActionPrompt.get(customPrompts, "EXPLAIN") - expect(template).toBe(codeActionPrompt.default.EXPLAIN) + const template = supportPrompt.get(customPrompts, "EXPLAIN") + expect(template).toBe(supportPrompt.default.EXPLAIN) }) }) @@ -105,7 +121,7 @@ describe("Code Action Prompts", () => { EXPLAIN: customTemplate, } - const prompt = codeActionPrompt.create( + const prompt = supportPrompt.create( "EXPLAIN", { filePath: testFilePath, @@ -123,7 +139,7 @@ describe("Code Action Prompts", () => { EXPLAIN: "Other template", } - const prompt = codeActionPrompt.create( + const prompt = supportPrompt.create( "EXPLAIN", { filePath: testFilePath, diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 4ce9f93..406e981 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -1,21 +1,4 @@ -// Separate enhance prompt type and definition -export type EnhanceConfig = { - prompt: string -} - -export const enhance: EnhanceConfig = { - prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", -} as const - -// Completely separate enhance prompt handling -export const enhancePrompt = { - default: enhance.prompt, - get: (customPrompts: Record | undefined): string => { - return customPrompts?.enhance ?? enhance.prompt - }, -} as const - -// Code action prompts +// Support prompts type PromptParams = Record const generateDiagnosticText = (diagnostics?: any[]) => { @@ -41,8 +24,7 @@ export const createPrompt = (template: string, params: PromptParams): string => return result } -const EXPLAIN_TEMPLATE = ` -Explain the following code from file path @/\${filePath}: +const EXPLAIN_TEMPLATE = `Explain the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -55,8 +37,7 @@ Please provide a clear and concise explanation of what this code does, including 3. Important patterns or techniques used ` -const FIX_TEMPLATE = ` -Fix any issues in the following code from file path @/\${filePath} +const FIX_TEMPLATE = `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} @@ -71,8 +52,7 @@ Please: 4. Explain what was fixed and why ` -const IMPROVE_TEMPLATE = ` -Improve the following code from file path @/\${filePath}: +const IMPROVE_TEMPLATE = `Improve the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -88,31 +68,36 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement. ` +const ENHANCE_TEMPLATE = + "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):" + // Get template based on prompt type const defaultTemplates = { EXPLAIN: EXPLAIN_TEMPLATE, FIX: FIX_TEMPLATE, IMPROVE: IMPROVE_TEMPLATE, + ENHANCE: ENHANCE_TEMPLATE, } as const -type CodeActionType = keyof typeof defaultTemplates +type SupportPromptType = keyof typeof defaultTemplates -export const codeActionPrompt = { +export const supportPrompt = { default: defaultTemplates, - get: (customPrompts: Record | undefined, type: CodeActionType): string => { + get: (customPrompts: Record | undefined, type: SupportPromptType): string => { return customPrompts?.[type] ?? defaultTemplates[type] }, - create: (type: CodeActionType, params: PromptParams, customPrompts?: Record): string => { - const template = codeActionPrompt.get(customPrompts, type) + create: (type: SupportPromptType, params: PromptParams, customPrompts?: Record): string => { + const template = supportPrompt.get(customPrompts, type) return createPrompt(template, params) }, } as const -export type { CodeActionType } +export type { SupportPromptType } -// User-friendly labels for code action types -export const codeActionLabels: Record = { +// User-friendly labels for support prompt types +export const supportPromptLabels: Record = { FIX: "Fix Issues", EXPLAIN: "Explain Code", IMPROVE: "Improve Code", + ENHANCE: "Enhance Prompt", } as const diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index a767fd4..6f5085a 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -9,12 +9,7 @@ import { } from "@vscode/webview-ui-toolkit/react" import { useExtensionState } from "../../context/ExtensionStateContext" import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes" -import { - enhancePrompt, - codeActionPrompt, - CodeActionType, - codeActionLabels, -} from "../../../../src/shared/support-prompt" +import { supportPrompt, SupportPromptType, supportPromptLabels } from "../../../../src/shared/support-prompt" import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" @@ -50,7 +45,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) - const [activeCodeActionTab, setActiveCodeActionTab] = useState("FIX") + const [activeSupportTab, setActiveSupportTab] = useState("EXPLAIN") // Direct update functions const updateAgentPrompt = useCallback( @@ -255,16 +250,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { return () => window.removeEventListener("message", handler) }, []) - const updateEnhancePrompt = (value: string | undefined) => { - vscode.postMessage({ - type: "updateSupportPrompt", - values: { - enhance: value, - }, - }) - } - - const updateCodeActionPrompt = (type: CodeActionType, value: string | undefined) => { + const updateSupportPrompt = (type: SupportPromptType, value: string | undefined) => { vscode.postMessage({ type: "updateSupportPrompt", values: { @@ -273,14 +259,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } - const handleEnhancePromptChange = (e: Event | React.FormEvent): void => { - const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value - const trimmedValue = value.trim() - if (trimmedValue !== enhancePrompt.default) { - updateEnhancePrompt(trimmedValue || enhancePrompt.default) - } - } - const handleAgentReset = (modeSlug: string) => { // Only reset role definition for built-in modes const existingPrompt = customPrompts?.[modeSlug] as PromptComponent @@ -290,26 +268,15 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } - const handleEnhanceReset = () => { - vscode.postMessage({ - type: "resetSupportPrompt", - text: "enhance", - }) - } - - const handleCodeActionReset = (type: CodeActionType) => { + const handleSupportReset = (type: SupportPromptType) => { vscode.postMessage({ type: "resetSupportPrompt", text: type, }) } - const getEnhancePromptValue = (): string => { - return enhancePrompt.get(customPrompts) - } - - const getCodeActionPromptValue = (type: CodeActionType): string => { - return codeActionPrompt.get(customPrompts, type) + const getSupportPromptValue = (type: SupportPromptType): string => { + return supportPrompt.get(customPrompts, type) } const handleTestEnhancement = () => { @@ -786,7 +753,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
-
Code Action Prompts
+
Support Prompts
{ paddingBottom: "4px", paddingRight: "20px", }}> - {Object.keys(codeActionPrompt.default).map((type) => ( + {Object.keys(supportPrompt.default).map((type) => ( ))}
{/* Show active tab content */} -
+
{ alignItems: "center", marginBottom: "4px", }}> -
{activeCodeActionTab} Prompt
+
{activeSupportTab} Prompt
handleCodeActionReset(activeCodeActionTab)} - title={`Reset ${activeCodeActionTab} prompt to default`}> + onClick={() => handleSupportReset(activeSupportTab)} + title={`Reset ${activeSupportTab} prompt to default`}>
+ + {activeSupportTab === "ENHANCE" && ( +
+
+ Use prompt enhancement to get tailored suggestions or improvements for your inputs. + This ensures Roo understands your intent and provides the best possible responses. +
+
+
+
API Configuration
+
+ You can select an API configuration to always use for enhancing prompts, or + just use whatever is currently selected +
+
+ { + const value = e.detail?.target?.value || e.target?.value + setEnhancementApiConfigId(value) + vscode.postMessage({ + type: "enhancementApiConfigId", + text: value, + }) + }} + style={{ width: "300px" }}> + Use currently selected API configuration + {(listApiConfigMeta || []).map((config) => ( + + {config.name} + + ))} + +
+
+ )} + { const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value const trimmedValue = value.trim() - updateCodeActionPrompt(activeCodeActionTab, trimmedValue || undefined) + updateSupportPrompt(activeSupportTab, trimmedValue || undefined) }} rows={4} resize="vertical" style={{ width: "100%" }} /> -
-
-

Prompt Enhancement

- -
- Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo - understands your intent and provides the best possible responses. -
- -
-
-
-
-
API Configuration
-
- You can select an API configuration to always use for enhancing prompts, or just use - whatever is currently selected -
-
- { - const value = e.detail?.target?.value || e.target?.value - setEnhancementApiConfigId(value) - vscode.postMessage({ - type: "enhancementApiConfigId", - text: value, - }) - }} - style={{ width: "300px" }}> - Use currently selected API configuration - {(listApiConfigMeta || []).map((config) => ( - - {config.name} - - ))} - -
- -
-
-
Enhancement Prompt
-
+ {activeSupportTab === "ENHANCE" && ( +
+ setTestPrompt((e.target as HTMLTextAreaElement).value)} + placeholder="Enter a prompt to test the enhancement" + rows={3} + resize="vertical" + style={{ width: "100%" }} + data-testid="test-prompt-textarea" + /> +
- + onClick={handleTestEnhancement} + disabled={isEnhancing} + appearance="primary"> + Preview Prompt Enhancement
-
- This prompt will be used to refine your input when you hit the sparkle icon in chat. -
-
- - -
- setTestPrompt((e.target as HTMLTextAreaElement).value)} - placeholder="Enter a prompt to test the enhancement" - rows={3} - resize="vertical" - style={{ width: "100%" }} - data-testid="test-prompt-textarea" - /> -
- - Preview Prompt Enhancement - -
-
+ )}
- - {/* Bottom padding */} -
{isCreateModeDialogOpen && ( diff --git a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx index 5619d26..93e8698 100644 --- a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx +++ b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx @@ -166,6 +166,10 @@ describe("PromptsView", () => { it("handles API configuration selection", () => { renderPromptsView() + // Click the ENHANCE tab first to show the API config dropdown + const enhanceTab = screen.getByTestId("ENHANCE-tab") + fireEvent.click(enhanceTab) + const dropdown = screen.getByTestId("api-config-dropdown") fireEvent( dropdown, From 149e86ed0a6464cbb76d2562239faa3c1e2a4b87 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 00:51:35 +0700 Subject: [PATCH 09/12] fix comment on pr --- package.json | 188 +++++------ src/core/CodeActionProvider.ts | 300 +++++++++--------- src/core/__tests__/CodeActionProvider.test.ts | 264 +++++++-------- src/extension.ts | 6 +- src/test/extension.test.ts | 43 ++- 5 files changed, 398 insertions(+), 403 deletions(-) diff --git a/package.json b/package.json index 7715001..e5cf882 100644 --- a/package.json +++ b/package.json @@ -72,102 +72,102 @@ "title": "New Task", "icon": "$(add)" }, - { - "command": "roo-cline.mcpButtonClicked", - "title": "MCP Servers", - "icon": "$(server)" - }, - { - "command": "roo-cline.promptsButtonClicked", - "title": "Prompts", - "icon": "$(notebook)" - }, - { - "command": "roo-cline.historyButtonClicked", - "title": "History", - "icon": "$(history)" - }, - { - "command": "roo-cline.popoutButtonClicked", - "title": "Open in Editor", - "icon": "$(link-external)" - }, - { - "command": "roo-cline.settingsButtonClicked", - "title": "Settings", - "icon": "$(settings-gear)" - }, - { - "command": "roo-cline.openInNewTab", - "title": "Open In New Tab", - "category": "Roo Code" - }, - { - "command": "roo-cline.explainCode", - "title": "Explain Code", - "category": "Roo Cline" - }, - { - "command": "roo-cline.fixCode", - "title": "Fix Code", - "category": "Roo Cline" - }, - { - "command": "roo-cline.improveCode", - "title": "Improve Code", - "category": "Roo Cline" - } + { + "command": "roo-cline.mcpButtonClicked", + "title": "MCP Servers", + "icon": "$(server)" + }, + { + "command": "roo-cline.promptsButtonClicked", + "title": "Prompts", + "icon": "$(notebook)" + }, + { + "command": "roo-cline.historyButtonClicked", + "title": "History", + "icon": "$(history)" + }, + { + "command": "roo-cline.popoutButtonClicked", + "title": "Open in Editor", + "icon": "$(link-external)" + }, + { + "command": "roo-cline.settingsButtonClicked", + "title": "Settings", + "icon": "$(settings-gear)" + }, + { + "command": "roo-cline.openInNewTab", + "title": "Open In New Tab", + "category": "Roo Code" + }, + { + "command": "roo-cline.explainCode", + "title": "Explain Code", + "category": "Roo Code" + }, + { + "command": "roo-cline.fixCode", + "title": "Fix Code", + "category": "Roo Code" + }, + { + "command": "roo-cline.improveCode", + "title": "Improve Code", + "category": "Roo Code" + } ], "menus": { - "editor/context": [ - { - "command": "roo-cline.explainCode", - "when": "editorHasSelection", - "group": "Roo Cline@1" - }, - { - "command": "roo-cline.fixCode", - "when": "editorHasSelection", - "group": "Roo Cline@2" - }, - { - "command": "roo-cline.improveCode", - "when": "editorHasSelection", - "group": "Roo Cline@3" - } - ], - "view/title": [ - { - "command": "roo-cline.plusButtonClicked", - "group": "navigation@1", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.promptsButtonClicked", - "group": "navigation@2", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.mcpButtonClicked", - "group": "navigation@3", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.historyButtonClicked", - "group": "navigation@4", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.popoutButtonClicked", - "group": "navigation@5", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.settingsButtonClicked", - "group": "navigation@6", - "when": "view == roo-cline.SidebarProvider" - } - ] + "editor/context": [ + { + "command": "roo-cline.explainCode", + "when": "editorHasSelection", + "group": "Roo Code@1" + }, + { + "command": "roo-cline.fixCode", + "when": "editorHasSelection", + "group": "Roo Code@2" + }, + { + "command": "roo-cline.improveCode", + "when": "editorHasSelection", + "group": "Roo Code@3" + } + ], + "view/title": [ + { + "command": "roo-cline.plusButtonClicked", + "group": "navigation@1", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.promptsButtonClicked", + "group": "navigation@2", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.mcpButtonClicked", + "group": "navigation@3", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.historyButtonClicked", + "group": "navigation@4", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "navigation@5", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.settingsButtonClicked", + "group": "navigation@6", + "when": "view == roo-cline.SidebarProvider" + } + ] }, "configuration": { "title": "Roo Code", diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index cac2bdc..d3b980a 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,181 +1,179 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; +import * as vscode from "vscode" +import * as path from "path" export const ACTION_NAMES = { - EXPLAIN: 'Roo Cline: Explain Code', - FIX: 'Roo Cline: Fix Code', - IMPROVE: 'Roo Cline: Improve Code' -} as const; + EXPLAIN: "Roo Code: Explain Code", + FIX: "Roo Code: Fix Code", + IMPROVE: "Roo Code: Improve Code", +} as const const COMMAND_IDS = { - EXPLAIN: 'roo-cline.explainCode', - FIX: 'roo-cline.fixCode', - IMPROVE: 'roo-cline.improveCode' -} as const; + EXPLAIN: "roo-cline.explainCode", + FIX: "roo-cline.fixCode", + IMPROVE: "roo-cline.improveCode", +} as const interface DiagnosticData { - message: string; - severity: vscode.DiagnosticSeverity; - code?: string | number | { value: string | number; target: vscode.Uri }; - source?: string; - range: vscode.Range; + message: string + severity: vscode.DiagnosticSeverity + code?: string | number | { value: string | number; target: vscode.Uri } + source?: string + range: vscode.Range } interface EffectiveRange { - range: vscode.Range; - text: string; + range: vscode.Range + text: string } export class CodeActionProvider implements vscode.CodeActionProvider { - public static readonly providedCodeActionKinds = [ - vscode.CodeActionKind.QuickFix, - vscode.CodeActionKind.RefactorRewrite, - ]; + public static readonly providedCodeActionKinds = [ + vscode.CodeActionKind.QuickFix, + vscode.CodeActionKind.RefactorRewrite, + ] - // Cache file paths for performance - private readonly filePathCache = new WeakMap(); + // Cache file paths for performance + private readonly filePathCache = new WeakMap() - private getEffectiveRange( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection - ): EffectiveRange | null { - try { - const selectedText = document.getText(range); - if (selectedText) { - return { range, text: selectedText }; - } + private getEffectiveRange( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + ): EffectiveRange | null { + try { + const selectedText = document.getText(range) + if (selectedText) { + return { range, text: selectedText } + } - const currentLine = document.lineAt(range.start.line); - if (!currentLine.text.trim()) { - return null; - } + const currentLine = document.lineAt(range.start.line) + if (!currentLine.text.trim()) { + return null + } - // Optimize range creation by checking bounds first - const startLine = Math.max(0, currentLine.lineNumber - 1); - const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1); - - // Only create new positions if needed - const effectiveRange = new vscode.Range( - startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), - endLine === currentLine.lineNumber ? range.end : new vscode.Position(endLine, document.lineAt(endLine).text.length) - ); + // Optimize range creation by checking bounds first + const startLine = Math.max(0, currentLine.lineNumber - 1) + const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) - return { - range: effectiveRange, - text: document.getText(effectiveRange) - }; - } catch (error) { - console.error('Error getting effective range:', error); - return null; - } - } + // Only create new positions if needed + const effectiveRange = new vscode.Range( + startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), + endLine === currentLine.lineNumber + ? range.end + : new vscode.Position(endLine, document.lineAt(endLine).text.length), + ) - private getFilePath(document: vscode.TextDocument): string { - // Check cache first - let filePath = this.filePathCache.get(document); - if (filePath) { - return filePath; - } + return { + range: effectiveRange, + text: document.getText(effectiveRange), + } + } catch (error) { + console.error("Error getting effective range:", error) + return null + } + } - try { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); - if (!workspaceFolder) { - filePath = document.uri.fsPath; - } else { - const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath); - filePath = (!relativePath || relativePath.startsWith('..')) ? document.uri.fsPath : relativePath; - } + private getFilePath(document: vscode.TextDocument): string { + // Check cache first + let filePath = this.filePathCache.get(document) + if (filePath) { + return filePath + } - // Cache the result - this.filePathCache.set(document, filePath); - return filePath; - } catch (error) { - console.error('Error getting file path:', error); - return document.uri.fsPath; - } - } + try { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri) + if (!workspaceFolder) { + filePath = document.uri.fsPath + } else { + const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath) + filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath + } - private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { - return { - message: diagnostic.message, - severity: diagnostic.severity, - code: diagnostic.code, - source: diagnostic.source, - range: diagnostic.range // Reuse the range object - }; - } + // Cache the result + this.filePathCache.set(document, filePath) + return filePath + } catch (error) { + console.error("Error getting file path:", error) + return document.uri.fsPath + } + } - private createAction( - title: string, - kind: vscode.CodeActionKind, - command: string, - args: any[] - ): vscode.CodeAction { - const action = new vscode.CodeAction(title, kind); - action.command = { command, title, arguments: args }; - return action; - } + private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { + return { + message: diagnostic.message, + severity: diagnostic.severity, + code: diagnostic.code, + source: diagnostic.source, + range: diagnostic.range, // Reuse the range object + } + } - private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { - // Optimize range intersection check - return !( - range2.end.line < range1.start.line || - range2.start.line > range1.end.line || - (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || - (range2.start.line === range1.end.line && range2.start.character > range1.end.character) - ); - } + private createAction(title: string, kind: vscode.CodeActionKind, command: string, args: any[]): vscode.CodeAction { + const action = new vscode.CodeAction(title, kind) + action.command = { command, title, arguments: args } + return action + } - public provideCodeActions( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - context: vscode.CodeActionContext - ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { - try { - const effectiveRange = this.getEffectiveRange(document, range); - if (!effectiveRange) { - return []; - } + private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { + // Optimize range intersection check + return !( + range2.end.line < range1.start.line || + range2.start.line > range1.end.line || + (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || + (range2.start.line === range1.end.line && range2.start.character > range1.end.character) + ) + } - const filePath = this.getFilePath(document); - const actions: vscode.CodeAction[] = []; + public provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + try { + const effectiveRange = this.getEffectiveRange(document, range) + if (!effectiveRange) { + return [] + } - // Create actions using helper method - actions.push(this.createAction( - ACTION_NAMES.EXPLAIN, - vscode.CodeActionKind.QuickFix, - COMMAND_IDS.EXPLAIN, - [filePath, effectiveRange.text] - )); + const filePath = this.getFilePath(document) + const actions: vscode.CodeAction[] = [] - // Only process diagnostics if they exist - if (context.diagnostics.length > 0) { - const relevantDiagnostics = context.diagnostics.filter(d => - this.hasIntersectingRange(effectiveRange.range, d.range) - ); + // Create actions using helper method + actions.push( + this.createAction(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [ + filePath, + effectiveRange.text, + ]), + ) - if (relevantDiagnostics.length > 0) { - const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData); - actions.push(this.createAction( - ACTION_NAMES.FIX, - vscode.CodeActionKind.QuickFix, - COMMAND_IDS.FIX, - [filePath, effectiveRange.text, diagnosticMessages] - )); - } - } + // Only process diagnostics if they exist + if (context.diagnostics.length > 0) { + const relevantDiagnostics = context.diagnostics.filter((d) => + this.hasIntersectingRange(effectiveRange.range, d.range), + ) - actions.push(this.createAction( - ACTION_NAMES.IMPROVE, - vscode.CodeActionKind.RefactorRewrite, - COMMAND_IDS.IMPROVE, - [filePath, effectiveRange.text] - )); + if (relevantDiagnostics.length > 0) { + const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData) + actions.push( + this.createAction(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [ + filePath, + effectiveRange.text, + diagnosticMessages, + ]), + ) + } + } - return actions; - } catch (error) { - console.error('Error providing code actions:', error); - return []; - } - } -} \ No newline at end of file + actions.push( + this.createAction(ACTION_NAMES.IMPROVE, vscode.CodeActionKind.RefactorRewrite, COMMAND_IDS.IMPROVE, [ + filePath, + effectiveRange.text, + ]), + ) + + return actions + } catch (error) { + console.error("Error providing code actions:", error) + return [] + } + } +} diff --git a/src/core/__tests__/CodeActionProvider.test.ts b/src/core/__tests__/CodeActionProvider.test.ts index cdc1acf..d0bfc8e 100644 --- a/src/core/__tests__/CodeActionProvider.test.ts +++ b/src/core/__tests__/CodeActionProvider.test.ts @@ -1,145 +1,147 @@ -import * as vscode from 'vscode'; -import { CodeActionProvider } from '../CodeActionProvider'; +import * as vscode from "vscode" +import { CodeActionProvider } from "../CodeActionProvider" // Mock VSCode API -jest.mock('vscode', () => ({ - CodeAction: jest.fn().mockImplementation((title, kind) => ({ - title, - kind, - command: undefined - })), - CodeActionKind: { - QuickFix: { value: 'quickfix' }, - RefactorRewrite: { value: 'refactor.rewrite' } - }, - Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ - start: { line: startLine, character: startChar }, - end: { line: endLine, character: endChar } - })), - Position: jest.fn().mockImplementation((line, character) => ({ - line, - character - })), - workspace: { - getWorkspaceFolder: jest.fn() - }, - DiagnosticSeverity: { - Error: 0, - Warning: 1, - Information: 2, - Hint: 3 - } -})); +jest.mock("vscode", () => ({ + CodeAction: jest.fn().mockImplementation((title, kind) => ({ + title, + kind, + command: undefined, + })), + CodeActionKind: { + QuickFix: { value: "quickfix" }, + RefactorRewrite: { value: "refactor.rewrite" }, + }, + Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar }, + })), + Position: jest.fn().mockImplementation((line, character) => ({ + line, + character, + })), + workspace: { + getWorkspaceFolder: jest.fn(), + }, + DiagnosticSeverity: { + Error: 0, + Warning: 1, + Information: 2, + Hint: 3, + }, +})) -describe('CodeActionProvider', () => { - let provider: CodeActionProvider; - let mockDocument: any; - let mockRange: any; - let mockContext: any; +describe("CodeActionProvider", () => { + let provider: CodeActionProvider + let mockDocument: any + let mockRange: any + let mockContext: any - beforeEach(() => { - provider = new CodeActionProvider(); - - // Mock document - mockDocument = { - getText: jest.fn(), - lineAt: jest.fn(), - lineCount: 10, - uri: { fsPath: '/test/file.ts' } - }; + beforeEach(() => { + provider = new CodeActionProvider() - // Mock range - mockRange = new vscode.Range(0, 0, 0, 10); + // Mock document + mockDocument = { + getText: jest.fn(), + lineAt: jest.fn(), + lineCount: 10, + uri: { fsPath: "/test/file.ts" }, + } - // Mock context - mockContext = { - diagnostics: [] - }; - }); + // Mock range + mockRange = new vscode.Range(0, 0, 0, 10) - describe('getEffectiveRange', () => { - it('should return selected text when available', () => { - mockDocument.getText.mockReturnValue('selected text'); - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange); - - expect(result).toEqual({ - range: mockRange, - text: 'selected text' - }); - }); + // Mock context + mockContext = { + diagnostics: [], + } + }) - it('should return null for empty line', () => { - mockDocument.getText.mockReturnValue(''); - mockDocument.lineAt.mockReturnValue({ text: '', lineNumber: 0 }); - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange); - - expect(result).toBeNull(); - }); - }); + describe("getEffectiveRange", () => { + it("should return selected text when available", () => { + mockDocument.getText.mockReturnValue("selected text") - describe('getFilePath', () => { - it('should return relative path when in workspace', () => { - const mockWorkspaceFolder = { - uri: { fsPath: '/test' } - }; - (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder); - - const result = (provider as any).getFilePath(mockDocument); - - expect(result).toBe('file.ts'); - }); + const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - it('should return absolute path when not in workspace', () => { - (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null); - - const result = (provider as any).getFilePath(mockDocument); - - expect(result).toBe('/test/file.ts'); - }); - }); + expect(result).toEqual({ + range: mockRange, + text: "selected text", + }) + }) - describe('provideCodeActions', () => { - beforeEach(() => { - mockDocument.getText.mockReturnValue('test code'); - mockDocument.lineAt.mockReturnValue({ text: 'test code', lineNumber: 0 }); - }); + it("should return null for empty line", () => { + mockDocument.getText.mockReturnValue("") + mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) - it('should provide explain and improve actions by default', () => { - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); - - expect(actions).toHaveLength(2); - expect((actions as any)[0].title).toBe('Roo Cline: Explain Code'); - expect((actions as any)[1].title).toBe('Roo Cline: Improve Code'); - }); + const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - it('should provide fix action when diagnostics exist', () => { - mockContext.diagnostics = [{ - message: 'test error', - severity: vscode.DiagnosticSeverity.Error, - range: mockRange - }]; - - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); - - expect(actions).toHaveLength(3); - expect((actions as any).some((a: any) => a.title === 'Roo Cline: Fix Code')).toBe(true); - }); + expect(result).toBeNull() + }) + }) - it('should handle errors gracefully', () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - mockDocument.getText.mockImplementation(() => { - throw new Error('Test error'); - }); - mockDocument.lineAt.mockReturnValue({ text: 'test', lineNumber: 0 }); - - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); - - expect(actions).toEqual([]); - expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting effective range:', expect.any(Error)); - - consoleErrorSpy.mockRestore(); - }); - }); -}); \ No newline at end of file + describe("getFilePath", () => { + it("should return relative path when in workspace", () => { + const mockWorkspaceFolder = { + uri: { fsPath: "/test" }, + } + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder) + + const result = (provider as any).getFilePath(mockDocument) + + expect(result).toBe("file.ts") + }) + + it("should return absolute path when not in workspace", () => { + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null) + + const result = (provider as any).getFilePath(mockDocument) + + expect(result).toBe("/test/file.ts") + }) + }) + + describe("provideCodeActions", () => { + beforeEach(() => { + mockDocument.getText.mockReturnValue("test code") + mockDocument.lineAt.mockReturnValue({ text: "test code", lineNumber: 0 }) + }) + + it("should provide explain and improve actions by default", () => { + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toHaveLength(2) + expect((actions as any)[0].title).toBe("Roo Code: Explain Code") + expect((actions as any)[1].title).toBe("Roo Code: Improve Code") + }) + + it("should provide fix action when diagnostics exist", () => { + mockContext.diagnostics = [ + { + message: "test error", + severity: vscode.DiagnosticSeverity.Error, + range: mockRange, + }, + ] + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toHaveLength(3) + expect((actions as any).some((a: any) => a.title === "Roo Code: Fix Code")).toBe(true) + }) + + it("should handle errors gracefully", () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) + mockDocument.getText.mockImplementation(() => { + throw new Error("Test error") + }) + mockDocument.lineAt.mockReturnValue({ text: "test", lineNumber: 0 }) + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toEqual([]) + expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error)) + + consoleErrorSpy.mockRestore() + }) + }) +}) diff --git a/src/extension.ts b/src/extension.ts index db011e0..b03e9e5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -201,7 +201,7 @@ export function activate(context: vscode.ExtensionContext) { context, "roo-cline.explainCode", "EXPLAIN", - "Any specific questions about this code?", + "What would you like Roo to explain?", "E.g. How does the error handling work?", ) @@ -209,7 +209,7 @@ export function activate(context: vscode.ExtensionContext) { context, "roo-cline.fixCode", "FIX", - "Any specific concerns about fixing this code?", + "What would you like Roo to fix?", "E.g. Maintain backward compatibility", ) @@ -217,7 +217,7 @@ export function activate(context: vscode.ExtensionContext) { context, "roo-cline.improveCode", "IMPROVE", - "Any specific aspects you want to improve?", + "What would you like Roo to improve?", "E.g. Focus on performance optimization", ) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index a3237e2..aa6b780 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -8,8 +8,8 @@ const dotenv = require("dotenv") const testEnvPath = path.join(__dirname, ".test_env") dotenv.config({ path: testEnvPath }) -suite("Roo Cline Extension Test Suite", () => { - vscode.window.showInformationMessage("Starting Roo Cline extension tests.") +suite("Roo Code Extension Test Suite", () => { + vscode.window.showInformationMessage("Starting Roo Code extension tests.") test("Extension should be present", () => { const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") @@ -117,16 +117,16 @@ suite("Roo Cline Extension Test Suite", () => { // Test core commands are registered const expectedCommands = [ - 'roo-cline.plusButtonClicked', - 'roo-cline.mcpButtonClicked', - 'roo-cline.historyButtonClicked', - 'roo-cline.popoutButtonClicked', - 'roo-cline.settingsButtonClicked', - 'roo-cline.openInNewTab', - 'roo-cline.explainCode', - 'roo-cline.fixCode', - 'roo-cline.improveCode' - ]; + "roo-cline.plusButtonClicked", + "roo-cline.mcpButtonClicked", + "roo-cline.historyButtonClicked", + "roo-cline.popoutButtonClicked", + "roo-cline.settingsButtonClicked", + "roo-cline.openInNewTab", + "roo-cline.explainCode", + "roo-cline.fixCode", + "roo-cline.improveCode", + ] for (const cmd of expectedCommands) { assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`) @@ -136,7 +136,7 @@ suite("Roo Cline Extension Test Suite", () => { test("Views should be registered", () => { const view = vscode.window.createWebviewPanel( "roo-cline.SidebarProvider", - "Roo Cline", + "Roo Code", vscode.ViewColumn.One, {}, ) @@ -184,17 +184,12 @@ suite("Roo Cline Extension Test Suite", () => { // Create webview panel with development options const extensionUri = extension.extensionUri - const panel = vscode.window.createWebviewPanel( - "roo-cline.SidebarProvider", - "Roo Cline", - vscode.ViewColumn.One, - { - enableScripts: true, - enableCommandUris: true, - retainContextWhenHidden: true, - localResourceRoots: [extensionUri], - }, - ) + const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, { + enableScripts: true, + enableCommandUris: true, + retainContextWhenHidden: true, + localResourceRoots: [extensionUri], + }) try { // Initialize webview with development context From 085d42873c8c8b6e04da2023198328dfc3b64781 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 01:14:48 +0700 Subject: [PATCH 10/12] refactor: generalize prompt enhancement into single completion handler - Rename enhance-prompt.ts to single-completion-handler.ts for better clarity - Refactor enhancement logic to be more generic and reusable - Update prompt template handling to use template literals - Adjust tests and imports accordingly --- src/core/webview/ClineProvider.ts | 14 +++++--- src/shared/__tests__/support-prompts.test.ts | 5 ++- src/shared/support-prompt.ts | 5 +-- src/utils/__tests__/enhance-prompt.test.ts | 32 +++++++++++++------ ...prompt.ts => single-completion-handler.ts} | 11 ++----- 5 files changed, 39 insertions(+), 28 deletions(-) rename src/utils/{enhance-prompt.ts => single-completion-handler.ts} (71%) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b28bad6..797def3 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -36,7 +36,7 @@ import { getNonce } from "./getNonce" import { getUri } from "./getUri" import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" import { checkExistKey } from "../../shared/checkExistApiConfig" -import { enhancePrompt } from "../../utils/enhance-prompt" +import { singleCompletionHandler } from "../../utils/single-completion-handler" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" @@ -996,11 +996,17 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } - const enhancedPrompt = await enhancePrompt( + const enhancedPrompt = await singleCompletionHandler( configToUse, - message.text, - supportPrompt.get(customPrompts, "ENHANCE"), + supportPrompt.create( + "ENHANCE", + { + userInput: message.text, + }, + customPrompts, + ), ) + await this.postMessageToWebview({ type: "enhancedPrompt", text: enhancedPrompt, diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts index ee7253e..cd27a38 100644 --- a/src/shared/__tests__/support-prompts.test.ts +++ b/src/shared/__tests__/support-prompts.test.ts @@ -77,12 +77,11 @@ describe("Code Action Prompts", () => { describe("ENHANCE action", () => { it("should format enhance prompt correctly", () => { const prompt = supportPrompt.create("ENHANCE", { - filePath: testFilePath, - selectedText: testCode, + userInput: "test", }) expect(prompt).toBe( - "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", + "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):\n\ntest", ) // Verify it ignores parameters since ENHANCE template doesn't use any expect(prompt).not.toContain(testFilePath) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 406e981..715d8c6 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -68,8 +68,9 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement. ` -const ENHANCE_TEMPLATE = - "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):" +const ENHANCE_TEMPLATE = `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes): + +\${userInput}` // Get template based on prompt type const defaultTemplates = { diff --git a/src/utils/__tests__/enhance-prompt.test.ts b/src/utils/__tests__/enhance-prompt.test.ts index 69fdd04..d3cca04 100644 --- a/src/utils/__tests__/enhance-prompt.test.ts +++ b/src/utils/__tests__/enhance-prompt.test.ts @@ -1,7 +1,7 @@ -import { enhancePrompt } from "../enhance-prompt" +import { singleCompletionHandler } from "../single-completion-handler" import { ApiConfiguration } from "../../shared/api" import { buildApiHandler, SingleCompletionHandler } from "../../api" -import { defaultPrompts } from "../../shared/modes" +import { supportPrompt } from "../../shared/support-prompt" // Mock the API handler jest.mock("../../api", () => ({ @@ -34,17 +34,29 @@ describe("enhancePrompt", () => { }) it("enhances prompt using default enhancement prompt when no custom prompt provided", async () => { - const result = await enhancePrompt(mockApiConfig, "Test prompt") + const result = await singleCompletionHandler(mockApiConfig, "Test prompt") expect(result).toBe("Enhanced prompt") const handler = buildApiHandler(mockApiConfig) - expect((handler as any).completePrompt).toHaveBeenCalledWith(`${defaultPrompts.enhance}\n\nTest prompt`) + expect((handler as any).completePrompt).toHaveBeenCalledWith(`Test prompt`) }) it("enhances prompt using custom enhancement prompt when provided", async () => { const customEnhancePrompt = "You are a custom prompt enhancer" + const customEnhancePromptWithTemplate = customEnhancePrompt + "\n\n${userInput}" - const result = await enhancePrompt(mockApiConfig, "Test prompt", customEnhancePrompt) + const result = await singleCompletionHandler( + mockApiConfig, + supportPrompt.create( + "ENHANCE", + { + userInput: "Test prompt", + }, + { + ENHANCE: customEnhancePromptWithTemplate, + }, + ), + ) expect(result).toBe("Enhanced prompt") const handler = buildApiHandler(mockApiConfig) @@ -52,11 +64,11 @@ describe("enhancePrompt", () => { }) it("throws error for empty prompt input", async () => { - await expect(enhancePrompt(mockApiConfig, "")).rejects.toThrow("No prompt text provided") + await expect(singleCompletionHandler(mockApiConfig, "")).rejects.toThrow("No prompt text provided") }) it("throws error for missing API configuration", async () => { - await expect(enhancePrompt({} as ApiConfiguration, "Test prompt")).rejects.toThrow( + await expect(singleCompletionHandler({} as ApiConfiguration, "Test prompt")).rejects.toThrow( "No valid API configuration provided", ) }) @@ -75,7 +87,7 @@ describe("enhancePrompt", () => { }), }) - await expect(enhancePrompt(mockApiConfig, "Test prompt")).rejects.toThrow( + await expect(singleCompletionHandler(mockApiConfig, "Test prompt")).rejects.toThrow( "The selected API provider does not support prompt enhancement", ) }) @@ -101,7 +113,7 @@ describe("enhancePrompt", () => { }), } as unknown as SingleCompletionHandler) - const result = await enhancePrompt(openRouterConfig, "Test prompt") + const result = await singleCompletionHandler(openRouterConfig, "Test prompt") expect(buildApiHandler).toHaveBeenCalledWith(openRouterConfig) expect(result).toBe("Enhanced prompt") @@ -121,6 +133,6 @@ describe("enhancePrompt", () => { }), } as unknown as SingleCompletionHandler) - await expect(enhancePrompt(mockApiConfig, "Test prompt")).rejects.toThrow("API Error") + await expect(singleCompletionHandler(mockApiConfig, "Test prompt")).rejects.toThrow("API Error") }) }) diff --git a/src/utils/enhance-prompt.ts b/src/utils/single-completion-handler.ts similarity index 71% rename from src/utils/enhance-prompt.ts rename to src/utils/single-completion-handler.ts index 3724757..5e049d4 100644 --- a/src/utils/enhance-prompt.ts +++ b/src/utils/single-completion-handler.ts @@ -1,16 +1,11 @@ import { ApiConfiguration } from "../shared/api" import { buildApiHandler, SingleCompletionHandler } from "../api" -import { defaultPrompts } from "../shared/modes" /** * Enhances a prompt using the configured API without creating a full Cline instance or task history. * This is a lightweight alternative that only uses the API's completion functionality. */ -export async function enhancePrompt( - apiConfiguration: ApiConfiguration, - promptText: string, - enhancePrompt?: string, -): Promise { +export async function singleCompletionHandler(apiConfiguration: ApiConfiguration, promptText: string): Promise { if (!promptText) { throw new Error("No prompt text provided") } @@ -25,7 +20,5 @@ export async function enhancePrompt( throw new Error("The selected API provider does not support prompt enhancement") } - const enhancePromptText = enhancePrompt ?? defaultPrompts.enhance - const prompt = `${enhancePromptText}\n\n${promptText}` - return (handler as SingleCompletionHandler).completePrompt(prompt) + return (handler as SingleCompletionHandler).completePrompt(promptText) } From f86e96d15712dab872d3616877cb80d12c557261 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 01:46:33 +0700 Subject: [PATCH 11/12] refactor: separate mode and support prompts - Rename customPrompts to customModePrompts for mode-specific prompts - Add new customSupportPrompts type for support action prompts - Update types to be more specific (CustomModePrompts and CustomSupportPrompts) - Fix all related tests and component implementations --- src/core/Cline.ts | 4 +- src/core/prompts/__tests__/system.test.ts | 32 ++++----- src/core/prompts/system.ts | 6 +- src/core/webview/ClineProvider.ts | 68 +++++++++++-------- .../webview/__tests__/ClineProvider.test.ts | 30 ++++---- src/shared/ExtensionMessage.ts | 6 +- src/shared/__tests__/support-prompts.test.ts | 16 ++--- src/shared/modes.ts | 6 +- src/shared/support-prompt.ts | 12 ++-- .../chat/__tests__/AutoApproveMenu.test.tsx | 7 +- .../src/components/prompts/PromptsView.tsx | 17 ++--- .../prompts/__tests__/PromptsView.test.tsx | 2 +- .../src/context/ExtensionStateContext.tsx | 12 ++-- 13 files changed, 119 insertions(+), 99 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 5f8af77..0e2872b 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -809,7 +809,7 @@ export class Cline { }) } - const { browserViewportSize, mode, customPrompts, preferredLanguage } = + const { browserViewportSize, mode, customModePrompts, preferredLanguage } = (await this.providerRef.deref()?.getState()) ?? {} const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} const systemPrompt = await (async () => { @@ -825,7 +825,7 @@ export class Cline { this.diffStrategy, browserViewportSize, mode, - customPrompts, + customModePrompts, customModes, this.customInstructions, preferredLanguage, diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 6ecf7ef..4138d11 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -162,7 +162,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -178,7 +178,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy "1280x800", // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -196,7 +196,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -212,7 +212,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -228,7 +228,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy "900x600", // different viewport size defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -244,7 +244,7 @@ describe("SYSTEM_PROMPT", () => { new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions undefined, // preferredLanguage @@ -264,7 +264,7 @@ describe("SYSTEM_PROMPT", () => { new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions undefined, // preferredLanguage @@ -284,7 +284,7 @@ describe("SYSTEM_PROMPT", () => { new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions undefined, // preferredLanguage @@ -304,7 +304,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions "Spanish", // preferredLanguage @@ -334,7 +334,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize "custom-mode", // mode - undefined, // customPrompts + undefined, // customModePrompts customModes, // customModes "Global instructions", // globalCustomInstructions ) @@ -351,7 +351,7 @@ describe("SYSTEM_PROMPT", () => { }) it("should use promptComponent roleDefinition when available", async () => { - const customPrompts = { + const customModePrompts = { [defaultModeSlug]: { roleDefinition: "Custom prompt role definition", customInstructions: "Custom prompt instructions", @@ -366,7 +366,7 @@ describe("SYSTEM_PROMPT", () => { undefined, undefined, defaultModeSlug, - customPrompts, + customModePrompts, undefined, ) @@ -377,7 +377,7 @@ describe("SYSTEM_PROMPT", () => { }) it("should fallback to modeConfig roleDefinition when promptComponent has no roleDefinition", async () => { - const customPrompts = { + const customModePrompts = { [defaultModeSlug]: { customInstructions: "Custom prompt instructions", // No roleDefinition provided @@ -392,7 +392,7 @@ describe("SYSTEM_PROMPT", () => { undefined, undefined, defaultModeSlug, - customPrompts, + customModePrompts, undefined, ) @@ -432,7 +432,7 @@ describe("addCustomInstructions", () => { undefined, // diffStrategy undefined, // browserViewportSize "architect", // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -448,7 +448,7 @@ describe("addCustomInstructions", () => { undefined, // diffStrategy undefined, // browserViewportSize "ask", // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 2546adc..b77d243 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,7 +1,7 @@ import { Mode, modes, - CustomPrompts, + CustomModePrompts, PromptComponent, getRoleDefinition, defaultModeSlug, @@ -97,7 +97,7 @@ export const SYSTEM_PROMPT = async ( diffStrategy?: DiffStrategy, browserViewportSize?: string, mode: Mode = defaultModeSlug, - customPrompts?: CustomPrompts, + customModePrompts?: CustomModePrompts, customModes?: ModeConfig[], globalCustomInstructions?: string, preferredLanguage?: string, @@ -115,7 +115,7 @@ export const SYSTEM_PROMPT = async ( } // Check if it's a custom mode - const promptComponent = getPromptComponent(customPrompts?.[mode]) + const promptComponent = getPromptComponent(customModePrompts?.[mode]) // Get full mode config from custom modes or fall back to built-in modes const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0] diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 797def3..1cf7028 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -22,7 +22,7 @@ import { WebviewMessage } from "../../shared/WebviewMessage" import { Mode, modes, - CustomPrompts, + CustomModePrompts, PromptComponent, ModeConfig, defaultModeSlug, @@ -40,7 +40,7 @@ import { singleCompletionHandler } from "../../utils/single-completion-handler" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" -import { supportPrompt } from "../../shared/support-prompt" +import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt" import { ACTION_NAMES } from "../CodeActionProvider" @@ -111,7 +111,8 @@ type GlobalStateKey = | "vsCodeLmModelSelector" | "mode" | "modeApiConfigs" - | "customPrompts" + | "customModePrompts" + | "customSupportPrompts" | "enhancementApiConfigId" | "experimentalDiffStrategy" | "autoApprovalEnabled" @@ -203,9 +204,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const { customPrompts } = await visibleProvider.getState() + const { customSupportPrompts } = await visibleProvider.getState() - const prompt = supportPrompt.create(promptType, params, customPrompts) + const prompt = supportPrompt.create(promptType, params, customSupportPrompts) await visibleProvider.initClineWithTask(prompt) } @@ -296,7 +297,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.clearTask() const { apiConfiguration, - customPrompts, + customModePrompts, diffEnabled, fuzzyMatchThreshold, mode, @@ -304,7 +305,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] as PromptComponent + const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -324,7 +325,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.clearTask() const { apiConfiguration, - customPrompts, + customModePrompts, diffEnabled, fuzzyMatchThreshold, mode, @@ -332,7 +333,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] as PromptComponent + const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -817,14 +818,14 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + const existingPrompts = (await this.getGlobalState("customSupportPrompts")) || {} const updatedPrompts = { ...existingPrompts, ...message.values, } - await this.updateGlobalState("customPrompts", updatedPrompts) + await this.updateGlobalState("customSupportPrompts", updatedPrompts) await this.postStateToWebview() } catch (error) { console.error("Error update support prompt:", error) @@ -837,10 +838,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const existingPrompts = ((await this.getGlobalState("customPrompts")) || {}) as Record< - string, - any - > + const existingPrompts = ((await this.getGlobalState("customSupportPrompts")) || + {}) as Record const updatedPrompts = { ...existingPrompts, @@ -848,7 +847,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { updatedPrompts[message.text] = undefined - await this.updateGlobalState("customPrompts", updatedPrompts) + await this.updateGlobalState("customSupportPrompts", updatedPrompts) await this.postStateToWebview() } catch (error) { console.error("Error reset support prompt:", error) @@ -857,21 +856,21 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { - const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + const existingPrompts = (await this.getGlobalState("customModePrompts")) || {} const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt, } - await this.updateGlobalState("customPrompts", updatedPrompts) + await this.updateGlobalState("customModePrompts", updatedPrompts) - // Get current state and explicitly include customPrompts + // Get current state and explicitly include customModePrompts const currentState = await this.getState() const stateWithPrompts = { ...currentState, - customPrompts: updatedPrompts, + customModePrompts: updatedPrompts, } // Post state with prompts @@ -981,8 +980,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "enhancePrompt": if (message.text) { try { - const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } = - await this.getState() + const { + apiConfiguration, + customSupportPrompts, + listApiConfigMeta, + enhancementApiConfigId, + } = await this.getState() // Try to get enhancement config first, fall back to current config let configToUse: ApiConfiguration = apiConfiguration @@ -1003,7 +1006,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { { userInput: message.text, }, - customPrompts, + customSupportPrompts, ), ) @@ -1024,7 +1027,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { try { const { apiConfiguration, - customPrompts, + customModePrompts, customInstructions, preferredLanguage, browserViewportSize, @@ -1054,7 +1057,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { diffStrategy, browserViewportSize ?? "900x600", mode, - customPrompts, + customModePrompts, customModes, customInstructions, preferredLanguage, @@ -1802,7 +1805,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { currentApiConfigName, listApiConfigMeta, mode, - customPrompts, + customModePrompts, + customSupportPrompts, enhancementApiConfigId, experimentalDiffStrategy, autoApprovalEnabled, @@ -1841,7 +1845,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], mode: mode ?? defaultModeSlug, - customPrompts: customPrompts ?? {}, + customModePrompts: customModePrompts ?? {}, + customSupportPrompts: customSupportPrompts ?? {}, enhancementApiConfigId, experimentalDiffStrategy: experimentalDiffStrategy ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false, @@ -1961,7 +1966,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { vsCodeLmModelSelector, mode, modeApiConfigs, - customPrompts, + customModePrompts, + customSupportPrompts, enhancementApiConfigId, experimentalDiffStrategy, autoApprovalEnabled, @@ -2026,7 +2032,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("vsCodeLmModelSelector") as Promise, this.getGlobalState("mode") as Promise, this.getGlobalState("modeApiConfigs") as Promise | undefined>, - this.getGlobalState("customPrompts") as Promise, + this.getGlobalState("customModePrompts") as Promise, + this.getGlobalState("customSupportPrompts") as Promise, this.getGlobalState("enhancementApiConfigId") as Promise, this.getGlobalState("experimentalDiffStrategy") as Promise, this.getGlobalState("autoApprovalEnabled") as Promise, @@ -2137,7 +2144,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], modeApiConfigs: modeApiConfigs ?? ({} as Record), - customPrompts: customPrompts ?? {}, + customModePrompts: customModePrompts ?? {}, + customSupportPrompts: customSupportPrompts ?? {}, enhancementApiConfigId, experimentalDiffStrategy: experimentalDiffStrategy ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 63dc2d5..cd1f295 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -555,7 +555,7 @@ describe("ClineProvider", () => { architect: "existing architect prompt", } ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { - if (key === "customPrompts") { + if (key === "customModePrompts") { return existingPrompts } return undefined @@ -569,7 +569,7 @@ describe("ClineProvider", () => { }) // Verify state was updated correctly - expect(mockContext.globalState.update).toHaveBeenCalledWith("customPrompts", { + expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", { ...existingPrompts, code: "new code prompt", }) @@ -579,7 +579,7 @@ describe("ClineProvider", () => { expect.objectContaining({ type: "state", state: expect.objectContaining({ - customPrompts: { + customModePrompts: { ...existingPrompts, code: "new code prompt", }, @@ -588,17 +588,17 @@ describe("ClineProvider", () => { ) }) - test("customPrompts defaults to empty object", async () => { - // Mock globalState.get to return undefined for customPrompts + test("customModePrompts defaults to empty object", async () => { + // Mock globalState.get to return undefined for customModePrompts ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { - if (key === "customPrompts") { + if (key === "customModePrompts") { return undefined } return null }) const state = await provider.getState() - expect(state.customPrompts).toEqual({}) + expect(state.customModePrompts).toEqual({}) }) test("uses mode-specific custom instructions in Cline initialization", async () => { @@ -611,7 +611,7 @@ describe("ClineProvider", () => { jest.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: mockApiConfig, - customPrompts: { + customModePrompts: { code: { customInstructions: modeCustomInstructions }, }, mode: "code", @@ -651,7 +651,7 @@ describe("ClineProvider", () => { }, } mockContext.globalState.get = jest.fn((key: string) => { - if (key === "customPrompts") { + if (key === "customModePrompts") { return existingPrompts } return undefined @@ -668,7 +668,7 @@ describe("ClineProvider", () => { }) // Verify state was updated correctly - expect(mockContext.globalState.update).toHaveBeenCalledWith("customPrompts", { + expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", { code: { roleDefinition: "Code role", customInstructions: "New instructions", @@ -978,7 +978,7 @@ describe("ClineProvider", () => { apiModelId: "test-model", openRouterModelInfo: { supportsComputerUse: true }, }, - customPrompts: {}, + customModePrompts: {}, mode: "code", mcpEnabled: false, browserViewportSize: "900x600", @@ -1007,7 +1007,7 @@ describe("ClineProvider", () => { }), "900x600", // browserViewportSize "code", // mode - {}, // customPrompts + {}, // customModePrompts {}, // customModes undefined, // effectiveInstructions undefined, // preferredLanguage @@ -1027,7 +1027,7 @@ describe("ClineProvider", () => { apiModelId: "test-model", openRouterModelInfo: { supportsComputerUse: true }, }, - customPrompts: {}, + customModePrompts: {}, mode: "code", mcpEnabled: false, browserViewportSize: "900x600", @@ -1056,7 +1056,7 @@ describe("ClineProvider", () => { }), "900x600", // browserViewportSize "code", // mode - {}, // customPrompts + {}, // customModePrompts {}, // customModes undefined, // effectiveInstructions undefined, // preferredLanguage @@ -1071,7 +1071,7 @@ describe("ClineProvider", () => { apiProvider: "openrouter", openRouterModelInfo: { supportsComputerUse: true }, }, - customPrompts: { + customModePrompts: { architect: { customInstructions: "Architect mode instructions" }, }, mode: "architect", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 3bcd8a0..ed5d78c 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -4,7 +4,8 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api" import { HistoryItem } from "./HistoryItem" import { McpServer } from "./mcp" import { GitCommit } from "../utils/git" -import { Mode, CustomPrompts, ModeConfig } from "./modes" +import { Mode, CustomModePrompts, ModeConfig } from "./modes" +import { CustomSupportPrompts } from "./support-prompt" export interface LanguageModelChatSelector { vendor?: string @@ -82,7 +83,8 @@ export interface ExtensionState { currentApiConfigName?: string listApiConfigMeta?: ApiConfigMeta[] customInstructions?: string - customPrompts?: CustomPrompts + customModePrompts?: CustomModePrompts + customSupportPrompts?: CustomSupportPrompts alwaysAllowReadOnly?: boolean alwaysAllowWrite?: boolean alwaysAllowExecute?: boolean diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts index cd27a38..edee6c2 100644 --- a/src/shared/__tests__/support-prompts.test.ts +++ b/src/shared/__tests__/support-prompts.test.ts @@ -97,18 +97,18 @@ describe("Code Action Prompts", () => { it("should return custom template when provided", () => { const customTemplate = "Custom template for explaining code" - const customPrompts = { + const customSupportPrompts = { EXPLAIN: customTemplate, } - const template = supportPrompt.get(customPrompts, "EXPLAIN") + const template = supportPrompt.get(customSupportPrompts, "EXPLAIN") expect(template).toBe(customTemplate) }) it("should return default template when custom prompts does not include type", () => { - const customPrompts = { + const customSupportPrompts = { SOMETHING_ELSE: "Other template", } - const template = supportPrompt.get(customPrompts, "EXPLAIN") + const template = supportPrompt.get(customSupportPrompts, "EXPLAIN") expect(template).toBe(supportPrompt.default.EXPLAIN) }) }) @@ -116,7 +116,7 @@ describe("Code Action Prompts", () => { describe("create with custom prompts", () => { it("should use custom template when provided", () => { const customTemplate = "Custom template for ${filePath}" - const customPrompts = { + const customSupportPrompts = { EXPLAIN: customTemplate, } @@ -126,7 +126,7 @@ describe("Code Action Prompts", () => { filePath: testFilePath, selectedText: testCode, }, - customPrompts, + customSupportPrompts, ) expect(prompt).toContain(`Custom template for ${testFilePath}`) @@ -134,7 +134,7 @@ describe("Code Action Prompts", () => { }) it("should use default template when custom prompts does not include type", () => { - const customPrompts = { + const customSupportPrompts = { EXPLAIN: "Other template", } @@ -144,7 +144,7 @@ describe("Code Action Prompts", () => { filePath: testFilePath, selectedText: testCode, }, - customPrompts, + customSupportPrompts, ) expect(prompt).toContain("Other template") diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 451e661..5b3358b 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -18,8 +18,8 @@ export type PromptComponent = { customInstructions?: string } -export type CustomPrompts = { - [key: string]: PromptComponent | undefined | string +export type CustomModePrompts = { + [key: string]: PromptComponent | undefined } // Helper to get all tools for a mode @@ -141,7 +141,7 @@ export function isToolAllowedForMode( } // Create the mode-specific default prompts -export const defaultPrompts: Readonly = Object.freeze( +export const defaultPrompts: Readonly = Object.freeze( Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])), ) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 715d8c6..9f18b1a 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -84,11 +84,11 @@ type SupportPromptType = keyof typeof defaultTemplates export const supportPrompt = { default: defaultTemplates, - get: (customPrompts: Record | undefined, type: SupportPromptType): string => { - return customPrompts?.[type] ?? defaultTemplates[type] + get: (customSupportPrompts: Record | undefined, type: SupportPromptType): string => { + return customSupportPrompts?.[type] ?? defaultTemplates[type] }, - create: (type: SupportPromptType, params: PromptParams, customPrompts?: Record): string => { - const template = supportPrompt.get(customPrompts, type) + create: (type: SupportPromptType, params: PromptParams, customSupportPrompts?: Record): string => { + const template = supportPrompt.get(customSupportPrompts, type) return createPrompt(template, params) }, } as const @@ -102,3 +102,7 @@ export const supportPromptLabels: Record = { IMPROVE: "Improve Code", ENHANCE: "Enhance Prompt", } as const + +export type CustomSupportPrompts = { + [key: string]: string | undefined +} diff --git a/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx b/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx index 76ee929..510b5b2 100644 --- a/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx +++ b/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx @@ -1,7 +1,7 @@ import { render, fireEvent, screen } from "@testing-library/react" import { useExtensionState } from "../../../context/ExtensionStateContext" import AutoApproveMenu from "../AutoApproveMenu" -import { codeMode, defaultPrompts } from "../../../../../src/shared/modes" +import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes" // Mock the ExtensionStateContext hook jest.mock("../../../context/ExtensionStateContext") @@ -29,8 +29,9 @@ describe("AutoApproveMenu", () => { requestDelaySeconds: 5, currentApiConfigName: "default", listApiConfigMeta: [], - mode: codeMode, - customPrompts: defaultPrompts, + mode: defaultModeSlug, + customModePrompts: defaultPrompts, + customSupportPrompts: {}, enhancementApiConfigId: "", didHydrateState: true, showWelcome: false, diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 6f5085a..c2f3cad 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -23,7 +23,8 @@ type PromptsViewProps = { const PromptsView = ({ onDone }: PromptsViewProps) => { const { - customPrompts, + customModePrompts, + customSupportPrompts, listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId, @@ -50,7 +51,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { // Direct update functions const updateAgentPrompt = useCallback( (mode: Mode, promptData: PromptComponent) => { - const existingPrompt = customPrompts?.[mode] as PromptComponent + const existingPrompt = customModePrompts?.[mode] as PromptComponent const updatedPrompt = { ...existingPrompt, ...promptData } // Only include properties that differ from defaults @@ -64,7 +65,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { customPrompt: updatedPrompt, }) }, - [customPrompts], + [customModePrompts], ) const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => { @@ -261,7 +262,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const handleAgentReset = (modeSlug: string) => { // Only reset role definition for built-in modes - const existingPrompt = customPrompts?.[modeSlug] as PromptComponent + const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent updateAgentPrompt(modeSlug, { ...existingPrompt, roleDefinition: undefined, @@ -276,7 +277,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { } const getSupportPromptValue = (type: SupportPromptType): string => { - return supportPrompt.get(customPrompts, type) + return supportPrompt.get(customSupportPrompts, type) } const handleTestEnhancement = () => { @@ -556,7 +557,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] as PromptComponent + const prompt = customModePrompts?.[mode] as PromptComponent return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode) })()} onChange={(e) => { @@ -673,7 +674,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] as PromptComponent + const prompt = customModePrompts?.[mode] as PromptComponent return customMode?.customInstructions ?? prompt?.customInstructions ?? "" })()} onChange={(e) => { @@ -689,7 +690,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } else { // For built-in modes, update the prompts - const existingPrompt = customPrompts?.[mode] as PromptComponent + const existingPrompt = customModePrompts?.[mode] as PromptComponent updateAgentPrompt(mode, { ...existingPrompt, customInstructions: value.trim() || undefined, diff --git a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx index 93e8698..95437a4 100644 --- a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx +++ b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx @@ -12,7 +12,7 @@ jest.mock("../../../utils/vscode", () => ({ })) const mockExtensionState = { - customPrompts: {}, + customModePrompts: {}, listApiConfigMeta: [ { id: "config1", name: "Config 1" }, { id: "config2", name: "Config 2" }, diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ea00a0c..2d9fda0 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -14,7 +14,8 @@ import { convertTextMateToHljs } from "../utils/textMateToHljs" import { findLastIndex } from "../../../src/shared/array" import { McpServer } from "../../../src/shared/mcp" import { checkExistKey } from "../../../src/shared/checkExistApiConfig" -import { Mode, CustomPrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes" +import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes" +import { CustomSupportPrompts } from "../../../src/shared/support-prompt" export interface ExtensionStateContextType extends ExtensionState { didHydrateState: boolean @@ -57,7 +58,8 @@ export interface ExtensionStateContextType extends ExtensionState { onUpdateApiConfig: (apiConfig: ApiConfiguration) => void mode: Mode setMode: (value: Mode) => void - setCustomPrompts: (value: CustomPrompts) => void + setCustomModePrompts: (value: CustomModePrompts) => void + setCustomSupportPrompts: (value: CustomSupportPrompts) => void enhancementApiConfigId?: string setEnhancementApiConfigId: (value: string) => void experimentalDiffStrategy: boolean @@ -93,7 +95,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode currentApiConfigName: "default", listApiConfigMeta: [], mode: defaultModeSlug, - customPrompts: defaultPrompts, + customModePrompts: defaultPrompts, + customSupportPrompts: {}, enhancementApiConfigId: "", experimentalDiffStrategy: false, autoApprovalEnabled: false, @@ -270,7 +273,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setListApiConfigMeta, onUpdateApiConfig, setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })), - setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })), + setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })), + setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })), setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })), setExperimentalDiffStrategy: (value) => From 3257dffa56676db3e48a52b3b97721b5897ba9b7 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 23 Jan 2025 15:56:24 -0800 Subject: [PATCH 12/12] Review feedback --- package.json | 6 +- src/extension.ts | 40 ++-- src/shared/support-prompt.ts | 79 ++++--- .../src/components/prompts/PromptsView.tsx | 194 +++++++++++------- 4 files changed, 179 insertions(+), 140 deletions(-) diff --git a/package.json b/package.json index e5cf882..7eab96e 100644 --- a/package.json +++ b/package.json @@ -104,17 +104,17 @@ }, { "command": "roo-cline.explainCode", - "title": "Explain Code", + "title": "Roo Code: Explain Code", "category": "Roo Code" }, { "command": "roo-cline.fixCode", - "title": "Fix Code", + "title": "Roo Code: Fix Code", "category": "Roo Code" }, { "command": "roo-cline.improveCode", - "title": "Improve Code", + "title": "Roo Code: Improve Code", "category": "Roo Code" } ], diff --git a/src/extension.ts b/src/extension.ts index b03e9e5..472968a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -171,17 +171,21 @@ export function activate(context: vscode.ExtensionContext) { context: vscode.ExtensionContext, command: string, promptType: keyof typeof ACTION_NAMES, - inputPrompt: string, - inputPlaceholder: string, + inputPrompt?: string, + inputPlaceholder?: string, ) => { + let userInput: string | undefined + context.subscriptions.push( vscode.commands.registerCommand( command, async (filePath: string, selectedText: string, diagnostics?: any[]) => { - const userInput = await vscode.window.showInputBox({ - prompt: inputPrompt, - placeHolder: inputPlaceholder, - }) + if (inputPrompt) { + userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder, + }) + } const params = { filePath, @@ -197,29 +201,11 @@ export function activate(context: vscode.ExtensionContext) { } // Register code action commands - registerCodeAction( - context, - "roo-cline.explainCode", - "EXPLAIN", - "What would you like Roo to explain?", - "E.g. How does the error handling work?", - ) + registerCodeAction(context, "roo-cline.explainCode", "EXPLAIN") - registerCodeAction( - context, - "roo-cline.fixCode", - "FIX", - "What would you like Roo to fix?", - "E.g. Maintain backward compatibility", - ) + registerCodeAction(context, "roo-cline.fixCode", "FIX") - registerCodeAction( - context, - "roo-cline.improveCode", - "IMPROVE", - "What would you like Roo to improve?", - "E.g. Focus on performance optimization", - ) + registerCodeAction(context, "roo-cline.improveCode", "IMPROVE") return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 9f18b1a..881c060 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -24,7 +24,26 @@ export const createPrompt = (template: string, params: PromptParams): string => return result } -const EXPLAIN_TEMPLATE = `Explain the following code from file path @/\${filePath}: +interface SupportPromptConfig { + label: string + description: string + template: string +} + +const supportPromptConfigs: Record = { + ENHANCE: { + label: "Enhance Prompt", + description: + "Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo understands your intent and provides the best possible responses. Available via the ✨ icon in chat.", + template: `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes): + +\${userInput}`, + }, + EXPLAIN: { + label: "Explain Code", + description: + "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in the editor context menu (right-click on selected code).", + template: `Explain the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -34,10 +53,13 @@ const EXPLAIN_TEMPLATE = `Explain the following code from file path @/\${filePat Please provide a clear and concise explanation of what this code does, including: 1. The purpose and functionality 2. Key components and their interactions -3. Important patterns or techniques used -` - -const FIX_TEMPLATE = `Fix any issues in the following code from file path @/\${filePath} +3. Important patterns or techniques used`, + }, + FIX: { + label: "Fix Issues", + description: + "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in the editor context menu (right-click on selected code).", + template: `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} @@ -49,10 +71,13 @@ Please: 1. Address all detected problems listed above (if any) 2. Identify any other potential bugs or issues 3. Provide corrected code -4. Explain what was fixed and why -` - -const IMPROVE_TEMPLATE = `Improve the following code from file path @/\${filePath}: +4. Explain what was fixed and why`, + }, + IMPROVE: { + label: "Improve Code", + description: + "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in the editor context menu (right-click on selected code).", + template: `Improve the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -65,27 +90,16 @@ Please suggest improvements for: 3. Best practices and patterns 4. Error handling and edge cases -Provide the improved code along with explanations for each enhancement. -` - -const ENHANCE_TEMPLATE = `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes): - -\${userInput}` - -// Get template based on prompt type -const defaultTemplates = { - EXPLAIN: EXPLAIN_TEMPLATE, - FIX: FIX_TEMPLATE, - IMPROVE: IMPROVE_TEMPLATE, - ENHANCE: ENHANCE_TEMPLATE, +Provide the improved code along with explanations for each enhancement.`, + }, } as const -type SupportPromptType = keyof typeof defaultTemplates +type SupportPromptType = keyof typeof supportPromptConfigs export const supportPrompt = { - default: defaultTemplates, + default: Object.fromEntries(Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.template])), get: (customSupportPrompts: Record | undefined, type: SupportPromptType): string => { - return customSupportPrompts?.[type] ?? defaultTemplates[type] + return customSupportPrompts?.[type] ?? supportPromptConfigs[type].template }, create: (type: SupportPromptType, params: PromptParams, customSupportPrompts?: Record): string => { const template = supportPrompt.get(customSupportPrompts, type) @@ -95,13 +109,14 @@ export const supportPrompt = { export type { SupportPromptType } -// User-friendly labels for support prompt types -export const supportPromptLabels: Record = { - FIX: "Fix Issues", - EXPLAIN: "Explain Code", - IMPROVE: "Improve Code", - ENHANCE: "Enhance Prompt", -} as const +// Expose labels and descriptions for UI +export const supportPromptLabels = Object.fromEntries( + Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.label]), +) as Record + +export const supportPromptDescriptions = Object.fromEntries( + Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.description]), +) as Record export type CustomSupportPrompts = { [key: string]: string | undefined diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index c2f3cad..c05f70f 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -9,7 +9,12 @@ import { } from "@vscode/webview-ui-toolkit/react" import { useExtensionState } from "../../context/ExtensionStateContext" import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes" -import { supportPrompt, SupportPromptType, supportPromptLabels } from "../../../../src/shared/support-prompt" +import { + supportPrompt, + SupportPromptType, + supportPromptLabels, + supportPromptDescriptions, +} from "../../../../src/shared/support-prompt" import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" @@ -46,7 +51,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) - const [activeSupportTab, setActiveSupportTab] = useState("EXPLAIN") + const [activeSupportTab, setActiveSupportTab] = useState("ENHANCE") // Direct update functions const updateAgentPrompt = useCallback( @@ -313,7 +318,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
-
+
Preferred Language