diff --git a/package.json b/package.json index 36928b9..b6160af 100644 --- a/package.json +++ b/package.json @@ -101,9 +101,41 @@ "command": "roo-cline.openInNewTab", "title": "Open In New Tab", "category": "Roo Code" + }, + { + "command": "roo-cline.explainCode", + "title": "Roo Code: Explain Code", + "category": "Roo Code" + }, + { + "command": "roo-cline.fixCode", + "title": "Roo Code: Fix Code", + "category": "Roo Code" + }, + { + "command": "roo-cline.improveCode", + "title": "Roo Code: Improve Code", + "category": "Roo Code" } ], "menus": { + "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", 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/CodeActionProvider.ts b/src/core/CodeActionProvider.ts new file mode 100644 index 0000000..d3b980a --- /dev/null +++ b/src/core/CodeActionProvider.ts @@ -0,0 +1,179 @@ +import * as vscode from "vscode" +import * as path from "path" + +export const ACTION_NAMES = { + 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 + +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 [] + } + } +} diff --git a/src/core/__tests__/CodeActionProvider.test.ts b/src/core/__tests__/CodeActionProvider.test.ts new file mode 100644 index 0000000..d0bfc8e --- /dev/null +++ b/src/core/__tests__/CodeActionProvider.test.ts @@ -0,0 +1,147 @@ +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 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/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 b3e5235..1cf7028 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" @@ -21,9 +22,8 @@ import { WebviewMessage } from "../../shared/WebviewMessage" import { Mode, modes, - CustomPrompts, + CustomModePrompts, PromptComponent, - enhance, ModeConfig, defaultModeSlug, getModeBySlug, @@ -36,10 +36,13 @@ 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" +import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt" + +import { ACTION_NAMES } from "../CodeActionProvider" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -108,7 +111,8 @@ type GlobalStateKey = | "vsCodeLmModelSelector" | "mode" | "modeApiConfigs" - | "customPrompts" + | "customModePrompts" + | "customSupportPrompts" | "enhancementApiConfigId" | "experimentalDiffStrategy" | "autoApprovalEnabled" @@ -181,6 +185,32 @@ export class ClineProvider implements vscode.WebviewViewProvider { return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } + public static async handleCodeAction( + promptType: keyof typeof ACTION_NAMES, + params: Record, + ): Promise { + 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 { customSupportPrompts } = await visibleProvider.getState() + + const prompt = supportPrompt.create(promptType, params, customSupportPrompts) + + 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 @@ -267,7 +297,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.clearTask() const { apiConfiguration, - customPrompts, + customModePrompts, diffEnabled, fuzzyMatchThreshold, mode, @@ -275,7 +305,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] + const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -295,7 +325,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.clearTask() const { apiConfiguration, - customPrompts, + customModePrompts, diffEnabled, fuzzyMatchThreshold, mode, @@ -303,7 +333,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] + const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -782,47 +812,65 @@ 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("customSupportPrompts")) || {} + + const updatedPrompts = { + ...existingPrompts, + ...message.values, + } + + await this.updateGlobalState("customSupportPrompts", 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("customSupportPrompts")) || + {}) as Record - // 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("customSupportPrompts", 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) { - 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 @@ -932,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 @@ -947,17 +999,17 @@ 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( + const enhancedPrompt = await singleCompletionHandler( configToUse, - message.text, - getEnhancePrompt(customPrompts?.enhance), + supportPrompt.create( + "ENHANCE", + { + userInput: message.text, + }, + customSupportPrompts, + ), ) + await this.postMessageToWebview({ type: "enhancedPrompt", text: enhancedPrompt, @@ -975,7 +1027,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { try { const { apiConfiguration, - customPrompts, + customModePrompts, customInstructions, preferredLanguage, browserViewportSize, @@ -1005,7 +1057,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { diffStrategy, browserViewportSize ?? "900x600", mode, - customPrompts, + customModePrompts, customModes, customInstructions, preferredLanguage, @@ -1753,7 +1805,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { currentApiConfigName, listApiConfigMeta, mode, - customPrompts, + customModePrompts, + customSupportPrompts, enhancementApiConfigId, experimentalDiffStrategy, autoApprovalEnabled, @@ -1792,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, @@ -1912,7 +1966,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { vsCodeLmModelSelector, mode, modeApiConfigs, - customPrompts, + customModePrompts, + customSupportPrompts, enhancementApiConfigId, experimentalDiffStrategy, autoApprovalEnabled, @@ -1977,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, @@ -2088,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/extension.ts b/src/extension.ts index 0cf3053..472968a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +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 { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" /* @@ -158,6 +159,54 @@ 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, + }), + ) + + // Helper function to handle code actions + const registerCodeAction = ( + context: vscode.ExtensionContext, + command: string, + promptType: keyof typeof ACTION_NAMES, + inputPrompt?: string, + inputPlaceholder?: string, + ) => { + let userInput: string | undefined + + context.subscriptions.push( + vscode.commands.registerCommand( + command, + async (filePath: string, selectedText: string, diagnostics?: any[]) => { + if (inputPrompt) { + userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder, + }) + } + + const params = { + filePath, + selectedText, + ...(diagnostics ? { diagnostics } : {}), + ...(userInput ? { userInput } : {}), + } + + await ClineProvider.handleCodeAction(promptType, params) + }, + ), + ) + } + + // Register code action commands + registerCodeAction(context, "roo-cline.explainCode", "EXPLAIN") + + registerCodeAction(context, "roo-cline.fixCode", "FIX") + + registerCodeAction(context, "roo-cline.improveCode", "IMPROVE") + return createClineAPI(outputChannel, sidebarProvider) } 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/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..edee6c2 --- /dev/null +++ b/src/shared/__tests__/support-prompts.test.ts @@ -0,0 +1,153 @@ +import { supportPrompt } 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 = supportPrompt.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 = supportPrompt.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 = supportPrompt.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 = supportPrompt.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("ENHANCE action", () => { + it("should format enhance prompt correctly", () => { + const prompt = supportPrompt.create("ENHANCE", { + 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):\n\ntest", + ) + // 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 = supportPrompt.get(undefined, "EXPLAIN") + expect(template).toBe(supportPrompt.default.EXPLAIN) + }) + + it("should return custom template when provided", () => { + const customTemplate = "Custom template for explaining code" + const customSupportPrompts = { + EXPLAIN: customTemplate, + } + const template = supportPrompt.get(customSupportPrompts, "EXPLAIN") + expect(template).toBe(customTemplate) + }) + + it("should return default template when custom prompts does not include type", () => { + const customSupportPrompts = { + SOMETHING_ELSE: "Other template", + } + const template = supportPrompt.get(customSupportPrompts, "EXPLAIN") + expect(template).toBe(supportPrompt.default.EXPLAIN) + }) + }) + + describe("create with custom prompts", () => { + it("should use custom template when provided", () => { + const customTemplate = "Custom template for ${filePath}" + const customSupportPrompts = { + EXPLAIN: customTemplate, + } + + const prompt = supportPrompt.create( + "EXPLAIN", + { + filePath: testFilePath, + selectedText: testCode, + }, + customSupportPrompts, + ) + + 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 customSupportPrompts = { + EXPLAIN: "Other template", + } + + const prompt = supportPrompt.create( + "EXPLAIN", + { + filePath: testFilePath, + selectedText: testCode, + }, + customSupportPrompts, + ) + + expect(prompt).toContain("Other template") + }) + }) +}) diff --git a/src/shared/modes.ts b/src/shared/modes.ts index c6ea89a..5b3358b 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 CustomModePrompts = { + [key: string]: PromptComponent | undefined +} + // Helper to get all tools for a mode export function getToolsForMode(groups: readonly ToolGroup[]): string[] { const tools = new Set() @@ -130,35 +140,8 @@ 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( +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..881c060 --- /dev/null +++ b/src/shared/support-prompt.ts @@ -0,0 +1,123 @@ +// Support 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 +} + +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} + +\`\`\` +\${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`, + }, + 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} + +\`\`\` +\${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`, + }, + 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} + +\`\`\` +\${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.`, + }, +} as const + +type SupportPromptType = keyof typeof supportPromptConfigs + +export const supportPrompt = { + default: Object.fromEntries(Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.template])), + get: (customSupportPrompts: Record | undefined, type: SupportPromptType): string => { + return customSupportPrompts?.[type] ?? supportPromptConfigs[type].template + }, + create: (type: SupportPromptType, params: PromptParams, customSupportPrompts?: Record): string => { + const template = supportPrompt.get(customSupportPrompts, type) + return createPrompt(template, params) + }, +} as const + +export type { SupportPromptType } + +// 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/src/test/extension.test.ts b/src/test/extension.test.ts index c67b3db..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") @@ -123,6 +123,9 @@ suite("Roo Cline Extension Test Suite", () => { "roo-cline.popoutButtonClicked", "roo-cline.settingsButtonClicked", "roo-cline.openInNewTab", + "roo-cline.explainCode", + "roo-cline.fixCode", + "roo-cline.improveCode", ] for (const cmd of expectedCommands) { @@ -133,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, {}, ) @@ -181,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 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) } 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 ebd4d0f..c05f70f 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" + 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" @@ -28,7 +28,8 @@ type PromptsViewProps = { const PromptsView = ({ onDone }: PromptsViewProps) => { const { - customPrompts, + customModePrompts, + customSupportPrompts, listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId, @@ -50,11 +51,12 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) + const [activeSupportTab, setActiveSupportTab] = useState("ENHANCE") // Direct update functions const updateAgentPrompt = useCallback( (mode: Mode, promptData: PromptComponent) => { - const existingPrompt = customPrompts?.[mode] + const existingPrompt = customModePrompts?.[mode] as PromptComponent const updatedPrompt = { ...existingPrompt, ...promptData } // Only include properties that differ from defaults @@ -68,7 +70,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { customPrompt: updatedPrompt, }) }, - [customPrompts], + [customModePrompts], ) const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => { @@ -254,36 +256,33 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { return () => window.removeEventListener("message", handler) }, []) - const updateEnhancePrompt = (value: string | undefined) => { + const updateSupportPrompt = (type: SupportPromptType, value: string | undefined) => { vscode.postMessage({ - type: "updateEnhancedPrompt", - text: value, + type: "updateSupportPrompt", + values: { + [type]: value, + }, }) } - 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] + const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent updateAgentPrompt(modeSlug, { ...existingPrompt, roleDefinition: undefined, }) } - const handleEnhanceReset = () => { - updateEnhancePrompt(undefined) + const handleSupportReset = (type: SupportPromptType) => { + vscode.postMessage({ + type: "resetSupportPrompt", + text: type, + }) } - const getEnhancePromptValue = (): string => { - return enhancePrompt.get(customPrompts) + const getSupportPromptValue = (type: SupportPromptType): string => { + return supportPrompt.get(customSupportPrompts, type) } const handleTestEnhancement = () => { @@ -319,7 +318,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
-
+
Preferred Language