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", "command": "roo-cline.openInNewTab",
"title": "Open In New Tab", "title": "Open In New Tab",
"category": "Roo Code" "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": { "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": [ "view/title": [
{ {
"command": "roo-cline.plusButtonClicked", "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()) ?? {} (await this.providerRef.deref()?.getState()) ?? {}
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
const systemPrompt = await (async () => { const systemPrompt = await (async () => {
@@ -825,7 +825,7 @@ export class Cline {
this.diffStrategy, this.diffStrategy,
browserViewportSize, browserViewportSize,
mode, mode,
customPrompts, customModePrompts,
customModes, customModes,
this.customInstructions, this.customInstructions,
preferredLanguage, 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, // diffStrategy
undefined, // browserViewportSize undefined, // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
) )
@@ -178,7 +178,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // diffStrategy undefined, // diffStrategy
"1280x800", // browserViewportSize "1280x800", // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
) )
@@ -196,7 +196,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // diffStrategy undefined, // diffStrategy
undefined, // browserViewportSize undefined, // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
) )
@@ -212,7 +212,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // diffStrategy undefined, // diffStrategy
undefined, // browserViewportSize undefined, // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
) )
@@ -228,7 +228,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // diffStrategy undefined, // diffStrategy
"900x600", // different viewport size "900x600", // different viewport size
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
) )
@@ -244,7 +244,7 @@ describe("SYSTEM_PROMPT", () => {
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
undefined, // browserViewportSize undefined, // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
undefined, // globalCustomInstructions undefined, // globalCustomInstructions
undefined, // preferredLanguage undefined, // preferredLanguage
@@ -264,7 +264,7 @@ describe("SYSTEM_PROMPT", () => {
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
undefined, // browserViewportSize undefined, // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
undefined, // globalCustomInstructions undefined, // globalCustomInstructions
undefined, // preferredLanguage undefined, // preferredLanguage
@@ -284,7 +284,7 @@ describe("SYSTEM_PROMPT", () => {
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
undefined, // browserViewportSize undefined, // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
undefined, // globalCustomInstructions undefined, // globalCustomInstructions
undefined, // preferredLanguage undefined, // preferredLanguage
@@ -304,7 +304,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // diffStrategy undefined, // diffStrategy
undefined, // browserViewportSize undefined, // browserViewportSize
defaultModeSlug, // mode defaultModeSlug, // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
undefined, // globalCustomInstructions undefined, // globalCustomInstructions
"Spanish", // preferredLanguage "Spanish", // preferredLanguage
@@ -334,7 +334,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // diffStrategy undefined, // diffStrategy
undefined, // browserViewportSize undefined, // browserViewportSize
"custom-mode", // mode "custom-mode", // mode
undefined, // customPrompts undefined, // customModePrompts
customModes, // customModes customModes, // customModes
"Global instructions", // globalCustomInstructions "Global instructions", // globalCustomInstructions
) )
@@ -351,7 +351,7 @@ describe("SYSTEM_PROMPT", () => {
}) })
it("should use promptComponent roleDefinition when available", async () => { it("should use promptComponent roleDefinition when available", async () => {
const customPrompts = { const customModePrompts = {
[defaultModeSlug]: { [defaultModeSlug]: {
roleDefinition: "Custom prompt role definition", roleDefinition: "Custom prompt role definition",
customInstructions: "Custom prompt instructions", customInstructions: "Custom prompt instructions",
@@ -366,7 +366,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, undefined,
undefined, undefined,
defaultModeSlug, defaultModeSlug,
customPrompts, customModePrompts,
undefined, undefined,
) )
@@ -377,7 +377,7 @@ describe("SYSTEM_PROMPT", () => {
}) })
it("should fallback to modeConfig roleDefinition when promptComponent has no roleDefinition", async () => { it("should fallback to modeConfig roleDefinition when promptComponent has no roleDefinition", async () => {
const customPrompts = { const customModePrompts = {
[defaultModeSlug]: { [defaultModeSlug]: {
customInstructions: "Custom prompt instructions", customInstructions: "Custom prompt instructions",
// No roleDefinition provided // No roleDefinition provided
@@ -392,7 +392,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, undefined,
undefined, undefined,
defaultModeSlug, defaultModeSlug,
customPrompts, customModePrompts,
undefined, undefined,
) )
@@ -432,7 +432,7 @@ describe("addCustomInstructions", () => {
undefined, // diffStrategy undefined, // diffStrategy
undefined, // browserViewportSize undefined, // browserViewportSize
"architect", // mode "architect", // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
) )
@@ -448,7 +448,7 @@ describe("addCustomInstructions", () => {
undefined, // diffStrategy undefined, // diffStrategy
undefined, // browserViewportSize undefined, // browserViewportSize
"ask", // mode "ask", // mode
undefined, // customPrompts undefined, // customModePrompts
undefined, // customModes undefined, // customModes
) )

View File

@@ -1,7 +1,7 @@
import { import {
Mode, Mode,
modes, modes,
CustomPrompts, CustomModePrompts,
PromptComponent, PromptComponent,
getRoleDefinition, getRoleDefinition,
defaultModeSlug, defaultModeSlug,
@@ -97,7 +97,7 @@ export const SYSTEM_PROMPT = async (
diffStrategy?: DiffStrategy, diffStrategy?: DiffStrategy,
browserViewportSize?: string, browserViewportSize?: string,
mode: Mode = defaultModeSlug, mode: Mode = defaultModeSlug,
customPrompts?: CustomPrompts, customModePrompts?: CustomModePrompts,
customModes?: ModeConfig[], customModes?: ModeConfig[],
globalCustomInstructions?: string, globalCustomInstructions?: string,
preferredLanguage?: string, preferredLanguage?: string,
@@ -115,7 +115,7 @@ export const SYSTEM_PROMPT = async (
} }
// Check if it's a custom mode // 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 // 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] 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 { Anthropic } from "@anthropic-ai/sdk"
import delay from "delay"
import axios from "axios" import axios from "axios"
import fs from "fs/promises" import fs from "fs/promises"
import os from "os" import os from "os"
@@ -21,9 +22,8 @@ import { WebviewMessage } from "../../shared/WebviewMessage"
import { import {
Mode, Mode,
modes, modes,
CustomPrompts, CustomModePrompts,
PromptComponent, PromptComponent,
enhance,
ModeConfig, ModeConfig,
defaultModeSlug, defaultModeSlug,
getModeBySlug, getModeBySlug,
@@ -36,10 +36,13 @@ import { getNonce } from "./getNonce"
import { getUri } from "./getUri" import { getUri } from "./getUri"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { checkExistKey } from "../../shared/checkExistApiConfig" 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 { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
import { ConfigManager } from "../config/ConfigManager" import { ConfigManager } from "../config/ConfigManager"
import { CustomModesManager } from "../config/CustomModesManager" 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 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" | "vsCodeLmModelSelector"
| "mode" | "mode"
| "modeApiConfigs" | "modeApiConfigs"
| "customPrompts" | "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId" | "enhancementApiConfigId"
| "experimentalDiffStrategy" | "experimentalDiffStrategy"
| "autoApprovalEnabled" | "autoApprovalEnabled"
@@ -181,6 +185,32 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) 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( resolveWebviewView(
webviewView: vscode.WebviewView | vscode.WebviewPanel, 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 //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() await this.clearTask()
const { const {
apiConfiguration, apiConfiguration,
customPrompts, customModePrompts,
diffEnabled, diffEnabled,
fuzzyMatchThreshold, fuzzyMatchThreshold,
mode, mode,
@@ -275,7 +305,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
experimentalDiffStrategy, experimentalDiffStrategy,
} = await this.getState() } = await this.getState()
const modePrompt = customPrompts?.[mode] const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
this.cline = new Cline( this.cline = new Cline(
@@ -295,7 +325,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.clearTask() await this.clearTask()
const { const {
apiConfiguration, apiConfiguration,
customPrompts, customModePrompts,
diffEnabled, diffEnabled,
fuzzyMatchThreshold, fuzzyMatchThreshold,
mode, mode,
@@ -303,7 +333,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
experimentalDiffStrategy, experimentalDiffStrategy,
} = await this.getState() } = await this.getState()
const modePrompt = customPrompts?.[mode] const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
this.cline = new Cline( this.cline = new Cline(
@@ -782,47 +812,65 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview() await this.postStateToWebview()
break break
case "updateEnhancedPrompt": case "updateSupportPrompt":
const existingPrompts = (await this.getGlobalState("customPrompts")) || {} try {
if (Object.keys(message?.values ?? {}).length === 0) {
return
}
const existingPrompts = (await this.getGlobalState("customSupportPrompts")) || {}
const updatedPrompts = { const updatedPrompts = {
...existingPrompts, ...existingPrompts,
enhance: message.text, ...message.values,
} }
await this.updateGlobalState("customPrompts", updatedPrompts) await this.updateGlobalState("customSupportPrompts", updatedPrompts)
await this.postStateToWebview()
// Get current state and explicitly include customPrompts } catch (error) {
const currentState = await this.getState() console.error("Error update support prompt:", error)
vscode.window.showErrorMessage("Failed to update support prompt")
const stateWithPrompts = { }
...currentState, break
customPrompts: updatedPrompts, case "resetSupportPrompt":
try {
if (!message?.text) {
return
} }
// Post state with prompts const existingPrompts = ((await this.getGlobalState("customSupportPrompts")) ||
this.view?.webview.postMessage({ {}) as Record<string, any>
type: "state",
state: stateWithPrompts, 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 break
case "updatePrompt": case "updatePrompt":
if (message.promptMode && message.customPrompt !== undefined) { if (message.promptMode && message.customPrompt !== undefined) {
const existingPrompts = (await this.getGlobalState("customPrompts")) || {} const existingPrompts = (await this.getGlobalState("customModePrompts")) || {}
const updatedPrompts = { const updatedPrompts = {
...existingPrompts, ...existingPrompts,
[message.promptMode]: message.customPrompt, [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 currentState = await this.getState()
const stateWithPrompts = { const stateWithPrompts = {
...currentState, ...currentState,
customPrompts: updatedPrompts, customModePrompts: updatedPrompts,
} }
// Post state with prompts // Post state with prompts
@@ -932,8 +980,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "enhancePrompt": case "enhancePrompt":
if (message.text) { if (message.text) {
try { try {
const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } = const {
await this.getState() apiConfiguration,
customSupportPrompts,
listApiConfigMeta,
enhancementApiConfigId,
} = await this.getState()
// Try to get enhancement config first, fall back to current config // Try to get enhancement config first, fall back to current config
let configToUse: ApiConfiguration = apiConfiguration let configToUse: ApiConfiguration = apiConfiguration
@@ -947,17 +999,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
} }
} }
const getEnhancePrompt = (value: string | PromptComponent | undefined): string => { const enhancedPrompt = await singleCompletionHandler(
if (typeof value === "string") {
return value
}
return enhance.prompt // Use the constant from modes.ts which we know is a string
}
const enhancedPrompt = await enhancePrompt(
configToUse, configToUse,
message.text, supportPrompt.create(
getEnhancePrompt(customPrompts?.enhance), "ENHANCE",
{
userInput: message.text,
},
customSupportPrompts,
),
) )
await this.postMessageToWebview({ await this.postMessageToWebview({
type: "enhancedPrompt", type: "enhancedPrompt",
text: enhancedPrompt, text: enhancedPrompt,
@@ -975,7 +1027,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try { try {
const { const {
apiConfiguration, apiConfiguration,
customPrompts, customModePrompts,
customInstructions, customInstructions,
preferredLanguage, preferredLanguage,
browserViewportSize, browserViewportSize,
@@ -1005,7 +1057,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffStrategy, diffStrategy,
browserViewportSize ?? "900x600", browserViewportSize ?? "900x600",
mode, mode,
customPrompts, customModePrompts,
customModes, customModes,
customInstructions, customInstructions,
preferredLanguage, preferredLanguage,
@@ -1753,7 +1805,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
currentApiConfigName, currentApiConfigName,
listApiConfigMeta, listApiConfigMeta,
mode, mode,
customPrompts, customModePrompts,
customSupportPrompts,
enhancementApiConfigId, enhancementApiConfigId,
experimentalDiffStrategy, experimentalDiffStrategy,
autoApprovalEnabled, autoApprovalEnabled,
@@ -1792,7 +1845,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
currentApiConfigName: currentApiConfigName ?? "default", currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [], listApiConfigMeta: listApiConfigMeta ?? [],
mode: mode ?? defaultModeSlug, mode: mode ?? defaultModeSlug,
customPrompts: customPrompts ?? {}, customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
enhancementApiConfigId, enhancementApiConfigId,
experimentalDiffStrategy: experimentalDiffStrategy ?? false, experimentalDiffStrategy: experimentalDiffStrategy ?? false,
autoApprovalEnabled: autoApprovalEnabled ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false,
@@ -1912,7 +1966,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
vsCodeLmModelSelector, vsCodeLmModelSelector,
mode, mode,
modeApiConfigs, modeApiConfigs,
customPrompts, customModePrompts,
customSupportPrompts,
enhancementApiConfigId, enhancementApiConfigId,
experimentalDiffStrategy, experimentalDiffStrategy,
autoApprovalEnabled, autoApprovalEnabled,
@@ -1977,7 +2032,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>, this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
this.getGlobalState("mode") as Promise<Mode | undefined>, this.getGlobalState("mode") as Promise<Mode | undefined>,
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | 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("enhancementApiConfigId") as Promise<string | undefined>,
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>, this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>, this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
@@ -2088,7 +2144,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
currentApiConfigName: currentApiConfigName ?? "default", currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [], listApiConfigMeta: listApiConfigMeta ?? [],
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>), modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
customPrompts: customPrompts ?? {}, customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
enhancementApiConfigId, enhancementApiConfigId,
experimentalDiffStrategy: experimentalDiffStrategy ?? false, experimentalDiffStrategy: experimentalDiffStrategy ?? false,
autoApprovalEnabled: autoApprovalEnabled ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false,

View File

@@ -555,7 +555,7 @@ describe("ClineProvider", () => {
architect: "existing architect prompt", architect: "existing architect prompt",
} }
;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
if (key === "customPrompts") { if (key === "customModePrompts") {
return existingPrompts return existingPrompts
} }
return undefined return undefined
@@ -569,7 +569,7 @@ describe("ClineProvider", () => {
}) })
// Verify state was updated correctly // Verify state was updated correctly
expect(mockContext.globalState.update).toHaveBeenCalledWith("customPrompts", { expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
...existingPrompts, ...existingPrompts,
code: "new code prompt", code: "new code prompt",
}) })
@@ -579,7 +579,7 @@ describe("ClineProvider", () => {
expect.objectContaining({ expect.objectContaining({
type: "state", type: "state",
state: expect.objectContaining({ state: expect.objectContaining({
customPrompts: { customModePrompts: {
...existingPrompts, ...existingPrompts,
code: "new code prompt", code: "new code prompt",
}, },
@@ -588,17 +588,17 @@ describe("ClineProvider", () => {
) )
}) })
test("customPrompts defaults to empty object", async () => { test("customModePrompts defaults to empty object", async () => {
// Mock globalState.get to return undefined for customPrompts // Mock globalState.get to return undefined for customModePrompts
;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
if (key === "customPrompts") { if (key === "customModePrompts") {
return undefined return undefined
} }
return null return null
}) })
const state = await provider.getState() const state = await provider.getState()
expect(state.customPrompts).toEqual({}) expect(state.customModePrompts).toEqual({})
}) })
test("uses mode-specific custom instructions in Cline initialization", async () => { test("uses mode-specific custom instructions in Cline initialization", async () => {
@@ -611,7 +611,7 @@ describe("ClineProvider", () => {
jest.spyOn(provider, "getState").mockResolvedValue({ jest.spyOn(provider, "getState").mockResolvedValue({
apiConfiguration: mockApiConfig, apiConfiguration: mockApiConfig,
customPrompts: { customModePrompts: {
code: { customInstructions: modeCustomInstructions }, code: { customInstructions: modeCustomInstructions },
}, },
mode: "code", mode: "code",
@@ -651,7 +651,7 @@ describe("ClineProvider", () => {
}, },
} }
mockContext.globalState.get = jest.fn((key: string) => { mockContext.globalState.get = jest.fn((key: string) => {
if (key === "customPrompts") { if (key === "customModePrompts") {
return existingPrompts return existingPrompts
} }
return undefined return undefined
@@ -668,7 +668,7 @@ describe("ClineProvider", () => {
}) })
// Verify state was updated correctly // Verify state was updated correctly
expect(mockContext.globalState.update).toHaveBeenCalledWith("customPrompts", { expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
code: { code: {
roleDefinition: "Code role", roleDefinition: "Code role",
customInstructions: "New instructions", customInstructions: "New instructions",
@@ -978,7 +978,7 @@ describe("ClineProvider", () => {
apiModelId: "test-model", apiModelId: "test-model",
openRouterModelInfo: { supportsComputerUse: true }, openRouterModelInfo: { supportsComputerUse: true },
}, },
customPrompts: {}, customModePrompts: {},
mode: "code", mode: "code",
mcpEnabled: false, mcpEnabled: false,
browserViewportSize: "900x600", browserViewportSize: "900x600",
@@ -1007,7 +1007,7 @@ describe("ClineProvider", () => {
}), }),
"900x600", // browserViewportSize "900x600", // browserViewportSize
"code", // mode "code", // mode
{}, // customPrompts {}, // customModePrompts
{}, // customModes {}, // customModes
undefined, // effectiveInstructions undefined, // effectiveInstructions
undefined, // preferredLanguage undefined, // preferredLanguage
@@ -1027,7 +1027,7 @@ describe("ClineProvider", () => {
apiModelId: "test-model", apiModelId: "test-model",
openRouterModelInfo: { supportsComputerUse: true }, openRouterModelInfo: { supportsComputerUse: true },
}, },
customPrompts: {}, customModePrompts: {},
mode: "code", mode: "code",
mcpEnabled: false, mcpEnabled: false,
browserViewportSize: "900x600", browserViewportSize: "900x600",
@@ -1056,7 +1056,7 @@ describe("ClineProvider", () => {
}), }),
"900x600", // browserViewportSize "900x600", // browserViewportSize
"code", // mode "code", // mode
{}, // customPrompts {}, // customModePrompts
{}, // customModes {}, // customModes
undefined, // effectiveInstructions undefined, // effectiveInstructions
undefined, // preferredLanguage undefined, // preferredLanguage
@@ -1071,7 +1071,7 @@ describe("ClineProvider", () => {
apiProvider: "openrouter", apiProvider: "openrouter",
openRouterModelInfo: { supportsComputerUse: true }, openRouterModelInfo: { supportsComputerUse: true },
}, },
customPrompts: { customModePrompts: {
architect: { customInstructions: "Architect mode instructions" }, architect: { customInstructions: "Architect mode instructions" },
}, },
mode: "architect", mode: "architect",

View File

@@ -5,6 +5,7 @@ import * as vscode from "vscode"
import { ClineProvider } from "./core/webview/ClineProvider" import { ClineProvider } from "./core/webview/ClineProvider"
import { createClineAPI } from "./exports" import { createClineAPI } from "./exports"
import "./utils/path" // necessary to have access to String.prototype.toPosix 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" 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 })) 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) return createClineAPI(outputChannel, sidebarProvider)
} }

View File

@@ -4,7 +4,8 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api"
import { HistoryItem } from "./HistoryItem" import { HistoryItem } from "./HistoryItem"
import { McpServer } from "./mcp" import { McpServer } from "./mcp"
import { GitCommit } from "../utils/git" 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 { export interface LanguageModelChatSelector {
vendor?: string vendor?: string
@@ -82,7 +83,8 @@ export interface ExtensionState {
currentApiConfigName?: string currentApiConfigName?: string
listApiConfigMeta?: ApiConfigMeta[] listApiConfigMeta?: ApiConfigMeta[]
customInstructions?: string customInstructions?: string
customPrompts?: CustomPrompts customModePrompts?: CustomModePrompts
customSupportPrompts?: CustomSupportPrompts
alwaysAllowReadOnly?: boolean alwaysAllowReadOnly?: boolean
alwaysAllowWrite?: boolean alwaysAllowWrite?: boolean
alwaysAllowExecute?: boolean alwaysAllowExecute?: boolean

View File

@@ -68,7 +68,8 @@ export interface WebviewMessage {
| "requestVsCodeLmModels" | "requestVsCodeLmModels"
| "mode" | "mode"
| "updatePrompt" | "updatePrompt"
| "updateEnhancedPrompt" | "updateSupportPrompt"
| "resetSupportPrompt"
| "getSystemPrompt" | "getSystemPrompt"
| "systemPrompt" | "systemPrompt"
| "enhancementApiConfigId" | "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 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 // Helper to get all tools for a mode
export function getToolsForMode(groups: readonly ToolGroup[]): string[] { export function getToolsForMode(groups: readonly ToolGroup[]): string[] {
const tools = new Set<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)) 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 // 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 }])), 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") const testEnvPath = path.join(__dirname, ".test_env")
dotenv.config({ path: testEnvPath }) dotenv.config({ path: testEnvPath })
suite("Roo Cline Extension Test Suite", () => { suite("Roo Code Extension Test Suite", () => {
vscode.window.showInformationMessage("Starting Roo Cline extension tests.") vscode.window.showInformationMessage("Starting Roo Code extension tests.")
test("Extension should be present", () => { test("Extension should be present", () => {
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
@@ -123,6 +123,9 @@ suite("Roo Cline Extension Test Suite", () => {
"roo-cline.popoutButtonClicked", "roo-cline.popoutButtonClicked",
"roo-cline.settingsButtonClicked", "roo-cline.settingsButtonClicked",
"roo-cline.openInNewTab", "roo-cline.openInNewTab",
"roo-cline.explainCode",
"roo-cline.fixCode",
"roo-cline.improveCode",
] ]
for (const cmd of expectedCommands) { for (const cmd of expectedCommands) {
@@ -133,7 +136,7 @@ suite("Roo Cline Extension Test Suite", () => {
test("Views should be registered", () => { test("Views should be registered", () => {
const view = vscode.window.createWebviewPanel( const view = vscode.window.createWebviewPanel(
"roo-cline.SidebarProvider", "roo-cline.SidebarProvider",
"Roo Cline", "Roo Code",
vscode.ViewColumn.One, vscode.ViewColumn.One,
{}, {},
) )
@@ -181,17 +184,12 @@ suite("Roo Cline Extension Test Suite", () => {
// Create webview panel with development options // Create webview panel with development options
const extensionUri = extension.extensionUri const extensionUri = extension.extensionUri
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
"roo-cline.SidebarProvider",
"Roo Cline",
vscode.ViewColumn.One,
{
enableScripts: true, enableScripts: true,
enableCommandUris: true, enableCommandUris: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [extensionUri], localResourceRoots: [extensionUri],
}, })
)
try { try {
// Initialize webview with development context // 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 { ApiConfiguration } from "../../shared/api"
import { buildApiHandler, SingleCompletionHandler } from "../../api" import { buildApiHandler, SingleCompletionHandler } from "../../api"
import { defaultPrompts } from "../../shared/modes" import { supportPrompt } from "../../shared/support-prompt"
// Mock the API handler // Mock the API handler
jest.mock("../../api", () => ({ jest.mock("../../api", () => ({
@@ -34,17 +34,29 @@ describe("enhancePrompt", () => {
}) })
it("enhances prompt using default enhancement prompt when no custom prompt provided", async () => { 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") expect(result).toBe("Enhanced prompt")
const handler = buildApiHandler(mockApiConfig) 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 () => { it("enhances prompt using custom enhancement prompt when provided", async () => {
const customEnhancePrompt = "You are a custom prompt enhancer" 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") expect(result).toBe("Enhanced prompt")
const handler = buildApiHandler(mockApiConfig) const handler = buildApiHandler(mockApiConfig)
@@ -52,11 +64,11 @@ describe("enhancePrompt", () => {
}) })
it("throws error for empty prompt input", async () => { 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 () => { 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", "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", "The selected API provider does not support prompt enhancement",
) )
}) })
@@ -101,7 +113,7 @@ describe("enhancePrompt", () => {
}), }),
} as unknown as SingleCompletionHandler) } as unknown as SingleCompletionHandler)
const result = await enhancePrompt(openRouterConfig, "Test prompt") const result = await singleCompletionHandler(openRouterConfig, "Test prompt")
expect(buildApiHandler).toHaveBeenCalledWith(openRouterConfig) expect(buildApiHandler).toHaveBeenCalledWith(openRouterConfig)
expect(result).toBe("Enhanced prompt") expect(result).toBe("Enhanced prompt")
@@ -121,6 +133,6 @@ describe("enhancePrompt", () => {
}), }),
} as unknown as SingleCompletionHandler) } 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 { ApiConfiguration } from "../shared/api"
import { buildApiHandler, SingleCompletionHandler } from "../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. * 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. * This is a lightweight alternative that only uses the API's completion functionality.
*/ */
export async function enhancePrompt( export async function singleCompletionHandler(apiConfiguration: ApiConfiguration, promptText: string): Promise<string> {
apiConfiguration: ApiConfiguration,
promptText: string,
enhancePrompt?: string,
): Promise<string> {
if (!promptText) { if (!promptText) {
throw new Error("No prompt text provided") 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") throw new Error("The selected API provider does not support prompt enhancement")
} }
const enhancePromptText = enhancePrompt ?? defaultPrompts.enhance return (handler as SingleCompletionHandler).completePrompt(promptText)
const prompt = `${enhancePromptText}\n\n${promptText}`
return (handler as SingleCompletionHandler).completePrompt(prompt)
} }

View File

@@ -1,7 +1,7 @@
import { render, fireEvent, screen } from "@testing-library/react" import { render, fireEvent, screen } from "@testing-library/react"
import { useExtensionState } from "../../../context/ExtensionStateContext" import { useExtensionState } from "../../../context/ExtensionStateContext"
import AutoApproveMenu from "../AutoApproveMenu" import AutoApproveMenu from "../AutoApproveMenu"
import { codeMode, defaultPrompts } from "../../../../../src/shared/modes" import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes"
// Mock the ExtensionStateContext hook // Mock the ExtensionStateContext hook
jest.mock("../../../context/ExtensionStateContext") jest.mock("../../../context/ExtensionStateContext")
@@ -29,8 +29,9 @@ describe("AutoApproveMenu", () => {
requestDelaySeconds: 5, requestDelaySeconds: 5,
currentApiConfigName: "default", currentApiConfigName: "default",
listApiConfigMeta: [], listApiConfigMeta: [],
mode: codeMode, mode: defaultModeSlug,
customPrompts: defaultPrompts, customModePrompts: defaultPrompts,
customSupportPrompts: {},
enhancementApiConfigId: "", enhancementApiConfigId: "",
didHydrateState: true, didHydrateState: true,
showWelcome: false, showWelcome: false,

View File

@@ -8,14 +8,14 @@ import {
VSCodeCheckbox, VSCodeCheckbox,
} from "@vscode/webview-ui-toolkit/react" } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../../context/ExtensionStateContext" import { useExtensionState } from "../../context/ExtensionStateContext"
import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes"
import { import {
Mode, supportPrompt,
PromptComponent, SupportPromptType,
getRoleDefinition, supportPromptLabels,
getAllModes, supportPromptDescriptions,
ModeConfig, } from "../../../../src/shared/support-prompt"
enhancePrompt,
} from "../../../../src/shared/modes"
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
import { vscode } from "../../utils/vscode" import { vscode } from "../../utils/vscode"
@@ -28,7 +28,8 @@ type PromptsViewProps = {
const PromptsView = ({ onDone }: PromptsViewProps) => { const PromptsView = ({ onDone }: PromptsViewProps) => {
const { const {
customPrompts, customModePrompts,
customSupportPrompts,
listApiConfigMeta, listApiConfigMeta,
enhancementApiConfigId, enhancementApiConfigId,
setEnhancementApiConfigId, setEnhancementApiConfigId,
@@ -50,11 +51,12 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isToolsEditMode, setIsToolsEditMode] = useState(false)
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
const [activeSupportTab, setActiveSupportTab] = useState<SupportPromptType>("ENHANCE")
// Direct update functions // Direct update functions
const updateAgentPrompt = useCallback( const updateAgentPrompt = useCallback(
(mode: Mode, promptData: PromptComponent) => { (mode: Mode, promptData: PromptComponent) => {
const existingPrompt = customPrompts?.[mode] const existingPrompt = customModePrompts?.[mode] as PromptComponent
const updatedPrompt = { ...existingPrompt, ...promptData } const updatedPrompt = { ...existingPrompt, ...promptData }
// Only include properties that differ from defaults // Only include properties that differ from defaults
@@ -68,7 +70,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
customPrompt: updatedPrompt, customPrompt: updatedPrompt,
}) })
}, },
[customPrompts], [customModePrompts],
) )
const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => { const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => {
@@ -254,36 +256,33 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
return () => window.removeEventListener("message", handler) return () => window.removeEventListener("message", handler)
}, []) }, [])
const updateEnhancePrompt = (value: string | undefined) => { const updateSupportPrompt = (type: SupportPromptType, value: string | undefined) => {
vscode.postMessage({ vscode.postMessage({
type: "updateEnhancedPrompt", type: "updateSupportPrompt",
text: value, 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) => { const handleAgentReset = (modeSlug: string) => {
// Only reset role definition for built-in modes // Only reset role definition for built-in modes
const existingPrompt = customPrompts?.[modeSlug] const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent
updateAgentPrompt(modeSlug, { updateAgentPrompt(modeSlug, {
...existingPrompt, ...existingPrompt,
roleDefinition: undefined, roleDefinition: undefined,
}) })
} }
const handleEnhanceReset = () => { const handleSupportReset = (type: SupportPromptType) => {
updateEnhancePrompt(undefined) vscode.postMessage({
type: "resetSupportPrompt",
text: type,
})
} }
const getEnhancePromptValue = (): string => { const getSupportPromptValue = (type: SupportPromptType): string => {
return enhancePrompt.get(customPrompts) return supportPrompt.get(customSupportPrompts, type)
} }
const handleTestEnhancement = () => { const handleTestEnhancement = () => {
@@ -319,7 +318,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div> </div>
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}> <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={{ marginBottom: "20px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Preferred Language</div> <div style={{ fontWeight: "bold", marginBottom: "4px" }}>Preferred Language</div>
<select <select
@@ -392,7 +391,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
style={{ width: "100%" }} style={{ width: "100%" }}
data-testid="global-custom-instructions-textarea" 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{" "} Instructions can also be loaded from{" "}
<span <span
style={{ style={{
@@ -416,7 +421,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div> </div>
</div> </div>
<div style={{ marginBottom: "20px" }}> <div style={{ marginTop: "20px" }}>
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -563,7 +568,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<VSCodeTextArea <VSCodeTextArea
value={(() => { value={(() => {
const customMode = findModeBySlug(mode, customModes) const customMode = findModeBySlug(mode, customModes)
const prompt = customPrompts?.[mode] const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode) return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
})()} })()}
onChange={(e) => { onChange={(e) => {
@@ -680,7 +685,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<VSCodeTextArea <VSCodeTextArea
value={(() => { value={(() => {
const customMode = findModeBySlug(mode, customModes) const customMode = findModeBySlug(mode, customModes)
const prompt = customPrompts?.[mode] const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.customInstructions ?? prompt?.customInstructions ?? "" return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
})()} })()}
onChange={(e) => { onChange={(e) => {
@@ -696,7 +701,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}) })
} else { } else {
// For built-in modes, update the prompts // For built-in modes, update the prompts
const existingPrompt = customPrompts?.[mode] const existingPrompt = customModePrompts?.[mode] as PromptComponent
updateAgentPrompt(mode, { updateAgentPrompt(mode, {
...existingPrompt, ...existingPrompt,
customInstructions: value.trim() || undefined, customInstructions: value.trim() || undefined,
@@ -742,7 +747,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div> </div>
</div> </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 <VSCodeButton
appearance="primary" appearance="primary"
onClick={() => { onClick={() => {
@@ -759,27 +771,112 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</VSCodeButton> </VSCodeButton>
</div> </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 <div
style={{ style={{
color: "var(--vscode-foreground)", color: "var(--vscode-foreground)",
fontSize: "13px", fontSize: "13px",
marginBottom: "20px", marginBottom: "20px",
marginTop: "5px", marginTop: "5px",
}}> }}></div>
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 style={{ marginBottom: "12px" }}> <div style={{ marginBottom: "12px" }}>
<div style={{ marginBottom: "8px" }}> <div style={{ marginBottom: "8px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>API Configuration</div> <div style={{ fontWeight: "bold", marginBottom: "4px" }}>
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)" }}> API Configuration
You can select an API configuration to always use for enhancing prompts, or just use </div>
whatever is currently selected <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>
</div> </div>
<VSCodeDropdown <VSCodeDropdown
@@ -794,7 +891,9 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}) })
}} }}
style={{ width: "300px" }}> style={{ width: "300px" }}>
<VSCodeOption value="">Use currently selected API configuration</VSCodeOption> <VSCodeOption value="">
Use currently selected API configuration
</VSCodeOption>
{(listApiConfigMeta || []).map((config) => ( {(listApiConfigMeta || []).map((config) => (
<VSCodeOption key={config.id} value={config.id}> <VSCodeOption key={config.id} value={config.id}>
{config.name} {config.name}
@@ -802,41 +901,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
))} ))}
</VSCodeDropdown> </VSCodeDropdown>
</div> </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>
<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" }}> <div style={{ marginTop: "12px" }}>
<VSCodeTextArea <VSCodeTextArea
@@ -864,11 +929,10 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</VSCodeButton> </VSCodeButton>
</div> </div>
</div> </div>
</>
)}
</div> </div>
</div> </div>
{/* Bottom padding */}
<div style={{ height: "20px" }} />
</div> </div>
{isCreateModeDialogOpen && ( {isCreateModeDialogOpen && (

View File

@@ -12,7 +12,7 @@ jest.mock("../../../utils/vscode", () => ({
})) }))
const mockExtensionState = { const mockExtensionState = {
customPrompts: {}, customModePrompts: {},
listApiConfigMeta: [ listApiConfigMeta: [
{ id: "config1", name: "Config 1" }, { id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" }, { id: "config2", name: "Config 2" },
@@ -166,6 +166,10 @@ describe("PromptsView", () => {
it("handles API configuration selection", () => { it("handles API configuration selection", () => {
renderPromptsView() 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") const dropdown = screen.getByTestId("api-config-dropdown")
fireEvent( fireEvent(
dropdown, dropdown,

View File

@@ -14,7 +14,8 @@ import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { findLastIndex } from "../../../src/shared/array" import { findLastIndex } from "../../../src/shared/array"
import { McpServer } from "../../../src/shared/mcp" import { McpServer } from "../../../src/shared/mcp"
import { checkExistKey } from "../../../src/shared/checkExistApiConfig" 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 { export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean didHydrateState: boolean
@@ -57,7 +58,8 @@ export interface ExtensionStateContextType extends ExtensionState {
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
mode: Mode mode: Mode
setMode: (value: Mode) => void setMode: (value: Mode) => void
setCustomPrompts: (value: CustomPrompts) => void setCustomModePrompts: (value: CustomModePrompts) => void
setCustomSupportPrompts: (value: CustomSupportPrompts) => void
enhancementApiConfigId?: string enhancementApiConfigId?: string
setEnhancementApiConfigId: (value: string) => void setEnhancementApiConfigId: (value: string) => void
experimentalDiffStrategy: boolean experimentalDiffStrategy: boolean
@@ -93,7 +95,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
currentApiConfigName: "default", currentApiConfigName: "default",
listApiConfigMeta: [], listApiConfigMeta: [],
mode: defaultModeSlug, mode: defaultModeSlug,
customPrompts: defaultPrompts, customModePrompts: defaultPrompts,
customSupportPrompts: {},
enhancementApiConfigId: "", enhancementApiConfigId: "",
experimentalDiffStrategy: false, experimentalDiffStrategy: false,
autoApprovalEnabled: false, autoApprovalEnabled: false,
@@ -270,7 +273,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setListApiConfigMeta, setListApiConfigMeta,
onUpdateApiConfig, onUpdateApiConfig,
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })), 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) => setEnhancementApiConfigId: (value) =>
setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })), setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
setExperimentalDiffStrategy: (value) => setExperimentalDiffStrategy: (value) =>