Skip to content

Commit ab7ccf1

Browse files
committed
feat: add integrated web preview with element selection (fixes #5971)
- Add WebPreviewProvider to manage preview panel in sidebar - Create React-based UI with URL navigation and viewport controls - Implement element selection with context extraction (HTML, CSS, XPath) - Add web_preview tool for AI integration - Register commands and update package.json - Update type definitions and tool configurations This implementation provides: - Web preview panel in VSCode sidebar - Click-to-select elements with visual highlighting - Element context extraction for AI assistance - Responsive viewport controls - Integration with Roo Code AI through web_preview tool
1 parent de13d8a commit ab7ccf1

File tree

16 files changed

+1026
-0
lines changed

16 files changed

+1026
-0
lines changed

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const toolNames = [
3434
"fetch_instructions",
3535
"codebase_search",
3636
"update_todo_list",
37+
"web_preview",
3738
] as const
3839

3940
export const toolNamesSchema = z.enum(toolNames)

packages/types/src/vscode.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const commandIds = [
5353
"focusInput",
5454
"acceptInput",
5555
"focusPanel",
56+
"openWebPreview",
57+
"getSelectedElement",
5658
] as const
5759

5860
export type CommandId = (typeof commandIds)[number]

src/activate/registerCommands.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { TelemetryService } from "@roo-code/telemetry"
77
import { Package } from "../shared/package"
88
import { getCommand } from "../utils/commands"
99
import { ClineProvider } from "../core/webview/ClineProvider"
10+
import { WebPreviewProvider } from "../core/webview/WebPreviewProvider"
1011
import { ContextProxy } from "../core/config/ContextProxy"
1112
import { focusPanel } from "../utils/focusPanel"
1213

@@ -32,6 +33,7 @@ export function getVisibleProviderOrLog(outputChannel: vscode.OutputChannel): Cl
3233
// Store panel references in both modes
3334
let sidebarPanel: vscode.WebviewView | undefined = undefined
3435
let tabPanel: vscode.WebviewPanel | undefined = undefined
36+
let webPreviewProvider: WebPreviewProvider | undefined = undefined
3537

3638
/**
3739
* Get the currently active panel
@@ -57,6 +59,10 @@ export function setPanel(
5759
}
5860
}
5961

62+
export function setWebPreviewProvider(provider: WebPreviewProvider) {
63+
webPreviewProvider = provider
64+
}
65+
6066
export type RegisterCommandOptions = {
6167
context: vscode.ExtensionContext
6268
outputChannel: vscode.OutputChannel
@@ -218,6 +224,27 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
218224

219225
visibleProvider.postMessageToWebview({ type: "acceptInput" })
220226
},
227+
openWebPreview: async (url?: string) => {
228+
if (!webPreviewProvider) {
229+
outputChannel.appendLine("Web Preview Provider not initialized")
230+
return
231+
}
232+
233+
if (url) {
234+
await webPreviewProvider.openUrl(url)
235+
} else {
236+
// Show the web preview panel without navigating
237+
await vscode.commands.executeCommand("roo-code.webPreview.focus")
238+
}
239+
},
240+
getSelectedElement: async () => {
241+
if (!webPreviewProvider) {
242+
outputChannel.appendLine("Web Preview Provider not initialized")
243+
return null
244+
}
245+
246+
return webPreviewProvider.getSelectedElementContext()
247+
},
221248
})
222249

223250
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { askFollowupQuestionTool } from "../tools/askFollowupQuestionTool"
2424
import { switchModeTool } from "../tools/switchModeTool"
2525
import { attemptCompletionTool } from "../tools/attemptCompletionTool"
2626
import { newTaskTool } from "../tools/newTaskTool"
27+
import { webPreviewTool } from "../tools/webPreviewTool"
2728

2829
import { checkpointSave } from "../checkpoints"
2930
import { updateTodoListTool } from "../tools/updateTodoListTool"
@@ -214,6 +215,8 @@ export async function presentAssistantMessage(cline: Task) {
214215
const modeName = getModeBySlug(mode, customModes)?.name ?? mode
215216
return `[${block.name} in ${modeName} mode: '${message}']`
216217
}
218+
case "web_preview":
219+
return `[${block.name} for '${block.params.action}'${block.params.url ? ` - ${block.params.url}` : ""}]`
217220
}
218221
}
219222

@@ -522,6 +525,9 @@ export async function presentAssistantMessage(cline: Task) {
522525
askFinishSubTaskApproval,
523526
)
524527
break
528+
case "web_preview":
529+
await webPreviewTool(cline, block, askApproval, handleError, pushToolResult)
530+
break
525531
}
526532

527533
break

src/core/tools/webPreviewTool.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as vscode from "vscode"
2+
import { Task } from "../task/Task"
3+
import { ClineSayTool } from "../../shared/ExtensionMessage"
4+
import { formatResponse } from "../prompts/responses"
5+
import { ToolUse, AskApproval, HandleError, PushToolResult } from "../../shared/tools"
6+
7+
export async function webPreviewTool(
8+
cline: Task,
9+
block: ToolUse,
10+
askApproval: AskApproval,
11+
handleError: HandleError,
12+
pushToolResult: PushToolResult,
13+
) {
14+
const action = block.params.action
15+
const url = block.params.url
16+
17+
if (!action) {
18+
cline.consecutiveMistakeCount++
19+
const errorMsg = await cline.sayAndCreateMissingParamError("web_preview", "action")
20+
pushToolResult(formatResponse.toolError(errorMsg))
21+
return
22+
}
23+
24+
if (action === "open" && !url) {
25+
cline.consecutiveMistakeCount++
26+
const errorMsg = await cline.sayAndCreateMissingParamError("web_preview", "url")
27+
pushToolResult(formatResponse.toolError(errorMsg))
28+
return
29+
}
30+
31+
try {
32+
// Handle partial message
33+
if (block.partial) {
34+
const partialMessage = JSON.stringify({
35+
tool: "web_preview",
36+
action,
37+
url,
38+
} satisfies ClineSayTool)
39+
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
40+
return
41+
}
42+
43+
// Ask for approval
44+
const completeMessage = JSON.stringify({
45+
tool: "web_preview",
46+
action,
47+
url,
48+
} satisfies ClineSayTool)
49+
50+
const { response, text, images } = await cline.ask("tool", completeMessage, false)
51+
52+
if (response !== "yesButtonClicked") {
53+
if (text) {
54+
await cline.say("user_feedback", text, images)
55+
}
56+
cline.didRejectTool = true
57+
pushToolResult(formatResponse.toolDenied())
58+
return
59+
}
60+
61+
if (text) {
62+
await cline.say("user_feedback", text, images)
63+
}
64+
65+
// Execute the web preview action
66+
if (action === "open") {
67+
// Open the web preview panel with the URL
68+
await vscode.commands.executeCommand("roo-code.openWebPreview", url)
69+
pushToolResult(formatResponse.toolResult(`Opened web preview for: ${url}`))
70+
} else if (action === "select") {
71+
// Get the current selected element context from the preview
72+
const selectedContext = await vscode.commands.executeCommand("roo-code.getSelectedElement")
73+
if (selectedContext) {
74+
pushToolResult(
75+
formatResponse.toolResult(`Selected element context:\n${JSON.stringify(selectedContext, null, 2)}`),
76+
)
77+
} else {
78+
pushToolResult(
79+
formatResponse.toolResult(
80+
"No element is currently selected. Click on an element in the preview to select it.",
81+
),
82+
)
83+
}
84+
} else {
85+
pushToolResult(formatResponse.toolError(`Unknown action: ${action}. Valid actions are: open, select`))
86+
}
87+
} catch (error) {
88+
await handleError("web preview operation", error instanceof Error ? error : new Error(String(error)))
89+
pushToolResult(formatResponse.toolError(error instanceof Error ? error.message : String(error)))
90+
}
91+
}

0 commit comments

Comments
 (0)