From c813feac55080b8f03401e7fd2e76a4dd76db040 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 11 Jul 2025 14:14:16 +0000 Subject: [PATCH] feat: add Current Editor Context option to @ context menu - Add EditorContext to ContextMenuOptionType enum - Update WebviewMessage types to support requestEditorContext and editorContext - Implement backend handler in webviewMessageHandler.ts using EditorUtils.getEditorContext() - Update ExtensionMessage interface to include editorContext field - Add Current Editor Context option to context menu with edit icon - Implement ChatTextArea handling for editor context selection and formatting - Update tests to reflect new context menu option count - Fix React Hook dependencies for proper linting The feature allows users to type @ in chat, select 'Current Editor Context' to insert a formatted mention containing current file context, line numbers, and selected text from the active editor. --- src/core/webview/webviewMessageHandler.ts | 20 +++++ src/shared/ExtensionMessage.ts | 8 ++ src/shared/WebviewMessage.ts | 9 ++ .../src/components/chat/ChatTextArea.tsx | 90 +++++++++++++++---- .../src/components/chat/ContextMenu.tsx | 4 + .../utils/__tests__/context-mentions.spec.ts | 3 +- webview-ui/src/utils/context-mentions.ts | 2 + 7 files changed, 118 insertions(+), 18 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a6577fb2fb..f39fbab9cf 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2195,6 +2195,26 @@ export const webviewMessageHandler = async ( break } + case "requestEditorContext": { + try { + const { EditorUtils } = await import("../../integrations/editor/EditorUtils") + const editorContext = await EditorUtils.getEditorContext() + + await provider.postMessageToWebview({ + type: "editorContext", + editorContext: editorContext || undefined, + }) + } catch (error) { + provider.log( + `Error getting editor context: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + await provider.postMessageToWebview({ + type: "editorContext", + editorContext: undefined, + }) + } + break + } case "switchTab": { if (message.tab) { // Capture tab shown event for all switchTab messages (which are user-initiated) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 953c0c1070..5d8c73b20e 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -105,8 +105,16 @@ export interface ExtensionMessage { | "shareTaskSuccess" | "codeIndexSettingsSaved" | "codeIndexSecretStatus" + | "editorContext" text?: string payload?: any // Add a generic payload for now, can refine later + editorContext?: { + filePath?: string + selectedText?: string + startLine?: number + endLine?: number + diagnostics?: any[] + } action?: | "chatButtonClicked" | "mcpButtonClicked" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index fa9fb67310..358363c974 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -193,6 +193,8 @@ export interface WebviewMessage { | "checkRulesDirectoryResult" | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" + | "requestEditorContext" + | "editorContext" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" @@ -252,6 +254,13 @@ export interface WebviewMessage { codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string } + editorContext?: { + filePath?: string + selectedText?: string + startLine?: number + endLine?: number + diagnostics?: any[] + } } export const checkoutDiffPayloadSchema = z.object({ diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index a38b4538d0..a5a7d440a9 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -96,6 +96,21 @@ const ChatTextArea = forwardRef( const [fileSearchResults, setFileSearchResults] = useState([]) const [searchLoading, setSearchLoading] = useState(false) const [searchRequestId, setSearchRequestId] = useState("") + const [isDraggingOver, setIsDraggingOver] = useState(false) + const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined) + const [showContextMenu, setShowContextMenu] = useState(false) + const [cursorPosition, setCursorPosition] = useState(0) + const [searchQuery, setSearchQuery] = useState("") + const textAreaRef = useRef(null) + const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false) + const highlightLayerRef = useRef(null) + const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1) + const [selectedType, setSelectedType] = useState(null) + const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false) + const [intendedCursorPosition, setIntendedCursorPosition] = useState(null) + const contextMenuContainerRef = useRef(null) + const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false) + const [isFocused, setIsFocused] = useState(false) // Close dropdown when clicking outside. useEffect(() => { @@ -135,28 +150,63 @@ const ChatTextArea = forwardRef( if (message.requestId === searchRequestId) { setFileSearchResults(message.results || []) } + } else if (message.type === "editorContext") { + // Handle editor context response + if (message.editorContext && textAreaRef.current) { + const editorContext = message.editorContext + let insertValue = "" + + if (editorContext.filePath) { + // Format: filename:startLine-endLine (selected text preview) + const fileName = editorContext.filePath.split("/").pop() || editorContext.filePath + let contextText = fileName + + if (editorContext.startLine !== undefined) { + if ( + editorContext.endLine !== undefined && + editorContext.endLine !== editorContext.startLine + ) { + contextText += `:${editorContext.startLine}-${editorContext.endLine}` + } else { + contextText += `:${editorContext.startLine}` + } + } + + if (editorContext.selectedText && editorContext.selectedText.trim()) { + const preview = editorContext.selectedText.trim().substring(0, 50) + contextText += ` (${preview}${editorContext.selectedText.length > 50 ? "..." : ""})` + } + + insertValue = contextText + } else { + insertValue = "current-editor" + } + + const { newValue, mentionIndex } = insertMention( + textAreaRef.current.value, + cursorPosition, + insertValue, + ) + + setInputValue(newValue) + const newCursorPosition = newValue.indexOf(" ", mentionIndex + insertValue.length) + 1 + setCursorPosition(newCursorPosition) + setIntendedCursorPosition(newCursorPosition) + + // Scroll to cursor + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.blur() + textAreaRef.current.focus() + } + }, 0) + } } } window.addEventListener("message", messageHandler) return () => window.removeEventListener("message", messageHandler) - }, [setInputValue, searchRequestId]) - - const [isDraggingOver, setIsDraggingOver] = useState(false) - const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined) - const [showContextMenu, setShowContextMenu] = useState(false) - const [cursorPosition, setCursorPosition] = useState(0) - const [searchQuery, setSearchQuery] = useState("") - const textAreaRef = useRef(null) - const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false) - const highlightLayerRef = useRef(null) - const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1) - const [selectedType, setSelectedType] = useState(null) - const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false) - const [intendedCursorPosition, setIntendedCursorPosition] = useState(null) - const contextMenuContainerRef = useRef(null) - const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false) - const [isFocused, setIsFocused] = useState(false) + }, [setInputValue, searchRequestId, cursorPosition]) // Use custom hook for prompt history navigation const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({ @@ -277,6 +327,12 @@ const ChatTextArea = forwardRef( insertValue = "problems" } else if (type === ContextMenuOptionType.Terminal) { insertValue = "terminal" + } else if (type === ContextMenuOptionType.EditorContext) { + // Request editor context from backend + vscode.postMessage({ type: "requestEditorContext" }) + setShowContextMenu(false) + setSelectedType(null) + return } else if (type === ContextMenuOptionType.Git) { insertValue = value || "" } diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 1672c35ee3..a9c2542baa 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -91,6 +91,8 @@ const ContextMenu: React.FC = ({ return Problems case ContextMenuOptionType.Terminal: return Terminal + case ContextMenuOptionType.EditorContext: + return Current Editor Context case ContextMenuOptionType.URL: return Paste URL to fetch contents case ContextMenuOptionType.NoResults: @@ -173,6 +175,8 @@ const ContextMenu: React.FC = ({ return "warning" case ContextMenuOptionType.Terminal: return "terminal" + case ContextMenuOptionType.EditorContext: + return "edit" case ContextMenuOptionType.URL: return "link" case ContextMenuOptionType.Git: diff --git a/webview-ui/src/utils/__tests__/context-mentions.spec.ts b/webview-ui/src/utils/__tests__/context-mentions.spec.ts index 50fb1b1c50..59319a821d 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.spec.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.spec.ts @@ -196,8 +196,9 @@ describe("getContextMenuOptions", () => { it("should return all option types for empty query", () => { const result = getContextMenuOptions("", "", null, []) - expect(result).toHaveLength(6) + expect(result).toHaveLength(7) expect(result.map((item) => item.type)).toEqual([ + ContextMenuOptionType.EditorContext, ContextMenuOptionType.Problems, ContextMenuOptionType.Terminal, ContextMenuOptionType.URL, diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 889dca9dbe..951307a0a0 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -105,6 +105,7 @@ export enum ContextMenuOptionType { Git = "git", NoResults = "noResults", Mode = "mode", // Add mode type + EditorContext = "editorContext", // Add editor context type } export interface ContextMenuQueryItem { @@ -192,6 +193,7 @@ export function getContextMenuOptions( } return [ + { type: ContextMenuOptionType.EditorContext }, { type: ContextMenuOptionType.Problems }, { type: ContextMenuOptionType.Terminal }, { type: ContextMenuOptionType.URL },