diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts index 1110aa8831..e1f0090e59 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts @@ -390,4 +390,49 @@ describe("getEnvironmentDetails", () => { const result = await getEnvironmentDetails(cline as Task) expect(result).toContain("REMINDERS") }) + + describe("Selection Context", () => { + it("should include selection context when available", async () => { + const clineWithSelection = { + ...mockCline, + selectionContext: { + selectedText: "const x = 1;\nconst y = 2;", + selectionFilePath: "src/test.ts", + selectionStartLine: 10, + selectionEndLine: 11, + }, + } + + const result = await getEnvironmentDetails(clineWithSelection as Task) + + expect(result).toContain("# Current Selection") + expect(result).toContain("File: src/test.ts:10-11") + expect(result).toContain("```") + expect(result).toContain("const x = 1;") + expect(result).toContain("const y = 2;") + }) + + it("should clear selection context after including it", async () => { + const clineWithSelection = { + ...mockCline, + selectionContext: { + selectedText: "test code", + selectionFilePath: "src/app.ts", + selectionStartLine: 5, + selectionEndLine: 5, + }, + } + + await getEnvironmentDetails(clineWithSelection as Task) + + // Selection context should be cleared after use + expect(clineWithSelection.selectionContext).toBeUndefined() + }) + + it("should not include selection section when no context is available", async () => { + const result = await getEnvironmentDetails(mockCline as Task) + + expect(result).not.toContain("# Current Selection") + }) + }) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 30d9cd0b0d..c01ec9bb83 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -32,6 +32,17 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo maxWorkspaceFiles = 200, } = state ?? {} + // Include selection context if available + if (cline.selectionContext) { + const { selectedText, selectionFilePath, selectionStartLine, selectionEndLine } = cline.selectionContext + details += "\n\n# Current Selection" + details += `\nFile: ${selectionFilePath}:${selectionStartLine}-${selectionEndLine}` + details += `\n\`\`\`\n${selectedText}\n\`\`\`` + + // Clear the selection context after including it once + cline.selectionContext = undefined + } + // It could be useful for cline to know if the user went from one or no // file to another between messages, so we always include this context. details += "\n\n# VSCode Visible Files" diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 4f2bdd72da..c28852c20b 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -155,6 +155,13 @@ export class Task extends EventEmitter implements TaskLike { todoList?: TodoItem[] + selectionContext?: { + selectedText: string + selectionFilePath: string + selectionStartLine: number + selectionEndLine: number + } + readonly rootTask: Task | undefined = undefined readonly parentTask: Task | undefined = undefined readonly taskNumber: number diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c85dea9d16..b890513b05 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -428,6 +428,61 @@ export const webviewMessageHandler = async ( } switch (message.type) { + case "requestSelectionContext": { + // Get the active editor and its selection + const editor = vscode.window.activeTextEditor + if (editor && !editor.selection.isEmpty) { + const selection = editor.selection + const selectedText = editor.document.getText(selection) + const filePath = editor.document.uri.fsPath + + // Convert to workspace-relative path if possible + const workspacePath = provider.cwd + let relativeFilePath: string + if (filePath.startsWith(workspacePath)) { + relativeFilePath = path.relative(workspacePath, filePath) + } else { + // File is outside workspace, use absolute path + relativeFilePath = filePath + } + + // VSCode uses 0-based line numbers, convert to 1-based for user-friendly display + const startLine = selection.start.line + 1 + const endLine = selection.end.line + 1 + + // Send selection context to webview + await provider.postMessageToWebview({ + type: "selectionContext", + selectedText, + selectionFilePath: relativeFilePath, + selectionStartLine: startLine, + selectionEndLine: endLine, + }) + + // Store selection context in current task for use in environment details + const currentTask = provider.getCurrentTask() + if (currentTask) { + currentTask.selectionContext = { + selectedText, + selectionFilePath: relativeFilePath, + selectionStartLine: startLine, + selectionEndLine: endLine, + } + } + } else { + // No selection, send empty context + await provider.postMessageToWebview({ + type: "selectionContext", + }) + + // Clear selection context in current task + const currentTask = provider.getCurrentTask() + if (currentTask) { + currentTask.selectionContext = undefined + } + } + break + } case "webviewDidLaunch": // Load custom modes first const customModes = await provider.customModesManager.getCustomModes() @@ -508,6 +563,18 @@ export const webviewMessageHandler = async ( // task. This essentially creates a fresh slate for the new task. try { await provider.createTask(message.text, message.images) + + // Store selection context in the newly created task + const newTask = provider.getCurrentTask() + if (newTask && message.selectedText) { + newTask.selectionContext = { + selectedText: message.selectedText, + selectionFilePath: message.selectionFilePath, + selectionStartLine: message.selectionStartLine, + selectionEndLine: message.selectionEndLine, + } + } + // Task created successfully - notify the UI to reset await provider.postMessageToWebview({ type: "invoke", invoke: "newChat" }) } catch (error) { @@ -523,9 +590,20 @@ export const webviewMessageHandler = async ( await provider.updateCustomInstructions(message.text) break - case "askResponse": - provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) + case "askResponse": { + // Store selection context in current task before handling response + const task = provider.getCurrentTask() + if (task && message.selectedText) { + task.selectionContext = { + selectedText: message.selectedText, + selectionFilePath: message.selectionFilePath, + selectionStartLine: message.selectionStartLine, + selectionEndLine: message.selectionEndLine, + } + } + task?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break + } case "updateSettings": if (message.updatedSettings) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 80c5532930..c8969842c0 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -128,7 +128,12 @@ export interface ExtensionMessage { | "dismissedUpsells" | "organizationSwitchResult" | "interactionRequired" + | "selectionContext" text?: string + selectedText?: string + selectionFilePath?: string + selectionStartLine?: number + selectionEndLine?: number payload?: any // Add a generic payload for now, can refine later // Checkpoint warning message checkpointWarning?: { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 02f0876ad3..64eeeb37a8 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -165,7 +165,12 @@ export interface WebviewMessage { | "dismissUpsell" | "getDismissedUpsells" | "updateSettings" + | "requestSelectionContext" text?: string + selectedText?: string + selectionFilePath?: string + selectionStartLine?: number + selectionEndLine?: number editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" disabled?: boolean diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 9adf603ee4..c9f6b3ffde 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -134,6 +134,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction(null) const [sendingDisabled, setSendingDisabled] = useState(false) const [selectedImages, setSelectedImages] = useState([]) + const [selectionContext, setSelectionContext] = useState<{ + selectedText?: string + selectionFilePath?: string + selectionStartLine?: number + selectionEndLine?: number + } | null>(null) // We need to hold on to the ask because useEffect > lastMessage will always // let us know when an ask comes in and handle it, but by the time @@ -563,7 +569,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction textAreaRef.current?.focus()) + useMount(() => { + textAreaRef.current?.focus() + // Request initial selection context when component mounts + vscode.postMessage({ type: "requestSelectionContext" }) + }) const visibleMessages = useMemo(() => { // Pre-compute checkpoint hashes that have associated user messages for O(1) lookup