mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Merge pull request #329 from samhvw8/feat/roo-cline-code-action
New Feature code action
This commit is contained in:
@@ -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,
|
||||
|
||||
179
src/core/CodeActionProvider.ts
Normal file
179
src/core/CodeActionProvider.ts
Normal 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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/core/__tests__/CodeActionProvider.test.ts
Normal file
147
src/core/__tests__/CodeActionProvider.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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 updatedPrompts = {
|
||||
...existingPrompts,
|
||||
enhance: message.text,
|
||||
const existingPrompts = (await this.getGlobalState("customSupportPrompts")) || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
...message.values,
|
||||
}
|
||||
|
||||
await this.updateGlobalState("customSupportPrompts", updatedPrompts)
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Error update support prompt:", error)
|
||||
vscode.window.showErrorMessage("Failed to update support prompt")
|
||||
}
|
||||
break
|
||||
case "resetSupportPrompt":
|
||||
try {
|
||||
if (!message?.text) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
const existingPrompts = ((await this.getGlobalState("customSupportPrompts")) ||
|
||||
{}) as Record<string, any>
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
const currentState = await this.getState()
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
}
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts,
|
||||
updatedPrompts[message.text] = undefined
|
||||
|
||||
await this.updateGlobalState("customSupportPrompts", updatedPrompts)
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Error reset support prompt:", error)
|
||||
vscode.window.showErrorMessage("Failed to reset support prompt")
|
||||
}
|
||||
|
||||
// Post state with prompts
|
||||
this.view?.webview.postMessage({
|
||||
type: "state",
|
||||
state: stateWithPrompts,
|
||||
})
|
||||
break
|
||||
case "updatePrompt":
|
||||
if (message.promptMode && message.customPrompt !== undefined) {
|
||||
const existingPrompts = (await this.getGlobalState("customPrompts")) || {}
|
||||
const existingPrompts = (await this.getGlobalState("customModePrompts")) || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
[message.promptMode]: message.customPrompt,
|
||||
}
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
await this.updateGlobalState("customModePrompts", updatedPrompts)
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
// Get current state and explicitly include customModePrompts
|
||||
const currentState = await this.getState()
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts,
|
||||
customModePrompts: updatedPrompts,
|
||||
}
|
||||
|
||||
// Post state with prompts
|
||||
@@ -932,8 +980,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
case "enhancePrompt":
|
||||
if (message.text) {
|
||||
try {
|
||||
const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } =
|
||||
await this.getState()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customSupportPrompts,
|
||||
listApiConfigMeta,
|
||||
enhancementApiConfigId,
|
||||
} = await this.getState()
|
||||
|
||||
// Try to get enhancement config first, fall back to current config
|
||||
let configToUse: ApiConfiguration = apiConfiguration
|
||||
@@ -947,17 +999,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const getEnhancePrompt = (value: string | PromptComponent | undefined): string => {
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
}
|
||||
return enhance.prompt // Use the constant from modes.ts which we know is a string
|
||||
}
|
||||
const enhancedPrompt = await enhancePrompt(
|
||||
const enhancedPrompt = await singleCompletionHandler(
|
||||
configToUse,
|
||||
message.text,
|
||||
getEnhancePrompt(customPrompts?.enhance),
|
||||
supportPrompt.create(
|
||||
"ENHANCE",
|
||||
{
|
||||
userInput: message.text,
|
||||
},
|
||||
customSupportPrompts,
|
||||
),
|
||||
)
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "enhancedPrompt",
|
||||
text: enhancedPrompt,
|
||||
@@ -975,7 +1027,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
try {
|
||||
const {
|
||||
apiConfiguration,
|
||||
customPrompts,
|
||||
customModePrompts,
|
||||
customInstructions,
|
||||
preferredLanguage,
|
||||
browserViewportSize,
|
||||
@@ -1005,7 +1057,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
diffStrategy,
|
||||
browserViewportSize ?? "900x600",
|
||||
mode,
|
||||
customPrompts,
|
||||
customModePrompts,
|
||||
customModes,
|
||||
customInstructions,
|
||||
preferredLanguage,
|
||||
@@ -1753,7 +1805,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
mode,
|
||||
customPrompts,
|
||||
customModePrompts,
|
||||
customSupportPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
@@ -1792,7 +1845,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
mode: mode ?? defaultModeSlug,
|
||||
customPrompts: customPrompts ?? {},
|
||||
customModePrompts: customModePrompts ?? {},
|
||||
customSupportPrompts: customSupportPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||
@@ -1912,7 +1966,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
vsCodeLmModelSelector,
|
||||
mode,
|
||||
modeApiConfigs,
|
||||
customPrompts,
|
||||
customModePrompts,
|
||||
customSupportPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
@@ -1977,7 +2032,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("vsCodeLmModelSelector") as Promise<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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -68,7 +68,8 @@ export interface WebviewMessage {
|
||||
| "requestVsCodeLmModels"
|
||||
| "mode"
|
||||
| "updatePrompt"
|
||||
| "updateEnhancedPrompt"
|
||||
| "updateSupportPrompt"
|
||||
| "resetSupportPrompt"
|
||||
| "getSystemPrompt"
|
||||
| "systemPrompt"
|
||||
| "enhancementApiConfigId"
|
||||
|
||||
153
src/shared/__tests__/support-prompts.test.ts
Normal file
153
src/shared/__tests__/support-prompts.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 }])),
|
||||
)
|
||||
|
||||
|
||||
123
src/shared/support-prompt.ts
Normal file
123
src/shared/support-prompt.ts
Normal 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
|
||||
}
|
||||
@@ -8,8 +8,8 @@ const dotenv = require("dotenv")
|
||||
const testEnvPath = path.join(__dirname, ".test_env")
|
||||
dotenv.config({ path: testEnvPath })
|
||||
|
||||
suite("Roo Cline Extension Test Suite", () => {
|
||||
vscode.window.showInformationMessage("Starting Roo Cline extension tests.")
|
||||
suite("Roo Code Extension Test Suite", () => {
|
||||
vscode.window.showInformationMessage("Starting Roo Code extension tests.")
|
||||
|
||||
test("Extension should be present", () => {
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
@@ -123,6 +123,9 @@ suite("Roo Cline Extension Test Suite", () => {
|
||||
"roo-cline.popoutButtonClicked",
|
||||
"roo-cline.settingsButtonClicked",
|
||||
"roo-cline.openInNewTab",
|
||||
"roo-cline.explainCode",
|
||||
"roo-cline.fixCode",
|
||||
"roo-cline.improveCode",
|
||||
]
|
||||
|
||||
for (const cmd of expectedCommands) {
|
||||
@@ -133,7 +136,7 @@ suite("Roo Cline Extension Test Suite", () => {
|
||||
test("Views should be registered", () => {
|
||||
const view = vscode.window.createWebviewPanel(
|
||||
"roo-cline.SidebarProvider",
|
||||
"Roo Cline",
|
||||
"Roo Code",
|
||||
vscode.ViewColumn.One,
|
||||
{},
|
||||
)
|
||||
@@ -181,17 +184,12 @@ suite("Roo Cline Extension Test Suite", () => {
|
||||
|
||||
// Create webview panel with development options
|
||||
const extensionUri = extension.extensionUri
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"roo-cline.SidebarProvider",
|
||||
"Roo Cline",
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [extensionUri],
|
||||
},
|
||||
)
|
||||
const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [extensionUri],
|
||||
})
|
||||
|
||||
try {
|
||||
// Initialize webview with development context
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user