mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add automatic selection context to user messages
- Add selection context fields to WebviewMessage and ExtensionMessage types - Implement selection capture in ChatView on component mount - Add backend message handler to capture editor selection with line numbers - Store selection context in Task instance for use in environment details - Include selection context at start of environment details sent to LLM - Automatically clear selection context after use to prevent persistence - Add comprehensive tests for selection context functionality - Convert VSCode 0-based line numbers to 1-based for user-friendly display - Handle both workspace-relative and absolute file paths This allows users to select text in the editor and ask questions like "fix this" or "what does this do" without explicitly pasting code. The selection is automatically included as hidden context for the LLM.
This commit is contained in:
parent
0fdbd392e8
commit
bb8d44d26f
7 changed files with 188 additions and 6 deletions
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -155,6 +155,13 @@ export class Task extends EventEmitter<TaskEvents> 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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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?: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [sendingDisabled, setSendingDisabled] = useState(false)
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([])
|
||||
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<ChatViewRef, ChatViewPro
|
|||
userRespondedRef.current = true
|
||||
|
||||
if (messagesRef.current.length === 0) {
|
||||
vscode.postMessage({ type: "newTask", text, images })
|
||||
vscode.postMessage({
|
||||
type: "newTask",
|
||||
text,
|
||||
images,
|
||||
...(selectionContext || {}),
|
||||
})
|
||||
} else if (clineAskRef.current) {
|
||||
if (clineAskRef.current === "followup") {
|
||||
markFollowUpAsAnswered()
|
||||
|
|
@ -588,19 +599,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
askResponse: "messageResponse",
|
||||
text,
|
||||
images,
|
||||
...(selectionContext || {}),
|
||||
})
|
||||
break
|
||||
// There is no other case that a textfield should be enabled.
|
||||
}
|
||||
} else {
|
||||
// This is a new message in an ongoing task.
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
text,
|
||||
images,
|
||||
...(selectionContext || {}),
|
||||
})
|
||||
}
|
||||
|
||||
handleChatReset()
|
||||
}
|
||||
},
|
||||
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length], // messagesRef and clineAskRef are stable
|
||||
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length, selectionContext], // messagesRef and clineAskRef are stable
|
||||
)
|
||||
|
||||
const handleSetChatBoxMessage = useCallback(
|
||||
|
|
@ -743,6 +761,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
break
|
||||
}
|
||||
break
|
||||
case "selectionContext":
|
||||
// Update selection context when received from extension
|
||||
setSelectionContext({
|
||||
selectedText: message.selectedText,
|
||||
selectionFilePath: message.selectionFilePath,
|
||||
selectionStartLine: message.selectionStartLine,
|
||||
selectionEndLine: message.selectionEndLine,
|
||||
})
|
||||
break
|
||||
case "selectedImages":
|
||||
// Only handle selectedImages if it's not for editing context
|
||||
// When context is "edit", ChatRow will handle the images
|
||||
|
|
@ -809,7 +836,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
useEvent("message", handleMessage)
|
||||
|
||||
// NOTE: the VSCode window needs to be focused for this to work.
|
||||
useMount(() => 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue