diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 2efb2cbdff..82a1b39121 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2190,6 +2190,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 98f3aa7d29..219fac2fa6 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -105,10 +105,18 @@ export interface ExtensionMessage { | "shareTaskSuccess" | "codeIndexSettingsSaved" | "codeIndexSecretStatus" + | "editorContext" | "showDeleteMessageDialog" | "showEditMessageDialog" 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 5d6ec0f41c..9f1d20404d 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -196,6 +196,8 @@ export interface WebviewMessage { | "checkRulesDirectoryResult" | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" + | "requestEditorContext" + | "editorContext" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" @@ -257,6 +259,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 6c541353eb..3010be2e4d 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -102,6 +102,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(() => { @@ -158,28 +173,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({ @@ -300,6 +350,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 },