ソースを参照

Merge pull request #656 from samhvw8/feat/add-to-context-code-action

Feat add to context code action & Fix some code action error
Matt Rubens 11 ヶ月 前
コミット
bedd6fd2a8

+ 10 - 0
package.json

@@ -118,6 +118,11 @@
 				"command": "roo-cline.improveCode",
 				"title": "Roo Code: Improve Code",
 				"category": "Roo Code"
+			},
+			{
+				"command": "roo-cline.addToContext",
+				"title": "Roo Code: Add To Context",
+				"category": "Roo Code"
 			}
 		],
 		"menus": {
@@ -136,6 +141,11 @@
 					"command": "roo-cline.improveCode",
 					"when": "editorHasSelection",
 					"group": "Roo Code@3"
+				},
+				{
+					"command": "roo-cline.addToContext",
+					"when": "editorHasSelection",
+					"group": "Roo Code@4"
 				}
 			],
 			"view/title": [

+ 24 - 108
src/core/CodeActionProvider.ts

@@ -1,113 +1,27 @@
 import * as vscode from "vscode"
-import * as path from "path"
-import { ClineProvider } from "./webview/ClineProvider"
+import { EditorUtils } from "./EditorUtils"
 
 export const ACTION_NAMES = {
 	EXPLAIN: "Roo Code: Explain Code",
 	FIX: "Roo Code: Fix Code",
+	FIX_LOGIC: "Roo Code: Fix Logic",
 	IMPROVE: "Roo Code: Improve Code",
+	ADD_TO_CONTEXT: "Roo Code: Add to Context",
 } as const
 
 const COMMAND_IDS = {
 	EXPLAIN: "roo-cline.explainCode",
 	FIX: "roo-cline.fixCode",
 	IMPROVE: "roo-cline.improveCode",
+	ADD_TO_CONTEXT: "roo-cline.addToContext",
 } 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 }
@@ -126,32 +40,20 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
 		]
 	}
 
-	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)
+			const effectiveRange = EditorUtils.getEffectiveRange(document, range)
 			if (!effectiveRange) {
 				return []
 			}
 
-			const filePath = this.getFilePath(document)
+			const filePath = EditorUtils.getFilePath(document)
 			const actions: vscode.CodeAction[] = []
 
-			// Create actions using helper method
-			// Add explain actions
 			actions.push(
 				...this.createActionPair(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [
 					filePath,
@@ -159,14 +61,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
 				]),
 			)
 
-			// Only process diagnostics if they exist
 			if (context.diagnostics.length > 0) {
 				const relevantDiagnostics = context.diagnostics.filter((d) =>
-					this.hasIntersectingRange(effectiveRange.range, d.range),
+					EditorUtils.hasIntersectingRange(effectiveRange.range, d.range),
 				)
 
 				if (relevantDiagnostics.length > 0) {
-					const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData)
+					const diagnosticMessages = relevantDiagnostics.map(EditorUtils.createDiagnosticData)
 					actions.push(
 						...this.createActionPair(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
 							filePath,
@@ -175,9 +76,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
 						]),
 					)
 				}
+			} else {
+				actions.push(
+					...this.createActionPair(ACTION_NAMES.FIX_LOGIC, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
+						filePath,
+						effectiveRange.text,
+					]),
+				)
 			}
 
-			// Add improve actions
 			actions.push(
 				...this.createActionPair(
 					ACTION_NAMES.IMPROVE,
@@ -187,6 +94,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
 				),
 			)
 
