Merge pull request #329 from samhvw8/feat/roo-cline-code-action

New Feature code action
This commit is contained in:
Matt Rubens
2025-01-23 17:19:45 -08:00
committed by GitHub
21 changed files with 1087 additions and 285 deletions

View File

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

View File

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

View File

@@ -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<vscode.TextDocument, string>()
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 []
}
}
}

View File

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

View File

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

View File

@@ -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]

View File

@@ -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<string, string | any[]>,
): Promise<void> {
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<unknown>, 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 existingPrompts = (await this.getGlobalState("customSupportPrompts")) || {}
const updatedPrompts = {
...existingPrompts,
enhance: message.text,
...message.values,
}
await this.updateGlobalState("customPrompts", updatedPrompts)
// Get current state and explicitly include customPrompts
const currentState = await this.getState()
const stateWithPrompts = {
...currentState,
customPrompts: updatedPrompts,
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
}
// Post state with prompts
this.view?.webview.postMessage({
type: "state",
state: stateWithPrompts,
})
const existingPrompts = ((await this.getGlobalState("customSupportPrompts")) ||
{}) as Record<string, any>
const updatedPrompts = {
...existingPrompts,
}
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")
}
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<vscode.LanguageModelChatSelector | undefined>,
this.getGlobalState("mode") as Promise<Mode | undefined>,
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
this.getGlobalState("customPrompts") as Promise<CustomPrompts | undefined>,
this.getGlobalState("customModePrompts") as Promise<CustomModePrompts | undefined>,
this.getGlobalState("customSupportPrompts") as Promise<CustomSupportPrompts | undefined>,
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
@@ -2088,7 +2144,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
customPrompts: customPrompts ?? {},
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
enhancementApiConfigId,
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
autoApprovalEnabled: autoApprovalEnabled ?? false,

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,8 @@ export interface WebviewMessage {
| "requestVsCodeLmModels"
| "mode"
| "updatePrompt"
| "updateEnhancedPrompt"
| "updateSupportPrompt"
| "resetSupportPrompt"
| "getSystemPrompt"
| "systemPrompt"
| "enhancementApiConfigId"

View File

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

View File

@@ -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<string>()
@@ -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<string, any> | undefined): string => {
return customPrompts?.enhance ?? enhance.prompt
},
} as const
// Create the mode-specific default prompts
export const defaultPrompts: Readonly<CustomPrompts> = Object.freeze(
export const defaultPrompts: Readonly<CustomModePrompts> = Object.freeze(
Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
)

View File

@@ -0,0 +1,123 @@
// Support prompts
type PromptParams = Record<string, string | any[]>
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<string, SupportPromptConfig> = {
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<string, any> | undefined, type: SupportPromptType): string => {
return customSupportPrompts?.[type] ?? supportPromptConfigs[type].template
},
create: (type: SupportPromptType, params: PromptParams, customSupportPrompts?: Record<string, any>): 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<SupportPromptType, string>
export const supportPromptDescriptions = Object.fromEntries(
Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.description]),
) as Record<SupportPromptType, string>
export type CustomSupportPrompts = {
[key: string]: string | undefined
}

View File

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

View File

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

View File

@@ -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<string> {
export async function singleCompletionHandler(apiConfiguration: ApiConfiguration, promptText: string): Promise<string> {
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)
}

View File

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

View File

@@ -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<SupportPromptType>("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<HTMLElement>): 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) => {
</div>
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
<div style={{ marginBottom: "20px" }}>
<div style={{ paddingBottom: "20px", borderBottom: "1px solid var(--vscode-input-border)" }}>
<div style={{ marginBottom: "20px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Preferred Language</div>
<select
@@ -392,7 +391,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
style={{ width: "100%" }}
data-testid="global-custom-instructions-textarea"
/>
<div style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", marginTop: "5px" }}>
<div
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
marginTop: "5px",
marginBottom: "40px",
}}>
Instructions can also be loaded from{" "}
<span
style={{
@@ -416,7 +421,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
<div style={{ marginBottom: "20px" }}>
<div style={{ marginTop: "20px" }}>
<div
style={{
display: "flex",
@@ -563,7 +568,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customPrompts?.[mode]
const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
})()}
onChange={(e) => {
@@ -680,7 +685,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customPrompts?.[mode]
const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
})()}
onChange={(e) => {
@@ -696,7 +701,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
})
} else {
// For built-in modes, update the prompts
const existingPrompt = customPrompts?.[mode]
const existingPrompt = customModePrompts?.[mode] as PromptComponent
updateAgentPrompt(mode, {
...existingPrompt,
customInstructions: value.trim() || undefined,
@@ -742,7 +747,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
</div>
<div style={{ marginBottom: "20px", display: "flex", justifyContent: "flex-start" }}>
<div
style={{
paddingBottom: "40px",
marginBottom: "20px",
borderBottom: "1px solid var(--vscode-input-border)",
display: "flex",
justifyContent: "flex-start",
}}>
<VSCodeButton
appearance="primary"
onClick={() => {
@@ -759,27 +771,112 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</VSCodeButton>
</div>
<h3 style={{ color: "var(--vscode-foreground)", margin: "40px 0 20px 0" }}>Prompt Enhancement</h3>
<div
style={{
marginTop: "20px",
paddingBottom: "60px",
borderBottom: "1px solid var(--vscode-input-border)",
}}>
<h3 style={{ color: "var(--vscode-foreground)", marginBottom: "12px" }}>Support Prompts</h3>
<div
style={{
display: "flex",
gap: "16px",
alignItems: "center",
marginBottom: "12px",
overflowX: "auto",
flexWrap: "nowrap",
paddingBottom: "4px",
paddingRight: "20px",
}}>
{Object.keys(supportPrompt.default).map((type) => (
<button
key={type}
data-testid={`${type}-tab`}
data-active={activeSupportTab === type ? "true" : "false"}
onClick={() => setActiveSupportTab(type as SupportPromptType)}
style={{
padding: "4px 8px",
border: "none",
background: activeSupportTab === type ? "var(--vscode-button-background)" : "none",
color:
activeSupportTab === type
? "var(--vscode-button-foreground)"
: "var(--vscode-foreground)",
cursor: "pointer",
opacity: activeSupportTab === type ? 1 : 0.8,
borderRadius: "3px",
fontWeight: "bold",
}}>
{supportPromptLabels[type as SupportPromptType]}
</button>
))}
</div>
{/* Support prompt description */}
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
margin: "8px 0 16px",
}}>
{supportPromptDescriptions[activeSupportTab]}
</div>
{/* Show active tab content */}
<div key={activeSupportTab}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Prompt</div>
<VSCodeButton
appearance="icon"
onClick={() => handleSupportReset(activeSupportTab)}
title={`Reset ${activeSupportTab} prompt to default`}>
<span className="codicon codicon-discard"></span>
</VSCodeButton>
</div>
<VSCodeTextArea
value={getSupportPromptValue(activeSupportTab)}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const trimmedValue = value.trim()
updateSupportPrompt(activeSupportTab, trimmedValue || undefined)
}}
rows={6}
resize="vertical"
style={{ width: "100%" }}
/>
{activeSupportTab === "ENHANCE" && (
<>
<div>
<div
style={{
color: "var(--vscode-foreground)",
fontSize: "13px",
marginBottom: "20px",
marginTop: "5px",
}}>
Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo
understands your intent and provides the best possible responses.
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<div>
}}></div>
<div style={{ marginBottom: "12px" }}>
<div style={{ marginBottom: "8px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>API Configuration</div>
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)" }}>
You can select an API configuration to always use for enhancing prompts, or just use
whatever is currently selected
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>
API Configuration
</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
}}>
You can select an API configuration to always use for enhancing prompts,
or just use whatever is currently selected
</div>
</div>
<VSCodeDropdown
@@ -794,7 +891,9 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
})
}}
style={{ width: "300px" }}>
<VSCodeOption value="">Use currently selected API configuration</VSCodeOption>
<VSCodeOption value="">
Use currently selected API configuration
</VSCodeOption>
{(listApiConfigMeta || []).map((config) => (
<VSCodeOption key={config.id} value={config.id}>
{config.name}
@@ -802,41 +901,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
))}
</VSCodeDropdown>
</div>
<div style={{ marginBottom: "8px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Enhancement Prompt</div>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeButton
appearance="icon"
onClick={handleEnhanceReset}
title="Revert to default">
<span className="codicon codicon-discard"></span>
</VSCodeButton>
</div>
</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
This prompt will be used to refine your input when you hit the sparkle icon in chat.
</div>
</div>
<VSCodeTextArea
value={getEnhancePromptValue()}
onChange={handleEnhancePromptChange}
rows={4}
resize="vertical"
style={{ width: "100%" }}
/>
<div style={{ marginTop: "12px" }}>
<VSCodeTextArea
@@ -864,11 +929,10 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</VSCodeButton>
</div>
</div>
</>
)}
</div>
</div>
{/* Bottom padding */}
<div style={{ height: "20px" }} />
</div>
{isCreateModeDialogOpen && (

View File

@@ -12,7 +12,7 @@ jest.mock("../../../utils/vscode", () => ({
}))
const mockExtensionState = {
customPrompts: {},
customModePrompts: {},
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
@@ -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,

View File

@@ -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) =>