+			actions.push(
+				this.createAction(
+					ACTION_NAMES.ADD_TO_CONTEXT,
+					vscode.CodeActionKind.QuickFix,
+					COMMAND_IDS.ADD_TO_CONTEXT,
+					[filePath, effectiveRange.text],
+				),
+			)
+
 			return actions
 		} catch (error) {
 			console.error("Error providing code actions:", error)

+ 141 - 0
src/core/EditorUtils.ts

@@ -0,0 +1,141 @@
+import * as vscode from "vscode"
+import * as path from "path"
+
+export interface EffectiveRange {
+	range: vscode.Range
+	text: string
+}
+
+export interface DiagnosticData {
+	message: string
+	severity: vscode.DiagnosticSeverity
+	code?: string | number | { value: string | number; target: vscode.Uri }
+	source?: string
+	range: vscode.Range
+}
+
+export interface EditorContext {
+	filePath: string
+	selectedText: string
+	diagnostics?: DiagnosticData[]
+}
+
+export class EditorUtils {
+	// Cache file paths for performance
+	private static readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
+
+	static 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
+		}
+	}
+
+	static 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
+		}
+	}
+
+	static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData {
+		return {
+			message: diagnostic.message,
+			severity: diagnostic.severity,
+			code: diagnostic.code,
+			source: diagnostic.source,
+			range: diagnostic.range,
+		}
+	}
+
+	static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean {
+		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)
+		)
+	}
+
+	static getEditorContext(editor?: vscode.TextEditor): EditorContext | null {
+		try {
+			if (!editor) {
+				editor = vscode.window.activeTextEditor
+			}
+			if (!editor) {
+				return null
+			}
+
+			const document = editor.document
+			const selection = editor.selection
+			const effectiveRange = this.getEffectiveRange(document, selection)
+
+			if (!effectiveRange) {
+				return null
+			}
+
+			const filePath = this.getFilePath(document)
+			const diagnostics = vscode.languages
+				.getDiagnostics(document.uri)
+				.filter((d) => this.hasIntersectingRange(effectiveRange.range, d.range))
+				.map(this.createDiagnosticData)
+
+			return {
+				filePath,
+				selectedText: effectiveRange.text,
+				...(diagnostics.length > 0 ? { diagnostics } : {}),
+			}
+		} catch (error) {
+			console.error("Error getting editor context:", error)
+			return null
+		}
+	}
+}

+ 41 - 62
src/core/__tests__/CodeActionProvider.test.ts

@@ -1,5 +1,6 @@
 import * as vscode from "vscode"
 import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider"
+import { EditorUtils } from "../EditorUtils"
 
 // Mock VSCode API
 jest.mock("vscode", () => ({
@@ -16,13 +17,6 @@ jest.mock("vscode", () => ({
 		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,
@@ -31,6 +25,16 @@ jest.mock("vscode", () => ({
 	},
 }))
 
+// Mock EditorUtils
+jest.mock("../EditorUtils", () => ({
+	EditorUtils: {
+		getEffectiveRange: jest.fn(),
+		getFilePath: jest.fn(),
+		hasIntersectingRange: jest.fn(),
+		createDiagnosticData: jest.fn(),
+	},
+}))
+
 describe("CodeActionProvider", () => {
 	let provider: CodeActionProvider
 	let mockDocument: any
@@ -55,68 +59,32 @@ describe("CodeActionProvider", () => {
 		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")
+		// Setup default EditorUtils mocks
+		;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue({
+			range: mockRange,
+			text: "test code",
 		})
+		;(EditorUtils.getFilePath as jest.Mock).mockReturnValue("/test/file.ts")
+		;(EditorUtils.hasIntersectingRange as jest.Mock).mockReturnValue(true)
+		;(EditorUtils.createDiagnosticData as jest.Mock).mockImplementation((d) => d)
 	})
 
 	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", () => {
+		it("should provide explain, improve, fix logic, and add to context actions by default", () => {
 			const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
 
-			expect(actions).toHaveLength(4)
+			expect(actions).toHaveLength(7) // 2 explain + 2 fix logic + 2 improve + 1 add to context
 			expect((actions as any)[0].title).toBe(`${ACTION_NAMES.EXPLAIN} in New Task`)
 			expect((actions as any)[1].title).toBe(`${ACTION_NAMES.EXPLAIN} in Current Task`)
-			expect((actions as any)[2].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`)
-			expect((actions as any)[3].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`)
+			expect((actions as any)[2].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in New Task`)
+			expect((actions as any)[3].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in Current Task`)
+			expect((actions as any)[4].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`)
+			expect((actions as any)[5].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`)
+			expect((actions as any)[6].title).toBe(ACTION_NAMES.ADD_TO_CONTEXT)
 		})
 
-		it("should provide fix action when diagnostics exist", () => {
+		it("should provide fix action instead of fix logic when diagnostics exist", () => {
 			mockContext.diagnostics = [
 				{
 					message: "test error",
@@ -127,22 +95,33 @@ describe("CodeActionProvider", () => {
 
 			const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
 
-			expect(actions).toHaveLength(6)
+			expect(actions).toHaveLength(7) // 2 explain + 2 fix + 2 improve + 1 add to context
 			expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in New Task`)).toBe(true)
 			expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in Current Task`)).toBe(true)
+			expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in New Task`)).toBe(false)
+			expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in Current Task`)).toBe(
+				false,
+			)
+		})
+
+		it("should return empty array when no effective range", () => {
+			;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue(null)
+
+			const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
+
+			expect(actions).toEqual([])
 		})
 
 		it("should handle errors gracefully", () => {
 			const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
-			mockDocument.getText.mockImplementation(() => {
+			;(EditorUtils.getEffectiveRange as jest.Mock).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))
+			expect(consoleErrorSpy).toHaveBeenCalledWith("Error providing code actions:", expect.any(Error))
 
 			consoleErrorSpy.mockRestore()
 		})

+ 75 - 0
src/core/__tests__/EditorUtils.test.ts

@@ -0,0 +1,75 @@
+import * as vscode from "vscode"
+import { EditorUtils } from "../EditorUtils"
+
+// Mock VSCode API
+jest.mock("vscode", () => ({
+	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(),
+	},
+}))
+
+describe("EditorUtils", () => {
+	let mockDocument: any
+
+	beforeEach(() => {
+		mockDocument = {
+			getText: jest.fn(),
+			lineAt: jest.fn(),
+			lineCount: 10,
+			uri: { fsPath: "/test/file.ts" },
+		}
+	})
+
+	describe("getEffectiveRange", () => {
+		it("should return selected text when available", () => {
+			const mockRange = new vscode.Range(0, 0, 0, 10)
+			mockDocument.getText.mockReturnValue("selected text")
+
+			const result = EditorUtils.getEffectiveRange(mockDocument, mockRange)
+
+			expect(result).toEqual({
+				range: mockRange,
+				text: "selected text",
+			})
+		})
+
+		it("should return null for empty line", () => {
+			const mockRange = new vscode.Range(0, 0, 0, 10)
+			mockDocument.getText.mockReturnValue("")
+			mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
+
+			const result = EditorUtils.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 = EditorUtils.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 = EditorUtils.getFilePath(mockDocument)
+
+			expect(result).toBe("/test/file.ts")
+		})
+	})
+})

+ 10 - 0
src/core/webview/ClineProvider.ts

@@ -238,6 +238,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 		const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
 
+		if (command.endsWith("addToContext")) {
+			await visibleProvider.postMessageToWebview({
+				type: "invoke",
+				invoke: "setChatBoxMessage",
+				text: prompt,
+			})
+
+			return
+		}
+
 		if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
 			await visibleProvider.postMessageToWebview({
 				type: "invoke",

+ 36 - 23
src/extension.ts

@@ -6,6 +6,7 @@ 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 { EditorUtils } from "./core/EditorUtils"
 import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
 
 /*
@@ -171,33 +172,43 @@ export function activate(context: vscode.ExtensionContext) {
 		context: vscode.ExtensionContext,
 		command: string,
 		promptType: keyof typeof ACTION_NAMES,
-		inNewTask: boolean,
 		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(command, promptType, params)
-				},
-			),
+			vscode.commands.registerCommand(command, async (...args: any[]) => {
+				if (inputPrompt) {
+					userInput = await vscode.window.showInputBox({
+						prompt: inputPrompt,
+						placeHolder: inputPlaceholder,
+					})
+				}
+
+				// Handle both code action and direct command cases
+				let filePath: string
+				let selectedText: string
+				let diagnostics: any[] | undefined
+
+				if (args.length > 1) {
+					// Called from code action
+					;[filePath, selectedText, diagnostics] = args
+				} else {
+					// Called directly from command palette
+					const context = EditorUtils.getEditorContext()
+					if (!context) return
+					;({ filePath, selectedText, diagnostics } = context)
+				}
+
+				const params = {
+					...{ filePath, selectedText },
+					...(diagnostics ? { diagnostics } : {}),
+					...(userInput ? { userInput } : {}),
+				}
+
+				await ClineProvider.handleCodeAction(command, promptType, params)
+			}),
 		)
 	}
 
@@ -210,10 +221,10 @@ export function activate(context: vscode.ExtensionContext) {
 		inputPlaceholder?: string,
 	) => {
 		// Register new task version
-		registerCodeAction(context, baseCommand, promptType, true, inputPrompt, inputPlaceholder)
+		registerCodeAction(context, baseCommand, promptType, inputPrompt, inputPlaceholder)
 
 		// Register current task version
-		registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, false, inputPrompt, inputPlaceholder)
+		registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, inputPrompt, inputPlaceholder)
 	}
 
 	// Register code action commands
@@ -241,6 +252,8 @@ export function activate(context: vscode.ExtensionContext) {
 		"E.g. Focus on performance optimization",
 	)
 
+	registerCodeAction(context, "roo-cline.addToContext", "ADD_TO_CONTEXT")
+
 	return createClineAPI(outputChannel, sidebarProvider)
 }
 

+ 1 - 1
src/shared/ExtensionMessage.ts

@@ -50,7 +50,7 @@ export interface ExtensionMessage {
 		| "historyButtonClicked"
 		| "promptsButtonClicked"
 		| "didBecomeVisible"
-	invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
+	invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
 	state?: ExtensionState
 	images?: string[]
 	ollamaModels?: string[]

+ 14 - 5
src/shared/support-prompt.ts

@@ -18,8 +18,8 @@ export const createPrompt = (template: string, params: PromptParams): string =>
 		}
 	}
 
-	// Replace any remaining user_input placeholders with empty string
-	result = result.replaceAll("${userInput}", "")
+	// Replace any remaining placeholders with empty strings
+	result = result.replaceAll(/\${[^}]*}/g, "")
 
 	return result
 }
@@ -42,7 +42,7 @@ const supportPromptConfigs: Record<string, SupportPromptConfig> = {
 	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).",
+			"Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
 		template: `Explain the following code from file path @/\${filePath}:
 \${userInput}
 
@@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including
 	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).",
+			"Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
 		template: `Fix any issues in the following code from file path @/\${filePath}
 \${diagnosticText}
 \${userInput}
@@ -76,7 +76,7 @@ Please:
 	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).",
+			"Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
 		template: `Improve the following code from file path @/\${filePath}:
 \${userInput}
 
@@ -92,6 +92,15 @@ Please suggest improvements for:
 
 Provide the improved code along with explanations for each enhancement.`,
 	},
+	ADD_TO_CONTEXT: {
+		label: "Add to Context",
+		description:
+			"Add context to your current task or conversation. Useful for providing additional information or clarifications. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).",
+		template: `@/\${filePath}:
+\`\`\`
+\${selectedText}
+\`\`\``,
+	},
 } as const
 
 type SupportPromptType = keyof typeof supportPromptConfigs

+ 18 - 0
webview-ui/src/components/chat/ChatView.tsx

@@ -330,6 +330,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 		[messages.length, clineAsk],
 	)
 
+	const handleSetChatBoxMessage = useCallback(
+		(text: string, images: string[]) => {
+			// Avoid nested template literals by breaking down the logic
+			let newValue = text
+			if (inputValue !== "") {
+				newValue = inputValue + " " + text
+			}
+
+			setInputValue(newValue)
+			setSelectedImages([...selectedImages, ...images])
+		},
+		[inputValue, selectedImages],
+	)
+
 	const startNewTask = useCallback(() => {
 		vscode.postMessage({ type: "clearTask" })
 	}, [])
@@ -469,6 +483,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 						case "sendMessage":
 							handleSendMessage(message.text ?? "", message.images ?? [])
 							break
+						case "setChatBoxMessage":
+							handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
+							break
 						case "primaryButtonClick":
 							handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
 							break
@@ -484,6 +501,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 			textAreaDisabled,
 			enableButtons,
 			handleSendMessage,
+			handleSetChatBoxMessage,
 			handlePrimaryButtonClick,
 			handleSecondaryButtonClick,
 		],