Просмотр исходного кода

refactor: remove browser use functionality entirely (#11392)

Hannes Rudolph 1 день назад
Родитель
Сommit
fa9dff4a06
100 измененных файлов с 498 добавлено и 7316 удалено
  1. 0 8
      apps/cli/src/agent/__tests__/extension-client.test.ts
  2. 1 4
      apps/cli/src/agent/agent-state.ts
  3. 1 30
      apps/cli/src/agent/ask-dispatcher.ts
  4. 0 2
      apps/cli/src/agent/extension-host.ts
  5. 0 18
      apps/cli/src/agent/json-event-emitter.ts
  6. 1 4
      apps/cli/src/ui/components/ChatHistoryItem.tsx
  7. 0 87
      apps/cli/src/ui/components/tools/BrowserTool.tsx
  8. 0 3
      apps/cli/src/ui/components/tools/index.ts
  9. 1 11
      apps/cli/src/ui/components/tools/types.ts
  10. 0 8
      apps/cli/src/ui/components/tools/utils.ts
  11. 0 8
      apps/cli/src/ui/types.ts
  12. 0 23
      apps/cli/src/ui/utils/tools.ts
  13. 4 4
      packages/evals/src/db/queries/__tests__/copyRun.spec.ts
  14. 2 2
      packages/types/src/__tests__/cloud.test.ts
  15. 0 14
      packages/types/src/global-settings.ts
  16. 1 14
      packages/types/src/message.ts
  17. 38 6
      packages/types/src/mode.ts
  18. 0 9
      packages/types/src/tool-params.ts
  19. 8 2
      packages/types/src/tool.ts
  20. 0 57
      packages/types/src/vscode-extension-host.ts
  21. 18 402
      pnpm-lock.yaml
  22. 0 15
      src/__tests__/command-mentions.spec.ts
  23. 0 26
      src/core/assistant-message/NativeToolCallParser.ts
  24. 2 2
      src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts
  25. 0 3
      src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts
  26. 0 3
      src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts
  27. 0 3
      src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts
  28. 0 40
      src/core/assistant-message/presentAssistantMessage.ts
  29. 1 6
      src/core/auto-approval/index.ts
  30. 8 27
      src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts
  31. 38 1
      src/core/config/__tests__/CustomModesSettings.spec.ts
  32. 47 9
      src/core/config/__tests__/ModeConfig.spec.ts
  33. 0 18
      src/core/environment/__tests__/getEnvironmentDetails.spec.ts
  34. 0 29
      src/core/environment/getEnvironmentDetails.ts
  35. 5 134
      src/core/mentions/__tests__/index.spec.ts
  36. 155 11
      src/core/mentions/__tests__/processUserContentMentions.spec.ts
  37. 2 84
      src/core/mentions/index.ts
  38. 104 8
      src/core/mentions/processUserContentMentions.ts
  39. 0 3
      src/core/prompts/__tests__/add-custom-instructions.spec.ts
  40. 0 51
      src/core/prompts/__tests__/system-prompt.spec.ts
  41. 0 3
      src/core/prompts/system.ts
  42. 1 6
      src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts
  43. 0 10
      src/core/prompts/tools/filter-tools-for-mode.ts
  44. 0 76
      src/core/prompts/tools/native-tools/browser_action.ts
  45. 0 2
      src/core/prompts/tools/native-tools/index.ts
  46. 0 1
      src/core/prompts/types.ts
  47. 17 4
      src/core/task-persistence/rooMessage.ts
  48. 4 123
      src/core/task/Task.ts
  49. 0 2
      src/core/task/__tests__/Task.dispose.test.ts
  50. 0 1
      src/core/task/__tests__/Task.spec.ts
  51. 0 2
      src/core/task/__tests__/Task.throttle.test.ts
  52. 2 2
      src/core/task/__tests__/native-tools-filtering.spec.ts
  53. 0 3
      src/core/task/build-tools.ts
  54. 0 280
      src/core/tools/BrowserActionTool.ts
  55. 0 22
      src/core/tools/ToolRepetitionDetector.ts
  56. 0 84
      src/core/tools/__tests__/BrowserActionTool.coordinateScaling.spec.ts
  57. 0 25
      src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts
  58. 0 160
      src/core/tools/__tests__/ToolRepetitionDetector.spec.ts
  59. 7 11
      src/core/tools/__tests__/validateToolUse.spec.ts
  60. 0 310
      src/core/webview/BrowserSessionPanelManager.ts
  61. 0 27
      src/core/webview/ClineProvider.ts
  62. 2 2
      src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts
  63. 6 176
      src/core/webview/__tests__/ClineProvider.spec.ts
  64. 2 2
      src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
  65. 2 2
      src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts
  66. 0 12
      src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts
  67. 0 79
      src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts
  68. 6 22
      src/core/webview/generateSystemPrompt.ts
  69. 0 102
      src/core/webview/webviewMessageHandler.ts
  70. 0 2
      src/package.json
  71. 0 913
      src/services/browser/BrowserSession.ts
  72. 0 143
      src/services/browser/UrlContentFetcher.ts
  73. 0 628
      src/services/browser/__tests__/BrowserSession.spec.ts
  74. 0 369
      src/services/browser/__tests__/UrlContentFetcher.spec.ts
  75. 0 181
      src/services/browser/browserDiscovery.ts
  76. 7 13
      src/shared/__tests__/modes.spec.ts
  77. 0 95
      src/shared/browserUtils.ts
  78. 1 18
      src/shared/tools.ts
  79. 0 12
      webview-ui/browser-panel.html
  80. 0 12
      webview-ui/src/browser-panel.tsx
  81. 0 61
      webview-ui/src/components/browser-session/BrowserPanelStateProvider.tsx
  82. 0 106
      webview-ui/src/components/browser-session/BrowserSessionPanel.tsx
  83. 0 5
      webview-ui/src/components/chat/AutoApproveDropdown.tsx
  84. 0 195
      webview-ui/src/components/chat/BrowserActionRow.tsx
  85. 0 1137
      webview-ui/src/components/chat/BrowserSessionRow.tsx
  86. 0 34
      webview-ui/src/components/chat/BrowserSessionStatusRow.tsx
  87. 0 4
      webview-ui/src/components/chat/ChatRow.tsx
  88. 0 11
      webview-ui/src/components/chat/ChatTextArea.tsx
  89. 2 82
      webview-ui/src/components/chat/ChatView.tsx
  90. 2 55
      webview-ui/src/components/chat/TaskHeader.tsx
  91. 0 55
      webview-ui/src/components/chat/__tests__/BrowserSessionRow.aspect-ratio.spec.tsx
  92. 0 42
      webview-ui/src/components/chat/__tests__/BrowserSessionRow.disconnect-button.spec.tsx
  93. 0 126
      webview-ui/src/components/chat/__tests__/BrowserSessionRow.spec.tsx
  94. 0 4
      webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx
  95. 0 6
      webview-ui/src/components/chat/__tests__/ChatView.notification-sound.spec.tsx
  96. 0 6
      webview-ui/src/components/chat/__tests__/ChatView.preserve-images.spec.tsx
  97. 0 6
      webview-ui/src/components/chat/__tests__/ChatView.spec.tsx
  98. 0 4
      webview-ui/src/components/settings/AutoApproveSettings.tsx
  99. 0 8
      webview-ui/src/components/settings/AutoApproveToggle.tsx
  100. 0 243
      webview-ui/src/components/settings/BrowserSettings.tsx

+ 0 - 8
apps/cli/src/agent/__tests__/extension-client.test.ts

@@ -93,13 +93,6 @@ describe("detectAgentState", () => {
 			expect(state.requiredAction).toBe("answer")
 		})
 
-		it("should detect waiting for browser_action_launch approval", () => {
-			const messages = [createMessage({ type: "ask", ask: "browser_action_launch", partial: false })]
-			const state = detectAgentState(messages)
-			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
-			expect(state.requiredAction).toBe("approve")
-		})
-
 		it("should detect waiting for use_mcp_server approval", () => {
 			const messages = [createMessage({ type: "ask", ask: "use_mcp_server", partial: false })]
 			const state = detectAgentState(messages)
@@ -202,7 +195,6 @@ describe("Type Guards", () => {
 			expect(isInteractiveAsk("tool")).toBe(true)
 			expect(isInteractiveAsk("command")).toBe(true)
 			expect(isInteractiveAsk("followup")).toBe(true)
-			expect(isInteractiveAsk("browser_action_launch")).toBe(true)
 			expect(isInteractiveAsk("use_mcp_server")).toBe(true)
 		})
 

+ 1 - 4
apps/cli/src/agent/agent-state.ts

@@ -116,7 +116,7 @@ export enum AgentLoopState {
  */
 export type RequiredAction =
 	| "none" // No action needed (running/streaming)
-	| "approve" // Can approve/reject (tool, command, browser, mcp)
+	| "approve" // Can approve/reject (tool, command, mcp)
 	| "answer" // Need to answer a question (followup)
 	| "retry_or_new_task" // Can retry or start new task (api_req_failed)
 	| "proceed_or_new_task" // Can proceed or start new task (mistake_limit)
@@ -221,7 +221,6 @@ function getRequiredAction(ask: ClineAsk): RequiredAction {
 			return "answer"
 		case "command":
 		case "tool":
-		case "browser_action_launch":
 		case "use_mcp_server":
 			return "approve"
 		case "command_output":
@@ -264,8 +263,6 @@ function getStateDescription(state: AgentLoopState, ask?: ClineAsk): string {
 					return "Agent wants to execute a command. Approve or reject."
 				case "tool":
 					return "Agent wants to perform a file operation. Approve or reject."
-				case "browser_action_launch":
-					return "Agent wants to use the browser. Approve or reject."
 				case "use_mcp_server":
 					return "Agent wants to use an MCP server. Approve or reject."
 				default:

+ 1 - 30
apps/cli/src/agent/ask-dispatcher.ts

@@ -244,7 +244,7 @@ export class AskDispatcher {
 	}
 
 	/**
-	 * Handle interactive asks (followup, command, tool, browser_action_launch, use_mcp_server).
+	 * Handle interactive asks (followup, command, tool, use_mcp_server).
 	 * These require user approval or input.
 	 */
 	private async handleInteractiveAsk(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
@@ -258,9 +258,6 @@ export class AskDispatcher {
 			case "tool":
 				return await this.handleToolApproval(ts, text)
 
-			case "browser_action_launch":
-				return await this.handleBrowserApproval(ts, text)
-
 			case "use_mcp_server":
 				return await this.handleMcpApproval(ts, text)
 
@@ -444,32 +441,6 @@ export class AskDispatcher {
 		}
 	}
 
-	/**
-	 * Handle browser action approval.
-	 */
-	private async handleBrowserApproval(ts: number, text: string): Promise<AskHandleResult> {
-		this.outputManager.output("\n[browser action request]")
-		if (text) {
-			this.outputManager.output(`  Action: ${text}`)
-		}
-		this.outputManager.markDisplayed(ts, text || "", false)
-
-		if (this.nonInteractive) {
-			// Auto-approved by extension settings
-			return { handled: true }
-		}
-
-		try {
-			const approved = await this.promptManager.promptForYesNo("Allow browser action? (y/n): ")
-			this.sendApprovalResponse(approved)
-			return { handled: true, response: approved ? "yesButtonClicked" : "noButtonClicked" }
-		} catch {
-			this.outputManager.output("[Defaulting to: no]")
-			this.sendApprovalResponse(false)
-			return { handled: true, response: "noButtonClicked" }
-		}
-	}
-
 	/**
 	 * Handle MCP server access approval.
 	 */

+ 0 - 2
apps/cli/src/agent/extension-host.ts

@@ -214,7 +214,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
 		const baseSettings: RooCodeSettings = {
 			mode: this.options.mode,
 			commandExecutionTimeout: 30,
-			browserToolEnabled: false,
 			enableCheckpoints: false,
 			...getProviderSettings(this.options.provider, this.options.apiKey, this.options.model),
 		}
@@ -227,7 +226,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
 					alwaysAllowWrite: true,
 					alwaysAllowWriteOutsideWorkspace: true,
 					alwaysAllowWriteProtected: true,
-					alwaysAllowBrowser: true,
 					alwaysAllowMcp: true,
 					alwaysAllowModeSwitch: true,
 					alwaysAllowSubtasks: true,

+ 0 - 18
apps/cli/src/agent/json-event-emitter.ts

@@ -258,15 +258,6 @@ export class JsonEventEmitter {
 				break
 			}
 
-			case "browser_action":
-			case "browser_action_result":
-				this.emitEvent({
-					type: "tool_result",
-					subtype: "browser",
-					tool_result: { name: "browser_action", output: msg.text },
-				})
-				break
-
 			case "mcp_server_response":
 				this.emitEvent({
 					type: "tool_result",
@@ -336,15 +327,6 @@ export class JsonEventEmitter {
 				})
 				break
 
-			case "browser_action_launch":
-				this.emitEvent({
-					type: "tool_use",
-					id: msg.ts,
-					subtype: "browser",
-					tool_use: { name: "browser_action", input: { raw: msg.text } },
-				})
-				break
-
 			case "use_mcp_server":
 				this.emitEvent({
 					type: "tool_use",

+ 1 - 4
apps/cli/src/ui/components/ChatHistoryItem.tsx

@@ -10,14 +10,13 @@ import { getToolRenderer } from "./tools/index.js"
 /**
  * Tool categories for styling
  */
-type ToolCategory = "file" | "directory" | "search" | "command" | "browser" | "mode" | "completion" | "other"
+type ToolCategory = "file" | "directory" | "search" | "command" | "mode" | "completion" | "other"
 
 function getToolCategory(toolName: string): ToolCategory {
 	const fileTools = ["readFile", "read_file", "writeToFile", "write_to_file", "applyDiff", "apply_diff"]
 	const dirTools = ["listFiles", "list_files", "listFilesRecursive", "listFilesTopLevel"]
 	const searchTools = ["searchFiles", "search_files"]
 	const commandTools = ["executeCommand", "execute_command"]
-	const browserTools = ["browserAction", "browser_action"]
 	const modeTools = ["switchMode", "switch_mode", "newTask", "new_task"]
 	const completionTools = ["attemptCompletion", "attempt_completion", "askFollowupQuestion", "ask_followup_question"]
 
@@ -25,7 +24,6 @@ function getToolCategory(toolName: string): ToolCategory {
 	if (dirTools.includes(toolName)) return "directory"
 	if (searchTools.includes(toolName)) return "search"
 	if (commandTools.includes(toolName)) return "command"
-	if (browserTools.includes(toolName)) return "browser"
 	if (modeTools.includes(toolName)) return "mode"
 	if (completionTools.includes(toolName)) return "completion"
 	return "other"
@@ -39,7 +37,6 @@ const CATEGORY_COLORS: Record<ToolCategory, string> = {
 	directory: theme.toolHeader,
 	search: theme.warningColor,
 	command: theme.successColor,
-	browser: theme.focusColor,
 	mode: theme.userHeader,
 	completion: theme.successColor,
 	other: theme.toolHeader,

+ 0 - 87
apps/cli/src/ui/components/tools/BrowserTool.tsx

@@ -1,87 +0,0 @@
-import { Box, Text } from "ink"
-
-import * as theme from "../../theme.js"
-import { Icon } from "../Icon.js"
-
-import type { ToolRendererProps } from "./types.js"
-import { getToolDisplayName, getToolIconName } from "./utils.js"
-
-const ACTION_LABELS: Record<string, string> = {
-	launch: "Launch Browser",
-	click: "Click",
-	hover: "Hover",
-	type: "Type Text",
-	press: "Press Key",
-	scroll_down: "Scroll Down",
-	scroll_up: "Scroll Up",
-	resize: "Resize Window",
-	close: "Close Browser",
-	screenshot: "Take Screenshot",
-}
-
-export function BrowserTool({ toolData }: ToolRendererProps) {
-	const iconName = getToolIconName(toolData.tool)
-	const displayName = getToolDisplayName(toolData.tool)
-	const action = toolData.action || ""
-	const url = toolData.url || ""
-	const coordinate = toolData.coordinate || ""
-	const content = toolData.content || "" // May contain text for type action.
-
-	const actionLabel = ACTION_LABELS[action] || action
-
-	return (
-		<Box flexDirection="column" paddingX={1}>
-			{/* Header */}
-			<Box>
-				<Icon name={iconName} color={theme.toolHeader} />
-				<Text bold color={theme.toolHeader}>
-					{" "}
-					{displayName}
-				</Text>
-				{action && (
-					<Text color={theme.focusColor} bold>
-						{" "}
-						→ {actionLabel}
-					</Text>
-				)}
-			</Box>
-
-			{/* Action details */}
-			<Box flexDirection="column" marginLeft={2}>
-				{/* URL for launch action */}
-				{url && (
-					<Box>
-						<Text color={theme.dimText}>url: </Text>
-						<Text color={theme.text} underline>
-							{url}
-						</Text>
-					</Box>
-				)}
-
-				{/* Coordinates for click/hover actions */}
-				{coordinate && (
-					<Box>
-						<Text color={theme.dimText}>at: </Text>
-						<Text color={theme.warningColor}>{coordinate}</Text>
-					</Box>
-				)}
-
-				{/* Text content for type action */}
-				{content && action === "type" && (
-					<Box>
-						<Text color={theme.dimText}>text: </Text>
-						<Text color={theme.text}>"{content}"</Text>
-					</Box>
-				)}
-
-				{/* Key for press action */}
-				{content && action === "press" && (
-					<Box>
-						<Text color={theme.dimText}>key: </Text>
-						<Text color={theme.successColor}>{content}</Text>
-					</Box>
-				)}
-			</Box>
-		</Box>
-	)
-}

+ 0 - 3
apps/cli/src/ui/components/tools/index.ts

@@ -15,7 +15,6 @@ import { FileReadTool } from "./FileReadTool.js"
 import { FileWriteTool } from "./FileWriteTool.js"
 import { SearchTool } from "./SearchTool.js"
 import { CommandTool } from "./CommandTool.js"
-import { BrowserTool } from "./BrowserTool.js"
 import { ModeTool } from "./ModeTool.js"
 import { CompletionTool } from "./CompletionTool.js"
 import { GenericTool } from "./GenericTool.js"
@@ -32,7 +31,6 @@ export { FileReadTool } from "./FileReadTool.js"
 export { FileWriteTool } from "./FileWriteTool.js"
 export { SearchTool } from "./SearchTool.js"
 export { CommandTool } from "./CommandTool.js"
-export { BrowserTool } from "./BrowserTool.js"
 export { ModeTool } from "./ModeTool.js"
 export { CompletionTool } from "./CompletionTool.js"
 export { GenericTool } from "./GenericTool.js"
@@ -45,7 +43,6 @@ const CATEGORY_RENDERERS: Record<string, React.FC<ToolRendererProps>> = {
 	"file-write": FileWriteTool,
 	search: SearchTool,
 	command: CommandTool,
-	browser: BrowserTool,
 	mode: ModeTool,
 	completion: CompletionTool,
 	other: GenericTool,

+ 1 - 11
apps/cli/src/ui/components/tools/types.ts

@@ -5,15 +5,7 @@ export interface ToolRendererProps {
 	rawContent?: string
 }
 
-export type ToolCategory =
-	| "file-read"
-	| "file-write"
-	| "search"
-	| "command"
-	| "browser"
-	| "mode"
-	| "completion"
-	| "other"
+export type ToolCategory = "file-read" | "file-write" | "search" | "command" | "mode" | "completion" | "other"
 
 export function getToolCategory(toolName: string): ToolCategory {
 	const fileReadTools = ["readFile", "read_file", "skill", "listFilesTopLevel", "listFilesRecursive", "list_files"]
@@ -29,7 +21,6 @@ export function getToolCategory(toolName: string): ToolCategory {
 
 	const searchTools = ["searchFiles", "search_files", "codebaseSearch", "codebase_search"]
 	const commandTools = ["execute_command", "executeCommand"]
-	const browserTools = ["browser_action", "browserAction"]
 	const modeTools = ["switchMode", "switch_mode", "newTask", "new_task", "finishTask"]
 	const completionTools = ["attempt_completion", "attemptCompletion", "ask_followup_question", "askFollowupQuestion"]
 
@@ -37,7 +28,6 @@ export function getToolCategory(toolName: string): ToolCategory {
 	if (fileWriteTools.includes(toolName)) return "file-write"
 	if (searchTools.includes(toolName)) return "search"
 	if (commandTools.includes(toolName)) return "command"
-	if (browserTools.includes(toolName)) return "browser"
 	if (modeTools.includes(toolName)) return "mode"
 	if (completionTools.includes(toolName)) return "completion"
 	return "other"

+ 0 - 8
apps/cli/src/ui/components/tools/utils.ts

@@ -73,10 +73,6 @@ export function getToolDisplayName(toolName: string): string {
 		execute_command: "Execute Command",
 		executeCommand: "Execute Command",
 
-		// Browser operations
-		browser_action: "Browser Action",
-		browserAction: "Browser Action",
-
 		// Mode operations
 		switchMode: "Switch Mode",
 		switch_mode: "Switch Mode",
@@ -129,10 +125,6 @@ export function getToolIconName(toolName: string): IconName {
 		execute_command: "terminal",
 		executeCommand: "terminal",
 
-		// Browser operations
-		browser_action: "browser",
-		browserAction: "browser",
-
 		// Mode operations
 		switchMode: "switch",
 		switch_mode: "switch",

+ 0 - 8
apps/cli/src/ui/types.ts

@@ -40,14 +40,6 @@ export interface ToolData {
 	/** Command output */
 	output?: string
 
-	// Browser operation fields
-	/** Browser action type */
-	action?: string
-	/** Browser URL */
-	url?: string
-	/** Click/hover coordinates */
-	coordinate?: string
-
 	// Batch operation fields
 	/** Batch file reads */
 	batchFiles?: Array<{

+ 0 - 23
apps/cli/src/ui/utils/tools.ts

@@ -57,17 +57,6 @@ export function extractToolData(toolInfo: Record<string, unknown>): ToolData {
 		toolData.output = toolInfo.output as string
 	}
 
-	// Extract browser-related fields
-	if (toolInfo.action !== undefined) {
-		toolData.action = toolInfo.action as string
-	}
-	if (toolInfo.url !== undefined) {
-		toolData.url = toolInfo.url as string
-	}
-	if (toolInfo.coordinate !== undefined) {
-		toolData.coordinate = toolInfo.coordinate as string
-	}
-
 	// Extract batch file operations
 	if (Array.isArray(toolInfo.files)) {
 		toolData.batchFiles = (toolInfo.files as Array<Record<string, unknown>>).map((f) => ({
@@ -165,12 +154,6 @@ export function formatToolOutput(toolInfo: Record<string, unknown>): string {
 			return `📁 ${listPath || "."}${recursive ? " (recursive)" : ""}`
 		}
 
-		case "browser_action": {
-			const action = toolInfo.action as string
-			const url = toolInfo.url as string
-			return `🌐 ${action || "action"}${url ? `: ${url}` : ""}`
-		}
-
 		case "attempt_completion": {
 			const result = toolInfo.result as string
 			if (result) {
@@ -248,12 +231,6 @@ export function formatToolAskMessage(toolInfo: Record<string, unknown>): string
 			return `Apply changes to: ${diffPath || "(no path)"}`
 		}
 
-		case "browser_action": {
-			const action = toolInfo.action as string
-			const url = toolInfo.url as string
-			return `Browser: ${action || "action"}${url ? ` - ${url}` : ""}`
-		}
-
 		default: {
 			const params = Object.entries(toolInfo)
 				.filter(([key]) => key !== "tool")

+ 4 - 4
packages/evals/src/db/queries/__tests__/copyRun.spec.ts

@@ -138,8 +138,8 @@ describe("copyRun", () => {
 		const toolError3 = await createToolError({
 			runId: sourceRunId,
 			taskId: null,
-			toolName: "browser_action",
-			error: "Browser connection timeout",
+			toolName: "write_to_file",
+			error: "Write timeout",
 		})
 
 		sourceToolErrorIds.push(toolError3.id)
@@ -234,8 +234,8 @@ describe("copyRun", () => {
 		expect(taskToolErrors).toHaveLength(2)
 		expect(runToolErrors).toHaveLength(1)
 
-		const browserError = runToolErrors.find((te) => te.toolName === "browser_action")!
-		expect(browserError.error).toBe("Browser connection timeout")
+		const writeError = runToolErrors.find((te) => te.toolName === "write_to_file")!
+		expect(writeError.error).toBe("Write timeout")
 
 		await db.delete(schema.toolErrors).where(eq(schema.toolErrors.runId, newRunId))
 		await db.delete(schema.tasks).where(eq(schema.tasks.runId, newRunId))

+ 2 - 2
packages/types/src/__tests__/cloud.test.ts

@@ -487,11 +487,11 @@ describe("userSettingsConfigSchema with llmEnhancedFeaturesEnabled", () => {
 describe("organizationDefaultSettingsSchema with disabledTools", () => {
 	it("should accept disabledTools as an array of valid tool names", () => {
 		const input: OrganizationDefaultSettings = {
-			disabledTools: ["execute_command", "browser_action"],
+			disabledTools: ["execute_command", "write_to_file"],
 		}
 		const result = organizationDefaultSettingsSchema.safeParse(input)
 		expect(result.success).toBe(true)
-		expect(result.data?.disabledTools).toEqual(["execute_command", "browser_action"])
+		expect(result.data?.disabledTools).toEqual(["execute_command", "write_to_file"])
 	})
 
 	it("should accept empty disabledTools array", () => {

+ 0 - 14
packages/types/src/global-settings.ts

@@ -102,7 +102,6 @@ export const globalSettingsSchema = z.object({
 	alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
 	alwaysAllowWriteProtected: z.boolean().optional(),
 	writeDelayMs: z.number().min(0).optional(),
-	alwaysAllowBrowser: z.boolean().optional(),
 	requestDelaySeconds: z.number().optional(),
 	alwaysAllowMcp: z.boolean().optional(),
 	alwaysAllowModeSwitch: z.boolean().optional(),
@@ -148,13 +147,6 @@ export const globalSettingsSchema = z.object({
 	 */
 	maxDiagnosticMessages: z.number().optional(),
 
-	browserToolEnabled: z.boolean().optional(),
-	browserViewportSize: z.string().optional(),
-	screenshotQuality: z.number().optional(),
-	remoteBrowserEnabled: z.boolean().optional(),
-	remoteBrowserHost: z.string().optional(),
-	cachedChromeHostUrl: z.string().optional(),
-
 	enableCheckpoints: z.boolean().optional(),
 	checkpointTimeout: z
 		.number()
@@ -338,7 +330,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
 	alwaysAllowWriteOutsideWorkspace: false,
 	alwaysAllowWriteProtected: false,
 	writeDelayMs: 1000,
-	alwaysAllowBrowser: true,
 	requestDelaySeconds: 10,
 	alwaysAllowMcp: true,
 	alwaysAllowModeSwitch: true,
@@ -351,11 +342,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
 	commandTimeoutAllowlist: [],
 	preventCompletionWithOpenTodos: false,
 
-	browserToolEnabled: false,
-	browserViewportSize: "900x600",
-	screenshotQuality: 75,
-	remoteBrowserEnabled: false,
-
 	ttsEnabled: false,
 	ttsSpeed: 1,
 	soundEnabled: false,

+ 1 - 14
packages/types/src/message.ts

@@ -21,7 +21,6 @@ import { z } from "zod"
  * - `resume_task`: Confirmation needed to resume a previously paused task
  * - `resume_completed_task`: Confirmation needed to resume a task that was already marked as completed
  * - `mistake_limit_reached`: Too many errors encountered, needs user guidance on how to proceed
- * - `browser_action_launch`: Permission to open or interact with a browser
  * - `use_mcp_server`: Permission to use Model Context Protocol (MCP) server functionality
  * - `auto_approval_max_req_reached`: Auto-approval limit has been reached, manual approval required
  */
@@ -35,7 +34,6 @@ export const clineAsks = [
 	"resume_task",
 	"resume_completed_task",
 	"mistake_limit_reached",
-	"browser_action_launch",
 	"use_mcp_server",
 	"auto_approval_max_req_reached",
 ] as const
@@ -83,13 +81,7 @@ export function isResumableAsk(ask: ClineAsk): ask is ResumableAsk {
  * Asks that put the task into an "user interaction required" state.
  */
 
-export const interactiveAsks = [
-	"followup",
-	"command",
-	"tool",
-	"browser_action_launch",
-	"use_mcp_server",
-] as const satisfies readonly ClineAsk[]
+export const interactiveAsks = ["followup", "command", "tool", "use_mcp_server"] as const satisfies readonly ClineAsk[]
 
 export type InteractiveAsk = (typeof interactiveAsks)[number]
 
@@ -138,8 +130,6 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
  * - `user_feedback_diff`: Diff-formatted feedback from user showing requested changes
  * - `command_output`: Output from an executed command
  * - `shell_integration_warning`: Warning about shell integration issues or limitations
- * - `browser_action`: Action performed in the browser
- * - `browser_action_result`: Result of a browser action
  * - `mcp_server_request_started`: MCP server request has been initiated
  * - `mcp_server_response`: Response received from MCP server
  * - `subtask_result`: Result of a completed subtask
@@ -167,9 +157,6 @@ export const clineSays = [
 	"user_feedback_diff",
 	"command_output",
 	"shell_integration_warning",
-	"browser_action",
-	"browser_action_result",
-	"browser_session_status",
 	"mcp_server_request_started",
 	"mcp_server_response",
 	"subtask_result",

+ 38 - 6
packages/types/src/mode.ts

@@ -1,6 +1,6 @@
 import { z } from "zod"
 
-import { toolGroupsSchema } from "./tool.js"
+import { deprecatedToolGroups, toolGroupsSchema } from "./tool.js"
 
 /**
  * GroupOptions
@@ -42,7 +42,24 @@ export type GroupEntry = z.infer<typeof groupEntrySchema>
  * ModeConfig
  */
 
-const groupEntryArraySchema = z.array(groupEntrySchema).refine(
+/**
+ * Checks if a group entry references a deprecated tool group.
+ * Handles both string entries ("browser") and tuple entries (["browser", { ... }]).
+ */
+function isDeprecatedGroupEntry(entry: unknown): boolean {
+	if (typeof entry === "string") {
+		return deprecatedToolGroups.includes(entry)
+	}
+	if (Array.isArray(entry) && entry.length >= 1 && typeof entry[0] === "string") {
+		return deprecatedToolGroups.includes(entry[0])
+	}
+	return false
+}
+
+/**
+ * Raw schema for validating group entries after deprecated groups are stripped.
+ */
+const rawGroupEntryArraySchema = z.array(groupEntrySchema).refine(
 	(groups) => {
 		const seen = new Set()
 
@@ -61,6 +78,21 @@ const groupEntryArraySchema = z.array(groupEntrySchema).refine(
 	{ message: "Duplicate groups are not allowed" },
 )
 
+/**
+ * Schema for mode group entries. Preprocesses the input to strip deprecated
+ * tool groups (e.g., "browser") before validation, ensuring backward compatibility
+ * with older user configs.
+ *
+ * The type assertion to `z.ZodType<GroupEntry[], z.ZodTypeDef, GroupEntry[]>` is
+ * required because `z.preprocess` erases the input type to `unknown`, which
+ * propagates through `modeConfigSchema → rooCodeSettingsSchema → createRunSchema`
+ * and breaks `zodResolver` generic inference in downstream consumers (e.g., web-evals).
+ */
+export const groupEntryArraySchema = z.preprocess((val) => {
+	if (!Array.isArray(val)) return val
+	return val.filter((entry) => !isDeprecatedGroupEntry(entry))
+}, rawGroupEntryArraySchema) as z.ZodType<GroupEntry[], z.ZodTypeDef, GroupEntry[]>
+
 export const modeConfigSchema = z.object({
 	slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
 	name: z.string().min(1, "Name is required"),
@@ -142,7 +174,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
 		whenToUse:
 			"Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.",
 		description: "Plan and design before implementation",
-		groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
+		groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "mcp"],
 		customInstructions:
 			"1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n   - Specific and actionable\n   - Listed in logical execution order\n   - Focused on a single, well-defined outcome\n   - Clear enough that another mode could execute it independently\n\n   **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\"\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**\n\n**CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.**\n\nUnless told otherwise, if you want to save a plan file, put it in the /plans directory",
 	},
@@ -154,7 +186,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
 		whenToUse:
 			"Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.",
 		description: "Write, modify, and refactor code",
-		groups: ["read", "edit", "browser", "command", "mcp"],
+		groups: ["read", "edit", "command", "mcp"],
 	},
 	{
 		slug: "ask",
@@ -164,7 +196,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
 		whenToUse:
 			"Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.",
 		description: "Get answers and explanations",
-		groups: ["read", "browser", "mcp"],
+		groups: ["read", "mcp"],
 		customInstructions:
 			"You can analyze code, explain concepts, and access external resources. Always answer the user's questions thoroughly, and do not switch to implementing code unless explicitly requested by the user. Include Mermaid diagrams when they clarify your response.",
 	},
@@ -176,7 +208,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
 		whenToUse:
 			"Use this mode when you're troubleshooting issues, investigating errors, or diagnosing problems. Specialized in systematic debugging, adding logging, analyzing stack traces, and identifying root causes before applying fixes.",
 		description: "Diagnose and fix software issues",
-		groups: ["read", "edit", "browser", "command", "mcp"],
+		groups: ["read", "edit", "command", "mcp"],
 		customInstructions:
 			"Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.",
 	},

+ 0 - 9
packages/types/src/tool-params.ts

@@ -102,15 +102,6 @@ export interface Size {
 	height: number
 }
 
-export interface BrowserActionParams {
-	action: "launch" | "click" | "hover" | "type" | "scroll_down" | "scroll_up" | "resize" | "close" | "screenshot"
-	url?: string
-	coordinate?: Coordinate
-	size?: Size
-	text?: string
-	path?: string
-}
-
 export interface GenerateImageParams {
 	prompt: string
 	path: string

+ 8 - 2
packages/types/src/tool.ts

@@ -4,10 +4,17 @@ import { z } from "zod"
  * ToolGroup
  */
 
-export const toolGroups = ["read", "edit", "browser", "command", "mcp", "modes"] as const
+export const toolGroups = ["read", "edit", "command", "mcp", "modes"] as const
 
 export const toolGroupsSchema = z.enum(toolGroups)
 
+/**
+ * Tool groups that have been removed but may still exist in user config files.
+ * Used by schema preprocessing to silently strip these before validation,
+ * preventing errors for users with older configs.
+ */
+export const deprecatedToolGroups: readonly string[] = ["browser"]
+
 export type ToolGroup = z.infer<typeof toolGroupsSchema>
 
 /**
@@ -27,7 +34,6 @@ export const toolNames = [
 	"apply_patch",
 	"search_files",
 	"list_files",
-	"browser_action",
 	"use_mcp_tool",
 	"access_mcp_resource",
 	"ask_followup_question",

+ 0 - 57
packages/types/src/vscode-extension-host.ts

@@ -59,9 +59,6 @@ export interface ExtensionMessage {
 		| "deleteCustomModeCheck"
 		| "currentCheckpointUpdated"
 		| "checkpointInitWarning"
-		| "browserToolEnabled"
-		| "browserConnectionResult"
-		| "remoteBrowserEnabled"
 		| "ttsStart"
 		| "ttsStop"
 		| "fileSearchResults"
@@ -92,8 +89,6 @@ export interface ExtensionMessage {
 		| "dismissedUpsells"
 		| "organizationSwitchResult"
 		| "interactionRequired"
-		| "browserSessionUpdate"
-		| "browserSessionNavigate"
 		| "customToolsResult"
 		| "modes"
 		| "taskWithAggregatedCosts"
@@ -180,9 +175,6 @@ export interface ExtensionMessage {
 	queuedMessages?: QueuedMessage[]
 	list?: string[] // For dismissedUpsells
 	organizationId?: string | null // For organizationSwitchResult
-	browserSessionMessages?: ClineMessage[] // For browser session panel updates
-	isBrowserSessionActive?: boolean // For browser session panel updates
-	stepIndex?: number // For browserSessionNavigate: the target step index to display
 	tools?: SerializedCustomToolDefinition[] // For customToolsResult
 	modes?: { slug: string; name: string }[] // For modes response
 	skills?: SkillMetadata[] // For skills response
@@ -264,7 +256,6 @@ export type ExtensionState = Pick<
 	| "alwaysAllowWrite"
 	| "alwaysAllowWriteOutsideWorkspace"
 	| "alwaysAllowWriteProtected"
-	| "alwaysAllowBrowser"
 	| "alwaysAllowMcp"
 	| "alwaysAllowModeSwitch"
 	| "alwaysAllowSubtasks"
@@ -275,12 +266,6 @@ export type ExtensionState = Pick<
 	| "deniedCommands"
 	| "allowedMaxRequests"
 	| "allowedMaxCost"
-	| "browserToolEnabled"
-	| "browserViewportSize"
-	| "screenshotQuality"
-	| "remoteBrowserEnabled"
-	| "cachedChromeHostUrl"
-	| "remoteBrowserHost"
 	| "ttsEnabled"
 	| "ttsSpeed"
 	| "soundEnabled"
@@ -367,8 +352,6 @@ export type ExtensionState = Pick<
 	organizationAllowList: OrganizationAllowList
 	organizationSettingsVersion?: number
 
-	isBrowserSessionActive: boolean // Actual browser session state
-
 	autoCondenseContext: boolean
 	autoCondenseContextPercent: number
 	marketplaceItems?: MarketplaceItem[]
@@ -508,8 +491,6 @@ export interface WebviewMessage {
 		| "deleteMcpServer"
 		| "codebaseIndexEnabled"
 		| "telemetrySetting"
-		| "testBrowserConnection"
-		| "browserConnectionResult"
 		| "searchFiles"
 		| "toggleApiConfigPin"
 		| "hasOpenedModeSelector"
@@ -566,11 +547,6 @@ export interface WebviewMessage {
 		| "allowedCommands"
 		| "getTaskWithAggregatedCosts"
 		| "deniedCommands"
-		| "killBrowserSession"
-		| "openBrowserSessionPanel"
-		| "showBrowserSessionPanelAtStep"
-		| "refreshBrowserSessionPanel"
-		| "browserPanelDidLaunch"
 		| "openDebugApiHistory"
 		| "openDebugUiHistory"
 		| "downloadErrorDiagnostics"
@@ -852,39 +828,6 @@ export interface ClineSayTool {
 	skill?: string
 }
 
-// Must keep in sync with system prompt.
-export const browserActions = [
-	"launch",
-	"click",
-	"hover",
-	"type",
-	"press",
-	"scroll_down",
-	"scroll_up",
-	"resize",
-	"close",
-	"screenshot",
-] as const
-
-export type BrowserAction = (typeof browserActions)[number]
-
-export interface ClineSayBrowserAction {
-	action: BrowserAction
-	coordinate?: string
-	size?: string
-	text?: string
-	executedCoordinate?: string
-}
-
-export type BrowserActionResult = {
-	screenshot?: string
-	logs?: string
-	currentUrl?: string
-	currentMousePosition?: string
-	viewportWidth?: number
-	viewportHeight?: number
-}
-
 export interface ClineAskUseMcpServer {
 	serverName: string
 	type: "use_mcp_tool" | "access_mcp_resource"

Разница между файлами не показана из-за своего большого размера
+ 18 - 402
pnpm-lock.yaml


+ 0 - 15
src/__tests__/command-mentions.spec.ts

@@ -1,28 +1,14 @@
 import { parseMentions } from "../core/mentions"
-import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
 import { getCommand } from "../services/command/commands"
 
 // Mock the dependencies
 vi.mock("../services/command/commands")
-vi.mock("../services/browser/UrlContentFetcher")
 
-const MockedUrlContentFetcher = vi.mocked(UrlContentFetcher)
 const mockGetCommand = vi.mocked(getCommand)
 
 describe("Command Mentions", () => {
-	let mockUrlContentFetcher: any
-
 	beforeEach(() => {
 		vi.clearAllMocks()
-
-		// Create a mock UrlContentFetcher instance
-		mockUrlContentFetcher = {
-			launchBrowser: vi.fn(),
-			urlToMarkdown: vi.fn(),
-			closeBrowser: vi.fn(),
-		}
-
-		MockedUrlContentFetcher.mockImplementation(() => mockUrlContentFetcher)
 	})
 
 	// Helper function to call parseMentions with required parameters
@@ -30,7 +16,6 @@ describe("Command Mentions", () => {
 		return parseMentions(
 			text,
 			"/test/cwd", // cwd
-			mockUrlContentFetcher, // urlContentFetcher
 			undefined, // fileContextTracker
 			undefined, // rooIgnoreController
 			false, // showRooIgnoredFiles

+ 0 - 26
src/core/assistant-message/NativeToolCallParser.ts

@@ -490,19 +490,6 @@ export class NativeToolCallParser {
 				}
 				break
 
-			case "browser_action":
-				if (partialArgs.action !== undefined) {
-					nativeArgs = {
-						action: partialArgs.action,
-						url: partialArgs.url,
-						coordinate: partialArgs.coordinate,
-						size: partialArgs.size,
-						text: partialArgs.text,
-						path: partialArgs.path,
-					}
-				}
-				break
-
 			case "codebase_search":
 				if (partialArgs.query !== undefined) {
 					nativeArgs = {
@@ -838,19 +825,6 @@ export class NativeToolCallParser {
 					}
 					break
 
-				case "browser_action":
-					if (args.action !== undefined) {
-						nativeArgs = {
-							action: args.action,
-							url: args.url,
-							coordinate: args.coordinate,
-							size: args.size,
-							text: args.text,
-							path: args.path,
-						} as NativeArgsFor<TName>
-					}
-					break
-
 				case "codebase_search":
 					if (args.query !== undefined) {
 						nativeArgs = {

+ 2 - 2
src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts

@@ -246,7 +246,7 @@ describe("NativeToolCallParser", () => {
 						name: "read_file" as const,
 						arguments: JSON.stringify({
 							files: JSON.stringify([
-								{ path: "src/services/browser/browserDiscovery.ts" },
+								{ path: "src/services/example/service.ts" },
 								{ path: "src/services/mcp/McpServerManager.ts" },
 							]),
 						}),
@@ -264,7 +264,7 @@ describe("NativeToolCallParser", () => {
 						}
 						expect(nativeArgs._legacyFormat).toBe(true)
 						expect(nativeArgs.files).toHaveLength(2)
-						expect(nativeArgs.files[0].path).toBe("src/services/browser/browserDiscovery.ts")
+						expect(nativeArgs.files[0].path).toBe("src/services/example/service.ts")
 						expect(nativeArgs.files[1].path).toBe("src/services/mcp/McpServerManager.ts")
 					}
 				})

+ 0 - 3
src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts

@@ -60,9 +60,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
 			api: {
 				getModel: () => ({ id: "test-model", info: {} }),
 			},
-			browserSession: {
-				closeBrowser: vi.fn().mockResolvedValue(undefined),
-			},
 			recordToolUsage: vi.fn(),
 			recordToolError: vi.fn(),
 			toolRepetitionDetector: {

+ 0 - 3
src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts

@@ -46,9 +46,6 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
 			api: {
 				getModel: () => ({ id: "test-model", info: {} }),
 			},
-			browserSession: {
-				closeBrowser: vi.fn().mockResolvedValue(undefined),
-			},
 			recordToolUsage: vi.fn(),
 			toolRepetitionDetector: {
 				check: vi.fn().mockReturnValue({ allowExecution: true }),

+ 0 - 3
src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts

@@ -41,9 +41,6 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
 			api: {
 				getModel: () => ({ id: "test-model", info: {} }),
 			},
-			browserSession: {
-				closeBrowser: vi.fn().mockResolvedValue(undefined),
-			},
 			recordToolUsage: vi.fn(),
 			recordToolError: vi.fn(),
 			toolRepetitionDetector: {

+ 0 - 40
src/core/assistant-message/presentAssistantMessage.ts

@@ -24,7 +24,6 @@ import { searchReplaceTool } from "../tools/SearchReplaceTool"
 import { editFileTool } from "../tools/EditFileTool"
 import { applyPatchTool } from "../tools/ApplyPatchTool"
 import { searchFilesTool } from "../tools/SearchFilesTool"
-import { browserActionTool } from "../tools/BrowserActionTool"
 import { executeCommandTool } from "../tools/ExecuteCommandTool"
 import { useMcpToolTool } from "../tools/UseMcpToolTool"
 import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
@@ -358,8 +357,6 @@ export async function presentAssistantMessage(cline: Task) {
 						return `[${block.name}]`
 					case "list_files":
 						return `[${block.name} for '${block.params.path}']`
-					case "browser_action":
-						return `[${block.name} for '${block.params.action}']`
 					case "use_mcp_tool":
 						return `[${block.name} for '${block.params.server_name}']`
 					case "access_mcp_resource":
@@ -559,34 +556,6 @@ export async function presentAssistantMessage(cline: Task) {
 				pushToolResult(formatResponse.toolError(errorString))
 			}
 
-			// Keep browser open during an active session so other tools can run.
-			// Session is active if we've seen any browser_action_result and the last browser_action is not "close".
-			try {
-				const messages = cline.clineMessages || []
-				const hasStarted = messages.some((m: any) => m.say === "browser_action_result")
-				let isClosed = false
-				for (let i = messages.length - 1; i >= 0; i--) {
-					const m = messages[i]
-					if (m.say === "browser_action") {
-						try {
-							const act = JSON.parse(m.text || "{}")
-							isClosed = act.action === "close"
-						} catch {}
-						break
-					}
-				}
-				const sessionActive = hasStarted && !isClosed
-				// Only auto-close when no active browser session is present, and this isn't a browser_action
-				if (!sessionActive && block.name !== "browser_action") {
-					await cline.browserSession.closeBrowser()
-				}
-			} catch {
-				// On any unexpected error, fall back to conservative behavior
-				if (block.name !== "browser_action") {
-					await cline.browserSession.closeBrowser()
-				}
-			}
-
 			if (!block.partial) {
 				// Check if this is a custom tool - if so, record as "custom_tool" (like MCP tools)
 				const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name)
@@ -798,15 +767,6 @@ export async function presentAssistantMessage(cline: Task) {
 						pushToolResult,
 					})
 					break
-				case "browser_action":
-					await browserActionTool(
-						cline,
-						block as ToolUse<"browser_action">,
-						askApproval,
-						handleError,
-						pushToolResult,
-					)
-					break
 				case "execute_command":
 					await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
 						askApproval,

+ 1 - 6
src/core/auto-approval/index.ts

@@ -13,11 +13,10 @@ import { isWriteToolAction, isReadOnlyToolAction } from "./tools"
 import { isMcpToolAlwaysAllowed } from "./mcp"
 import { getCommandDecision } from "./commands"
 
-// We have 10 different actions that can be auto-approved.
+// We have auto-approval actions for different categories.
 export type AutoApprovalState =
 	| "alwaysAllowReadOnly"
 	| "alwaysAllowWrite"
-	| "alwaysAllowBrowser"
 	| "alwaysAllowMcp"
 	| "alwaysAllowModeSwitch"
 	| "alwaysAllowSubtasks"
@@ -90,10 +89,6 @@ export async function checkAutoApproval({
 		}
 	}
 
-	if (ask === "browser_action_launch") {
-		return state.alwaysAllowBrowser === true ? { decision: "approve" } : { decision: "ask" }
-	}
-
 	if (ask === "use_mcp_server") {
 		if (!text) {
 			return { decision: "ask" }

+ 8 - 27
src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts

@@ -227,11 +227,7 @@ describe("CustomModesManager - YAML Edge Cases", () => {
 						slug: "test-mode",
 						name: "Test Mode",
 						roleDefinition: "Test role",
-						groups: [
-							"read",
-							["edit", { fileRegex: "\\.md$", description: "Markdown files only" }],
-							"browser",
-						],
+						groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }]],
 					},
 				],
 			})
@@ -245,20 +241,19 @@ describe("CustomModesManager - YAML Edge Cases", () => {
 
 			// Should successfully parse the complex fileRegex syntax
 			expect(modes).toHaveLength(1)
-			expect(modes[0].groups).toHaveLength(3)
+			expect(modes[0].groups).toHaveLength(2)
 			expect(modes[0].groups[1]).toEqual(["edit", { fileRegex: "\\.md$", description: "Markdown files only" }])
 		})
 
 		it("should handle invalid fileRegex syntax with clear error", async () => {
 			// This YAML has invalid structure that might cause parsing issues
 			const invalidYaml = `customModes:
-	 - slug: "test-mode"
-	   name: "Test Mode"
-	   roleDefinition: "Test role"
-	   groups:
-	     - read
-	     - ["edit", { fileRegex: "\\.md$" }]  # This line has invalid YAML syntax
-	     - browser`
+		- slug: "test-mode"
+			 name: "Test Mode"
+			 roleDefinition: "Test role"
+			 groups:
+			   - read
+			   - ["edit", { fileRegex: "\\.md$" }]  # This line has invalid YAML syntax`
 
 			mockFsReadFile({
 				[mockRoomodes]: invalidYaml,
@@ -433,13 +428,6 @@ describe("CustomModesManager - YAML Edge Cases", () => {
 									description: "Markdown files with \u2018special\u2019 chars",
 								},
 							],
-							[
-								"browser",
-								{
-									fileRegex: "\\.html?$",
-									description: "HTML files\u00A0only",
-								},
-							],
 						],
 					},
 				],
@@ -462,13 +450,6 @@ describe("CustomModesManager - YAML Edge Cases", () => {
 					description: "Markdown files with 'special' chars",
 				},
 			])
-			expect(modes[0].groups[2]).toEqual([
-				"browser",
-				{
-					fileRegex: "\\.html?$",
-					description: "HTML files only",
-				},
-			])
 		})
 	})
 })

+ 38 - 1
src/core/config/__tests__/CustomModesSettings.spec.ts

@@ -130,7 +130,7 @@ describe("CustomModesSettings", () => {
 				customModes: [
 					{
 						...validMode,
-						groups: ["read", "edit", "browser"] as const,
+						groups: ["read", "edit"] as const,
 					},
 				],
 			}
@@ -168,4 +168,41 @@ describe("CustomModesSettings", () => {
 			expect(settings.customModes[0].customInstructions).toBeDefined()
 		})
 	})
+
+	describe("deprecated tool group migration", () => {
+		it("should strip deprecated 'browser' group when validating custom modes settings", () => {
+			const result = customModesSettingsSchema.parse({
+				customModes: [
+					{
+						slug: "test-mode",
+						name: "Test Mode",
+						roleDefinition: "Test role",
+						groups: ["read", "browser", "edit"],
+					},
+				],
+			})
+			expect(result.customModes[0].groups).toEqual(["read", "edit"])
+		})
+
+		it("should strip deprecated 'browser' from multiple modes in settings", () => {
+			const result = customModesSettingsSchema.parse({
+				customModes: [
+					{
+						slug: "mode-a",
+						name: "Mode A",
+						roleDefinition: "Role A",
+						groups: ["read", "browser"],
+					},
+					{
+						slug: "mode-b",
+						name: "Mode B",
+						roleDefinition: "Role B",
+						groups: ["browser", "edit", "command"],
+					},
+				],
+			})
+			expect(result.customModes[0].groups).toEqual(["read"])
+			expect(result.customModes[1].groups).toEqual(["edit", "command"])
+		})
+	})
 })

+ 47 - 9
src/core/config/__tests__/ModeConfig.spec.ts

@@ -26,7 +26,7 @@ describe("CustomModeSchema", () => {
 				slug: "test",
 				name: "Test Mode",
 				roleDefinition: "Test role definition",
-				groups: ["read", "edit", "browser"] as const,
+				groups: ["read", "edit"] as const,
 			} satisfies ModeConfig
 
 			expect(() => validateCustomMode(validMode)).not.toThrow()
@@ -121,18 +121,14 @@ describe("CustomModeSchema", () => {
 				slug: "markdown-editor",
 				name: "Markdown Editor",
 				roleDefinition: "Markdown editing mode",
-				groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
+				groups: ["read", ["edit", { fileRegex: "\\.md$" }]],
 			}
 
 			const modeWithDescription = {
 				slug: "docs-editor",
 				name: "Documentation Editor",
 				roleDefinition: "Documentation editing mode",
-				groups: [
-					"read",
-					["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
-					"browser",
-				],
+				groups: ["read", ["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }]],
 			}
 
 			expect(() => modeConfigSchema.parse(modeWithJustRegex)).not.toThrow()
@@ -195,7 +191,7 @@ describe("CustomModeSchema", () => {
 		test("accepts multiple groups", () => {
 			const mode = {
 				...validBaseMode,
-				groups: ["read", "edit", "browser"] as const,
+				groups: ["read", "edit"] as const,
 			} satisfies ModeConfig
 
 			expect(() => modeConfigSchema.parse(mode)).not.toThrow()
@@ -204,7 +200,7 @@ describe("CustomModeSchema", () => {
 		test("accepts all available groups", () => {
 			const mode = {
 				...validBaseMode,
-				groups: ["read", "edit", "browser", "command", "mcp"] as const,
+				groups: ["read", "edit", "command", "mcp"] as const,
 			} satisfies ModeConfig
 
 			expect(() => modeConfigSchema.parse(mode)).not.toThrow()
@@ -252,4 +248,46 @@ describe("CustomModeSchema", () => {
 			expect(() => modeConfigSchema.parse(modeWithUndefined)).toThrow()
 		})
 	})
+
+	describe("deprecated tool group migration", () => {
+		it("should strip deprecated 'browser' string group from mode config", () => {
+			const result = modeConfigSchema.parse({
+				slug: "test-mode",
+				name: "Test Mode",
+				roleDefinition: "Test role",
+				groups: ["read", "browser", "edit"],
+			})
+			expect(result.groups).toEqual(["read", "edit"])
+		})
+
+		it("should strip deprecated 'browser' tuple group from mode config", () => {
+			const result = modeConfigSchema.parse({
+				slug: "test-mode",
+				name: "Test Mode",
+				roleDefinition: "Test role",
+				groups: ["read", ["browser", { fileRegex: ".*", description: "test" }], "edit"],
+			})
+			expect(result.groups).toEqual(["read", "edit"])
+		})
+
+		it("should handle mode config where all groups are deprecated", () => {
+			const result = modeConfigSchema.parse({
+				slug: "test-mode",
+				name: "Test Mode",
+				roleDefinition: "Test role",
+				groups: ["browser"],
+			})
+			expect(result.groups).toEqual([])
+		})
+
+		it("should still reject other invalid group names", () => {
+			const result = modeConfigSchema.safeParse({
+				slug: "test-mode",
+				name: "Test Mode",
+				roleDefinition: "Test role",
+				groups: ["read", "nonexistent"],
+			})
+			expect(result.success).toBe(false)
+		})
+	})
 })

+ 0 - 18
src/core/environment/__tests__/getEnvironmentDetails.spec.ts

@@ -117,10 +117,6 @@ describe("getEnvironmentDetails", () => {
 				deref: vi.fn().mockReturnValue(mockProvider),
 				[Symbol.toStringTag]: "WeakRef",
 			} as unknown as WeakRef<ClineProvider>,
-			browserSession: {
-				isSessionActive: vi.fn().mockReturnValue(false),
-				getViewportSize: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-			} as any,
 		}
 
 		// Mock other dependencies.
@@ -448,18 +444,4 @@ describe("getEnvironmentDetails", () => {
 
 		expect(getGitStatus).toHaveBeenCalledWith(mockCwd, 5)
 	})
-
-	it("should NOT include Browser Session Status when inactive", async () => {
-		const result = await getEnvironmentDetails(mockCline as Task)
-		expect(result).not.toContain("# Browser Session Status")
-	})
-
-	it("should include Browser Session Status with current viewport when active", async () => {
-		;(mockCline.browserSession as any).isSessionActive = vi.fn().mockReturnValue(true)
-		;(mockCline.browserSession as any).getViewportSize = vi.fn().mockReturnValue({ width: 1280, height: 720 })
-
-		const result = await getEnvironmentDetails(mockCline as Task)
-		expect(result).toContain("Active - A browser session is currently open and ready for browser_action commands")
-		expect(result).toContain("Current viewport size: 1280x720 pixels.")
-	})
 })

+ 0 - 29
src/core/environment/getEnvironmentDetails.ts

@@ -226,35 +226,6 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
 	details += `<name>${modeDetails.name}</name>\n`
 	details += `<model>${modelId}</model>\n`
 
-	// Add browser session status - Only show when active to prevent cluttering context
-	const isBrowserActive = cline.browserSession.isSessionActive()
-
-	if (isBrowserActive) {
-		// Build viewport info for status (prefer actual viewport if available, else fallback to configured setting)
-		const configuredViewport = (state?.browserViewportSize as string | undefined) ?? "900x600"
-		let configuredWidth: number | undefined
-		let configuredHeight: number | undefined
-		if (configuredViewport.includes("x")) {
-			const parts = configuredViewport.split("x").map((v) => Number(v))
-			configuredWidth = parts[0]
-			configuredHeight = parts[1]
-		}
-
-		let actualWidth: number | undefined
-		let actualHeight: number | undefined
-		const vp = cline.browserSession.getViewportSize?.()
-		if (vp) {
-			actualWidth = vp.width
-			actualHeight = vp.height
-		}
-
-		const width = actualWidth ?? configuredWidth
-		const height = actualHeight ?? configuredHeight
-		const viewportInfo = width && height ? `\nCurrent viewport size: ${width}x${height} pixels.` : ""
-
-		details += `\n# Browser Session Status\nActive - A browser session is currently open and ready for browser_action commands${viewportInfo}\n`
-	}
-
 	if (includeFileDetails) {
 		details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n`
 		const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))

+ 5 - 134
src/core/mentions/__tests__/index.spec.ts

@@ -3,7 +3,6 @@
 import * as vscode from "vscode"
 
 import { parseMentions } from "../index"
-import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
 
 // Mock vscode
 vi.mock("vscode", () => ({
@@ -17,143 +16,15 @@ vi.mock("../../../i18n", () => ({
 	t: vi.fn((key: string) => key),
 }))
 
-describe("parseMentions - URL error handling", () => {
-	let mockUrlContentFetcher: UrlContentFetcher
-	let consoleErrorSpy: any
-
+describe("parseMentions - URL mention handling", () => {
 	beforeEach(() => {
 		vi.clearAllMocks()
-		consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
-
-		mockUrlContentFetcher = {
-			launchBrowser: vi.fn(),
-			urlToMarkdown: vi.fn(),
-			closeBrowser: vi.fn(),
-		} as any
-	})
-
-	it("should handle timeout errors with appropriate message", async () => {
-		const timeoutError = new Error("Navigation timeout of 30000 ms exceeded")
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(timeoutError)
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching URL https://example.com:", timeoutError)
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
-		expect(result.text).toContain("Error fetching content: Navigation timeout of 30000 ms exceeded")
-	})
-
-	it("should handle DNS resolution errors", async () => {
-		const dnsError = new Error("net::ERR_NAME_NOT_RESOLVED")
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(dnsError)
-
-		const result = await parseMentions("Check @https://nonexistent.example", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
-		expect(result.text).toContain("Error fetching content: net::ERR_NAME_NOT_RESOLVED")
-	})
-
-	it("should handle network disconnection errors", async () => {
-		const networkError = new Error("net::ERR_INTERNET_DISCONNECTED")
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(networkError)
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
-		expect(result.text).toContain("Error fetching content: net::ERR_INTERNET_DISCONNECTED")
-	})
-
-	it("should handle 403 Forbidden errors", async () => {
-		const forbiddenError = new Error("403 Forbidden")
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(forbiddenError)
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
-		expect(result.text).toContain("Error fetching content: 403 Forbidden")
-	})
-
-	it("should handle 404 Not Found errors", async () => {
-		const notFoundError = new Error("404 Not Found")
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(notFoundError)
-
-		const result = await parseMentions("Check @https://example.com/missing", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
-		expect(result.text).toContain("Error fetching content: 404 Not Found")
 	})
 
-	it("should handle generic errors with fallback message", async () => {
-		const genericError = new Error("Some unexpected error")
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(genericError)
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
-		expect(result.text).toContain("Error fetching content: Some unexpected error")
-	})
-
-	it("should handle non-Error objects thrown", async () => {
-		const nonErrorObject = { code: "UNKNOWN", details: "Something went wrong" }
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(nonErrorObject)
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
-		expect(result.text).toContain("Error fetching content:")
-	})
-
-	it("should handle browser launch errors correctly", async () => {
-		const launchError = new Error("Failed to launch browser")
-		vi.mocked(mockUrlContentFetcher.launchBrowser).mockRejectedValue(launchError)
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
-			"Error fetching content for https://example.com: Failed to launch browser",
-		)
-		expect(result.text).toContain("Error fetching content: Failed to launch browser")
-		// Should not attempt to fetch URL if browser launch failed
-		expect(mockUrlContentFetcher.urlToMarkdown).not.toHaveBeenCalled()
-	})
-
-	it("should handle browser launch errors without message property", async () => {
-		const launchError = "String error"
-		vi.mocked(mockUrlContentFetcher.launchBrowser).mockRejectedValue(launchError)
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
-			"Error fetching content for https://example.com: String error",
-		)
-		expect(result.text).toContain("Error fetching content: String error")
-	})
-
-	it("should successfully fetch URL content when no errors occur", async () => {
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockResolvedValue("# Example Content\n\nThis is the content.")
-
-		const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
-
-		expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
-		expect(result.text).toContain('<url_content url="https://example.com">')
-		expect(result.text).toContain("# Example Content\n\nThis is the content.")
-		expect(result.text).toContain("</url_content>")
-	})
-
-	it("should handle multiple URLs with mixed success and failure", async () => {
-		vi.mocked(mockUrlContentFetcher.urlToMarkdown)
-			.mockResolvedValueOnce("# First Site")
-			.mockRejectedValueOnce(new Error("timeout"))
-
-		const result = await parseMentions(
-			"Check @https://example1.com and @https://example2.com",
-			"/test",
-			mockUrlContentFetcher,
-		)
+	it("should replace URL mentions with quoted URL reference", async () => {
+		const result = await parseMentions("Check @https://example.com", "/test")
 
-		expect(result.text).toContain('<url_content url="https://example1.com">')
-		expect(result.text).toContain("# First Site")
-		expect(result.text).toContain('<url_content url="https://example2.com">')
-		expect(result.text).toContain("Error fetching content: timeout")
+		// URL mentions are now replaced with a quoted reference (no fetching)
+		expect(result.text).toContain("'https://example.com'")
 	})
 })

+ 155 - 11
src/core/mentions/__tests__/processUserContentMentions.spec.ts

@@ -2,7 +2,6 @@
 
 import { processUserContentMentions } from "../processUserContentMentions"
 import { parseMentions } from "../index"
-import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
 import { FileContextTracker } from "../../context-tracking/FileContextTracker"
 
 // Mock the parseMentions function
@@ -11,14 +10,12 @@ vi.mock("../index", () => ({
 }))
 
 describe("processUserContentMentions", () => {
-	let mockUrlContentFetcher: UrlContentFetcher
 	let mockFileContextTracker: FileContextTracker
 	let mockRooIgnoreController: any
 
 	beforeEach(() => {
 		vi.clearAllMocks()
 
-		mockUrlContentFetcher = {} as UrlContentFetcher
 		mockFileContextTracker = {} as FileContextTracker
 		mockRooIgnoreController = {}
 
@@ -42,7 +39,6 @@ describe("processUserContentMentions", () => {
 			const result = await processUserContentMentions({
 				userContent,
 				cwd: "/test",
-				urlContentFetcher: mockUrlContentFetcher,
 				fileContextTracker: mockFileContextTracker,
 			})
 
@@ -65,7 +61,6 @@ describe("processUserContentMentions", () => {
 			const result = await processUserContentMentions({
 				userContent,
 				cwd: "/test",
-				urlContentFetcher: mockUrlContentFetcher,
 				fileContextTracker: mockFileContextTracker,
 			})
 
@@ -74,6 +69,78 @@ describe("processUserContentMentions", () => {
 			expect(result.mode).toBeUndefined()
 		})
 
+		it("should process tool_result blocks with string content", async () => {
+			const userContent = [
+				{
+					type: "tool_result" as const,
+					tool_use_id: "123",
+					content: "<user_message>Tool feedback</user_message>",
+				},
+			]
+
+			const result = await processUserContentMentions({
+				userContent,
+				cwd: "/test",
+				fileContextTracker: mockFileContextTracker,
+			})
+
+			expect(parseMentions).toHaveBeenCalled()
+			// String content is now converted to array format to support content blocks
+			expect(result.content[0]).toEqual({
+				type: "tool_result",
+				tool_use_id: "123",
+				content: [
+					{
+						type: "text",
+						text: "parsed: <user_message>Tool feedback</user_message>",
+					},
+				],
+			})
+			expect(result.mode).toBeUndefined()
+		})
+
+		it("should process tool_result blocks with array content", async () => {
+			const userContent = [
+				{
+					type: "tool_result" as const,
+					tool_use_id: "123",
+					content: [
+						{
+							type: "text" as const,
+							text: "<user_message>Array task</user_message>",
+						},
+						{
+							type: "text" as const,
+							text: "Regular text",
+						},
+					],
+				},
+			]
+
+			const result = await processUserContentMentions({
+				userContent,
+				cwd: "/test",
+				fileContextTracker: mockFileContextTracker,
+			})
+
+			expect(parseMentions).toHaveBeenCalledTimes(1)
+			expect(result.content[0]).toEqual({
+				type: "tool_result",
+				tool_use_id: "123",
+				content: [
+					{
+						type: "text",
+						text: "parsed: <user_message>Array task</user_message>",
+					},
+					{
+						type: "text",
+						text: "Regular text",
+					},
+				],
+			})
+			expect(result.mode).toBeUndefined()
+		})
+
 		it("should handle mixed content types (text + image)", async () => {
 			const userContent = [
 				{
@@ -90,7 +157,6 @@ describe("processUserContentMentions", () => {
 			const result = await processUserContentMentions({
 				userContent: userContent as any,
 				cwd: "/test",
-				urlContentFetcher: mockUrlContentFetcher,
 				fileContextTracker: mockFileContextTracker,
 			})
 
@@ -117,14 +183,12 @@ describe("processUserContentMentions", () => {
 			await processUserContentMentions({
 				userContent,
 				cwd: "/test",
-				urlContentFetcher: mockUrlContentFetcher,
 				fileContextTracker: mockFileContextTracker,
 			})
 
 			expect(parseMentions).toHaveBeenCalledWith(
 				"<user_message>Test default</user_message>",
 				"/test",
-				mockUrlContentFetcher,
 				mockFileContextTracker,
 				undefined,
 				false, // showRooIgnoredFiles should default to false
@@ -144,7 +208,6 @@ describe("processUserContentMentions", () => {
 			await processUserContentMentions({
 				userContent,
 				cwd: "/test",
-				urlContentFetcher: mockUrlContentFetcher,
 				fileContextTracker: mockFileContextTracker,
 				showRooIgnoredFiles: false,
 			})
@@ -152,7 +215,6 @@ describe("processUserContentMentions", () => {
 			expect(parseMentions).toHaveBeenCalledWith(
 				"<user_message>Test explicit false</user_message>",
 				"/test",
-				mockUrlContentFetcher,
 				mockFileContextTracker,
 				undefined,
 				false,
@@ -181,7 +243,6 @@ describe("processUserContentMentions", () => {
 			const result = await processUserContentMentions({
 				userContent,
 				cwd: "/test",
-				urlContentFetcher: mockUrlContentFetcher,
 				fileContextTracker: mockFileContextTracker,
 			})
 
@@ -195,5 +256,88 @@ describe("processUserContentMentions", () => {
 				text: "command help",
 			})
 		})
+
+		it("should include slash command content in tool_result string content", async () => {
+			vi.mocked(parseMentions).mockResolvedValueOnce({
+				text: "parsed tool output",
+				slashCommandHelp: "command help",
+				mode: undefined,
+				contentBlocks: [],
+			})
+
+			const userContent = [
+				{
+					type: "tool_result" as const,
+					tool_use_id: "123",
+					content: "<user_message>Tool output</user_message>",
+				},
+			]
+
+			const result = await processUserContentMentions({
+				userContent,
+				cwd: "/test",
+				fileContextTracker: mockFileContextTracker,
+			})
+
+			expect(result.content).toHaveLength(1)
+			expect(result.content[0]).toEqual({
+				type: "tool_result",
+				tool_use_id: "123",
+				content: [
+					{
+						type: "text",
+						text: "parsed tool output",
+					},
+					{
+						type: "text",
+						text: "command help",
+					},
+				],
+			})
+		})
+
+		it("should include slash command content in tool_result array content", async () => {
+			vi.mocked(parseMentions).mockResolvedValueOnce({
+				text: "parsed array item",
+				slashCommandHelp: "command help",
+				mode: undefined,
+				contentBlocks: [],
+			})
+
+			const userContent = [
+				{
+					type: "tool_result" as const,
+					tool_use_id: "123",
+					content: [
+						{
+							type: "text" as const,
+							text: "<user_message>Array item</user_message>",
+						},
+					],
+				},
+			]
+
+			const result = await processUserContentMentions({
+				userContent,
+				cwd: "/test",
+				fileContextTracker: mockFileContextTracker,
+			})
+
+			expect(result.content).toHaveLength(1)
+			expect(result.content[0]).toEqual({
+				type: "tool_result",
+				tool_use_id: "123",
+				content: [
+					{
+						type: "text",
+						text: "parsed array item",
+					},
+					{
+						type: "text",
+						text: "command help",
+					},
+				],
+			})
+		})
 	})
 })

+ 2 - 84
src/core/mentions/index.ts

@@ -13,42 +13,11 @@ import { extractTextFromFileWithMetadata, type ExtractTextResult } from "../../i
 import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
 import { DEFAULT_LINE_LIMIT } from "../prompts/tools/native-tools/read_file"
 
-import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
-
 import { FileContextTracker } from "../context-tracking/FileContextTracker"
 
 import { RooIgnoreController } from "../ignore/RooIgnoreController"
 import { getCommand, type Command } from "../../services/command/commands"
 
-import { t } from "../../i18n"
-
-function getUrlErrorMessage(error: unknown): string {
-	const errorMessage = error instanceof Error ? error.message : String(error)
-
-	// Check for common error patterns and return appropriate message
-	if (errorMessage.includes("timeout")) {
-		return t("common:errors.url_timeout")
-	}
-	if (errorMessage.includes("net::ERR_NAME_NOT_RESOLVED")) {
-		return t("common:errors.url_not_found")
-	}
-	if (errorMessage.includes("net::ERR_INTERNET_DISCONNECTED")) {
-		return t("common:errors.no_internet")
-	}
-	if (errorMessage.includes("net::ERR_ABORTED")) {
-		return t("common:errors.url_request_aborted")
-	}
-	if (errorMessage.includes("403") || errorMessage.includes("Forbidden")) {
-		return t("common:errors.url_forbidden")
-	}
-	if (errorMessage.includes("404") || errorMessage.includes("Not Found")) {
-		return t("common:errors.url_page_not_found")
-	}
-
-	// Default error message
-	return t("common:errors.url_fetch_failed", { error: errorMessage })
-}
-
 export async function openMention(cwd: string, mention?: string): Promise<void> {
 	if (!mention) {
 		return
@@ -128,7 +97,6 @@ ${result.content}`
 export async function parseMentions(
 	text: string,
 	cwd: string,
-	urlContentFetcher: UrlContentFetcher,
 	fileContextTracker?: FileContextTracker,
 	rooIgnoreController?: RooIgnoreController,
 	showRooIgnoredFiles: boolean = false,
@@ -180,8 +148,7 @@ export async function parseMentions(
 	parsedText = parsedText.replace(mentionRegexGlobal, (match, mention) => {
 		mentions.add(mention)
 		if (mention.startsWith("http")) {
-			// Keep old style for URLs (still XML-based)
-			return `'${mention}' (see below for site content)`
+			return `'${mention}'`
 		} else if (mention.startsWith("/")) {
 			// Clean path reference - no "see below" since we format like tool results
 			const mentionPath = mention.slice(1)
@@ -198,49 +165,8 @@ export async function parseMentions(
 		return match
 	})
 
-	const urlMention = Array.from(mentions).find((mention) => mention.startsWith("http"))
-	let launchBrowserError: Error | undefined
-	if (urlMention) {
-		try {
-			await urlContentFetcher.launchBrowser()
-		} catch (error) {
-			launchBrowserError = error
-			const errorMessage = error instanceof Error ? error.message : String(error)
-			vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${errorMessage}`)
-		}
-	}
-
 	for (const mention of mentions) {
-		if (mention.startsWith("http")) {
-			let result: string
-			if (launchBrowserError) {
-				const errorMessage =
-					launchBrowserError instanceof Error ? launchBrowserError.message : String(launchBrowserError)
-				result = `Error fetching content: ${errorMessage}`
-			} else {
-				try {
-					const markdown = await urlContentFetcher.urlToMarkdown(mention)
-					result = markdown
-				} catch (error) {
-					console.error(`Error fetching URL ${mention}:`, error)
-
-					// Get raw error message for AI
-					const rawErrorMessage = error instanceof Error ? error.message : String(error)
-
-					// Get localized error message for UI notification
-					const localizedErrorMessage = getUrlErrorMessage(error)
-
-					vscode.window.showErrorMessage(
-						t("common:errors.url_fetch_error_with_url", { url: mention, error: localizedErrorMessage }),
-					)
-
-					// Send raw error message to AI model
-					result = `Error fetching content: ${rawErrorMessage}`
-				}
-			}
-			// URLs still use XML format (appended to text for backwards compat)
-			parsedText += `\n\n<url_content url="${mention}">\n${result}\n</url_content>`
-		} else if (mention.startsWith("/")) {
+		if (mention.startsWith("/")) {
 			const mentionPath = mention.slice(1)
 			try {
 				const fileResult = await getFileOrFolderContentWithMetadata(
@@ -305,14 +231,6 @@ export async function parseMentions(
 		}
 	}
 
-	if (urlMention) {
-		try {
-			await urlContentFetcher.closeBrowser()
-		} catch (error) {
-			console.error(`Error closing browser: ${error.message}`)
-		}
-	}
-
 	return {
 		text: parsedText,
 		contentBlocks,

+ 104 - 8
src/core/mentions/processUserContentMentions.ts

@@ -1,10 +1,9 @@
-import type { TextPart, ImagePart } from "../task-persistence/rooMessage"
+import type { TextPart, ImagePart, LegacyToolResultBlock } from "../task-persistence/rooMessage"
 import { parseMentions, ParseMentionsResult, MentionContentBlock } from "./index"
-import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
 import { FileContextTracker } from "../context-tracking/FileContextTracker"
 
 export interface ProcessUserContentMentionsResult {
-	content: Array<TextPart | ImagePart>
+	content: Array<TextPart | ImagePart | LegacyToolResultBlock>
 	mode?: string // Mode from the first slash command that has one
 }
 
@@ -30,16 +29,14 @@ function contentBlocksToTextParts(contentBlocks: MentionContentBlock[]): TextPar
 export async function processUserContentMentions({
 	userContent,
 	cwd,
-	urlContentFetcher,
 	fileContextTracker,
 	rooIgnoreController,
 	showRooIgnoredFiles = false,
 	includeDiagnosticMessages = true,
 	maxDiagnosticMessages = 50,
 }: {
-	userContent: Array<TextPart | ImagePart>
+	userContent: Array<TextPart | ImagePart | LegacyToolResultBlock>
 	cwd: string
-	urlContentFetcher: UrlContentFetcher
 	fileContextTracker: FileContextTracker
 	rooIgnoreController?: any
 	showRooIgnoredFiles?: boolean
@@ -61,7 +58,6 @@ export async function processUserContentMentions({
 						const result = await parseMentions(
 							block.text,
 							cwd,
-							urlContentFetcher,
 							fileContextTracker,
 							rooIgnoreController,
 							showRooIgnoredFiles,
@@ -98,6 +94,106 @@ export async function processUserContentMentions({
 						return blocks
 					}
 
+					return block
+				} else if (block.type === "tool_result") {
+					if (typeof block.content === "string") {
+						if (shouldProcessMentions(block.content)) {
+							const result = await parseMentions(
+								block.content,
+								cwd,
+								fileContextTracker,
+								rooIgnoreController,
+								showRooIgnoredFiles,
+								includeDiagnosticMessages,
+								maxDiagnosticMessages,
+							)
+							// Capture the first mode found
+							if (!commandMode && result.mode) {
+								commandMode = result.mode
+							}
+
+							// Build content array with file blocks included
+							const contentParts: Array<{ type: "text"; text: string }> = [
+								{
+									type: "text" as const,
+									text: result.text,
+								},
+							]
+
+							// Add file/folder content blocks
+							for (const contentBlock of result.contentBlocks) {
+								contentParts.push({
+									type: "text" as const,
+									text: contentBlock.content,
+								})
+							}
+
+							if (result.slashCommandHelp) {
+								contentParts.push({
+									type: "text" as const,
+									text: result.slashCommandHelp,
+								})
+							}
+
+							return {
+								...block,
+								content: contentParts,
+							}
+						}
+
+						return block
+					} else if (Array.isArray(block.content)) {
+						const parsedContent = (
+							await Promise.all(
+								block.content.map(async (contentBlock) => {
+									if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) {
+										const result = await parseMentions(
+											contentBlock.text,
+											cwd,
+											fileContextTracker,
+											rooIgnoreController,
+											showRooIgnoredFiles,
+											includeDiagnosticMessages,
+											maxDiagnosticMessages,
+										)
+										// Capture the first mode found
+										if (!commandMode && result.mode) {
+											commandMode = result.mode
+										}
+
+										// Build blocks array with file content
+										const blocks: Array<{ type: "text"; text: string }> = [
+											{
+												...contentBlock,
+												text: result.text,
+											},
+										]
+
+										// Add file/folder content blocks
+										for (const cb of result.contentBlocks) {
+											blocks.push({
+												type: "text" as const,
+												text: cb.content,
+											})
+										}
+
+										if (result.slashCommandHelp) {
+											blocks.push({
+												type: "text" as const,
+												text: result.slashCommandHelp,
+											})
+										}
+										return blocks
+									}
+
+									return contentBlock
+								}),
+							)
+						).flat()
+
+						return { ...block, content: parsedContent }
+					}
+
 					return block
 				}
 
@@ -108,5 +204,5 @@ export async function processUserContentMentions({
 		)
 	).flat()
 
-	return { content: content as Array<TextPart | ImagePart>, mode: commandMode }
+	return { content: content as Array<TextPart | ImagePart | LegacyToolResultBlock>, mode: commandMode }
 }

+ 0 - 3
src/core/prompts/__tests__/add-custom-instructions.spec.ts

@@ -205,7 +205,6 @@ describe("addCustomInstructions", () => {
 			false, // supportsImages
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			"architect", // mode
 			undefined, // customModePrompts
 			undefined, // customModes
@@ -226,7 +225,6 @@ describe("addCustomInstructions", () => {
 			false, // supportsImages
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			"ask", // mode
 			undefined, // customModePrompts
 			undefined, // customModes
@@ -249,7 +247,6 @@ describe("addCustomInstructions", () => {
 			false, // supportsImages
 			mockMcpHub, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes,

+ 0 - 51
src/core/prompts/__tests__/system-prompt.spec.ts

@@ -220,7 +220,6 @@ describe("SYSTEM_PROMPT", () => {
 			false, // supportsImages
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes
@@ -233,26 +232,6 @@ describe("SYSTEM_PROMPT", () => {
 		expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/consistent-system-prompt.snap")
 	})
 
-	it("should include browser actions when supportsImages is true", async () => {
-		const prompt = await SYSTEM_PROMPT(
-			mockContext,
-			"/test/path",
-			true, // supportsImages
-			undefined, // mcpHub
-			undefined, // diffStrategy
-			"1280x800", // browserViewportSize
-			defaultModeSlug, // mode
-			undefined, // customModePrompts
-			undefined, // customModes,
-			undefined, // globalCustomInstructions
-			experiments,
-			undefined, // language
-			undefined, // rooIgnoreInstructions
-		)
-
-		expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-computer-use-support.snap")
-	})
-
 	it("should include MCP server info when mcpHub is provided", async () => {
 		mockMcpHub = createMockMcpHub(true)
 
@@ -262,7 +241,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			mockMcpHub, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes,
@@ -282,7 +260,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // explicitly undefined mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes,
@@ -295,26 +272,6 @@ describe("SYSTEM_PROMPT", () => {
 		expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-undefined-mcp-hub.snap")
 	})
 
-	it("should handle different browser viewport sizes", async () => {
-		const prompt = await SYSTEM_PROMPT(
-			mockContext,
-			"/test/path",
-			false,
-			undefined, // mcpHub
-			undefined, // diffStrategy
-			"900x600", // different viewport size
-			defaultModeSlug, // mode
-			undefined, // customModePrompts
-			undefined, // customModes,
-			undefined, // globalCustomInstructions
-			experiments,
-			undefined, // language
-			undefined, // rooIgnoreInstructions
-		)
-
-		expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-different-viewport-size.snap")
-	})
-
 	it("should include vscode language in custom instructions", async () => {
 		// Mock vscode.env.language
 		const vscode = vi.mocked(await import("vscode")) as any
@@ -349,7 +306,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes
@@ -407,7 +363,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			"custom-mode", // mode
 			undefined, // customModePrompts
 			customModes, // customModes
@@ -442,7 +397,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug as Mode, // mode
 			customModePrompts, // customModePrompts
 			undefined, // customModes
@@ -472,7 +426,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug as Mode, // mode
 			customModePrompts, // customModePrompts
 			undefined, // customModes
@@ -499,7 +452,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes
@@ -528,7 +480,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes
@@ -557,7 +508,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes
@@ -586,7 +536,6 @@ describe("SYSTEM_PROMPT", () => {
 			false,
 			undefined, // mcpHub
 			undefined, // diffStrategy
-			undefined, // browserViewportSize
 			defaultModeSlug, // mode
 			undefined, // customModePrompts
 			undefined, // customModes

+ 0 - 3
src/core/prompts/system.ts

@@ -45,7 +45,6 @@ async function generatePrompt(
 	mode: Mode,
 	mcpHub?: McpHub,
 	diffStrategy?: DiffStrategy,
-	browserViewportSize?: string,
 	promptComponent?: PromptComponent,
 	customModeConfigs?: ModeConfig[],
 	globalCustomInstructions?: string,
@@ -116,7 +115,6 @@ export const SYSTEM_PROMPT = async (
 	supportsComputerUse: boolean,
 	mcpHub?: McpHub,
 	diffStrategy?: DiffStrategy,
-	browserViewportSize?: string,
 	mode: Mode = defaultModeSlug,
 	customModePrompts?: CustomModePrompts,
 	customModes?: ModeConfig[],
@@ -146,7 +144,6 @@ export const SYSTEM_PROMPT = async (
 		currentMode.slug,
 		mcpHub,
 		diffStrategy,
-		browserViewportSize,
 		promptComponent,
 		customModes,
 		globalCustomInstructions,

+ 1 - 6
src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts

@@ -20,21 +20,19 @@ describe("filterNativeToolsForMode - disabledTools", () => {
 		makeTool("execute_command"),
 		makeTool("read_file"),
 		makeTool("write_to_file"),
-		makeTool("browser_action"),
 		makeTool("apply_diff"),
 		makeTool("edit"),
 	]
 
 	it("removes tools listed in settings.disabledTools", () => {
 		const settings = {
-			disabledTools: ["execute_command", "browser_action"],
+			disabledTools: ["execute_command"],
 		}
 
 		const result = filterNativeToolsForMode(nativeTools, "code", undefined, undefined, undefined, settings)
 
 		const resultNames = result.map((t) => (t as any).function.name)
 		expect(resultNames).not.toContain("execute_command")
-		expect(resultNames).not.toContain("browser_action")
 		expect(resultNames).toContain("read_file")
 		expect(resultNames).toContain("write_to_file")
 		expect(resultNames).toContain("apply_diff")
@@ -51,7 +49,6 @@ describe("filterNativeToolsForMode - disabledTools", () => {
 		expect(resultNames).toContain("execute_command")
 		expect(resultNames).toContain("read_file")
 		expect(resultNames).toContain("write_to_file")
-		expect(resultNames).toContain("browser_action")
 		expect(resultNames).toContain("apply_diff")
 	})
 
@@ -67,7 +64,6 @@ describe("filterNativeToolsForMode - disabledTools", () => {
 
 	it("combines disabledTools with other setting-based exclusions", () => {
 		const settings = {
-			browserToolEnabled: false,
 			disabledTools: ["execute_command"],
 		}
 
@@ -75,7 +71,6 @@ describe("filterNativeToolsForMode - disabledTools", () => {
 
 		const resultNames = result.map((t) => (t as any).function.name)
 		expect(resultNames).not.toContain("execute_command")
-		expect(resultNames).not.toContain("browser_action")
 		expect(resultNames).toContain("read_file")
 	})
 

+ 0 - 10
src/core/prompts/tools/filter-tools-for-mode.ts

@@ -291,11 +291,6 @@ export function filterNativeToolsForMode(
 		allowedToolNames.delete("run_slash_command")
 	}
 
-	// Conditionally exclude browser_action if disabled in settings
-	if (settings?.browserToolEnabled === false) {
-		allowedToolNames.delete("browser_action")
-	}
-
 	// Remove tools that are explicitly disabled via the disabledTools setting
 	if (settings?.disabledTools?.length) {
 		for (const toolName of settings.disabledTools) {
@@ -387,11 +382,6 @@ export function isToolAllowedInMode(
 		return true
 	}
 
-	// Check for browser_action being disabled by user settings
-	if (toolName === "browser_action" && settings?.browserToolEnabled === false) {
-		return false
-	}
-
 	// Check if the tool is allowed by the mode's groups
 	// Resolve to canonical name and check that single value
 	const canonicalTool = resolveToolAlias(toolName)

+ 0 - 76
src/core/prompts/tools/native-tools/browser_action.ts

@@ -1,76 +0,0 @@
-import type OpenAI from "openai"
-
-const BROWSER_ACTION_DESCRIPTION = `Request to interact with a Puppeteer-controlled browser. Every action, except close, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.
-
-This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. Use it at key stages of web development tasks - such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. Analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
-
-The user may ask generic non-development tasks (such as "what's the latest news" or "look up the weather"), in which case you might use this tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.
-
-Browser Session Lifecycle:
-- Browser sessions start with launch and end with close
-- The session remains active across multiple messages and tool uses
-- You can use other tools while the browser session is active - it will stay open in the background`
-
-const ACTION_PARAMETER_DESCRIPTION = `Browser action to perform`
-
-const URL_PARAMETER_DESCRIPTION = `URL to open when performing the launch action; must include protocol`
-
-const COORDINATE_PARAMETER_DESCRIPTION = `Screen coordinate for hover or click actions in format 'x,y@WIDTHxHEIGHT' where x,y is the target position on the screenshot image and WIDTHxHEIGHT is the exact pixel dimensions of the screenshot image (not the browser viewport). Example: '450,203@900x600' means click at (450,203) on a 900x600 screenshot. The coordinates will be automatically scaled to match the actual viewport dimensions.`
-
-const SIZE_PARAMETER_DESCRIPTION = `Viewport dimensions for the resize action in format 'WIDTHxHEIGHT' or 'WIDTH,HEIGHT'. Example: '1280x800' or '1280,800'`
-
-const TEXT_PARAMETER_DESCRIPTION = `Text to type when performing the type action, or key name to press when performing the press action (e.g., 'Enter', 'Tab', 'Escape')`
-
-const PATH_PARAMETER_DESCRIPTION = `File path where the screenshot should be saved (relative to workspace). Required for screenshot action. Supports .png, .jpeg, and .webp extensions. Example: 'screenshots/result.png'`
-
-export default {
-	type: "function",
-	function: {
-		name: "browser_action",
-		description: BROWSER_ACTION_DESCRIPTION,
-		strict: false,
-		parameters: {
-			type: "object",
-			properties: {
-				action: {
-					type: "string",
-					description: ACTION_PARAMETER_DESCRIPTION,
-					enum: [
-						"launch",
-						"click",
-						"hover",
-						"type",
-						"press",
-						"scroll_down",
-						"scroll_up",
-						"resize",
-						"close",
-						"screenshot",
-					],
-				},
-				url: {
-					type: ["string", "null"],
-					description: URL_PARAMETER_DESCRIPTION,
-				},
-				coordinate: {
-					type: ["string", "null"],
-					description: COORDINATE_PARAMETER_DESCRIPTION,
-				},
-				size: {
-					type: ["string", "null"],
-					description: SIZE_PARAMETER_DESCRIPTION,
-				},
-				text: {
-					type: ["string", "null"],
-					description: TEXT_PARAMETER_DESCRIPTION,
-				},
-				path: {
-					type: ["string", "null"],
-					description: PATH_PARAMETER_DESCRIPTION,
-				},
-			},
-			required: ["action"],
-			additionalProperties: false,
-		},
-	},
-} satisfies OpenAI.Chat.ChatCompletionTool

+ 0 - 2
src/core/prompts/tools/native-tools/index.ts

@@ -4,7 +4,6 @@ import { apply_diff } from "./apply_diff"
 import applyPatch from "./apply_patch"
 import askFollowupQuestion from "./ask_followup_question"
 import attemptCompletion from "./attempt_completion"
-import browserAction from "./browser_action"
 import codebaseSearch from "./codebase_search"
 import editTool from "./edit"
 import executeCommand from "./execute_command"
@@ -53,7 +52,6 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
 		applyPatch,
 		askFollowupQuestion,
 		attemptCompletion,
-		browserAction,
 		codebaseSearch,
 		executeCommand,
 		generateImage,

+ 0 - 1
src/core/prompts/types.ts

@@ -3,7 +3,6 @@
  */
 export interface SystemPromptSettings {
 	todoListEnabled: boolean
-	browserToolEnabled?: boolean
 	useAgentRules: boolean
 	/** When true, recursively discover and load .roo/rules from subdirectories */
 	enableSubfolderRules?: boolean

+ 17 - 4
src/core/task-persistence/rooMessage.ts

@@ -17,8 +17,10 @@ import type { TextPart, ImagePart, FilePart, ToolCallPart, ToolResultPart } from
 
 /**
  * Union of content parts that can appear in a user message's content array.
+ * Includes `LegacyToolResultBlock` for backward compatibility with persisted
+ * data that stores Anthropic-format tool_result blocks inline in user messages.
  */
-export type UserContentPart = TextPart | ImagePart | FilePart
+export type UserContentPart = TextPart | ImagePart | FilePart | LegacyToolResultBlock
 
 /**
  * A minimal content block with a type discriminator and optional text.
@@ -73,9 +75,14 @@ export interface RooMessageMetadata {
 
 /**
  * A user-authored message. Content may be a plain string or an array of
- * text, image, and file parts. Extends AI SDK `UserModelMessage` with metadata.
+ * text, image, file, and legacy tool-result parts.
+ * Overrides the AI SDK `content` field to include `LegacyToolResultBlock`
+ * for backward compatibility with persisted data.
  */
-export type RooUserMessage = UserModelMessage & RooMessageMetadata
+export type RooUserMessage = Omit<UserModelMessage, "content"> &
+	RooMessageMetadata & {
+		content: string | UserContentPart[]
+	}
 
 /**
  * An assistant-authored message. Content may be a plain string or an array of
@@ -215,11 +222,17 @@ export interface LegacyToolUseBlock {
 	input: unknown
 }
 
+/** A text content block within a legacy Anthropic tool result. */
+export interface LegacyToolResultTextBlock {
+	type: "text"
+	text: string
+}
+
 /** Legacy Anthropic `tool_result` content block shape (persisted data from older versions). */
 export interface LegacyToolResultBlock {
 	type: "tool_result"
 	tool_use_id: string
-	content?: string | ContentBlockParam[]
+	content?: string | LegacyToolResultTextBlock[]
 	is_error?: boolean
 }
 

+ 4 - 123
src/core/task/Task.ts

@@ -71,13 +71,11 @@ import { combineCommandSequences } from "../../shared/combineCommandSequences"
 import { t } from "../../i18n"
 import { getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged } from "../../shared/getApiMetrics"
 import { ClineAskResponse } from "../../shared/WebviewMessage"
-import { defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
+import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
 import { DiffStrategy, type ToolUse, type ToolParamName, toolParamNames } from "../../shared/tools"
 import { getModelMaxOutputTokens } from "../../shared/api"
 
 // services
-import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
-import { BrowserSession } from "../../services/browser/BrowserSession"
 import { McpHub } from "../../services/mcp/McpHub"
 import { McpServerManager } from "../../services/mcp/McpServerManager"
 import { RepoPerTaskCheckpointService } from "../../services/checkpoints"
@@ -326,12 +324,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 	rooIgnoreController?: RooIgnoreController
 	rooProtectedController?: RooProtectedController
 	fileContextTracker: FileContextTracker
-	urlContentFetcher: UrlContentFetcher
 	terminalProcess?: RooTerminalProcess
 
-	// Computer User
-	browserSession: BrowserSession
-
 	// Editing
 	diffViewProvider: DiffViewProvider
 	diffStrategy?: DiffStrategy
@@ -644,29 +638,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 		this.api = buildApiHandler(this.apiConfiguration)
 		this.autoApprovalHandler = new AutoApprovalHandler()
 
-		this.urlContentFetcher = new UrlContentFetcher(provider.context)
-		this.browserSession = new BrowserSession(provider.context, (isActive: boolean) => {
-			// Add a message to indicate browser session status change
-			this.say("browser_session_status", isActive ? "Browser session opened" : "Browser session closed")
-			// Broadcast to browser panel
-			this.broadcastBrowserSessionUpdate()
-
-			// When a browser session becomes active, automatically open/reveal the Browser Session tab
-			if (isActive) {
-				try {
-					// Lazy-load to avoid circular imports at module load time
-					const { BrowserSessionPanelManager } = require("../webview/BrowserSessionPanelManager")
-					const providerRef = this.providerRef.deref()
-					if (providerRef) {
-						BrowserSessionPanelManager.getInstance(providerRef)
-							.show()
-							.catch(() => {})
-					}
-				} catch (err) {
-					console.error("[Task] Failed to auto-open Browser Session panel:", err)
-				}
-			}
-		})
 		this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
 		this.providerRef = new WeakRef(provider)
 		this.globalStoragePath = provider.context.globalStorageUri.fsPath
@@ -1601,12 +1572,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 
 			if (message) {
 				// Check if this is a tool approval ask that needs to be handled.
-				if (
-					type === "tool" ||
-					type === "command" ||
-					type === "browser_action_launch" ||
-					type === "use_mcp_server"
-				) {
+				if (type === "tool" || type === "command" || type === "use_mcp_server") {
 					// For tool approvals, we need to approve first, then send
 					// the message if there's text/images.
 					this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
@@ -1633,12 +1599,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 					if (message) {
 						// If this is a tool approval ask, we need to approve first (yesButtonClicked)
 						// and include any queued text/images.
-						if (
-							type === "tool" ||
-							type === "command" ||
-							type === "browser_action_launch" ||
-							type === "use_mcp_server"
-						) {
+						if (type === "tool" || type === "command" || type === "use_mcp_server") {
 							this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
 						} else {
 							this.handleWebviewAskResponse("messageResponse", message.text, message.images)
@@ -1836,7 +1797,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				customModes: state?.customModes,
 				experiments: state?.experiments,
 				apiConfiguration,
-				browserToolEnabled: state?.browserToolEnabled ?? true,
 				disabledTools: state?.disabledTools,
 				modelInfo,
 				includeAllToolsWithRestrictions: false,
@@ -2030,11 +1990,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				contextTruncation,
 			})
 		}
-
-		// Broadcast browser session updates to panel when browser-related messages are added
-		if (type === "browser_action" || type === "browser_action_result" || type === "browser_session_status") {
-			this.broadcastBrowserSessionUpdate()
-		}
 	}
 
 	async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) {
@@ -2572,28 +2527,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				console.error("Error cleaning up command output artifacts:", error)
 			})
 
-		try {
-			this.urlContentFetcher.closeBrowser()
-		} catch (error) {
-			console.error("Error closing URL content fetcher browser:", error)
-		}
-
-		try {
-			this.browserSession.closeBrowser()
-		} catch (error) {
-			console.error("Error closing browser session:", error)
-		}
-		// Also close the Browser Session panel when the task is disposed
-		try {
-			const provider = this.providerRef.deref()
-			if (provider) {
-				const { BrowserSessionPanelManager } = require("../webview/BrowserSessionPanelManager")
-				BrowserSessionPanelManager.getInstance(provider).dispose()
-			}
-		} catch (error) {
-			console.error("Error closing browser session panel:", error)
-		}
-
 		try {
 			if (this.rooIgnoreController) {
 				this.rooIgnoreController.dispose()
@@ -2846,7 +2779,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 			const { content: parsedUserContent, mode: slashCommandMode } = await processUserContentMentions({
 				userContent: currentUserContent as Array<TextPart | ImagePart>,
 				cwd: this.cwd,
-				urlContentFetcher: this.urlContentFetcher,
 				fileContextTracker: this.fileContextTracker,
 				rooIgnoreController: this.rooIgnoreController,
 				showRooIgnoredFiles,
@@ -3985,13 +3917,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 		const state = await this.providerRef.deref()?.getState()
 
 		const {
-			browserViewportSize,
 			mode,
 			customModes,
 			customModePrompts,
 			customInstructions,
 			experiments,
-			browserToolEnabled,
 			language,
 			apiConfiguration,
 			enableSubfolderRules,
@@ -4004,24 +3934,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				throw new Error("Provider not available")
 			}
 
-			// Align browser tool enablement with generateSystemPrompt: require model image support,
-			// mode to include the browser group, and the user setting to be enabled.
-			const modeConfig = getModeBySlug(mode ?? defaultModeSlug, customModes)
-			const modeSupportsBrowser = modeConfig?.groups.some((group) => getGroupName(group) === "browser") ?? false
-
-			// Check if model supports browser capability (images)
 			const modelInfo = this.api.getModel().info
-			const modelSupportsBrowser = (modelInfo as any)?.supportsImages === true
-
-			const canUseBrowserTool = modelSupportsBrowser && modeSupportsBrowser && (browserToolEnabled ?? true)
 
 			return SYSTEM_PROMPT(
 				provider.context,
 				this.cwd,
-				canUseBrowserTool,
+				false,
 				mcpHub,
 				this.diffStrategy,
-				browserViewportSize ?? "900x600",
 				mode ?? defaultModeSlug,
 				customModePrompts,
 				customModes,
@@ -4031,7 +3951,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				rooIgnoreInstructions,
 				{
 					todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
-					browserToolEnabled: browserToolEnabled ?? true,
 					useAgentRules:
 						vscode.workspace.getConfiguration(Package.name).get<boolean>("useAgentRules") ?? true,
 					enableSubfolderRules: enableSubfolderRules ?? false,
@@ -4092,7 +4011,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				customModes: state?.customModes,
 				experiments: state?.experiments,
 				apiConfiguration,
-				browserToolEnabled: state?.browserToolEnabled ?? true,
 				disabledTools: state?.disabledTools,
 				modelInfo,
 				includeAllToolsWithRestrictions: false,
@@ -4307,7 +4225,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 						customModes: state?.customModes,
 						experiments: state?.experiments,
 						apiConfiguration,
-						browserToolEnabled: state?.browserToolEnabled ?? true,
 						disabledTools: state?.disabledTools,
 						modelInfo,
 						includeAllToolsWithRestrictions: false,
@@ -4472,7 +4389,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				customModes: state?.customModes,
 				experiments: state?.experiments,
 				apiConfiguration,
-				browserToolEnabled: state?.browserToolEnabled ?? true,
 				disabledTools: state?.disabledTools,
 				modelInfo,
 				includeAllToolsWithRestrictions: supportsAllowedFunctionNames,
@@ -4944,41 +4860,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 		return this._messageManager
 	}
 
-	/**
-	 * Broadcast browser session updates to the browser panel (if open)
-	 */
-	private broadcastBrowserSessionUpdate(): void {
-		const provider = this.providerRef.deref()
-		if (!provider) {
-			return
-		}
-
-		try {
-			const { BrowserSessionPanelManager } = require("../webview/BrowserSessionPanelManager")
-			const panelManager = BrowserSessionPanelManager.getInstance(provider)
-
-			// Get browser session messages
-			const browserSessionStartIndex = this.clineMessages.findIndex(
-				(m) =>
-					m.ask === "browser_action_launch" ||
-					(m.say === "browser_session_status" && m.text?.includes("opened")),
-			)
-
-			const browserSessionMessages =
-				browserSessionStartIndex !== -1 ? this.clineMessages.slice(browserSessionStartIndex) : []
-
-			const isBrowserSessionActive = this.browserSession?.isSessionActive() ?? false
-
-			// Update the panel asynchronously
-			panelManager.updateBrowserSession(browserSessionMessages, isBrowserSessionActive).catch((error: Error) => {
-				console.error("Failed to broadcast browser session update:", error)
-			})
-		} catch (error) {
-			// Silently fail if panel manager is not available
-			console.debug("Browser panel not available for update:", error)
-		}
-	}
-
 	/**
 	 * Process any queued messages by dequeuing and submitting them.
 	 * This ensures that queued user messages are sent when appropriate,

+ 0 - 2
src/core/task/__tests__/Task.dispose.test.ts

@@ -13,8 +13,6 @@ vi.mock("../../../integrations/terminal/TerminalRegistry", () => ({
 vi.mock("../../ignore/RooIgnoreController")
 vi.mock("../../protect/RooProtectedController")
 vi.mock("../../context-tracking/FileContextTracker")
-vi.mock("../../../services/browser/UrlContentFetcher")
-vi.mock("../../../services/browser/BrowserSession")
 vi.mock("../../../integrations/editor/DiffViewProvider")
 vi.mock("../../tools/ToolRepetitionDetector")
 vi.mock("../../../api", () => ({

+ 0 - 1
src/core/task/__tests__/Task.spec.ts

@@ -985,7 +985,6 @@ describe("Cline", () => {
 					const { content: processedContent } = await processUserContentMentions({
 						userContent,
 						cwd: cline.cwd,
-						urlContentFetcher: cline.urlContentFetcher,
 						fileContextTracker: cline.fileContextTracker,
 					})
 

+ 0 - 2
src/core/task/__tests__/Task.throttle.test.ts

@@ -14,8 +14,6 @@ vi.mock("../../../integrations/terminal/TerminalRegistry", () => ({
 vi.mock("../../ignore/RooIgnoreController")
 vi.mock("../../protect/RooProtectedController")
 vi.mock("../../context-tracking/FileContextTracker")
-vi.mock("../../../services/browser/UrlContentFetcher")
-vi.mock("../../../services/browser/BrowserSession")
 vi.mock("../../../integrations/editor/DiffViewProvider")
 vi.mock("../../tools/ToolRepetitionDetector")
 vi.mock("../../../api", () => ({

+ 2 - 2
src/core/task/__tests__/native-tools-filtering.spec.ts

@@ -10,14 +10,14 @@ describe("Native Tools Filtering by Mode", () => {
 				slug: "architect",
 				name: "Architect",
 				roleDefinition: "Test architect",
-				groups: ["read", "browser", "mcp"] as const,
+				groups: ["read", "mcp"] as const,
 			}
 
 			const codeMode: ModeConfig = {
 				slug: "code",
 				name: "Code",
 				roleDefinition: "Test code",
-				groups: ["read", "edit", "browser", "command", "mcp"] as const,
+				groups: ["read", "edit", "command", "mcp"] as const,
 			}
 
 			// Import the functions we need to test

+ 0 - 3
src/core/task/build-tools.ts

@@ -22,7 +22,6 @@ interface BuildToolsOptions {
 	customModes: ModeConfig[] | undefined
 	experiments: Record<string, boolean> | undefined
 	apiConfiguration: ProviderSettings | undefined
-	browserToolEnabled: boolean
 	disabledTools?: string[]
 	modelInfo?: ModelInfo
 	/**
@@ -88,7 +87,6 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
 		customModes,
 		experiments,
 		apiConfiguration,
-		browserToolEnabled,
 		disabledTools,
 		modelInfo,
 		includeAllToolsWithRestrictions,
@@ -103,7 +101,6 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
 	// Build settings object for tool filtering.
 	const filterSettings = {
 		todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
-		browserToolEnabled: browserToolEnabled ?? true,
 		disabledTools,
 		modelInfo,
 	}

+ 0 - 280
src/core/tools/BrowserActionTool.ts

@@ -1,280 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-
-import { BrowserAction, BrowserActionResult, browserActions, ClineSayBrowserAction } from "@roo-code/types"
-
-import { Task } from "../task/Task"
-import { ToolUse, AskApproval, HandleError, PushToolResult } from "../../shared/tools"
-import { formatResponse } from "../prompts/responses"
-
-import { scaleCoordinate } from "../../shared/browserUtils"
-
-export async function browserActionTool(
-	cline: Task,
-	block: ToolUse,
-	askApproval: AskApproval,
-	handleError: HandleError,
-	pushToolResult: PushToolResult,
-) {
-	const action: BrowserAction | undefined = block.params.action as BrowserAction
-	const url: string | undefined = block.params.url
-	const coordinate: string | undefined = block.params.coordinate
-	const text: string | undefined = block.params.text
-	const size: string | undefined = block.params.size
-	const filePath: string | undefined = block.params.path
-
-	if (!action || !browserActions.includes(action)) {
-		// checking for action to ensure it is complete and valid
-		if (!block.partial) {
-			// if the block is complete and we don't have a valid action cline is a mistake
-			cline.consecutiveMistakeCount++
-			cline.recordToolError("browser_action")
-			cline.didToolFailInCurrentTurn = true
-			pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "action"))
-			// Do not close the browser on parameter validation errors
-		}
-
-		return
-	}
-
-	try {
-		if (block.partial) {
-			if (action === "launch") {
-				await cline.ask("browser_action_launch", url ?? "", block.partial).catch(() => {})
-			} else {
-				await cline.say(
-					"browser_action",
-					JSON.stringify({
-						action: action as BrowserAction,
-						coordinate: coordinate ?? "",
-						text: text ?? "",
-						size: size ?? "",
-					} satisfies ClineSayBrowserAction),
-					undefined,
-					block.partial,
-				)
-			}
-			return
-		} else {
-			// Initialize with empty object to avoid "used before assigned" errors
-			let browserActionResult: BrowserActionResult = {}
-
-			if (action === "launch") {
-				if (!url) {
-					cline.consecutiveMistakeCount++
-					cline.recordToolError("browser_action")
-					cline.didToolFailInCurrentTurn = true
-					pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "url"))
-					// Do not close the browser on parameter validation errors
-					return
-				}
-
-				cline.consecutiveMistakeCount = 0
-				const didApprove = await askApproval("browser_action_launch", url)
-
-				if (!didApprove) {
-					return
-				}
-
-				// NOTE: It's okay that we call cline message since the partial inspect_site is finished streaming.
-				// The only scenario we have to avoid is sending messages WHILE a partial message exists at the end of the messages array.
-				// For example the api_req_finished message would interfere with the partial message, so we needed to remove that.
-
-				// Launch browser first (this triggers "Browser session opened" status message)
-				await cline.browserSession.launchBrowser()
-
-				// Create browser_action say message AFTER launching so status appears first
-				await cline.say(
-					"browser_action",
-					JSON.stringify({
-						action: "launch" as BrowserAction,
-						text: url,
-					} satisfies ClineSayBrowserAction),
-					undefined,
-					false,
-				)
-
-				browserActionResult = await cline.browserSession.navigateToUrl(url)
-			} else {
-				// Variables to hold validated and processed parameters
-				let processedCoordinate = coordinate
-
-				if (action === "click" || action === "hover") {
-					if (!coordinate) {
-						cline.consecutiveMistakeCount++
-						cline.recordToolError("browser_action")
-						cline.didToolFailInCurrentTurn = true
-						pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "coordinate"))
-						// Do not close the browser on parameter validation errors
-						return // can't be within an inner switch
-					}
-
-					// Get viewport dimensions from the browser session
-					const viewportSize = cline.browserSession.getViewportSize()
-					const viewportWidth = viewportSize.width || 900 // default to 900 if not available
-					const viewportHeight = viewportSize.height || 600 // default to 600 if not available
-
-					// Scale coordinate from image dimensions to viewport dimensions
-					try {
-						processedCoordinate = scaleCoordinate(coordinate, viewportWidth, viewportHeight)
-					} catch (error) {
-						cline.consecutiveMistakeCount++
-						cline.recordToolError("browser_action")
-						cline.didToolFailInCurrentTurn = true
-						pushToolResult(
-							await cline.sayAndCreateMissingParamError(
-								"browser_action",
-								"coordinate",
-								error instanceof Error ? error.message : String(error),
-							),
-						)
-						return
-					}
-				}
-
-				if (action === "type" || action === "press") {
-					if (!text) {
-						cline.consecutiveMistakeCount++
-						cline.recordToolError("browser_action")
-						cline.didToolFailInCurrentTurn = true
-						pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "text"))
-						// Do not close the browser on parameter validation errors
-						return
-					}
-				}
-
-				if (action === "resize") {
-					if (!size) {
-						cline.consecutiveMistakeCount++
-						cline.recordToolError("browser_action")
-						cline.didToolFailInCurrentTurn = true
-						pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "size"))
-						// Do not close the browser on parameter validation errors
-						return
-					}
-				}
-
-				if (action === "screenshot") {
-					if (!filePath) {
-						cline.consecutiveMistakeCount++
-						cline.recordToolError("browser_action")
-						cline.didToolFailInCurrentTurn = true
-						pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "path"))
-						// Do not close the browser on parameter validation errors
-						return
-					}
-				}
-
-				cline.consecutiveMistakeCount = 0
-
-				// Prepare say payload; include executedCoordinate for pointer actions
-				const sayPayload: ClineSayBrowserAction & { executedCoordinate?: string } = {
-					action: action as BrowserAction,
-					coordinate,
-					text,
-					size,
-				}
-				if ((action === "click" || action === "hover") && processedCoordinate) {
-					sayPayload.executedCoordinate = processedCoordinate
-				}
-				await cline.say("browser_action", JSON.stringify(sayPayload), undefined, false)
-
-				switch (action) {
-					case "click":
-						browserActionResult = await cline.browserSession.click(processedCoordinate!)
-						break
-					case "hover":
-						browserActionResult = await cline.browserSession.hover(processedCoordinate!)
-						break
-					case "type":
-						browserActionResult = await cline.browserSession.type(text!)
-						break
-					case "press":
-						browserActionResult = await cline.browserSession.press(text!)
-						break
-					case "scroll_down":
-						browserActionResult = await cline.browserSession.scrollDown()
-						break
-					case "scroll_up":
-						browserActionResult = await cline.browserSession.scrollUp()
-						break
-					case "resize":
-						browserActionResult = await cline.browserSession.resize(size!)
-						break
-					case "screenshot":
-						browserActionResult = await cline.browserSession.saveScreenshot(filePath!, cline.cwd)
-						break
-					case "close":
-						browserActionResult = await cline.browserSession.closeBrowser()
-						break
-				}
-			}
-
-			switch (action) {
-				case "launch":
-				case "click":
-				case "hover":
-				case "type":
-				case "press":
-				case "scroll_down":
-				case "scroll_up":
-				case "resize":
-				case "screenshot": {
-					await cline.say("browser_action_result", JSON.stringify(browserActionResult))
-
-					const images = browserActionResult?.screenshot ? [browserActionResult.screenshot] : []
-
-					let messageText =
-						action === "screenshot"
-							? `Screenshot saved to: ${filePath}`
-							: `The browser action has been executed.`
-
-					messageText += `\n\n**CRITICAL**: When providing click/hover coordinates:`
-					messageText += `\n1. Screenshot dimensions != Browser viewport dimensions`
-					messageText += `\n2. Measure x,y on the screenshot image you see below`
-					messageText += `\n3. Use format: <coordinate>x,y@WIDTHxHEIGHT</coordinate> where WIDTHxHEIGHT is the EXACT pixel size of the screenshot image`
-					messageText += `\n4. Never use the browser viewport size for WIDTHxHEIGHT - it is only for reference and is often larger than the screenshot`
-					messageText += `\n5. Screenshots are often downscaled - always use the dimensions you see in the image`
-					messageText += `\nExample: Viewport 1280x800, screenshot 1000x625, click (500,300) -> <coordinate>500,300@1000x625</coordinate>`
-
-					// Include browser viewport dimensions (for reference only)
-					if (browserActionResult?.viewportWidth && browserActionResult?.viewportHeight) {
-						messageText += `\n\nBrowser viewport: ${browserActionResult.viewportWidth}x${browserActionResult.viewportHeight}`
-					}
-
-					// Include cursor position if available
-					if (browserActionResult?.currentMousePosition) {
-						messageText += `\nCursor position: ${browserActionResult.currentMousePosition}`
-					}
-
-					messageText += `\n\nConsole logs:\n${browserActionResult?.logs || "(No new logs)"}\n`
-
-					if (images.length > 0) {
-						const blocks = [
-							...formatResponse.imageBlocks(images),
-							{ type: "text", text: messageText } as Anthropic.TextBlockParam,
-						]
-						pushToolResult(blocks)
-					} else {
-						pushToolResult(messageText)
-					}
-
-					break
-				}
-				case "close":
-					pushToolResult(
-						formatResponse.toolResult(
-							`The browser has been closed. You may now proceed to using other tools.`,
-						),
-					)
-
-					break
-			}
-
-			return
-		}
-	} catch (error) {
-		// Keep the browser session alive on errors; report the error without terminating the session
-		await handleError("executing browser action", error)
-		return
-	}
-}

+ 0 - 22
src/core/tools/ToolRepetitionDetector.ts

@@ -33,13 +33,6 @@ export class ToolRepetitionDetector {
 			messageDetail: string
 		}
 	} {
-		// Browser scroll actions should not be subject to repetition detection
-		// as they are frequently needed for navigating through web pages
-		if (this.isBrowserScrollAction(currentToolCallBlock)) {
-			// Allow browser scroll actions without counting them as repetitions
-			return { allowExecution: true }
-		}
-
 		// Serialize the block to a canonical JSON string for comparison
 		const currentToolCallJson = this.serializeToolUse(currentToolCallBlock)
 
@@ -74,21 +67,6 @@ export class ToolRepetitionDetector {
 		return { allowExecution: true }
 	}
 
-	/**
-	 * Checks if a tool use is a browser scroll action
-	 *
-	 * @param toolUse The ToolUse object to check
-	 * @returns true if the tool is a browser_action with scroll_down or scroll_up action
-	 */
-	private isBrowserScrollAction(toolUse: ToolUse): boolean {
-		if (toolUse.name !== "browser_action") {
-			return false
-		}
-
-		const action = toolUse.params.action as string
-		return action === "scroll_down" || action === "scroll_up"
-	}
-
 	/**
 	 * Serializes a ToolUse object into a canonical JSON string for comparison
 	 *

+ 0 - 84
src/core/tools/__tests__/BrowserActionTool.coordinateScaling.spec.ts

@@ -1,84 +0,0 @@
-// Test coordinate scaling functionality in browser actions
-import { describe, it, expect } from "vitest"
-import { scaleCoordinate } from "../../../shared/browserUtils"
-
-describe("Browser Action Coordinate Scaling", () => {
-	describe("Coordinate format validation", () => {
-		it("should match valid coordinate format with image dimensions", () => {
-			const validFormats = [
-				"450,300@1024x768",
-				"0,0@1920x1080",
-				"1920,1080@1920x1080",
-				"100,200@800x600",
-				" 273 , 273 @ 1280x800 ",
-				"267,273@1280,800", // comma separator for dimensions
-				"450,300@1024,768", // comma separator for dimensions
-			]
-
-			validFormats.forEach((coord) => {
-				// Should not throw
-				expect(() => scaleCoordinate(coord, 900, 600)).not.toThrow()
-			})
-		})
-
-		it("should not match invalid coordinate formats", () => {
-			const invalidFormats = [
-				"450,300", // missing image dimensions
-				"450,300@", // incomplete dimensions
-				"450,300@1024", // missing height
-				"450,300@1024x", // missing height value
-				"@1024x768", // missing coordinates
-				"450@1024x768", // missing y coordinate
-				",300@1024x768", // missing x coordinate
-				"450,300@1024x768x2", // extra dimension
-				"a,b@1024x768", // non-numeric coordinates
-				"450,300@axb", // non-numeric dimensions
-			]
-
-			invalidFormats.forEach((coord) => {
-				expect(() => scaleCoordinate(coord, 900, 600)).toThrow()
-			})
-		})
-	})
-
-	describe("Coordinate scaling logic", () => {
-		it("should correctly scale coordinates from image to viewport", () => {
-			// Test case 1: Same dimensions (no scaling)
-			expect(scaleCoordinate("450,300@900x600", 900, 600)).toBe("450,300")
-
-			// Test case 2: Half dimensions (2x upscale)
-			expect(scaleCoordinate("225,150@450x300", 900, 600)).toBe("450,300")
-
-			// Test case 3: Double dimensions (0.5x downscale)
-			expect(scaleCoordinate("900,600@1800x1200", 900, 600)).toBe("450,300")
-
-			// Test case 4: Different aspect ratio
-			expect(scaleCoordinate("512,384@1024x768", 1920, 1080)).toBe("960,540")
-
-			// Test case 5: Edge cases (0,0)
-			expect(scaleCoordinate("0,0@1024x768", 1920, 1080)).toBe("0,0")
-
-			// Test case 6: Edge cases (max coordinates)
-			expect(scaleCoordinate("1024,768@1024x768", 1920, 1080)).toBe("1920,1080")
-		})
-
-		it("should throw error for invalid coordinate format", () => {
-			// Test invalid formats
-			expect(() => scaleCoordinate("450,300", 900, 600)).toThrow("Invalid coordinate format")
-			expect(() => scaleCoordinate("450,300@1024", 900, 600)).toThrow("Invalid coordinate format")
-			expect(() => scaleCoordinate("invalid", 900, 600)).toThrow("Invalid coordinate format")
-		})
-
-		it("should handle rounding correctly", () => {
-			// Test rounding behavior
-			// 333 / 1000 * 900 = 299.7 -> rounds to 300
-			expect(scaleCoordinate("333,333@1000x1000", 900, 900)).toBe("300,300")
-
-			// 666 / 1000 * 900 = 599.4 -> rounds to 599
-			expect(scaleCoordinate("666,666@1000x1000", 900, 900)).toBe("599,599")
-
-			// 500 / 1000 * 900 = 450.0 -> rounds to 450
-			expect(scaleCoordinate("500,500@1000x1000", 900, 900)).toBe("450,450")
-		})
-	})
-})

+ 0 - 25
src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts

@@ -1,25 +0,0 @@
-import { browserActions } from "@roo-code/types"
-
-describe("Browser Action Screenshot", () => {
-	describe("browserActions array", () => {
-		it("should include screenshot action", () => {
-			expect(browserActions).toContain("screenshot")
-		})
-
-		it("should have screenshot as a valid browser action type", () => {
-			const allActions = [
-				"launch",
-				"click",
-				"hover",
-				"type",
-				"press",
-				"scroll_down",
-				"scroll_up",
-				"resize",
-				"close",
-				"screenshot",
-			]
-			expect(browserActions).toEqual(allActions)
-		})
-	})
-})

+ 0 - 160
src/core/tools/__tests__/ToolRepetitionDetector.spec.ts

@@ -403,166 +403,6 @@ describe("ToolRepetitionDetector", () => {
 		})
 	})
 
-	// ===== Browser Scroll Action Exclusion tests =====
-	describe("browser scroll action exclusion", () => {
-		it("should not count browser scroll_down actions as repetitions", () => {
-			const detector = new ToolRepetitionDetector(2)
-
-			// Create browser_action tool use with scroll_down
-			const scrollDownTool: ToolUse = {
-				type: "tool_use",
-				name: "browser_action" as ToolName,
-				params: { action: "scroll_down" },
-				partial: false,
-			}
-
-			// Should allow unlimited scroll_down actions
-			for (let i = 0; i < 10; i++) {
-				const result = detector.check(scrollDownTool)
-				expect(result.allowExecution).toBe(true)
-				expect(result.askUser).toBeUndefined()
-			}
-		})
-
-		it("should not count browser scroll_up actions as repetitions", () => {
-			const detector = new ToolRepetitionDetector(2)
-
-			// Create browser_action tool use with scroll_up
-			const scrollUpTool: ToolUse = {
-				type: "tool_use",
-				name: "browser_action" as ToolName,
-				params: { action: "scroll_up" },
-				partial: false,
-			}
-
-			// Should allow unlimited scroll_up actions
-			for (let i = 0; i < 10; i++) {
-				const result = detector.check(scrollUpTool)
-				expect(result.allowExecution).toBe(true)
-				expect(result.askUser).toBeUndefined()
-			}
-		})
-
-		it("should not count alternating scroll_down and scroll_up as repetitions", () => {
-			const detector = new ToolRepetitionDetector(2)
-
-			const scrollDownTool: ToolUse = {
-				type: "tool_use",
-				name: "browser_action" as ToolName,
-				params: { action: "scroll_down" },
-				partial: false,
-			}
-
-			const scrollUpTool: ToolUse = {
-				type: "tool_use",
-				name: "browser_action" as ToolName,
-				params: { action: "scroll_up" },
-				partial: false,
-			}
-
-			// Alternate between scroll_down and scroll_up
-			for (let i = 0; i < 5; i++) {
-				let result = detector.check(scrollDownTool)
-				expect(result.allowExecution).toBe(true)
-				expect(result.askUser).toBeUndefined()
-
-				result = detector.check(scrollUpTool)
-				expect(result.allowExecution).toBe(true)
-				expect(result.askUser).toBeUndefined()
-			}
-		})
-
-		it("should still apply repetition detection to other browser_action types", () => {
-			const detector = new ToolRepetitionDetector(2)
-
-			// Create browser_action tool use with click action
-			const clickTool: ToolUse = {
-				type: "tool_use",
-				name: "browser_action" as ToolName,
-				params: { action: "click", coordinate: "[100, 200]" },
-				partial: false,
-			}
-
-			// First call allowed
-			expect(detector.check(clickTool).allowExecution).toBe(true)
-
-			// Second call allowed
-			expect(detector.check(clickTool).allowExecution).toBe(true)
-
-			// Third identical call should be blocked (limit is 2)
-			const result = detector.check(clickTool)
-			expect(result.allowExecution).toBe(false)
-			expect(result.askUser).toBeDefined()
-		})
-
-		it("should still apply repetition detection to non-browser tools", () => {
-			const detector = new ToolRepetitionDetector(2)
-
-			const readFileTool = createToolUse("read_file", "read_file", { path: "test.txt" })
-
-			// First call allowed
-			expect(detector.check(readFileTool).allowExecution).toBe(true)
-
-			// Second call allowed
-			expect(detector.check(readFileTool).allowExecution).toBe(true)
-
-			// Third identical call should be blocked (limit is 2)
-			const result = detector.check(readFileTool)
-			expect(result.allowExecution).toBe(false)
-			expect(result.askUser).toBeDefined()
-		})
-
-		it("should not interfere with repetition detection of other tools when scroll actions are interspersed", () => {
-			const detector = new ToolRepetitionDetector(2)
-
-			const scrollTool: ToolUse = {
-				type: "tool_use",
-				name: "browser_action" as ToolName,
-				params: { action: "scroll_down" },
-				partial: false,
-			}
-
-			const otherTool = createToolUse("execute_command", "execute_command", { command: "ls" })
-
-			// First execute_command
-			expect(detector.check(otherTool).allowExecution).toBe(true)
-
-			// Scroll actions in between (should not affect counter)
-			expect(detector.check(scrollTool).allowExecution).toBe(true)
-			expect(detector.check(scrollTool).allowExecution).toBe(true)
-
-			// Second execute_command
-			expect(detector.check(otherTool).allowExecution).toBe(true)
-
-			// More scroll actions
-			expect(detector.check(scrollTool).allowExecution).toBe(true)
-
-			// Third execute_command should be blocked
-			const result = detector.check(otherTool)
-			expect(result.allowExecution).toBe(false)
-			expect(result.askUser).toBeDefined()
-		})
-
-		it("should handle browser_action with missing or invalid action parameter gracefully", () => {
-			const detector = new ToolRepetitionDetector(2)
-
-			// Browser action without action parameter
-			const noActionTool: ToolUse = {
-				type: "tool_use",
-				name: "browser_action" as ToolName,
-				params: {},
-				partial: false,
-			}
-
-			// Should apply normal repetition detection
-			expect(detector.check(noActionTool).allowExecution).toBe(true)
-			expect(detector.check(noActionTool).allowExecution).toBe(true)
-			const result = detector.check(noActionTool)
-			expect(result.allowExecution).toBe(false)
-			expect(result.askUser).toBeDefined()
-		})
-	})
-
 	// ===== Native Protocol (nativeArgs) tests =====
 	describe("native protocol with nativeArgs", () => {
 		it("should differentiate read_file calls with different files in nativeArgs", () => {

+ 7 - 11
src/core/tools/__tests__/validateToolUse.spec.ts

@@ -30,12 +30,8 @@ describe("mode-validator", () => {
 
 		describe("architect mode", () => {
 			it("allows configured tools", () => {
-				// Architect mode has read, browser, and mcp groups
-				const architectTools = [
-					...TOOL_GROUPS.read.tools,
-					...TOOL_GROUPS.browser.tools,
-					...TOOL_GROUPS.mcp.tools,
-				]
+				// Architect mode has read and mcp groups
+				const architectTools = [...TOOL_GROUPS.read.tools, ...TOOL_GROUPS.mcp.tools]
 				architectTools.forEach((tool) => {
 					expect(isToolAllowedForMode(tool, architectMode, [])).toBe(true)
 				})
@@ -44,8 +40,8 @@ describe("mode-validator", () => {
 
 		describe("ask mode", () => {
 			it("allows configured tools", () => {
-				// Ask mode has read, browser, and mcp groups
-				const askTools = [...TOOL_GROUPS.read.tools, ...TOOL_GROUPS.browser.tools, ...TOOL_GROUPS.mcp.tools]
+				// Ask mode has read and mcp groups
+				const askTools = [...TOOL_GROUPS.read.tools, ...TOOL_GROUPS.mcp.tools]
 				askTools.forEach((tool) => {
 					expect(isToolAllowedForMode(tool, askMode, [])).toBe(true)
 				})
@@ -211,7 +207,7 @@ describe("mode-validator", () => {
 		})
 
 		it("blocks tool when disabledTools is converted to toolRequirements", () => {
-			const disabledTools = ["execute_command", "browser_action"]
+			const disabledTools = ["execute_command", "search_files"]
 			const toolRequirements = disabledTools.reduce(
 				(acc: Record<string, boolean>, tool: string) => {
 					acc[tool] = false
@@ -223,8 +219,8 @@ describe("mode-validator", () => {
 			expect(() => validateToolUse("execute_command", codeMode, [], toolRequirements)).toThrow(
 				'Tool "execute_command" is not allowed in code mode.',
 			)
-			expect(() => validateToolUse("browser_action", codeMode, [], toolRequirements)).toThrow(
-				'Tool "browser_action" is not allowed in code mode.',
+			expect(() => validateToolUse("search_files", codeMode, [], toolRequirements)).toThrow(
+				'Tool "search_files" is not allowed in code mode.',
 			)
 		})
 

+ 0 - 310
src/core/webview/BrowserSessionPanelManager.ts

@@ -1,310 +0,0 @@
-import * as vscode from "vscode"
-import type { ClineMessage } from "@roo-code/types"
-import { getUri } from "./getUri"
-import { getNonce } from "./getNonce"
-import type { ClineProvider } from "./ClineProvider"
-import { webviewMessageHandler } from "./webviewMessageHandler"
-
-export class BrowserSessionPanelManager {
-	private static instances: WeakMap<ClineProvider, BrowserSessionPanelManager> = new WeakMap()
-	private panel: vscode.WebviewPanel | undefined
-	private disposables: vscode.Disposable[] = []
-	private isReady: boolean = false
-	private pendingUpdate?: { messages: ClineMessage[]; isActive: boolean }
-	private pendingNavigateIndex?: number
-	private userManuallyClosedPanel: boolean = false
-
-	private constructor(private readonly provider: ClineProvider) {}
-
-	/**
-	 * Get or create a BrowserSessionPanelManager instance for the given provider
-	 */
-	public static getInstance(provider: ClineProvider): BrowserSessionPanelManager {
-		let instance = BrowserSessionPanelManager.instances.get(provider)
-		if (!instance) {
-			instance = new BrowserSessionPanelManager(provider)
-			BrowserSessionPanelManager.instances.set(provider, instance)
-		}
-		return instance
-	}
-
-	/**
-	 * Show the browser session panel, creating it if necessary
-	 */
-	public async show(): Promise<void> {
-		await this.createOrShowPanel()
-
-		// Send initial browser session data
-		const task = this.provider.getCurrentTask()
-		if (task) {
-			const messages = task.clineMessages || []
-			const browserSessionStartIndex = messages.findIndex(
-				(m) =>
-					m.ask === "browser_action_launch" ||
-					(m.say === "browser_session_status" && m.text?.includes("opened")),
-			)
-			const browserSessionMessages =
-				browserSessionStartIndex !== -1 ? messages.slice(browserSessionStartIndex) : []
-			const isBrowserSessionActive = task.browserSession?.isSessionActive() ?? false
-
-			await this.updateBrowserSession(browserSessionMessages, isBrowserSessionActive)
-		}
-	}
-
-	private async createOrShowPanel(): Promise<void> {
-		// If panel already exists, show it
-		if (this.panel) {
-			this.panel.reveal(vscode.ViewColumn.One)
-			return
-		}
-
-		const extensionUri = this.provider.context.extensionUri
-		const extensionMode = this.provider.context.extensionMode
-
-		// Create new panel
-		this.panel = vscode.window.createWebviewPanel("roo.browserSession", "Browser Session", vscode.ViewColumn.One, {
-			enableScripts: true,
-			retainContextWhenHidden: true,
-			localResourceRoots: [extensionUri],
-		})
-
-		// Set up the webview's HTML content
-		this.panel.webview.html =
-			extensionMode === vscode.ExtensionMode.Development
-				? await this.getHMRHtmlContent(this.panel.webview, extensionUri)
-				: this.getHtmlContent(this.panel.webview, extensionUri)
-
-		// Wire message channel for this panel (state handshake + actions)
-		this.panel.webview.onDidReceiveMessage(
-			async (message: any) => {
-				try {
-					// Let the shared handler process commands that work for any webview
-					if (message?.type) {
-						await webviewMessageHandler(this.provider as any, message)
-					}
-					// Panel-specific readiness and initial state
-					if (message?.type === "webviewDidLaunch") {
-						this.isReady = true
-						// Send full extension state to this panel (the sidebar postState targets the main webview)
-						const state = await (this.provider as any).getStateToPostToWebview?.()
-						if (state) {
-							await this.panel?.webview.postMessage({ type: "state", state })
-						}
-						// Flush any pending browser session update queued before readiness
-						if (this.pendingUpdate) {
-							await this.updateBrowserSession(this.pendingUpdate.messages, this.pendingUpdate.isActive)
-							this.pendingUpdate = undefined
-						}
-						// Flush any pending navigation request queued before readiness
-						if (this.pendingNavigateIndex !== undefined) {
-							await this.navigateToStep(this.pendingNavigateIndex)
-							this.pendingNavigateIndex = undefined
-						}
-					}
-				} catch (err) {
-					console.error("[BrowserSessionPanel] onDidReceiveMessage error:", err)
-				}
-			},
-			undefined,
-			this.disposables,
-		)
-
-		// Handle panel disposal - track that user closed it manually
-		this.panel.onDidDispose(
-			() => {
-				// Mark that user manually closed the panel (unless we're programmatically disposing)
-				if (this.panel) {
-					this.userManuallyClosedPanel = true
-				}
-				this.panel = undefined
-				this.dispose()
-			},
-			null,
-			this.disposables,
-		)
-	}
-
-	public async updateBrowserSession(messages: ClineMessage[], isBrowserSessionActive: boolean): Promise<void> {
-		if (!this.panel) {
-			return
-		}
-		// If the panel isn't ready yet, queue the latest snapshot to post after handshake
-		if (!this.isReady) {
-			this.pendingUpdate = { messages, isActive: isBrowserSessionActive }
-			return
-		}
-
-		await this.panel.webview.postMessage({
-			type: "browserSessionUpdate",
-			browserSessionMessages: messages,
-			isBrowserSessionActive,
-		})
-	}
-
-	/**
-	 * Navigate the Browser Session panel to a specific step index.
-	 * If the panel isn't ready yet, queue the navigation to run after handshake.
-	 */
-	public async navigateToStep(stepIndex: number): Promise<void> {
-		if (!this.panel) {
-			return
-		}
-		if (!this.isReady) {
-			this.pendingNavigateIndex = stepIndex
-			return
-		}
-
-		await this.panel.webview.postMessage({
-			type: "browserSessionNavigate",
-			stepIndex,
-		})
-	}
-
-	/**
-	 * Reset the manual close flag (call this when a new browser session launches)
-	 */
-	public resetManualCloseFlag(): void {
-		this.userManuallyClosedPanel = false
-	}
-
-	/**
-	 * Check if auto-opening should be allowed (not manually closed by user)
-	 */
-	public shouldAllowAutoOpen(): boolean {
-		return !this.userManuallyClosedPanel
-	}
-
-	/**
-	 * Whether the Browser Session panel is currently open.
-	 */
-	public isOpen(): boolean {
-		return !!this.panel
-	}
-
-	/**
-	 * Toggle the Browser Session panel visibility.
-	 * - If open: closes it
-	 * - If closed: opens it and sends initial session snapshot
-	 */
-	public async toggle(): Promise<void> {
-		if (this.panel) {
-			this.dispose()
-		} else {
-			await this.show()
-		}
-	}
-
-	public dispose(): void {
-		// Clear the panel reference before disposing to prevent marking as manual close
-		const panelToDispose = this.panel
-		this.panel = undefined
-
-		while (this.disposables.length) {
-			const disposable = this.disposables.pop()
-			if (disposable) {
-				disposable.dispose()
-			}
-		}
-		try {
-			panelToDispose?.dispose()
-		} catch {}
-		this.isReady = false
-		this.pendingUpdate = undefined
-	}
-
-	private async getHMRHtmlContent(webview: vscode.Webview, extensionUri: vscode.Uri): Promise<string> {
-		const fs = require("fs")
-		const path = require("path")
-		let localPort = "5173"
-
-		try {
-			const portFilePath = path.resolve(__dirname, "../../.vite-port")
-			if (fs.existsSync(portFilePath)) {
-				localPort = fs.readFileSync(portFilePath, "utf8").trim()
-			}
-		} catch (err) {
-			console.error("[BrowserSessionPanel:Vite] Failed to read port file:", err)
-		}
-
-		const localServerUrl = `localhost:${localPort}`
-		const nonce = getNonce()
-
-		const stylesUri = getUri(webview, extensionUri, ["webview-ui", "build", "assets", "index.css"])
-		const codiconsUri = getUri(webview, extensionUri, ["assets", "codicons", "codicon.css"])
-
-		const scriptUri = `http://${localServerUrl}/src/browser-panel.tsx`
-
-		const reactRefresh = `
-			<script nonce="${nonce}" type="module">
-				import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
-				RefreshRuntime.injectIntoGlobalHook(window)
-				window.$RefreshReg$ = () => {}
-				window.$RefreshSig$ = () => (type) => type
-				window.__vite_plugin_react_preamble_installed__ = true
-			</script>
-		`
-
-		const csp = [
-			"default-src 'none'",
-			`font-src ${webview.cspSource} data:`,
-			`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl}`,
-			`img-src ${webview.cspSource} data:`,
-			`script-src 'unsafe-eval' ${webview.cspSource} http://${localServerUrl} 'nonce-${nonce}'`,
-			`connect-src ${webview.cspSource} ws://${localServerUrl} http://${localServerUrl}`,
-		]
-
-		return `
-			<!DOCTYPE html>
-			<html lang="en">
-				<head>
-					<meta charset="utf-8">
-					<meta name="viewport" content="width=device-width,initial-scale=1">
-					<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
-					<link rel="stylesheet" type="text/css" href="${stylesUri}">
-					<link href="${codiconsUri}" rel="stylesheet" />
-					<title>Browser Session</title>
-				</head>
-				<body>
-					<div id="root"></div>
-					${reactRefresh}
-					<script type="module" src="${scriptUri}"></script>
-				</body>
-			</html>
-		`
-	}
-
-	private getHtmlContent(webview: vscode.Webview, extensionUri: vscode.Uri): string {
-		const stylesUri = getUri(webview, extensionUri, ["webview-ui", "build", "assets", "index.css"])
-		const scriptUri = getUri(webview, extensionUri, ["webview-ui", "build", "assets", "browser-panel.js"])
-		const codiconsUri = getUri(webview, extensionUri, ["assets", "codicons", "codicon.css"])
-
-		const nonce = getNonce()
-
-		const csp = [
-			"default-src 'none'",
-			`font-src ${webview.cspSource} data:`,
-			`style-src ${webview.cspSource} 'unsafe-inline'`,
-			`img-src ${webview.cspSource} data:`,
-			`script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}'`,
-			`connect-src ${webview.cspSource}`,
-		]
-
-		return `
-			<!DOCTYPE html>
-			<html lang="en">
-				<head>
-					<meta charset="utf-8">
-					<meta name="viewport" content="width=device-width,initial-scale=1">
-					<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
-					<link rel="stylesheet" type="text/css" href="${stylesUri}">
-					<link href="${codiconsUri}" rel="stylesheet" />
-					<title>Browser Session</title>
-				</head>
-				<body>
-					<div id="root"></div>
-					<script nonce="${nonce}" type="module" src="${scriptUri}"></script>
-				</body>
-			</html>
-		`
-	}
-}

+ 0 - 27
src/core/webview/ClineProvider.ts

@@ -2091,7 +2091,6 @@ export class ClineProvider
 			alwaysAllowExecute,
 			allowedCommands,
 			deniedCommands,
-			alwaysAllowBrowser,
 			alwaysAllowMcp,
 			alwaysAllowModeSwitch,
 			alwaysAllowSubtasks,
@@ -2106,11 +2105,6 @@ export class ClineProvider
 			checkpointTimeout,
 			taskHistory,
 			soundVolume,
-			browserViewportSize,
-			screenshotQuality,
-			remoteBrowserHost,
-			remoteBrowserEnabled,
-			cachedChromeHostUrl,
 			writeDelayMs,
 			terminalShellIntegrationTimeout,
 			terminalShellIntegrationDisabled,
@@ -2133,7 +2127,6 @@ export class ClineProvider
 			experiments,
 			maxOpenTabsContext,
 			maxWorkspaceFiles,
-			browserToolEnabled,
 			disabledTools,
 			telemetrySetting,
 			showRooIgnoredFiles,
@@ -2168,7 +2161,6 @@ export class ClineProvider
 			openRouterImageApiKey,
 			openRouterImageGenerationSelectedModel,
 			featureRoomoteControlEnabled,
-			isBrowserSessionActive,
 			lockApiConfigAcrossModes,
 		} = await this.getState()
 
@@ -2210,11 +2202,9 @@ export class ClineProvider
 			alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
 			alwaysAllowWriteProtected: alwaysAllowWriteProtected ?? false,
 			alwaysAllowExecute: alwaysAllowExecute ?? false,
-			alwaysAllowBrowser: alwaysAllowBrowser ?? false,
 			alwaysAllowMcp: alwaysAllowMcp ?? false,
 			alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
 			alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
-			isBrowserSessionActive,
 			allowedMaxRequests,
 			allowedMaxCost,
 			autoCondenseContext: autoCondenseContext ?? true,
@@ -2239,11 +2229,6 @@ export class ClineProvider
 			allowedCommands: mergedAllowedCommands,
 			deniedCommands: mergedDeniedCommands,
 			soundVolume: soundVolume ?? 0.5,
-			browserViewportSize: browserViewportSize ?? "900x600",
-			screenshotQuality: screenshotQuality ?? 75,
-			remoteBrowserHost,
-			remoteBrowserEnabled: remoteBrowserEnabled ?? false,
-			cachedChromeHostUrl: cachedChromeHostUrl,
 			writeDelayMs: writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
 			terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
 			terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? true,
@@ -2268,7 +2253,6 @@ export class ClineProvider
 			maxOpenTabsContext: maxOpenTabsContext ?? 20,
 			maxWorkspaceFiles: maxWorkspaceFiles ?? 200,
 			cwd,
-			browserToolEnabled: browserToolEnabled ?? true,
 			disabledTools,
 			telemetrySetting,
 			telemetryKey,
@@ -2442,9 +2426,6 @@ export class ClineProvider
 			)
 		}
 
-		// Get actual browser session state
-		const isBrowserSessionActive = this.getCurrentTask()?.browserSession?.isSessionActive() ?? false
-
 		// Return the same structure as before.
 		return {
 			apiConfiguration: providerSettings,
@@ -2457,12 +2438,10 @@ export class ClineProvider
 			alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
 			alwaysAllowWriteProtected: stateValues.alwaysAllowWriteProtected ?? false,
 			alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
-			alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
 			alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
 			alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
 			alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
 			alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false,
-			isBrowserSessionActive,
 			followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000,
 			diagnosticsEnabled: stateValues.diagnosticsEnabled ?? true,
 			allowedMaxRequests: stateValues.allowedMaxRequests,
@@ -2478,11 +2457,6 @@ export class ClineProvider
 			enableCheckpoints: stateValues.enableCheckpoints ?? true,
 			checkpointTimeout: stateValues.checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
 			soundVolume: stateValues.soundVolume,
-			browserViewportSize: stateValues.browserViewportSize ?? "900x600",
-			screenshotQuality: stateValues.screenshotQuality ?? 75,
-			remoteBrowserHost: stateValues.remoteBrowserHost,
-			remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
-			cachedChromeHostUrl: stateValues.cachedChromeHostUrl as string | undefined,
 			writeDelayMs: stateValues.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
 			terminalShellIntegrationTimeout:
 				stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
@@ -2509,7 +2483,6 @@ export class ClineProvider
 			customModes,
 			maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
 			maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
-			browserToolEnabled: stateValues.browserToolEnabled ?? true,
 			disabledTools: stateValues.disabledTools,
 			telemetrySetting: stateValues.telemetrySetting || "unset",
 			showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? false,

+ 2 - 2
src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts

@@ -122,7 +122,7 @@ vi.mock("../../../shared/modes", () => {
 			slug: "code",
 			name: "Code Mode",
 			roleDefinition: "You are a code assistant",
-			groups: ["read", "edit", "browser"],
+			groups: ["read", "edit"],
 		},
 		{
 			slug: "architect",
@@ -171,7 +171,7 @@ vi.mock("../../../shared/modes", () => {
 			slug: "code",
 			name: "Code Mode",
 			roleDefinition: "You are a code assistant",
-			groups: ["read", "edit", "browser"],
+			groups: ["read", "edit"],
 		}),
 		defaultModeSlug: "code",
 	}

+ 6 - 176
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -78,34 +78,6 @@ vi.mock("@modelcontextprotocol/sdk/types.js", () => ({
 	},
 }))
 
-vi.mock("../../../services/browser/BrowserSession", () => ({
-	BrowserSession: vi.fn().mockImplementation(() => ({
-		testConnection: vi.fn().mockImplementation(async (url) => {
-			if (url === "http://localhost:9222") {
-				return {
-					success: true,
-					message: "Successfully connected to Chrome",
-					endpoint: "ws://localhost:9222/devtools/browser/123",
-				}
-			} else {
-				return {
-					success: false,
-					message: "Failed to connect to Chrome",
-					endpoint: undefined,
-				}
-			}
-		}),
-	})),
-}))
-
-vi.mock("../../../services/browser/browserDiscovery", () => ({
-	discoverChromeHostUrl: vi.fn().mockResolvedValue("http://localhost:9222"),
-	tryChromeHostUrl: vi.fn().mockImplementation(async (url) => {
-		return url === "http://localhost:9222"
-	}),
-	testBrowserConnection: vi.fn(),
-}))
-
 // Remove duplicate mock - it's already defined below.
 
 const mockAddCustomInstructions = vi.fn().mockResolvedValue("Combined instructions")
@@ -248,7 +220,7 @@ vi.mock("../../../shared/modes", () => ({
 			slug: "code",
 			name: "Code Mode",
 			roleDefinition: "You are a code assistant",
-			groups: ["read", "edit", "browser"],
+			groups: ["read", "edit"],
 		},
 		{
 			slug: "architect",
@@ -267,7 +239,7 @@ vi.mock("../../../shared/modes", () => ({
 		slug: "code",
 		name: "Code Mode",
 		roleDefinition: "You are a code assistant",
-		groups: ["read", "edit", "browser"],
+		groups: ["read", "edit"],
 	}),
 	getGroupName: vi.fn().mockImplementation((group: string) => {
 		// Return appropriate group names for different tool groups
@@ -276,8 +248,6 @@ vi.mock("../../../shared/modes", () => ({
 				return "Read Tools"
 			case "edit":
 				return "Edit Tools"
-			case "browser":
-				return "Browser Tools"
 			case "mcp":
 				return "MCP Tools"
 			default:
@@ -537,7 +507,6 @@ describe("ClineProvider", () => {
 
 		const mockState: ExtensionState = {
 			version: "1.0.0",
-			isBrowserSessionActive: false,
 			clineMessages: [],
 			taskHistory: [],
 			shouldShowAnnouncement: false,
@@ -557,21 +526,18 @@ describe("ClineProvider", () => {
 			},
 			alwaysAllowWriteOutsideWorkspace: false,
 			alwaysAllowExecute: false,
-			alwaysAllowBrowser: false,
 			alwaysAllowMcp: false,
 			uriScheme: "vscode",
 			soundEnabled: false,
 			ttsEnabled: false,
 			enableCheckpoints: false,
 			writeDelayMs: 1000,
-			browserViewportSize: "900x600",
 			mcpEnabled: true,
 			mode: defaultModeSlug,
 			customModes: [],
 			experiments: experimentDefault,
 			maxOpenTabsContext: 20,
 			maxWorkspaceFiles: 200,
-			browserToolEnabled: true,
 			telemetrySetting: "unset",
 			showRooIgnoredFiles: false,
 			enableSubfolderRules: false,
@@ -804,7 +770,6 @@ describe("ClineProvider", () => {
 		expect(state).toHaveProperty("alwaysAllowReadOnly")
 		expect(state).toHaveProperty("alwaysAllowWrite")
 		expect(state).toHaveProperty("alwaysAllowExecute")
-		expect(state).toHaveProperty("alwaysAllowBrowser")
 		expect(state).toHaveProperty("taskHistory")
 		expect(state).toHaveProperty("soundEnabled")
 		expect(state).toHaveProperty("ttsEnabled")
@@ -1006,21 +971,6 @@ describe("ClineProvider", () => {
 		expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ id: "config-id-123" })
 	})
 
-	test("handles browserToolEnabled setting", async () => {
-		await provider.resolveWebviewView(mockWebviewView)
-		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
-
-		// Test browserToolEnabled
-		await messageHandler({ type: "updateSettings", updatedSettings: { browserToolEnabled: true } })
-		expect(mockContext.globalState.update).toHaveBeenCalledWith("browserToolEnabled", true)
-		expect(mockPostMessage).toHaveBeenCalled()
-
-		// Verify state includes browserToolEnabled
-		const state = await provider.getState()
-		expect(state).toHaveProperty("browserToolEnabled")
-		expect(state.browserToolEnabled).toBe(true) // Default value should be true
-	})
-
 	test("handles showRooIgnoredFiles setting", async () => {
 		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
@@ -1205,7 +1155,7 @@ describe("ClineProvider", () => {
 				{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
 				{ ts: 2000, type: "say", say: "tool" }, // Tool message
 				{ ts: 3000, type: "say", say: "text" }, // Message before delete
-				{ ts: 4000, type: "say", say: "browser_action" }, // Message to delete
+				{ ts: 4000, type: "say", say: "tool" }, // Message to delete
 				{ ts: 5000, type: "say", say: "user_feedback" }, // Next user message
 				{ ts: 6000, type: "say", say: "user_feedback" }, // Final message
 			] as ClineMessage[]
@@ -1293,7 +1243,7 @@ describe("ClineProvider", () => {
 				{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
 				{ ts: 2000, type: "say", say: "tool" }, // Tool message
 				{ ts: 3000, type: "say", say: "text" }, // Message before edit
-				{ ts: 4000, type: "say", say: "browser_action" }, // Message to edit
+				{ ts: 4000, type: "say", say: "tool" }, // Message to edit
 				{ ts: 5000, type: "say", say: "user_feedback" }, // Next user message
 				{ ts: 6000, type: "say", say: "user_feedback" }, // Final message
 			] as ClineMessage[]
@@ -1486,7 +1436,6 @@ describe("ClineProvider", () => {
 				},
 				mode: "architect",
 				mcpEnabled: false,
-				browserViewportSize: "900x600",
 				experiments: experimentDefault,
 			} as any)
 
@@ -1503,54 +1452,6 @@ describe("ClineProvider", () => {
 				}),
 			)
 		})
-
-		// Tests for browser tool support - simplified to focus on behavior
-		test("generates system prompt with different browser tool configurations", async () => {
-			await provider.resolveWebviewView(mockWebviewView)
-			const handler = getMessageHandler()
-
-			// Test 1: Browser tools enabled with compatible model and mode
-			vi.spyOn(provider, "getState").mockResolvedValueOnce({
-				apiConfiguration: {
-					apiProvider: "openrouter",
-				},
-				browserToolEnabled: true,
-				mode: "code", // code mode includes browser tool group
-				experiments: experimentDefault,
-			} as any)
-
-			await handler({ type: "getSystemPrompt", mode: "code" })
-
-			expect(mockPostMessage).toHaveBeenCalledWith(
-				expect.objectContaining({
-					type: "systemPrompt",
-					text: expect.any(String),
-					mode: "code",
-				}),
-			)
-
-			mockPostMessage.mockClear()
-
-			// Test 2: Browser tools disabled
-			vi.spyOn(provider, "getState").mockResolvedValueOnce({
-				apiConfiguration: {
-					apiProvider: "openrouter",
-				},
-				browserToolEnabled: false,
-				mode: "code",
-				experiments: experimentDefault,
-			} as any)
-
-			await handler({ type: "getSystemPrompt", mode: "code" })
-
-			expect(mockPostMessage).toHaveBeenCalledWith(
-				expect.objectContaining({
-					type: "systemPrompt",
-					text: expect.any(String),
-					mode: "code",
-				}),
-			)
-		})
 	})
 
 	describe("handleModeSwitch", () => {
@@ -1646,7 +1547,7 @@ describe("ClineProvider", () => {
 					slug: "code",
 					name: "Code Mode",
 					roleDefinition: "You are a code assistant",
-					groups: ["read", "edit", "browser"],
+					groups: ["read", "edit"],
 				}) // Subsequent calls return default mode
 
 			// Mock provider settings manager
@@ -1845,7 +1746,7 @@ describe("ClineProvider", () => {
 				slug: "code",
 				name: "Code Mode",
 				roleDefinition: "You are a code assistant",
-				groups: ["read", "edit", "browser"],
+				groups: ["read", "edit"],
 			})
 
 			// Mock provider settings manager to throw error
@@ -2096,77 +1997,6 @@ describe("ClineProvider", () => {
 			])
 		})
 	})
-
-	describe("browser connection features", () => {
-		beforeEach(async () => {
-			// Reset mocks
-			vi.clearAllMocks()
-			await provider.resolveWebviewView(mockWebviewView)
-		})
-
-		// These mocks are already defined at the top of the file
-
-		test("handles testBrowserConnection with provided URL", async () => {
-			// Get the message handler
-			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
-
-			// Test with valid URL
-			await messageHandler({
-				type: "testBrowserConnection",
-				text: "http://localhost:9222",
-			})
-
-			// Verify postMessage was called with success result
-			expect(mockPostMessage).toHaveBeenCalledWith(
-				expect.objectContaining({
-					type: "browserConnectionResult",
-					success: true,
-					text: expect.stringContaining("Successfully connected to Chrome"),
-				}),
-			)
-
-			// Reset mock
-			mockPostMessage.mockClear()
-
-			// Test with invalid URL
-			await messageHandler({
-				type: "testBrowserConnection",
-				text: "http://inlocalhost:9222",
-			})
-
-			// Verify postMessage was called with failure result
-			expect(mockPostMessage).toHaveBeenCalledWith(
-				expect.objectContaining({
-					type: "browserConnectionResult",
-					success: false,
-					text: expect.stringContaining("Failed to connect to Chrome"),
-				}),
-			)
-		})
-
-		test("handles testBrowserConnection with auto-discovery", async () => {
-			// Get the message handler
-			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
-
-			// Test auto-discovery (no URL provided)
-			await messageHandler({
-				type: "testBrowserConnection",
-			})
-
-			// Verify discoverChromeHostUrl was called
-			const { discoverChromeHostUrl } = await import("../../../services/browser/browserDiscovery")
-			expect(discoverChromeHostUrl).toHaveBeenCalled()
-
-			// Verify postMessage was called with success result
-			expect(mockPostMessage).toHaveBeenCalledWith(
-				expect.objectContaining({
-					type: "browserConnectionResult",
-					success: true,
-					text: expect.stringContaining("Auto-discovered and tested connection to Chrome"),
-				}),
-			)
-		})
-	})
 })
 
 describe("Project MCP Settings", () => {

+ 2 - 2
src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts

@@ -124,7 +124,7 @@ vi.mock("../../../shared/modes", () => ({
 			slug: "code",
 			name: "Code Mode",
 			roleDefinition: "You are a code assistant",
-			groups: ["read", "edit", "browser"],
+			groups: ["read", "edit"],
 		},
 		{
 			slug: "architect",
@@ -137,7 +137,7 @@ vi.mock("../../../shared/modes", () => ({
 		slug: "code",
 		name: "Code Mode",
 		roleDefinition: "You are a code assistant",
-		groups: ["read", "edit", "browser"],
+		groups: ["read", "edit"],
 	}),
 	defaultModeSlug: "code",
 }))

+ 2 - 2
src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts

@@ -126,7 +126,7 @@ vi.mock("../../../shared/modes", () => ({
 			slug: "code",
 			name: "Code Mode",
 			roleDefinition: "You are a code assistant",
-			groups: ["read", "edit", "browser"],
+			groups: ["read", "edit"],
 		},
 		{
 			slug: "architect",
@@ -139,7 +139,7 @@ vi.mock("../../../shared/modes", () => ({
 		slug: "code",
 		name: "Code Mode",
 		roleDefinition: "You are a code assistant",
-		groups: ["read", "edit", "browser"],
+		groups: ["read", "edit"],
 	}),
 	defaultModeSlug: "code",
 }))

+ 0 - 12
src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts

@@ -67,18 +67,6 @@ vi.mock("@modelcontextprotocol/sdk/types.js", () => ({
 	},
 }))
 
-vi.mock("../../../services/browser/BrowserSession", () => ({
-	BrowserSession: vi.fn().mockImplementation(() => ({
-		testConnection: vi.fn().mockResolvedValue({ success: false }),
-	})),
-}))
-
-vi.mock("../../../services/browser/browserDiscovery", () => ({
-	discoverChromeHostUrl: vi.fn().mockResolvedValue("http://localhost:9222"),
-	tryChromeHostUrl: vi.fn().mockResolvedValue(false),
-	testBrowserConnection: vi.fn(),
-}))
-
 vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
 	Client: vi.fn().mockImplementation(() => ({
 		connect: vi.fn().mockResolvedValue(undefined),

+ 0 - 79
src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts

@@ -1,79 +0,0 @@
-import { describe, test, expect, vi } from "vitest"
-
-// Module under test
-import { generateSystemPrompt } from "../generateSystemPrompt"
-
-// Mock SYSTEM_PROMPT to capture its third argument (browser capability flag)
-vi.mock("../../prompts/system", () => ({
-	SYSTEM_PROMPT: vi.fn(async (_ctx, _cwd, canUseBrowserTool: boolean) => {
-		// return a simple string to satisfy return type
-		return `SYSTEM_PROMPT:${canUseBrowserTool}`
-	}),
-}))
-
-// Mock API handler so we control model.info flags
-vi.mock("../../../api", () => ({
-	buildApiHandler: vi.fn((_config) => ({
-		getModel: () => ({
-			id: "mock-model",
-			info: {
-				supportsImages: true,
-				contextWindow: 200_000,
-				maxTokens: 8192,
-				supportsPromptCache: false,
-			},
-		}),
-	})),
-}))
-
-// Minimal mode utilities: provide a custom mode that includes the "browser" group
-const mockCustomModes = [
-	{
-		slug: "test-mode",
-		name: "Test Mode",
-		roleDefinition: "Test role",
-		description: "",
-		groups: ["browser"], // critical: include browser group
-	},
-]
-
-// Minimal ClineProvider stub
-function makeProviderStub() {
-	return {
-		cwd: "/tmp",
-		context: {} as any,
-		customModesManager: {
-			getCustomModes: async () => mockCustomModes,
-		},
-		getCurrentTask: () => ({
-			rooIgnoreController: { getInstructions: () => undefined },
-		}),
-		getMcpHub: () => undefined,
-		getSkillsManager: () => undefined,
-		// State must enable browser tool and provide apiConfiguration
-		getState: async () => ({
-			apiConfiguration: {
-				apiProvider: "openrouter", // not used by the test beyond handler creation
-			},
-			customModePrompts: undefined,
-			customInstructions: undefined,
-			browserViewportSize: "900x600",
-			mcpEnabled: false,
-			experiments: {},
-			browserToolEnabled: true, // critical: enabled in settings
-			language: "en",
-		}),
-	} as any
-}
-
-describe("generateSystemPrompt browser capability (supportsImages=true)", () => {
-	test("passes canUseBrowserTool=true when mode has browser group and setting enabled", async () => {
-		const provider = makeProviderStub()
-		const message = { mode: "test-mode" } as any
-
-		const result = await generateSystemPrompt(provider, message)
-
-		// SYSTEM_PROMPT mock encodes the boolean into the returned string
-		expect(result).toBe("SYSTEM_PROMPT:true")
-	})
-})

+ 6 - 22
src/core/webview/generateSystemPrompt.ts

@@ -1,6 +1,6 @@
 import * as vscode from "vscode"
 import { WebviewMessage } from "../../shared/WebviewMessage"
-import { defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
+import { defaultModeSlug } from "../../shared/modes"
 import { buildApiHandler } from "../../api"
 
 import { SYSTEM_PROMPT } from "../prompts/system"
@@ -14,10 +14,8 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
 		apiConfiguration,
 		customModePrompts,
 		customInstructions,
-		browserViewportSize,
 		mcpEnabled,
 		experiments,
-		browserToolEnabled,
 		language,
 		enableSubfolderRules,
 	} = await provider.getState()
@@ -31,36 +29,22 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
 
 	const rooIgnoreInstructions = provider.getCurrentTask()?.rooIgnoreController?.getInstructions()
 
-	// Determine if browser tools can be used based on model support, mode, and user settings
-	let modelInfo: any = undefined
-
-	// Create a temporary API handler to check if the model supports browser capability
-	// This avoids relying on an active Cline instance which might not exist during preview
+	// Create a temporary API handler to check model info for stealth mode.
+	// This avoids relying on an active Cline instance which might not exist during preview.
+	let modelInfo: { isStealthModel?: boolean } | undefined
 	try {
 		const tempApiHandler = buildApiHandler(apiConfiguration)
 		modelInfo = tempApiHandler.getModel().info
 	} catch (error) {
-		console.error("Error checking if model supports browser capability:", error)
+		console.error("Error fetching model info for system prompt preview:", error)
 	}
 
-	// Check if the current mode includes the browser tool group
-	const modeConfig = getModeBySlug(mode, customModes)
-	const modeSupportsBrowser = modeConfig?.groups.some((group) => getGroupName(group) === "browser") ?? false
-
-	// Check if model supports browser capability (images)
-	const modelSupportsBrowser = modelInfo && (modelInfo as any)?.supportsImages === true
-
-	// Only enable browser tools if the model supports it, the mode includes browser tools,
-	// and browser tools are enabled in settings
-	const canUseBrowserTool = modelSupportsBrowser && modeSupportsBrowser && (browserToolEnabled ?? true)
-
 	const systemPrompt = await SYSTEM_PROMPT(
 		provider.context,
 		cwd,
-		canUseBrowserTool,
+		false, // supportsComputerUse — browser removed
 		mcpEnabled ? provider.getMcpHub() : undefined,
 		diffStrategy,
-		browserViewportSize ?? "900x600",
 		mode,
 		customModePrompts,
 		customModes,

+ 0 - 102
src/core/webview/webviewMessageHandler.ts

@@ -29,7 +29,6 @@ import { type ApiMessage } from "../task-persistence/apiMessages"
 import { saveTaskMessages } from "../task-persistence"
 
 import { ClineProvider } from "./ClineProvider"
-import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager"
 import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
 import { generateErrorDiagnostics } from "./diagnosticsHandler"
 import {
@@ -52,7 +51,6 @@ import { openFile } from "../../integrations/misc/open-file"
 import { openImage, saveImage } from "../../integrations/misc/image-handler"
 import { selectImages } from "../../integrations/misc/process-images"
 import { getTheme } from "../../integrations/theme/getTheme"
-import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
 import { searchWorkspaceFiles } from "../../services/search/file-search"
 import { fileExistsAtPath } from "../../utils/fs"
 import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
@@ -1185,69 +1183,6 @@ export const webviewMessageHandler = async (
 			// Cancel any pending auto-approval timeout for the current task
 			provider.getCurrentTask()?.cancelAutoApprovalTimeout()
 			break
-		case "killBrowserSession":
-			{
-				const task = provider.getCurrentTask()
-				if (task?.browserSession) {
-					await task.browserSession.closeBrowser()
-					await provider.postStateToWebview()
-				}
-			}
-			break
-		case "openBrowserSessionPanel":
-			{
-				// Toggle the Browser Session panel (open if closed, close if open)
-				const panelManager = BrowserSessionPanelManager.getInstance(provider)
-				await panelManager.toggle()
-			}
-			break
-		case "showBrowserSessionPanelAtStep":
-			{
-				const panelManager = BrowserSessionPanelManager.getInstance(provider)
-
-				// If this is a launch action, reset the manual close flag
-				if (message.isLaunchAction) {
-					panelManager.resetManualCloseFlag()
-				}
-
-				// Show panel if:
-				// 1. Manual click (forceShow) - always show
-				// 2. Launch action - always show and reset flag
-				// 3. Auto-open for non-launch action - only if user hasn't manually closed
-				if (message.forceShow || message.isLaunchAction || panelManager.shouldAllowAutoOpen()) {
-					// Ensure panel is shown and populated
-					await panelManager.show()
-
-					// Navigate to a specific step if provided
-					// For launch actions: navigate to step 0
-					// For manual clicks: navigate to the clicked step
-					// For auto-opens of regular actions: don't navigate, let BrowserSessionRow's
-					// internal auto-advance logic handle it (only advances if user is on most recent step)
-					if (typeof message.stepIndex === "number" && message.stepIndex >= 0) {
-						await panelManager.navigateToStep(message.stepIndex)
-					}
-				}
-			}
-			break
-		case "refreshBrowserSessionPanel":
-			{
-				// Re-send the latest browser session snapshot to the panel
-				const panelManager = BrowserSessionPanelManager.getInstance(provider)
-				const task = provider.getCurrentTask()
-				if (task) {
-					const messages = task.clineMessages || []
-					const browserSessionStartIndex = messages.findIndex(
-						(m) =>
-							m.ask === "browser_action_launch" ||
-							(m.say === "browser_session_status" && m.text?.includes("opened")),
-					)
-					const browserSessionMessages =
-						browserSessionStartIndex !== -1 ? messages.slice(browserSessionStartIndex) : []
-					const isBrowserSessionActive = task.browserSession?.isSessionActive() ?? false
-					await panelManager.updateBrowserSession(browserSessionMessages, isBrowserSessionActive)
-				}
-			}
-			break
 		case "allowedCommands": {
 			// Validate and sanitize the commands array
 			const commands = message.commands ?? []
@@ -1476,43 +1411,6 @@ export const webviewMessageHandler = async (
 			stopTts()
 			break
 
-		case "testBrowserConnection":
-			// If no text is provided, try auto-discovery
-			if (!message.text) {
-				// Use testBrowserConnection for auto-discovery
-				const chromeHostUrl = await discoverChromeHostUrl()
-
-				if (chromeHostUrl) {
-					// Send the result back to the webview
-					await provider.postMessageToWebview({
-						type: "browserConnectionResult",
-						success: !!chromeHostUrl,
-						text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`,
-						values: { endpoint: chromeHostUrl },
-					})
-				} else {
-					await provider.postMessageToWebview({
-						type: "browserConnectionResult",
-						success: false,
-						text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
-					})
-				}
-			} else {
-				// Test the provided URL
-				const customHostUrl = message.text
-				const hostIsValid = await tryChromeHostUrl(message.text)
-
-				// Send the result back to the webview
-				await provider.postMessageToWebview({
-					type: "browserConnectionResult",
-					success: hostIsValid,
-					text: hostIsValid
-						? `Successfully connected to Chrome: ${customHostUrl}`
-						: "Failed to connect to Chrome",
-				})
-			}
-			break
-
 		case "updateVSCodeSetting": {
 			const { setting, value } = message
 

+ 0 - 2
src/package.json

@@ -512,8 +512,6 @@
 		"pretty-bytes": "^7.0.0",
 		"proper-lockfile": "^4.1.2",
 		"ps-tree": "^1.2.0",
-		"puppeteer-chromium-resolver": "^24.0.0",
-		"puppeteer-core": "^23.4.0",
 		"reconnecting-eventsource": "^1.6.4",
 		"safe-stable-stringify": "^2.5.0",
 		"sambanova-ai-provider": "^1.2.2",

+ 0 - 913
src/services/browser/BrowserSession.ts

@@ -1,913 +0,0 @@
-import * as vscode from "vscode"
-import * as fs from "fs/promises"
-import * as path from "path"
-import { Browser, Page, ScreenshotOptions, TimeoutError, launch, connect, KeyInput } from "puppeteer-core"
-// @ts-ignore
-import PCR from "puppeteer-chromium-resolver"
-import pWaitFor from "p-wait-for"
-import delay from "delay"
-
-import { type BrowserActionResult } from "@roo-code/types"
-
-import { fileExistsAtPath } from "../../utils/fs"
-
-import { discoverChromeHostUrl, tryChromeHostUrl } from "./browserDiscovery"
-
-// Timeout constants
-const BROWSER_NAVIGATION_TIMEOUT = 15_000 // 15 seconds
-
-interface PCRStats {
-	puppeteer: { launch: typeof launch }
-	executablePath: string
-}
-
-export class BrowserSession {
-	private context: vscode.ExtensionContext
-	private browser?: Browser
-	private page?: Page
-	private currentMousePosition?: string
-	private lastConnectionAttempt?: number
-	private isUsingRemoteBrowser: boolean = false
-	private onStateChange?: (isActive: boolean) => void
-
-	// Track last known viewport to surface in environment details
-	private lastViewportWidth?: number
-	private lastViewportHeight?: number
-
-	constructor(context: vscode.ExtensionContext, onStateChange?: (isActive: boolean) => void) {
-		this.context = context
-		this.onStateChange = onStateChange
-	}
-
-	private async ensureChromiumExists(): Promise<PCRStats> {
-		const globalStoragePath = this.context?.globalStorageUri?.fsPath
-		if (!globalStoragePath) {
-			throw new Error("Global storage uri is invalid")
-		}
-
-		const puppeteerDir = path.join(globalStoragePath, "puppeteer")
-		const dirExists = await fileExistsAtPath(puppeteerDir)
-		if (!dirExists) {
-			await fs.mkdir(puppeteerDir, { recursive: true })
-		}
-
-		// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
-		// if it does exist it will return the path to existing chromium
-		const stats: PCRStats = await PCR({
-			downloadPath: puppeteerDir,
-		})
-
-		return stats
-	}
-
-	/**
-	 * Gets the viewport size from global state or returns default
-	 */
-	private getViewport() {
-		const size = (this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600"
-		const [width, height] = size.split("x").map(Number)
-		return { width, height }
-	}
-
-	/**
-	 * Launches a local browser instance
-	 */
-	private async launchLocalBrowser(): Promise<void> {
-		console.log("Launching local browser")
-		const stats = await this.ensureChromiumExists()
-		this.browser = await stats.puppeteer.launch({
-			args: [
-				"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
-			],
-			executablePath: stats.executablePath,
-			defaultViewport: this.getViewport(),
-			// headless: false,
-		})
-		this.isUsingRemoteBrowser = false
-	}
-
-	/**
-	 * Connects to a browser using a WebSocket URL
-	 */
-	private async connectWithChromeHostUrl(chromeHostUrl: string): Promise<boolean> {
-		try {
-			this.browser = await connect({
-				browserURL: chromeHostUrl,
-				defaultViewport: this.getViewport(),
-			})
-
-			// Cache the successful endpoint
-			console.log(`Connected to remote browser at ${chromeHostUrl}`)
-			this.context.globalState.update("cachedChromeHostUrl", chromeHostUrl)
-			this.lastConnectionAttempt = Date.now()
-			this.isUsingRemoteBrowser = true
-
-			return true
-		} catch (error) {
-			console.log(`Failed to connect using WebSocket endpoint: ${error}`)
-			return false
-		}
-	}
-
-	/**
-	 * Attempts to connect to a remote browser using various methods
-	 * Returns true if connection was successful, false otherwise
-	 */
-	private async connectToRemoteBrowser(): Promise<boolean> {
-		let remoteBrowserHost = this.context.globalState.get("remoteBrowserHost") as string | undefined
-		let reconnectionAttempted = false
-
-		// Try to connect with cached endpoint first if it exists and is recent (less than 1 hour old)
-		const cachedChromeHostUrl = this.context.globalState.get("cachedChromeHostUrl") as string | undefined
-		if (cachedChromeHostUrl && this.lastConnectionAttempt && Date.now() - this.lastConnectionAttempt < 3_600_000) {
-			console.log(`Attempting to connect using cached Chrome Host Url: ${cachedChromeHostUrl}`)
-			if (await this.connectWithChromeHostUrl(cachedChromeHostUrl)) {
-				return true
-			}
-
-			console.log(`Failed to connect using cached Chrome Host Url: ${cachedChromeHostUrl}`)
-			// Clear the cached endpoint since it's no longer valid
-			this.context.globalState.update("cachedChromeHostUrl", undefined)
-
-			// User wants to give up after one reconnection attempt
-			if (remoteBrowserHost) {
-				reconnectionAttempted = true
-			}
-		}
-
-		// If user provided a remote browser host, try to connect to it
-		else if (remoteBrowserHost && !reconnectionAttempted) {
-			console.log(`Attempting to connect to remote browser at ${remoteBrowserHost}`)
-			try {
-				const hostIsValid = await tryChromeHostUrl(remoteBrowserHost)
-
-				if (!hostIsValid) {
-					throw new Error("Could not find chromeHostUrl in the response")
-				}
-
-				console.log(`Found WebSocket endpoint: ${remoteBrowserHost}`)
-
-				if (await this.connectWithChromeHostUrl(remoteBrowserHost)) {
-					return true
-				}
-			} catch (error) {
-				console.error(`Failed to connect to remote browser: ${error}`)
-				// Fall back to auto-discovery if remote connection fails
-			}
-		}
-
-		try {
-			console.log("Attempting browser auto-discovery...")
-			const chromeHostUrl = await discoverChromeHostUrl()
-
-			if (chromeHostUrl && (await this.connectWithChromeHostUrl(chromeHostUrl))) {
-				return true
-			}
-		} catch (error) {
-			console.error(`Auto-discovery failed: ${error}`)
-			// Fall back to local browser if auto-discovery fails
-		}
-
-		return false
-	}
-
-	async launchBrowser(): Promise<void> {
-		console.log("launch browser called")
-
-		// Check if remote browser connection is enabled
-		const remoteBrowserEnabled = this.context.globalState.get("remoteBrowserEnabled") as boolean | undefined
-
-		if (!remoteBrowserEnabled) {
-			console.log("Launching local browser")
-			if (this.browser) {
-				// throw new Error("Browser already launched")
-				await this.closeBrowser() // this may happen when the model launches a browser again after having used it already before
-			} else {
-				// If browser wasn't open, just reset the state
-				this.resetBrowserState()
-			}
-			await this.launchLocalBrowser()
-		} else {
-			console.log("Connecting to remote browser")
-			// Remote browser connection is enabled
-			const remoteConnected = await this.connectToRemoteBrowser()
-
-			// If all remote connection attempts fail, fall back to local browser
-			if (!remoteConnected) {
-				console.log("Falling back to local browser")
-				await this.launchLocalBrowser()
-			}
-		}
-
-		// Notify that browser session is now active
-		if (this.browser && this.onStateChange) {
-			this.onStateChange(true)
-		}
-	}
-
-	/**
-	 * Closes the browser and resets browser state
-	 */
-	async closeBrowser(): Promise<BrowserActionResult> {
-		const wasActive = !!(this.browser || this.page)
-
-		if (wasActive) {
-			if (this.isUsingRemoteBrowser && this.browser) {
-				await this.browser.disconnect().catch(() => {})
-			} else {
-				await this.browser?.close().catch(() => {})
-			}
-			this.resetBrowserState()
-
-			// Notify that browser session is now inactive
-			if (this.onStateChange) {
-				this.onStateChange(false)
-			}
-		}
-		return {}
-	}
-
-	/**
-	 * Resets all browser state variables
-	 */
-	private resetBrowserState(): void {
-		this.browser = undefined
-		this.page = undefined
-		this.currentMousePosition = undefined
-		this.isUsingRemoteBrowser = false
-		this.lastViewportWidth = undefined
-		this.lastViewportHeight = undefined
-	}
-
-	async doAction(action: (page: Page) => Promise<void>): Promise<BrowserActionResult> {
-		if (!this.page) {
-			throw new Error(
-				"Cannot perform browser action: no active browser session. The browser must be launched first using the 'launch' action before other browser actions can be performed.",
-			)
-		}
-
-		const logs: string[] = []
-		let lastLogTs = Date.now()
-
-		const consoleListener = (msg: any) => {
-			if (msg.type() === "log") {
-				logs.push(msg.text())
-			} else {
-				logs.push(`[${msg.type()}] ${msg.text()}`)
-			}
-			lastLogTs = Date.now()
-		}
-
-		const errorListener = (err: Error) => {
-			logs.push(`[Page Error] ${err.toString()}`)
-			lastLogTs = Date.now()
-		}
-
-		// Add the listeners
-		this.page.on("console", consoleListener)
-		this.page.on("pageerror", errorListener)
-
-		try {
-			await action(this.page)
-		} catch (err) {
-			if (!(err instanceof TimeoutError)) {
-				logs.push(`[Error] ${err.toString()}`)
-			}
-		}
-
-		// Wait for console inactivity, with a timeout
-		await pWaitFor(() => Date.now() - lastLogTs >= 500, {
-			timeout: 3_000,
-			interval: 100,
-		}).catch(() => {})
-
-		// Draw cursor indicator if we have a cursor position
-		if (this.currentMousePosition) {
-			await this.drawCursorIndicator(this.page, this.currentMousePosition)
-		}
-
-		let options: ScreenshotOptions = {
-			encoding: "base64",
-
-			// clip: {
-			// 	x: 0,
-			// 	y: 0,
-			// 	width: 900,
-			// 	height: 600,
-			// },
-		}
-
-		let screenshotBase64 = await this.page.screenshot({
-			...options,
-			type: "webp",
-			quality: ((await this.context.globalState.get("screenshotQuality")) as number | undefined) ?? 75,
-		})
-		let screenshot = `data:image/webp;base64,${screenshotBase64}`
-
-		if (!screenshotBase64) {
-			console.log("webp screenshot failed, trying png")
-			screenshotBase64 = await this.page.screenshot({
-				...options,
-				type: "png",
-			})
-			screenshot = `data:image/png;base64,${screenshotBase64}`
-		}
-
-		if (!screenshotBase64) {
-			throw new Error("Failed to take screenshot.")
-		}
-
-		// Remove cursor indicator after taking screenshot
-		if (this.currentMousePosition) {
-			await this.removeCursorIndicator(this.page)
-		}
-
-		// this.page.removeAllListeners() <- causes the page to crash!
-		this.page.off("console", consoleListener)
-		this.page.off("pageerror", errorListener)
-
-		// Get actual viewport dimensions
-		const viewport = this.page.viewport()
-
-		// Persist last known viewport dimensions
-		this.lastViewportWidth = viewport?.width
-		this.lastViewportHeight = viewport?.height
-
-		return {
-			screenshot,
-			logs: logs.join("\n"),
-			currentUrl: this.page.url(),
-			currentMousePosition: this.currentMousePosition,
-			viewportWidth: viewport?.width,
-			viewportHeight: viewport?.height,
-		}
-	}
-
-	/**
-	 * Extract the root domain from a URL
-	 * e.g., http://localhost:3000/path -> localhost:3000
-	 * e.g., https://example.com/path -> example.com
-	 */
-	private getRootDomain(url: string): string {
-		try {
-			const urlObj = new URL(url)
-			// Remove www. prefix if present
-			return urlObj.host.replace(/^www\./, "")
-		} catch (error) {
-			// If URL parsing fails, return the original URL
-			return url
-		}
-	}
-
-	/**
-	 * Navigate to a URL with standard loading options
-	 */
-	private async navigatePageToUrl(page: Page, url: string): Promise<void> {
-		await page.goto(url, { timeout: BROWSER_NAVIGATION_TIMEOUT, waitUntil: ["domcontentloaded", "networkidle2"] })
-		await this.waitTillHTMLStable(page)
-	}
-
-	/**
-	 * Creates a new tab and navigates to the specified URL
-	 */
-	private async createNewTab(url: string): Promise<BrowserActionResult> {
-		if (!this.browser) {
-			throw new Error("Browser is not launched")
-		}
-
-		// Create a new page
-		const newPage = await this.browser.newPage()
-
-		// Set the new page as the active page
-		this.page = newPage
-
-		// Navigate to the URL
-		const result = await this.doAction(async (page) => {
-			await this.navigatePageToUrl(page, url)
-		})
-
-		return result
-	}
-
-	async navigateToUrl(url: string): Promise<BrowserActionResult> {
-		if (!this.browser) {
-			throw new Error("Browser is not launched")
-		}
-		// Remove trailing slash for comparison
-		const normalizedNewUrl = url.replace(/\/$/, "")
-
-		// Extract the root domain from the URL
-		const rootDomain = this.getRootDomain(normalizedNewUrl)
-
-		// Get all current pages
-		const pages = await this.browser.pages()
-
-		// Try to find a page with the same root domain
-		let existingPage: Page | undefined
-
-		for (const page of pages) {
-			try {
-				const pageUrl = page.url()
-				if (pageUrl && this.getRootDomain(pageUrl) === rootDomain) {
-					existingPage = page
-					break
-				}
-			} catch (error) {
-				// Skip pages that might have been closed or have errors
-				console.log(`Error checking page URL: ${error}`)
-				continue
-			}
-		}
-
-		if (existingPage) {
-			// Tab with the same root domain exists, switch to it
-			console.log(`Tab with domain ${rootDomain} already exists, switching to it`)
-
-			// Update the active page
-			this.page = existingPage
-			existingPage.bringToFront()
-
-			// Navigate to the new URL if it's different]
-			const currentUrl = existingPage.url().replace(/\/$/, "") // Remove trailing / if present
-			if (this.getRootDomain(currentUrl) === rootDomain && currentUrl !== normalizedNewUrl) {
-				console.log(`Navigating to new URL: ${normalizedNewUrl}`)
-				console.log(`Current URL: ${currentUrl}`)
-				console.log(`Root domain: ${this.getRootDomain(currentUrl)}`)
-				console.log(`New URL: ${normalizedNewUrl}`)
-				// Navigate to the new URL
-				return this.doAction(async (page) => {
-					await this.navigatePageToUrl(page, normalizedNewUrl)
-				})
-			} else {
-				console.log(`Tab with domain ${rootDomain} already exists, and URL is the same: ${normalizedNewUrl}`)
-				// URL is the same, just reload the page to ensure it's up to date
-				console.log(`Reloading page: ${normalizedNewUrl}`)
-				console.log(`Current URL: ${currentUrl}`)
-				console.log(`Root domain: ${this.getRootDomain(currentUrl)}`)
-				console.log(`New URL: ${normalizedNewUrl}`)
-				return this.doAction(async (page) => {
-					await page.reload({
-						timeout: BROWSER_NAVIGATION_TIMEOUT,
-						waitUntil: ["domcontentloaded", "networkidle2"],
-					})
-					await this.waitTillHTMLStable(page)
-				})
-			}
-		} else {
-			// No tab with this root domain exists, create a new one
-			console.log(`No tab with domain ${rootDomain} exists, creating a new one`)
-			return this.createNewTab(normalizedNewUrl)
-		}
-	}
-
-	// page.goto { waitUntil: "networkidle0" } may not ever resolve, and not waiting could return page content too early before js has loaded
-	// https://stackoverflow.com/questions/52497252/puppeteer-wait-until-page-is-completely-loaded/61304202#61304202
-	private async waitTillHTMLStable(page: Page, timeout = 5_000) {
-		const checkDurationMsecs = 500 // 1000
-		const maxChecks = timeout / checkDurationMsecs
-		let lastHTMLSize = 0
-		let checkCounts = 1
-		let countStableSizeIterations = 0
-		const minStableSizeIterations = 3
-
-		while (checkCounts++ <= maxChecks) {
-			let html = await page.content()
-			let currentHTMLSize = html.length
-
-			// let bodyHTMLSize = await page.evaluate(() => document.body.innerHTML.length)
-			console.log("last: ", lastHTMLSize, " <> curr: ", currentHTMLSize)
-
-			if (lastHTMLSize !== 0 && currentHTMLSize === lastHTMLSize) {
-				countStableSizeIterations++
-			} else {
-				countStableSizeIterations = 0 //reset the counter
-			}
-
-			if (countStableSizeIterations >= minStableSizeIterations) {
-				console.log("Page rendered fully...")
-				break
-			}
-
-			lastHTMLSize = currentHTMLSize
-			await delay(checkDurationMsecs)
-		}
-	}
-
-	/**
-	 * Force links and window.open to navigate in the same tab.
-	 * This makes clicks on anchors with target="_blank" stay in the current page
-	 * and also intercepts window.open so SPA/open-in-new-tab patterns don't spawn popups.
-	 */
-	private async forceLinksToSameTab(page: Page): Promise<void> {
-		try {
-			await page.evaluate(() => {
-				try {
-					// Ensure we only install once per document
-					if ((window as any).__ROO_FORCE_SAME_TAB__) return
-					;(window as any).__ROO_FORCE_SAME_TAB__ = true
-
-					// Override window.open to navigate current tab instead of creating a new one
-					const originalOpen = window.open
-					window.open = function (url: string | URL, target?: string, features?: string) {
-						try {
-							const href = typeof url === "string" ? url : String(url)
-							location.href = href
-						} catch {
-							// fall back to original if something unexpected occurs
-							try {
-								return originalOpen.apply(window, [url as any, "_self", features]) as any
-							} catch {}
-						}
-						return null as any
-					} as any
-
-					// Rewrite anchors that explicitly open new tabs
-					document.querySelectorAll('a[target="_blank"]').forEach((a) => {
-						a.setAttribute("target", "_self")
-					})
-
-					// Defensive capture: if an element still tries to open in a new tab, force same-tab
-					document.addEventListener(
-						"click",
-						(ev) => {
-							const el = (ev.target as HTMLElement | null)?.closest?.(
-								'a[target="_blank"]',
-							) as HTMLAnchorElement | null
-							if (el && el.href) {
-								ev.preventDefault()
-								try {
-									location.href = el.href
-								} catch {}
-							}
-						},
-						{ capture: true, passive: false },
-					)
-				} catch {
-					// no-op; forcing same-tab is best-effort
-				}
-			})
-		} catch {
-			// If evaluate fails (e.g., cross-origin/state), continue without breaking the action
-		}
-	}
-
-	/**
-	 * Handles mouse interaction with network activity monitoring
-	 */
-	private async handleMouseInteraction(
-		page: Page,
-		coordinate: string,
-		action: (x: number, y: number) => Promise<void>,
-	): Promise<void> {
-		const [x, y] = coordinate.split(",").map(Number)
-
-		// Force any new-tab behavior (target="_blank", window.open) to stay in the same tab
-		await this.forceLinksToSameTab(page)
-
-		// Set up network request monitoring
-		let hasNetworkActivity = false
-		const requestListener = () => {
-			hasNetworkActivity = true
-		}
-		page.on("request", requestListener)
-
-		// Perform the mouse action
-		await action(x, y)
-		this.currentMousePosition = coordinate
-
-		// Small delay to check if action triggered any network activity
-		await delay(100)
-
-		if (hasNetworkActivity) {
-			// If we detected network activity, wait for navigation/loading
-			await page
-				.waitForNavigation({
-					waitUntil: ["domcontentloaded", "networkidle2"],
-					timeout: BROWSER_NAVIGATION_TIMEOUT,
-				})
-				.catch(() => {})
-			await this.waitTillHTMLStable(page)
-		}
-
-		// Clean up listener
-		page.off("request", requestListener)
-	}
-
-	async click(coordinate: string): Promise<BrowserActionResult> {
-		return this.doAction(async (page) => {
-			await this.handleMouseInteraction(page, coordinate, async (x, y) => {
-				await page.mouse.click(x, y)
-			})
-		})
-	}
-
-	async type(text: string): Promise<BrowserActionResult> {
-		return this.doAction(async (page) => {
-			await page.keyboard.type(text)
-		})
-	}
-
-	async press(key: string): Promise<BrowserActionResult> {
-		return this.doAction(async (page) => {
-			// Parse key combinations (e.g., "Cmd+K", "Shift+Enter")
-			const parts = key.split("+").map((k) => k.trim())
-			const modifiers: string[] = []
-			let mainKey = parts[parts.length - 1]
-
-			// Identify modifiers
-			for (let i = 0; i < parts.length - 1; i++) {
-				const part = parts[i].toLowerCase()
-				if (part === "cmd" || part === "command" || part === "meta") {
-					modifiers.push("Meta")
-				} else if (part === "ctrl" || part === "control") {
-					modifiers.push("Control")
-				} else if (part === "shift") {
-					modifiers.push("Shift")
-				} else if (part === "alt" || part === "option") {
-					modifiers.push("Alt")
-				}
-			}
-
-			// Map common key aliases to Puppeteer KeyInput values
-			const mapping: Record<string, KeyInput | string> = {
-				esc: "Escape",
-				return: "Enter",
-				escape: "Escape",
-				enter: "Enter",
-				tab: "Tab",
-				space: "Space",
-				arrowup: "ArrowUp",
-				arrowdown: "ArrowDown",
-				arrowleft: "ArrowLeft",
-				arrowright: "ArrowRight",
-			}
-			mainKey = (mapping[mainKey.toLowerCase()] ?? mainKey) as string
-
-			// Avoid new-tab behavior from Enter on links/buttons
-			await this.forceLinksToSameTab(page)
-
-			// Track inflight requests so we can detect brief network bursts
-			let inflight = 0
-			const onRequest = () => {
-				inflight++
-			}
-			const onRequestDone = () => {
-				inflight = Math.max(0, inflight - 1)
-			}
-			page.on("request", onRequest)
-			page.on("requestfinished", onRequestDone)
-			page.on("requestfailed", onRequestDone)
-
-			// Start a short navigation wait in parallel; if no nav, it times out harmlessly
-			const HARD_CAP_MS = 3000
-			const navPromise = page
-				.waitForNavigation({
-					// domcontentloaded is enough to confirm a submit navigated
-					waitUntil: ["domcontentloaded"],
-					timeout: HARD_CAP_MS,
-				})
-				.catch(() => undefined)
-
-			// Press key combination
-			if (modifiers.length > 0) {
-				// Hold down modifiers
-				for (const modifier of modifiers) {
-					await page.keyboard.down(modifier as KeyInput)
-				}
-
-				// Press main key
-				await page.keyboard.press(mainKey as KeyInput)
-
-				// Release modifiers
-				for (const modifier of modifiers) {
-					await page.keyboard.up(modifier as KeyInput)
-				}
-			} else {
-				// Single key press
-				await page.keyboard.press(mainKey as KeyInput)
-			}
-
-			// Give time for any requests to kick off
-			await delay(120)
-
-			// Hard-cap the wait to avoid UI hangs
-			await Promise.race([
-				navPromise,
-				pWaitFor(() => inflight === 0, { timeout: HARD_CAP_MS, interval: 100 }).catch(() => {}),
-				delay(HARD_CAP_MS),
-			])
-
-			// Stabilize DOM briefly before capturing screenshot (shorter cap)
-			await this.waitTillHTMLStable(page, 2_000)
-
-			// Cleanup
-			page.off("request", onRequest)
-			page.off("requestfinished", onRequestDone)
-			page.off("requestfailed", onRequestDone)
-		})
-	}
-
-	/**
-	 * Scrolls the page by the specified amount
-	 */
-	private async scrollPage(page: Page, direction: "up" | "down"): Promise<void> {
-		const { height } = this.getViewport()
-		const scrollAmount = direction === "down" ? height : -height
-
-		await page.evaluate((scrollHeight) => {
-			window.scrollBy({
-				top: scrollHeight,
-				behavior: "auto",
-			})
-		}, scrollAmount)
-
-		await delay(300)
-	}
-
-	async scrollDown(): Promise<BrowserActionResult> {
-		return this.doAction(async (page) => {
-			await this.scrollPage(page, "down")
-		})
-	}
-
-	async scrollUp(): Promise<BrowserActionResult> {
-		return this.doAction(async (page) => {
-			await this.scrollPage(page, "up")
-		})
-	}
-
-	async hover(coordinate: string): Promise<BrowserActionResult> {
-		return this.doAction(async (page) => {
-			await this.handleMouseInteraction(page, coordinate, async (x, y) => {
-				await page.mouse.move(x, y)
-				// Small delay to allow any hover effects to appear
-				await delay(300)
-			})
-		})
-	}
-
-	async resize(size: string): Promise<BrowserActionResult> {
-		return this.doAction(async (page) => {
-			const [width, height] = size.split(",").map(Number)
-			const session = await page.createCDPSession()
-			await page.setViewport({ width, height })
-			const { windowId } = await session.send("Browser.getWindowForTarget")
-			await session.send("Browser.setWindowBounds", {
-				bounds: { width, height },
-				windowId,
-			})
-		})
-	}
-
-	/**
-	 * Determines image type from file extension
-	 */
-	private getImageTypeFromPath(filePath: string): "png" | "jpeg" | "webp" {
-		const ext = path.extname(filePath).toLowerCase()
-		if (ext === ".jpg" || ext === ".jpeg") return "jpeg"
-		if (ext === ".webp") return "webp"
-		return "png"
-	}
-
-	/**
-	 * Takes a screenshot and saves it to the specified file path.
-	 * @param filePath - The destination file path (relative to workspace)
-	 * @param cwd - Current working directory for resolving relative paths
-	 * @returns BrowserActionResult with screenshot data and saved file path
-	 * @throws Error if the resolved path escapes the workspace directory
-	 */
-	async saveScreenshot(filePath: string, cwd: string): Promise<BrowserActionResult> {
-		// Always resolve the path against the workspace root
-		const normalizedCwd = path.resolve(cwd)
-		const fullPath = path.resolve(cwd, filePath)
-
-		// Validate that the resolved path stays within the workspace (before calling doAction)
-		if (!fullPath.startsWith(normalizedCwd + path.sep) && fullPath !== normalizedCwd) {
-			throw new Error(
-				`Screenshot path "${filePath}" resolves to "${fullPath}" which is outside the workspace "${normalizedCwd}". ` +
-					`Paths must be relative to the workspace and cannot escape it.`,
-			)
-		}
-
-		return this.doAction(async (page) => {
-			// Ensure directory exists
-			await fs.mkdir(path.dirname(fullPath), { recursive: true })
-
-			// Determine image type from extension
-			const imageType = this.getImageTypeFromPath(filePath)
-
-			// Take screenshot directly to file (more efficient than base64 for file saving)
-			await page.screenshot({
-				path: fullPath,
-				type: imageType,
-				quality:
-					imageType === "png"
-						? undefined
-						: ((this.context.globalState.get("screenshotQuality") as number | undefined) ?? 75),
-			})
-		})
-	}
-
-	/**
-	 * Draws a cursor indicator on the page at the specified position
-	 */
-	private async drawCursorIndicator(page: Page, coordinate: string): Promise<void> {
-		const [x, y] = coordinate.split(",").map(Number)
-
-		try {
-			await page.evaluate(
-				(cursorX: number, cursorY: number) => {
-					// Create a cursor indicator element
-					const cursor = document.createElement("div")
-					cursor.id = "__roo_cursor_indicator__"
-					cursor.style.cssText = `
-						position: fixed;
-						left: ${cursorX}px;
-						top: ${cursorY}px;
-						width: 35px;
-						height: 35px;
-						pointer-events: none;
-						z-index: 2147483647;
-					`
-
-					// Create SVG cursor pointer
-					const svg = `
-						<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
-							<path d="M5 3L5 17L9 13L12 19L14 18L11 12L17 12L5 3Z"
-								  fill="white"
-								  stroke="black"
-								  stroke-width="1.5"/>
-							<path d="M5 3L5 17L9 13L12 19L14 18L11 12L17 12L5 3Z"
-								  fill="black"
-								  stroke="white"
-								  stroke-width=".5"/>
-						</svg>
-					`
-					cursor.innerHTML = svg
-
-					document.body.appendChild(cursor)
-				},
-				x,
-				y,
-			)
-		} catch (error) {
-			console.error("Failed to draw cursor indicator:", error)
-		}
-	}
-
-	/**
-	 * Removes the cursor indicator from the page
-	 */
-	private async removeCursorIndicator(page: Page): Promise<void> {
-		try {
-			await page.evaluate(() => {
-				const cursor = document.getElementById("__roo_cursor_indicator__")
-				if (cursor) {
-					cursor.remove()
-				}
-			})
-		} catch (error) {
-			console.error("Failed to remove cursor indicator:", error)
-		}
-	}
-
-	/**
-	 * Returns whether a browser session is currently active
-	 */
-	isSessionActive(): boolean {
-		return !!(this.browser && this.page)
-	}
-
-	/**
-	 * Returns the last known viewport size (if any)
-	 *
-	 * Prefer the live page viewport when available so we stay accurate after:
-	 * - browser_action resize
-	 * - manual window resizes (especially with remote browsers)
-	 *
-	 * Falls back to the configured default viewport when no prior information exists.
-	 */
-	getViewportSize(): { width?: number; height?: number } {
-		// If we have an active page, ask Puppeteer for the current viewport.
-		// This keeps us in sync with any resizes that happen outside of our own
-		// browser_action lifecycle (e.g. user dragging the window).
-		if (this.page) {
-			const vp = this.page.viewport()
-			if (vp?.width) this.lastViewportWidth = vp.width
-			if (vp?.height) this.lastViewportHeight = vp.height
-		}
-
-		// If we've ever observed a viewport, use that.
-		if (this.lastViewportWidth && this.lastViewportHeight) {
-			return {
-				width: this.lastViewportWidth,
-				height: this.lastViewportHeight,
-			}
-		}
-
-		// Otherwise fall back to the configured default so the tool can still
-		// operate before the first screenshot-based action has run.
-		const { width, height } = this.getViewport()
-		return { width, height }
-	}
-}

+ 0 - 143
src/services/browser/UrlContentFetcher.ts

@@ -1,143 +0,0 @@
-import * as vscode from "vscode"
-import * as fs from "fs/promises"
-import * as path from "path"
-import { Browser, Page, launch } from "puppeteer-core"
-import * as cheerio from "cheerio"
-import TurndownService from "turndown"
-// @ts-ignore
-import PCR from "puppeteer-chromium-resolver"
-import { fileExistsAtPath } from "../../utils/fs"
-import { serializeError } from "serialize-error"
-
-// Timeout constants
-const URL_FETCH_TIMEOUT = 30_000 // 30 seconds
-const URL_FETCH_FALLBACK_TIMEOUT = 20_000 // 20 seconds for fallback
-
-interface PCRStats {
-	puppeteer: { launch: typeof launch }
-	executablePath: string
-}
-
-export class UrlContentFetcher {
-	private context: vscode.ExtensionContext
-	private browser?: Browser
-	private page?: Page
-
-	constructor(context: vscode.ExtensionContext) {
-		this.context = context
-	}
-
-	private async ensureChromiumExists(): Promise<PCRStats> {
-		const globalStoragePath = this.context?.globalStorageUri?.fsPath
-		if (!globalStoragePath) {
-			throw new Error("Global storage uri is invalid")
-		}
-		const puppeteerDir = path.join(globalStoragePath, "puppeteer")
-		const dirExists = await fileExistsAtPath(puppeteerDir)
-		if (!dirExists) {
-			await fs.mkdir(puppeteerDir, { recursive: true })
-		}
-		// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
-		// if it does exist it will return the path to existing chromium
-		const stats: PCRStats = await PCR({
-			downloadPath: puppeteerDir,
-		})
-		return stats
-	}
-
-	async launchBrowser(): Promise<void> {
-		if (this.browser) {
-			return
-		}
-		const stats = await this.ensureChromiumExists()
-		const args = [
-			"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
-			"--disable-dev-shm-usage",
-			"--disable-accelerated-2d-canvas",
-			"--no-first-run",
-			"--disable-gpu",
-			"--disable-features=VizDisplayCompositor",
-		]
-		if (process.platform === "linux") {
-			// Fixes network errors on Linux hosts (see https://github.com/puppeteer/puppeteer/issues/8246)
-			args.push("--no-sandbox")
-		}
-		this.browser = await stats.puppeteer.launch({
-			args,
-			executablePath: stats.executablePath,
-		})
-		// (latest version of puppeteer does not add headless to user agent)
-		this.page = await this.browser?.newPage()
-
-		// Set additional page configurations to improve loading success
-		if (this.page) {
-			await this.page.setViewport({ width: 1280, height: 720 })
-			await this.page.setExtraHTTPHeaders({
-				"Accept-Language": "en-US,en;q=0.9",
-			})
-		}
-	}
-
-	async closeBrowser(): Promise<void> {
-		await this.browser?.close()
-		this.browser = undefined
-		this.page = undefined
-	}
-
-	// must make sure to call launchBrowser before and closeBrowser after using this
-	async urlToMarkdown(url: string): Promise<string> {
-		if (!this.browser || !this.page) {
-			throw new Error("Browser not initialized")
-		}
-		/*
-		- In Puppeteer, "networkidle2" waits until there are no more than 2 network connections for at least 500 ms (roughly equivalent to Playwright's "networkidle").
-		- "domcontentloaded" is when the basic DOM is loaded.
-		This should be sufficient for most doc sites.
-		*/
-		try {
-			await this.page.goto(url, {
-				timeout: URL_FETCH_TIMEOUT,
-				waitUntil: ["domcontentloaded", "networkidle2"],
-			})
-		} catch (error) {
-			// Use serialize-error to safely extract error information
-			const serializedError = serializeError(error)
-			const errorMessage = serializedError.message || String(error)
-			const errorName = serializedError.name
-
-			// Only retry for timeout or network-related errors
-			const shouldRetry =
-				errorMessage.includes("timeout") ||
-				errorMessage.includes("net::") ||
-				errorMessage.includes("NetworkError") ||
-				errorMessage.includes("ERR_") ||
-				errorName === "TimeoutError"
-
-			if (shouldRetry) {
-				// If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback
-				console.warn(
-					`Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`,
-				)
-				await this.page.goto(url, {
-					timeout: URL_FETCH_FALLBACK_TIMEOUT,
-					waitUntil: ["domcontentloaded"],
-				})
-			} else {
-				// For other errors, throw them as-is
-				throw error
-			}
-		}
-
-		const content = await this.page.content()
-
-		// use cheerio to parse and clean up the HTML
-		const $ = cheerio.load(content)
-		$("script, style, nav, footer, header").remove()
-
-		// convert cleaned HTML to markdown
-		const turndownService = new TurndownService()
-		const markdown = turndownService.turndown($.html())
-
-		return markdown
-	}
-}

+ 0 - 628
src/services/browser/__tests__/BrowserSession.spec.ts

@@ -1,628 +0,0 @@
-// npx vitest services/browser/__tests__/BrowserSession.spec.ts
-
-import * as path from "path"
-import { BrowserSession } from "../BrowserSession"
-import { discoverChromeHostUrl, tryChromeHostUrl } from "../browserDiscovery"
-
-// Mock dependencies
-vi.mock("vscode", () => ({
-	ExtensionContext: vi.fn(),
-	Uri: {
-		file: vi.fn((path) => ({ fsPath: path })),
-	},
-}))
-
-// Mock puppeteer-core
-vi.mock("puppeteer-core", () => {
-	const mockBrowser = {
-		newPage: vi.fn().mockResolvedValue({
-			goto: vi.fn().mockResolvedValue(undefined),
-			on: vi.fn(),
-			off: vi.fn(),
-			screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-			url: vi.fn().mockReturnValue("https://example.com"),
-		}),
-		pages: vi.fn().mockResolvedValue([]),
-		close: vi.fn().mockResolvedValue(undefined),
-		disconnect: vi.fn().mockResolvedValue(undefined),
-	}
-
-	return {
-		Browser: vi.fn(),
-		Page: vi.fn(),
-		TimeoutError: class TimeoutError extends Error {},
-		launch: vi.fn().mockResolvedValue(mockBrowser),
-		connect: vi.fn().mockResolvedValue(mockBrowser),
-	}
-})
-
-// Mock PCR
-vi.mock("puppeteer-chromium-resolver", () => {
-	return {
-		default: vi.fn().mockResolvedValue({
-			puppeteer: {
-				launch: vi.fn().mockImplementation(async () => {
-					const { launch } = await import("puppeteer-core")
-					return launch()
-				}),
-			},
-			executablePath: "/mock/path/to/chromium",
-		}),
-	}
-})
-
-// Mock fs
-vi.mock("fs/promises", () => ({
-	mkdir: vi.fn().mockResolvedValue(undefined),
-	readFile: vi.fn(),
-	writeFile: vi.fn(),
-	access: vi.fn(),
-}))
-
-// Mock fileExistsAtPath
-vi.mock("../../../utils/fs", () => ({
-	fileExistsAtPath: vi.fn().mockResolvedValue(false),
-}))
-
-// Mock browser discovery functions
-vi.mock("../browserDiscovery", () => ({
-	discoverChromeHostUrl: vi.fn().mockResolvedValue(null),
-	tryChromeHostUrl: vi.fn().mockResolvedValue(false),
-}))
-
-// Mock delay
-vi.mock("delay", () => ({
-	default: vi.fn().mockResolvedValue(undefined),
-}))
-
-// Mock p-wait-for
-vi.mock("p-wait-for", () => ({
-	default: vi.fn().mockResolvedValue(undefined),
-}))
-
-describe("BrowserSession", () => {
-	let browserSession: BrowserSession
-	let mockContext: any
-
-	beforeEach(() => {
-		vi.clearAllMocks()
-
-		// Set up mock context
-		mockContext = {
-			globalState: {
-				get: vi.fn(),
-				update: vi.fn(),
-			},
-			globalStorageUri: {
-				fsPath: "/mock/global/storage/path",
-			},
-			extensionUri: {
-				fsPath: "/mock/extension/path",
-			},
-		}
-
-		// Create browser session
-		browserSession = new BrowserSession(mockContext)
-	})
-
-	describe("Remote browser disabled", () => {
-		it("should launch a local browser when remote browser is disabled", async () => {
-			// Mock context to indicate remote browser is disabled
-			mockContext.globalState.get.mockImplementation((key: string) => {
-				if (key === "remoteBrowserEnabled") return false
-				return undefined
-			})
-
-			await browserSession.launchBrowser()
-
-			const puppeteerCore = await import("puppeteer-core")
-
-			// Verify that a local browser was launched
-			expect(puppeteerCore.launch).toHaveBeenCalled()
-
-			// Verify that remote browser connection was not attempted
-			expect(discoverChromeHostUrl).not.toHaveBeenCalled()
-			expect(tryChromeHostUrl).not.toHaveBeenCalled()
-
-			expect((browserSession as any).isUsingRemoteBrowser).toBe(false)
-		})
-	})
-
-	describe("Remote browser successfully connects", () => {
-		it("should connect to a remote browser when enabled and connection succeeds", async () => {
-			// Mock context to indicate remote browser is enabled
-			mockContext.globalState.get.mockImplementation((key: string) => {
-				if (key === "remoteBrowserEnabled") return true
-				if (key === "remoteBrowserHost") return "http://remote-browser:9222"
-				return undefined
-			})
-
-			// Mock successful remote browser connection
-			vi.mocked(tryChromeHostUrl).mockResolvedValue(true)
-
-			await browserSession.launchBrowser()
-
-			const puppeteerCore = await import("puppeteer-core")
-
-			// Verify that connect was called
-			expect(puppeteerCore.connect).toHaveBeenCalled()
-
-			// Verify that local browser was not launched
-			expect(puppeteerCore.launch).not.toHaveBeenCalled()
-
-			expect((browserSession as any).isUsingRemoteBrowser).toBe(true)
-		})
-	})
-
-	describe("Remote browser enabled but falls back to local", () => {
-		it("should fall back to local browser when remote connection fails", async () => {
-			// Mock context to indicate remote browser is enabled
-			mockContext.globalState.get.mockImplementation((key: string) => {
-				if (key === "remoteBrowserEnabled") return true
-				if (key === "remoteBrowserHost") return "http://remote-browser:9222"
-				return undefined
-			})
-
-			// Mock failed remote browser connection
-			vi.mocked(tryChromeHostUrl).mockResolvedValue(false)
-			vi.mocked(discoverChromeHostUrl).mockResolvedValue(null)
-
-			await browserSession.launchBrowser()
-
-			// Import puppeteer-core to check if launch was called
-			const puppeteerCore = await import("puppeteer-core")
-
-			// Verify that local browser was launched as fallback
-			expect(puppeteerCore.launch).toHaveBeenCalled()
-
-			// Verify that isUsingRemoteBrowser is false
-			expect((browserSession as any).isUsingRemoteBrowser).toBe(false)
-		})
-	})
-
-	describe("closeBrowser", () => {
-		it("should close a local browser properly", async () => {
-			const puppeteerCore = await import("puppeteer-core")
-
-			// Create a mock browser directly
-			const mockBrowser = {
-				newPage: vi.fn().mockResolvedValue({}),
-				pages: vi.fn().mockResolvedValue([]),
-				close: vi.fn().mockResolvedValue(undefined),
-				disconnect: vi.fn().mockResolvedValue(undefined),
-			}
-
-			// Set browser and page on the session
-			;(browserSession as any).browser = mockBrowser
-			;(browserSession as any).page = {}
-			;(browserSession as any).isUsingRemoteBrowser = false
-
-			await browserSession.closeBrowser()
-
-			// Verify that browser.close was called
-			expect(mockBrowser.close).toHaveBeenCalled()
-			expect(mockBrowser.disconnect).not.toHaveBeenCalled()
-
-			// Verify that browser state was reset
-			expect((browserSession as any).browser).toBeUndefined()
-			expect((browserSession as any).page).toBeUndefined()
-			expect((browserSession as any).isUsingRemoteBrowser).toBe(false)
-		})
-
-		it("should disconnect from a remote browser properly", async () => {
-			// Create a mock browser directly
-			const mockBrowser = {
-				newPage: vi.fn().mockResolvedValue({}),
-				pages: vi.fn().mockResolvedValue([]),
-				close: vi.fn().mockResolvedValue(undefined),
-				disconnect: vi.fn().mockResolvedValue(undefined),
-			}
-
-			// Set browser and page on the session
-			;(browserSession as any).browser = mockBrowser
-			;(browserSession as any).page = {}
-			;(browserSession as any).isUsingRemoteBrowser = true
-
-			await browserSession.closeBrowser()
-
-			// Verify that browser.disconnect was called
-			expect(mockBrowser.disconnect).toHaveBeenCalled()
-			expect(mockBrowser.close).not.toHaveBeenCalled()
-		})
-	})
-
-	it("forces same-tab behavior before click", async () => {
-		// Prepare a minimal mock page with required APIs
-		const page: any = {
-			on: vi.fn(),
-			off: vi.fn(),
-			screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-			url: vi.fn().mockReturnValue("https://example.com"),
-			viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-			waitForNavigation: vi.fn().mockResolvedValue(undefined),
-			evaluate: vi.fn().mockResolvedValue(undefined),
-			mouse: {
-				click: vi.fn().mockResolvedValue(undefined),
-				move: vi.fn().mockResolvedValue(undefined),
-			},
-		}
-
-		;(browserSession as any).page = page
-
-		// Spy on the forceLinksToSameTab helper to ensure it's invoked
-		const forceSpy = vi.fn().mockResolvedValue(undefined)
-		;(browserSession as any).forceLinksToSameTab = forceSpy
-
-		await browserSession.click("10,20")
-
-		expect(forceSpy).toHaveBeenCalledTimes(1)
-		expect(forceSpy).toHaveBeenCalledWith(page)
-		expect(page.mouse.click).toHaveBeenCalledWith(10, 20)
-	})
-})
-
-describe("keyboard press", () => {
-	it("presses a keyboard key", async () => {
-		// Prepare a minimal mock page with required APIs
-		const page: any = {
-			on: vi.fn(),
-			off: vi.fn(),
-			screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-			url: vi.fn().mockReturnValue("https://example.com"),
-			viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-			waitForNavigation: vi.fn().mockResolvedValue(undefined),
-			evaluate: vi.fn().mockResolvedValue(undefined),
-			keyboard: {
-				press: vi.fn().mockResolvedValue(undefined),
-				type: vi.fn().mockResolvedValue(undefined),
-			},
-		}
-
-		// Create a fresh BrowserSession with a mock context
-		const mockCtx: any = {
-			globalState: { get: vi.fn(), update: vi.fn() },
-			globalStorageUri: { fsPath: "/mock/global/storage/path" },
-			extensionUri: { fsPath: "/mock/extension/path" },
-		}
-		const session = new BrowserSession(mockCtx)
-
-		;(session as any).page = page
-
-		await session.press("Enter")
-
-		expect(page.keyboard.press).toHaveBeenCalledTimes(1)
-		expect(page.keyboard.press).toHaveBeenCalledWith("Enter")
-	})
-})
-
-describe("cursor visualization", () => {
-	it("should draw cursor indicator when cursor position exists", async () => {
-		// Prepare a minimal mock page with required APIs
-		const page: any = {
-			on: vi.fn(),
-			off: vi.fn(),
-			screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-			url: vi.fn().mockReturnValue("https://example.com"),
-			viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-			evaluate: vi.fn().mockResolvedValue(undefined),
-			mouse: {
-				click: vi.fn().mockResolvedValue(undefined),
-			},
-		}
-
-		// Create a fresh BrowserSession with a mock context
-		const mockCtx: any = {
-			globalState: { get: vi.fn(), update: vi.fn() },
-			globalStorageUri: { fsPath: "/mock/global/storage/path" },
-			extensionUri: { fsPath: "/mock/extension/path" },
-		}
-		const session = new BrowserSession(mockCtx)
-
-		;(session as any).page = page
-
-		// Perform a click action which sets cursor position
-		const result = await session.click("100,200")
-
-		// Verify cursor indicator was drawn and removed
-		// evaluate is called 3 times: 1 for forceLinksToSameTab, 1 for draw cursor, 1 for remove cursor
-		expect(page.evaluate).toHaveBeenCalled()
-
-		// Verify the result includes cursor position
-		expect(result.currentMousePosition).toBe("100,200")
-	})
-
-	it("should include cursor position in action result", async () => {
-		// Prepare a minimal mock page with required APIs
-		const page: any = {
-			on: vi.fn(),
-			off: vi.fn(),
-			screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-			url: vi.fn().mockReturnValue("https://example.com"),
-			viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-			evaluate: vi.fn().mockResolvedValue(undefined),
-			mouse: {
-				move: vi.fn().mockResolvedValue(undefined),
-			},
-		}
-
-		// Create a fresh BrowserSession with a mock context
-		const mockCtx: any = {
-			globalState: { get: vi.fn(), update: vi.fn() },
-			globalStorageUri: { fsPath: "/mock/global/storage/path" },
-			extensionUri: { fsPath: "/mock/extension/path" },
-		}
-		const session = new BrowserSession(mockCtx)
-
-		;(session as any).page = page
-
-		// Perform a hover action which sets cursor position
-		const result = await session.hover("150,250")
-
-		// Verify the result includes cursor position
-		expect(result.currentMousePosition).toBe("150,250")
-		expect(result.viewportWidth).toBe(900)
-		expect(result.viewportHeight).toBe(600)
-	})
-
-	it("should not draw cursor indicator when no cursor position exists", async () => {
-		// Prepare a minimal mock page with required APIs
-		const page: any = {
-			on: vi.fn(),
-			off: vi.fn(),
-			screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-			url: vi.fn().mockReturnValue("https://example.com"),
-			viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-			evaluate: vi.fn().mockResolvedValue(undefined),
-		}
-
-		// Create a fresh BrowserSession with a mock context
-		const mockCtx: any = {
-			globalState: { get: vi.fn(), update: vi.fn() },
-			globalStorageUri: { fsPath: "/mock/global/storage/path" },
-			extensionUri: { fsPath: "/mock/extension/path" },
-		}
-		const session = new BrowserSession(mockCtx)
-
-		;(session as any).page = page
-
-		// Perform scroll action which doesn't set cursor position
-		const result = await session.scrollDown()
-
-		// Verify evaluate was called only for scroll operation (not for cursor drawing/removal)
-		// scrollDown calls evaluate once for scrolling
-		expect(page.evaluate).toHaveBeenCalledTimes(1)
-
-		// Verify no cursor position in result
-		expect(result.currentMousePosition).toBeUndefined()
-	})
-
-	describe("saveScreenshot", () => {
-		// Use a cross-platform workspace path for testing
-		const testWorkspace = path.resolve("/workspace")
-
-		it("should save screenshot to specified path with png format", async () => {
-			const mockFs = await import("fs/promises")
-			const page: any = {
-				on: vi.fn(),
-				off: vi.fn(),
-				screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-				url: vi.fn().mockReturnValue("https://example.com"),
-				viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-				evaluate: vi.fn().mockResolvedValue(undefined),
-			}
-
-			const mockCtx: any = {
-				globalState: { get: vi.fn(), update: vi.fn() },
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(mockCtx)
-			;(session as any).page = page
-
-			await session.saveScreenshot("screenshots/test.png", testWorkspace)
-
-			expect(mockFs.mkdir).toHaveBeenCalledWith(path.join(testWorkspace, "screenshots"), { recursive: true })
-			expect(page.screenshot).toHaveBeenCalledWith(
-				expect.objectContaining({
-					path: path.join(testWorkspace, "screenshots", "test.png"),
-					type: "png",
-				}),
-			)
-		})
-
-		it("should save screenshot with jpeg format for .jpg extension", async () => {
-			const page: any = {
-				on: vi.fn(),
-				off: vi.fn(),
-				screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-				url: vi.fn().mockReturnValue("https://example.com"),
-				viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-				evaluate: vi.fn().mockResolvedValue(undefined),
-			}
-
-			const mockCtx: any = {
-				globalState: { get: vi.fn().mockReturnValue(80), update: vi.fn() },
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(mockCtx)
-			;(session as any).page = page
-
-			await session.saveScreenshot("screenshots/test.jpg", testWorkspace)
-
-			expect(page.screenshot).toHaveBeenCalledWith(
-				expect.objectContaining({
-					path: path.join(testWorkspace, "screenshots", "test.jpg"),
-					type: "jpeg",
-					quality: 80,
-				}),
-			)
-		})
-
-		it("should save screenshot with webp format", async () => {
-			const page: any = {
-				on: vi.fn(),
-				off: vi.fn(),
-				screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-				url: vi.fn().mockReturnValue("https://example.com"),
-				viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-				evaluate: vi.fn().mockResolvedValue(undefined),
-			}
-
-			const mockCtx: any = {
-				globalState: { get: vi.fn().mockReturnValue(75), update: vi.fn() },
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(mockCtx)
-			;(session as any).page = page
-
-			await session.saveScreenshot("test.webp", testWorkspace)
-
-			expect(page.screenshot).toHaveBeenCalledWith(
-				expect.objectContaining({
-					path: path.join(testWorkspace, "test.webp"),
-					type: "webp",
-					quality: 75,
-				}),
-			)
-		})
-
-		it("should reject absolute file paths outside workspace", async () => {
-			// Create a cross-platform absolute path for testing
-			const absolutePath = path.resolve("/absolute/path/screenshot.png")
-			const page: any = {
-				on: vi.fn(),
-				off: vi.fn(),
-				screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-				url: vi.fn().mockReturnValue("https://example.com"),
-				viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-				evaluate: vi.fn().mockResolvedValue(undefined),
-			}
-
-			const mockCtx: any = {
-				globalState: { get: vi.fn(), update: vi.fn() },
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(mockCtx)
-			;(session as any).page = page
-
-			await expect(session.saveScreenshot(absolutePath, testWorkspace)).rejects.toThrow(/outside the workspace/)
-
-			expect(page.screenshot).not.toHaveBeenCalled()
-		})
-
-		it("should reject paths with .. that escape the workspace", async () => {
-			const page: any = {
-				on: vi.fn(),
-				off: vi.fn(),
-				screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-				url: vi.fn().mockReturnValue("https://example.com"),
-				viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-				evaluate: vi.fn().mockResolvedValue(undefined),
-			}
-
-			const mockCtx: any = {
-				globalState: { get: vi.fn(), update: vi.fn() },
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(mockCtx)
-			;(session as any).page = page
-
-			await expect(session.saveScreenshot("../../etc/passwd", testWorkspace)).rejects.toThrow(
-				/outside the workspace/,
-			)
-
-			expect(page.screenshot).not.toHaveBeenCalled()
-		})
-
-		it("should allow paths with .. that stay within workspace", async () => {
-			const mockFs = await import("fs/promises")
-			const page: any = {
-				on: vi.fn(),
-				off: vi.fn(),
-				screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
-				url: vi.fn().mockReturnValue("https://example.com"),
-				viewport: vi.fn().mockReturnValue({ width: 900, height: 600 }),
-				evaluate: vi.fn().mockResolvedValue(undefined),
-			}
-
-			const mockCtx: any = {
-				globalState: { get: vi.fn(), update: vi.fn() },
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(mockCtx)
-			;(session as any).page = page
-
-			// Path like "subdir/../screenshot.png" should resolve to "screenshot.png" within workspace
-			await session.saveScreenshot("subdir/../screenshot.png", testWorkspace)
-
-			expect(page.screenshot).toHaveBeenCalledWith(
-				expect.objectContaining({
-					path: path.join(testWorkspace, "screenshot.png"),
-					type: "png",
-				}),
-			)
-		})
-	})
-
-	describe("getViewportSize", () => {
-		it("falls back to configured viewport when no page or last viewport is available", () => {
-			const localCtx: any = {
-				globalState: {
-					get: vi.fn((key: string) => {
-						if (key === "browserViewportSize") return "1024x768"
-						return undefined
-					}),
-					update: vi.fn(),
-				},
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-
-			const session = new BrowserSession(localCtx)
-			const vp = (session as any).getViewportSize()
-			expect(vp).toEqual({ width: 1024, height: 768 })
-		})
-
-		it("returns live page viewport when available and updates lastViewport cache", () => {
-			const localCtx: any = {
-				globalState: {
-					get: vi.fn(),
-					update: vi.fn(),
-				},
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(localCtx)
-			;(session as any).page = {
-				viewport: vi.fn().mockReturnValue({ width: 1111, height: 555 }),
-			}
-
-			const vp = (session as any).getViewportSize()
-			expect(vp).toEqual({ width: 1111, height: 555 })
-			expect((session as any).lastViewportWidth).toBe(1111)
-			expect((session as any).lastViewportHeight).toBe(555)
-		})
-
-		it("returns cached last viewport when page no longer exists", () => {
-			const localCtx: any = {
-				globalState: {
-					get: vi.fn(),
-					update: vi.fn(),
-				},
-				globalStorageUri: { fsPath: "/mock/global/storage/path" },
-				extensionUri: { fsPath: "/mock/extension/path" },
-			}
-			const session = new BrowserSession(localCtx)
-			;(session as any).lastViewportWidth = 800
-			;(session as any).lastViewportHeight = 600
-
-			const vp = (session as any).getViewportSize()
-			expect(vp).toEqual({ width: 800, height: 600 })
-		})
-	})
-})

+ 0 - 369
src/services/browser/__tests__/UrlContentFetcher.spec.ts

@@ -1,369 +0,0 @@
-// npx vitest services/browser/__tests__/UrlContentFetcher.spec.ts
-
-import * as path from "path"
-
-import { UrlContentFetcher } from "../UrlContentFetcher"
-
-// Mock dependencies
-vi.mock("vscode", () => ({
-	ExtensionContext: vi.fn(),
-	Uri: {
-		file: vi.fn((path) => ({ fsPath: path })),
-	},
-}))
-
-// Mock fs/promises
-vi.mock("fs/promises", () => ({
-	default: {
-		mkdir: vi.fn().mockResolvedValue(undefined),
-	},
-	mkdir: vi.fn().mockResolvedValue(undefined),
-}))
-
-// Mock utils/fs
-vi.mock("../../../utils/fs", () => ({
-	fileExistsAtPath: vi.fn().mockResolvedValue(true),
-}))
-
-// Mock cheerio
-vi.mock("cheerio", () => ({
-	load: vi.fn(() => {
-		const $ = vi.fn((selector) => ({
-			remove: vi.fn().mockReturnThis(),
-		})) as any
-		$.html = vi.fn().mockReturnValue("<html><body>Test content</body></html>")
-		return $
-	}),
-}))
-
-// Mock turndown
-vi.mock("turndown", () => {
-	return {
-		default: class MockTurndownService {
-			turndown = vi.fn().mockReturnValue("# Test content")
-		},
-	}
-})
-
-// Mock puppeteer-chromium-resolver
-vi.mock("puppeteer-chromium-resolver", () => ({
-	default: vi.fn().mockResolvedValue({
-		puppeteer: {
-			launch: vi.fn().mockResolvedValue({
-				newPage: vi.fn().mockResolvedValue({
-					goto: vi.fn(),
-					content: vi.fn().mockResolvedValue("<html><body>Test content</body></html>"),
-					setViewport: vi.fn().mockResolvedValue(undefined),
-					setExtraHTTPHeaders: vi.fn().mockResolvedValue(undefined),
-				}),
-				close: vi.fn().mockResolvedValue(undefined),
-			}),
-		},
-		executablePath: "/path/to/chromium",
-	}),
-}))
-
-// Mock serialize-error
-vi.mock("serialize-error", () => ({
-	serializeError: vi.fn((error) => {
-		if (error instanceof Error) {
-			return { message: error.message, name: error.name }
-		} else if (typeof error === "string") {
-			return { message: error }
-		} else if (error && typeof error === "object" && "message" in error) {
-			return { message: String(error.message), name: "name" in error ? String(error.name) : undefined }
-		} else {
-			return { message: String(error) }
-		}
-	}),
-}))
-
-describe("UrlContentFetcher", () => {
-	let urlContentFetcher: UrlContentFetcher
-	let mockContext: any
-	let mockPage: any
-	let mockBrowser: any
-	let PCR: any
-
-	beforeEach(async () => {
-		vi.clearAllMocks()
-
-		mockContext = {
-			globalStorageUri: {
-				fsPath: "/test/storage",
-			},
-		}
-
-		mockPage = {
-			goto: vi.fn(),
-			content: vi.fn().mockResolvedValue("<html><body>Test content</body></html>"),
-			setViewport: vi.fn().mockResolvedValue(undefined),
-			setExtraHTTPHeaders: vi.fn().mockResolvedValue(undefined),
-		}
-
-		mockBrowser = {
-			newPage: vi.fn().mockResolvedValue(mockPage),
-			close: vi.fn().mockResolvedValue(undefined),
-		}
-
-		// Reset PCR mock
-		// @ts-ignore
-		PCR = (await import("puppeteer-chromium-resolver")).default
-		vi.mocked(PCR).mockResolvedValue({
-			puppeteer: {
-				launch: vi.fn().mockResolvedValue(mockBrowser),
-			},
-			executablePath: "/path/to/chromium",
-		})
-
-		urlContentFetcher = new UrlContentFetcher(mockContext)
-	})
-
-	afterEach(() => {
-		vi.restoreAllMocks()
-	})
-
-	describe("launchBrowser", () => {
-		it("should launch browser with correct arguments on non-Linux platforms", async () => {
-			// Ensure we're not on Linux for this test
-			const originalPlatform = process.platform
-			Object.defineProperty(process, "platform", {
-				value: "darwin", // macOS
-			})
-
-			try {
-				await urlContentFetcher.launchBrowser()
-
-				expect(vi.mocked(PCR)).toHaveBeenCalledWith({
-					downloadPath: path.join("/test/storage", "puppeteer"),
-				})
-
-				const stats = await vi.mocked(PCR).mock.results[0].value
-				expect(stats.puppeteer.launch).toHaveBeenCalledWith({
-					args: [
-						"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
-						"--disable-dev-shm-usage",
-						"--disable-accelerated-2d-canvas",
-						"--no-first-run",
-						"--disable-gpu",
-						"--disable-features=VizDisplayCompositor",
-					],
-					executablePath: "/path/to/chromium",
-				})
-			} finally {
-				// Restore original platform
-				Object.defineProperty(process, "platform", {
-					value: originalPlatform,
-				})
-			}
-		})
-
-		it("should launch browser with Linux-specific arguments", async () => {
-			// Mock process.platform to be linux
-			const originalPlatform = process.platform
-			Object.defineProperty(process, "platform", {
-				value: "linux",
-			})
-
-			try {
-				// Create a new instance to ensure fresh state
-				const linuxFetcher = new UrlContentFetcher(mockContext)
-				await linuxFetcher.launchBrowser()
-
-				expect(vi.mocked(PCR)).toHaveBeenCalledWith({
-					downloadPath: path.join("/test/storage", "puppeteer"),
-				})
-
-				const stats = await vi.mocked(PCR).mock.results[vi.mocked(PCR).mock.results.length - 1].value
-				expect(stats.puppeteer.launch).toHaveBeenCalledWith({
-					args: [
-						"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
-						"--disable-dev-shm-usage",
-						"--disable-accelerated-2d-canvas",
-						"--no-first-run",
-						"--disable-gpu",
-						"--disable-features=VizDisplayCompositor",
-						"--no-sandbox", // Linux-specific argument
-					],
-					executablePath: "/path/to/chromium",
-				})
-			} finally {
-				// Restore original platform
-				Object.defineProperty(process, "platform", {
-					value: originalPlatform,
-				})
-			}
-		})
-
-		it("should set viewport and headers after launching", async () => {
-			await urlContentFetcher.launchBrowser()
-
-			expect(mockPage.setViewport).toHaveBeenCalledWith({ width: 1280, height: 720 })
-			expect(mockPage.setExtraHTTPHeaders).toHaveBeenCalledWith({
-				"Accept-Language": "en-US,en;q=0.9",
-			})
-		})
-
-		it("should not launch browser if already launched", async () => {
-			await urlContentFetcher.launchBrowser()
-			const initialCallCount = vi.mocked(PCR).mock.calls.length
-
-			await urlContentFetcher.launchBrowser()
-			expect(vi.mocked(PCR)).toHaveBeenCalledTimes(initialCallCount)
-		})
-	})
-
-	describe("urlToMarkdown", () => {
-		beforeEach(async () => {
-			await urlContentFetcher.launchBrowser()
-		})
-
-		it("should successfully fetch and convert URL to markdown", async () => {
-			mockPage.goto.mockResolvedValueOnce(undefined)
-
-			const result = await urlContentFetcher.urlToMarkdown("https://example.com")
-
-			expect(mockPage.goto).toHaveBeenCalledWith("https://example.com", {
-				timeout: 30000,
-				waitUntil: ["domcontentloaded", "networkidle2"],
-			})
-			expect(result).toBe("# Test content")
-		})
-
-		it("should retry with domcontentloaded only when networkidle2 fails", async () => {
-			const timeoutError = new Error("Navigation timeout of 30000 ms exceeded")
-			mockPage.goto.mockRejectedValueOnce(timeoutError).mockResolvedValueOnce(undefined)
-
-			const result = await urlContentFetcher.urlToMarkdown("https://example.com")
-
-			expect(mockPage.goto).toHaveBeenCalledTimes(2)
-			expect(mockPage.goto).toHaveBeenNthCalledWith(1, "https://example.com", {
-				timeout: 30000,
-				waitUntil: ["domcontentloaded", "networkidle2"],
-			})
-			expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", {
-				timeout: 20000,
-				waitUntil: ["domcontentloaded"],
-			})
-			expect(result).toBe("# Test content")
-		})
-
-		it("should retry for network errors", async () => {
-			const networkError = new Error("net::ERR_CONNECTION_REFUSED")
-			mockPage.goto.mockRejectedValueOnce(networkError).mockResolvedValueOnce(undefined)
-
-			const result = await urlContentFetcher.urlToMarkdown("https://example.com")
-
-			expect(mockPage.goto).toHaveBeenCalledTimes(2)
-			expect(result).toBe("# Test content")
-		})
-
-		it("should retry for TimeoutError", async () => {
-			const timeoutError = new Error("TimeoutError: Navigation timeout")
-			timeoutError.name = "TimeoutError"
-			mockPage.goto.mockRejectedValueOnce(timeoutError).mockResolvedValueOnce(undefined)
-
-			const result = await urlContentFetcher.urlToMarkdown("https://example.com")
-
-			expect(mockPage.goto).toHaveBeenCalledTimes(2)
-			expect(result).toBe("# Test content")
-		})
-
-		it("should not retry for non-network/timeout errors", async () => {
-			const otherError = new Error("Some other error")
-			mockPage.goto.mockRejectedValueOnce(otherError)
-
-			await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Some other error")
-			expect(mockPage.goto).toHaveBeenCalledTimes(1)
-		})
-
-		it("should throw error if browser not initialized", async () => {
-			const newFetcher = new UrlContentFetcher(mockContext)
-
-			await expect(newFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Browser not initialized")
-		})
-
-		it("should handle errors without message property", async () => {
-			const errorWithoutMessage = { code: "UNKNOWN_ERROR" }
-			mockPage.goto.mockRejectedValueOnce(errorWithoutMessage)
-
-			// serialize-error will convert this to a proper error with the object stringified
-			await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow()
-
-			// Should not retry for non-network errors
-			expect(mockPage.goto).toHaveBeenCalledTimes(1)
-		})
-
-		it("should handle error objects with message property", async () => {
-			const errorWithMessage = { message: "Custom error", code: "CUSTOM_ERROR" }
-			mockPage.goto.mockRejectedValueOnce(errorWithMessage)
-
-			await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Custom error")
-
-			// Should not retry for error objects with message property (they're treated as known errors)
-			expect(mockPage.goto).toHaveBeenCalledTimes(1)
-		})
-
-		it("should retry for error objects with network-related messages", async () => {
-			const errorWithNetworkMessage = { message: "net::ERR_CONNECTION_REFUSED", code: "NETWORK_ERROR" }
-			mockPage.goto.mockRejectedValueOnce(errorWithNetworkMessage).mockResolvedValueOnce(undefined)
-
-			const result = await urlContentFetcher.urlToMarkdown("https://example.com")
-
-			// Should retry for network-related errors even in non-Error objects
-			expect(mockPage.goto).toHaveBeenCalledTimes(2)
-			expect(result).toBe("# Test content")
-		})
-
-		it("should handle string errors", async () => {
-			const stringError = "Simple string error"
-			mockPage.goto.mockRejectedValueOnce(stringError)
-
-			await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Simple string error")
-			expect(mockPage.goto).toHaveBeenCalledTimes(1)
-		})
-
-		it("should retry net::ERR_ABORTED like other network errors", async () => {
-			const abortedError = new Error("net::ERR_ABORTED at https://example.com")
-			mockPage.goto.mockRejectedValueOnce(abortedError).mockResolvedValueOnce(undefined)
-
-			const result = await urlContentFetcher.urlToMarkdown("https://example.com")
-
-			expect(mockPage.goto).toHaveBeenCalledTimes(2)
-			expect(mockPage.goto).toHaveBeenNthCalledWith(1, "https://example.com", {
-				timeout: 30000,
-				waitUntil: ["domcontentloaded", "networkidle2"],
-			})
-			expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", {
-				timeout: 20000,
-				waitUntil: ["domcontentloaded"],
-			})
-			expect(result).toBe("# Test content")
-		})
-
-		it("should throw error when ERR_ABORTED retry also fails", async () => {
-			const abortedError = new Error("net::ERR_ABORTED at https://example.com")
-			const retryError = new Error("net::ERR_CONNECTION_REFUSED")
-			mockPage.goto.mockRejectedValueOnce(abortedError).mockRejectedValueOnce(retryError)
-
-			await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow(
-				"net::ERR_CONNECTION_REFUSED",
-			)
-
-			expect(mockPage.goto).toHaveBeenCalledTimes(2)
-		})
-	})
-
-	describe("closeBrowser", () => {
-		it("should close browser and reset state", async () => {
-			await urlContentFetcher.launchBrowser()
-			await urlContentFetcher.closeBrowser()
-
-			expect(mockBrowser.close).toHaveBeenCalled()
-		})
-
-		it("should handle closing when browser not initialized", async () => {
-			await expect(urlContentFetcher.closeBrowser()).resolves.not.toThrow()
-		})
-	})
-})

+ 0 - 181
src/services/browser/browserDiscovery.ts

@@ -1,181 +0,0 @@
-import * as net from "net"
-import axios from "axios"
-import * as dns from "dns"
-
-/**
- * Check if a port is open on a given host
- */
-export async function isPortOpen(host: string, port: number, timeout = 1000): Promise<boolean> {
-	return new Promise((resolve) => {
-		const socket = new net.Socket()
-		let status = false
-
-		// Set timeout
-		socket.setTimeout(timeout)
-
-		// Handle successful connection
-		socket.on("connect", () => {
-			status = true
-			socket.destroy()
-		})
-
-		// Handle any errors
-		socket.on("error", () => {
-			socket.destroy()
-		})
-
-		// Handle timeout
-		socket.on("timeout", () => {
-			socket.destroy()
-		})
-
-		// Handle close
-		socket.on("close", () => {
-			resolve(status)
-		})
-
-		// Attempt to connect
-		socket.connect(port, host)
-	})
-}
-
-/**
- * Try to connect to Chrome at a specific IP address
- */
-export async function tryChromeHostUrl(chromeHostUrl: string): Promise<boolean> {
-	try {
-		console.log(`Trying to connect to Chrome at: ${chromeHostUrl}/json/version`)
-		await axios.get(`${chromeHostUrl}/json/version`, { timeout: 1000 })
-		return true
-	} catch (error) {
-		return false
-	}
-}
-
-/**
- * Get Docker host IP
- */
-export async function getDockerHostIP(): Promise<string | null> {
-	try {
-		// Try to resolve host.docker.internal (works on Docker Desktop)
-		return new Promise((resolve) => {
-			dns.lookup("host.docker.internal", (err: any, address: string) => {
-				if (err) {
-					resolve(null)
-				} else {
-					resolve(address)
-				}
-			})
-		})
-	} catch (error) {
-		console.log("Could not determine Docker host IP:", error)
-		return null
-	}
-}
-
-/**
- * Scan a network range for Chrome debugging port
- */
-export async function scanNetworkForChrome(baseIP: string, port: number): Promise<string | null> {
-	if (!baseIP || !baseIP.match(/^\d+\.\d+\.\d+\./)) {
-		return null
-	}
-
-	// Extract the network prefix (e.g., "192.168.65.")
-	const networkPrefix = baseIP.split(".").slice(0, 3).join(".") + "."
-
-	// Common Docker host IPs to try first
-	const priorityIPs = [
-		networkPrefix + "1", // Common gateway
-		networkPrefix + "2", // Common host
-		networkPrefix + "254", // Common host in some Docker setups
-	]
-
-	console.log(`Scanning priority IPs in network ${networkPrefix}*`)
-
-	// Check priority IPs first
-	for (const ip of priorityIPs) {
-		const isOpen = await isPortOpen(ip, port)
-		if (isOpen) {
-			console.log(`Found Chrome debugging port open on ${ip}`)
-			return ip
-		}
-	}
-
-	return null
-}
-
-// Function to discover Chrome instances on the network
-const discoverChromeHosts = async (port: number): Promise<string | null> => {
-	// Get all network interfaces
-	const ipAddresses = []
-
-	// Try to get Docker host IP
-	const hostIP = await getDockerHostIP()
-	if (hostIP) {
-		console.log("Found Docker host IP:", hostIP)
-		ipAddresses.push(hostIP)
-	}
-
-	// Remove duplicates
-	const uniqueIPs = [...new Set(ipAddresses)]
-	console.log("IP Addresses to try:", uniqueIPs)
-
-	// Try connecting to each IP address
-	for (const ip of uniqueIPs) {
-		const hostEndpoint = `http://${ip}:${port}`
-
-		const hostIsValid = await tryChromeHostUrl(hostEndpoint)
-		if (hostIsValid) {
-			// Store the successful IP for future use
-			console.log(`✅ Found Chrome at ${hostEndpoint}`)
-
-			// Return the host URL and endpoint
-			return hostEndpoint
-		}
-	}
-
-	return null
-}
-
-/**
- * Test connection to a remote browser debugging websocket.
- * First tries specific hosts, then attempts auto-discovery if needed.
- * @param browserHostUrl Optional specific host URL to check first
- * @param port Browser debugging port (default: 9222)
- * @returns WebSocket debugger URL if connection is successful, null otherwise
- */
-export async function discoverChromeHostUrl(port: number = 9222): Promise<string | null> {
-	// First try specific hosts
-	const hostsToTry = [`http://localhost:${port}`, `http://127.0.0.1:${port}`]
-
-	// Try each host directly first
-	for (const hostUrl of hostsToTry) {
-		console.log(`Trying to connect to: ${hostUrl}`)
-		try {
-			const hostIsValid = await tryChromeHostUrl(hostUrl)
-			if (hostIsValid) return hostUrl
-		} catch (error) {
-			console.log(`Failed to connect to ${hostUrl}: ${error instanceof Error ? error.message : error}`)
-		}
-	}
-
-	// If direct connections failed, attempt auto-discovery
-	console.log("Direct connections failed. Attempting auto-discovery...")
-
-	const discoveredHostUrl = await discoverChromeHosts(port)
-	if (discoveredHostUrl) {
-		console.log(`Trying to connect to discovered host: ${discoveredHostUrl}`)
-		try {
-			const hostIsValid = await tryChromeHostUrl(discoveredHostUrl)
-			if (hostIsValid) return discoveredHostUrl
-			console.log(`Failed to connect to discovered host ${discoveredHostUrl}`)
-		} catch (error) {
-			console.log(`Error connecting to discovered host: ${error instanceof Error ? error.message : error}`)
-		}
-	} else {
-		console.log("No browser instances discovered on network")
-	}
-
-	return null
-}

+ 7 - 13
src/shared/__tests__/modes.spec.ts

@@ -19,19 +19,19 @@ describe("isToolAllowedForMode", () => {
 			slug: "markdown-editor",
 			name: "Markdown Editor",
 			roleDefinition: "You are a markdown editor",
-			groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
+			groups: ["read", ["edit", { fileRegex: "\\.md$" }]],
 		},
 		{
 			slug: "css-editor",
 			name: "CSS Editor",
 			roleDefinition: "You are a CSS editor",
-			groups: ["read", ["edit", { fileRegex: "\\.css$" }], "browser"],
+			groups: ["read", ["edit", { fileRegex: "\\.css$" }]],
 		},
 		{
 			slug: "test-exp-mode",
 			name: "Test Exp Mode",
 			roleDefinition: "You are an experimental tester",
-			groups: ["read", "edit", "browser"],
+			groups: ["read", "edit"],
 		},
 	]
 
@@ -42,7 +42,6 @@ describe("isToolAllowedForMode", () => {
 
 	it("allows unrestricted tools", () => {
 		expect(isToolAllowedForMode("read_file", "markdown-editor", customModes)).toBe(true)
-		expect(isToolAllowedForMode("browser_action", "markdown-editor", customModes)).toBe(true)
 	})
 
 	describe("file restrictions", () => {
@@ -151,11 +150,7 @@ describe("isToolAllowedForMode", () => {
 					slug: "docs-editor",
 					name: "Documentation Editor",
 					roleDefinition: "You are a documentation editor",
-					groups: [
-						"read",
-						["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
-						"browser",
-					],
+					groups: ["read", ["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }]],
 				},
 			]
 
@@ -243,7 +238,6 @@ describe("isToolAllowedForMode", () => {
 
 			// Should maintain read capabilities
 			expect(isToolAllowedForMode("read_file", "architect", [])).toBe(true)
-			expect(isToolAllowedForMode("browser_action", "architect", [])).toBe(true)
 			expect(isToolAllowedForMode("use_mcp_tool", "architect", [])).toBe(true)
 		})
 
@@ -535,7 +529,7 @@ describe("isToolAllowedForMode", () => {
 				slug: "test-custom-tools",
 				name: "Test Custom Tools Mode",
 				roleDefinition: "You are a test mode",
-				groups: ["read", "edit", "browser"],
+				groups: ["read", "edit"],
 			},
 		]
 
@@ -567,7 +561,7 @@ describe("isToolAllowedForMode", () => {
 					slug: "no-edit-mode",
 					name: "No Edit Mode",
 					roleDefinition: "You have no edit powers",
-					groups: ["read", "browser"], // No edit group
+					groups: ["read"], // No edit group
 				},
 			]
 
@@ -619,7 +613,7 @@ describe("FileRestrictionError", () => {
 				name: "🪲 Debug",
 				roleDefinition:
 					"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
-				groups: ["read", "edit", "browser", "command", "mcp"],
+				groups: ["read", "edit", "command", "mcp"],
 			})
 			expect(debugMode?.customInstructions).toContain(
 				"Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.",

+ 0 - 95
src/shared/browserUtils.ts

@@ -1,95 +0,0 @@
-/**
- * Parses coordinate string and scales from image dimensions to viewport dimensions
- * The LLM examines the screenshot it receives (which may be downscaled by the API)
- * and reports coordinates in format: "x,y@widthxheight" where widthxheight is what the LLM observed
- *
- * Format: "x,y@widthxheight" (required)
- * Returns: scaled coordinate string "x,y" in viewport coordinates
- * Throws: Error if format is invalid or missing image dimensions
- */
-export function scaleCoordinate(coordinate: string, viewportWidth: number, viewportHeight: number): string {
-	// Parse coordinate with required image dimensions (accepts both 'x' and ',' as dimension separators)
-	const match = coordinate.match(/^\s*(\d+)\s*,\s*(\d+)\s*@\s*(\d+)\s*[x,]\s*(\d+)\s*$/)
-
-	if (!match) {
-		throw new Error(
-			`Invalid coordinate format: "${coordinate}". ` +
-				`Expected format: "x,y@widthxheight" (e.g., "450,300@1024x768")`,
-		)
-	}
-
-	const [, xStr, yStr, imgWidthStr, imgHeightStr] = match
-	const x = parseInt(xStr, 10)
-	const y = parseInt(yStr, 10)
-	const imgWidth = parseInt(imgWidthStr, 10)
-	const imgHeight = parseInt(imgHeightStr, 10)
-
-	// Scale coordinates from image dimensions to viewport dimensions
-	const scaledX = Math.round((x / imgWidth) * viewportWidth)
-	const scaledY = Math.round((y / imgHeight) * viewportHeight)
-
-	return `${scaledX},${scaledY}`
-}
-
-/**
- * Formats a key string into a more readable format (e.g., "Control+c" -> "Ctrl + C")
- */
-export function prettyKey(k?: string): string {
-	if (!k) return ""
-	return k
-		.split("+")
-		.map((part) => {
-			const p = part.trim()
-			const lower = p.toLowerCase()
-			const map: Record<string, string> = {
-				enter: "Enter",
-				tab: "Tab",
-				escape: "Esc",
-				esc: "Esc",
-				backspace: "Backspace",
-				space: "Space",
-				shift: "Shift",
-				control: "Ctrl",
-				ctrl: "Ctrl",
-				alt: "Alt",
-				meta: "Meta",
-				command: "Cmd",
-				cmd: "Cmd",
-				arrowup: "Arrow Up",
-				arrowdown: "Arrow Down",
-				arrowleft: "Arrow Left",
-				arrowright: "Arrow Right",
-				pageup: "Page Up",
-				pagedown: "Page Down",
-				home: "Home",
-				end: "End",
-			}
-			if (map[lower]) return map[lower]
-			const keyMatch = /^Key([A-Z])$/.exec(p)
-			if (keyMatch) return keyMatch[1].toUpperCase()
-			const digitMatch = /^Digit([0-9])$/.exec(p)
-			if (digitMatch) return digitMatch[1]
-			const spaced = p.replace(/([a-z])([A-Z])/g, "$1 $2")
-			return spaced.charAt(0).toUpperCase() + spaced.slice(1)
-		})
-		.join(" + ")
-}
-
-/**
- * Wrapper around scaleCoordinate that handles failures gracefully by checking for simple coordinates
- */
-export function getViewportCoordinate(
-	coord: string | undefined,
-	viewportWidth: number,
-	viewportHeight: number,
-): string {
-	if (!coord) return ""
-
-	try {
-		return scaleCoordinate(coord, viewportWidth, viewportHeight)
-	} catch (e) {
-		// Fallback to simple x,y parsing or return as is
-		const simpleMatch = /^\s*(\d+)\s*,\s*(\d+)/.exec(coord)
-		return simpleMatch ? `${simpleMatch[1]},${simpleMatch[2]}` : coord
-	}
-}

+ 1 - 18
src/shared/tools.ts

@@ -1,13 +1,6 @@
 import type { TextPart, ImagePart } from "../core/task-persistence/rooMessage"
 
-import type {
-	ClineAsk,
-	ToolProgressStatus,
-	ToolGroup,
-	ToolName,
-	BrowserActionParams,
-	GenerateImageParams,
-} from "@roo-code/types"
+import type { ClineAsk, ToolProgressStatus, ToolGroup, ToolName, GenerateImageParams } from "@roo-code/types"
 
 export type ToolResponse = string | Array<TextPart | ImagePart>
 
@@ -113,7 +106,6 @@ export type NativeToolArgs = {
 		question: string
 		follow_up: Array<{ text: string; mode?: string }>
 	}
-	browser_action: BrowserActionParams
 	codebase_search: { query: string; path?: string }
 	generate_image: GenerateImageParams
 	run_slash_command: { command: string; args?: string }
@@ -220,11 +212,6 @@ export interface ListFilesToolUse extends ToolUse<"list_files"> {
 	params: Partial<Pick<Record<ToolParamName, string>, "path" | "recursive">>
 }
 
-export interface BrowserActionToolUse extends ToolUse<"browser_action"> {
-	name: "browser_action"
-	params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text" | "size" | "path">>
-}
-
 export interface UseMcpToolToolUse extends ToolUse<"use_mcp_tool"> {
 	name: "use_mcp_tool"
 	params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "tool_name" | "arguments">>
@@ -290,7 +277,6 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
 	apply_patch: "apply patches using codex format",
 	search_files: "search files",
 	list_files: "list files",
-	browser_action: "use a browser",
 	use_mcp_tool: "use mcp tools",
 	access_mcp_resource: "access mcp resources",
 	ask_followup_question: "ask questions",
@@ -314,9 +300,6 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
 		tools: ["apply_diff", "write_to_file", "generate_image"],
 		customTools: ["edit", "search_replace", "edit_file", "apply_patch"],
 	},
-	browser: {
-		tools: ["browser_action"],
-	},
 	command: {
 		tools: ["execute_command", "read_command_output"],
 	},

+ 0 - 12
webview-ui/browser-panel.html

@@ -1,12 +0,0 @@
-<!doctype html>
-<html lang="en">
-	<head>
-		<meta charset="UTF-8" />
-		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-		<title>Browser Session</title>
-	</head>
-	<body>
-		<div id="root"></div>
-		<script type="module" src="/src/browser-panel.tsx"></script>
-	</body>
-</html>

+ 0 - 12
webview-ui/src/browser-panel.tsx

@@ -1,12 +0,0 @@
-import { StrictMode } from "react"
-import { createRoot } from "react-dom/client"
-
-import "./index.css"
-import BrowserSessionPanel from "./components/browser-session/BrowserSessionPanel"
-import "../node_modules/@vscode/codicons/dist/codicon.css"
-
-createRoot(document.getElementById("root")!).render(
-	<StrictMode>
-		<BrowserSessionPanel />
-	</StrictMode>,
-)

+ 0 - 61
webview-ui/src/components/browser-session/BrowserPanelStateProvider.tsx

@@ -1,61 +0,0 @@
-import React, { createContext, useContext, useState, useEffect, useCallback } from "react"
-
-import { type ExtensionMessage } from "@roo-code/types"
-
-interface BrowserPanelState {
-	browserViewportSize: string
-	isBrowserSessionActive: boolean
-	language: string
-}
-
-const BrowserPanelStateContext = createContext<BrowserPanelState | undefined>(undefined)
-
-export const BrowserPanelStateProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
-	const [state, setState] = useState<BrowserPanelState>({
-		browserViewportSize: "900x600",
-		isBrowserSessionActive: false,
-		language: "en",
-	})
-
-	const handleMessage = useCallback((event: MessageEvent) => {
-		const message: ExtensionMessage = event.data
-
-		switch (message.type) {
-			case "state":
-				if (message.state) {
-					setState((prev) => ({
-						...prev,
-						browserViewportSize: message.state?.browserViewportSize || "900x600",
-						isBrowserSessionActive: message.state?.isBrowserSessionActive || false,
-						language: message.state?.language || "en",
-					}))
-				}
-				break
-			case "browserSessionUpdate":
-				if (message.isBrowserSessionActive !== undefined) {
-					setState((prev) => ({
-						...prev,
-						isBrowserSessionActive: message.isBrowserSessionActive || false,
-					}))
-				}
-				break
-		}
-	}, [])
-
-	useEffect(() => {
-		window.addEventListener("message", handleMessage)
-		return () => {
-			window.removeEventListener("message", handleMessage)
-		}
-	}, [handleMessage])
-
-	return <BrowserPanelStateContext.Provider value={state}>{children}</BrowserPanelStateContext.Provider>
-}
-
-export const useBrowserPanelState = () => {
-	const context = useContext(BrowserPanelStateContext)
-	if (context === undefined) {
-		throw new Error("useBrowserPanelState must be used within a BrowserPanelStateProvider")
-	}
-	return context
-}

+ 0 - 106
webview-ui/src/components/browser-session/BrowserSessionPanel.tsx

@@ -1,106 +0,0 @@
-import React, { useEffect, useState } from "react"
-
-import { type ClineMessage, type ExtensionMessage } from "@roo-code/types"
-
-import { TooltipProvider } from "@src/components/ui/tooltip"
-import TranslationProvider from "@src/i18n/TranslationContext"
-import { vscode } from "@src/utils/vscode"
-
-import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
-
-import BrowserSessionRow from "../chat/BrowserSessionRow"
-import ErrorBoundary from "../ErrorBoundary"
-
-import { BrowserPanelStateProvider, useBrowserPanelState } from "./BrowserPanelStateProvider"
-
-interface BrowserSessionPanelState {
-	messages: ClineMessage[]
-}
-
-const BrowserSessionPanelContent: React.FC = () => {
-	const { browserViewportSize, isBrowserSessionActive } = useBrowserPanelState()
-	const [state, setState] = useState<BrowserSessionPanelState>({
-		messages: [],
-	})
-	// Target page index to navigate BrowserSessionRow to
-	const [navigateToStepIndex, setNavigateToStepIndex] = useState<number | undefined>(undefined)
-
-	const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
-
-	useEffect(() => {
-		const handleMessage = (event: MessageEvent) => {
-			const message: ExtensionMessage = event.data
-
-			switch (message.type) {
-				case "browserSessionUpdate":
-					if (message.browserSessionMessages) {
-						setState((prev) => ({
-							...prev,
-							messages: message.browserSessionMessages || [],
-						}))
-					}
-					break
-				case "browserSessionNavigate":
-					if (typeof message.stepIndex === "number" && message.stepIndex >= 0) {
-						setNavigateToStepIndex(message.stepIndex)
-					}
-					break
-			}
-		}
-
-		window.addEventListener("message", handleMessage)
-
-		return () => {
-			window.removeEventListener("message", handleMessage)
-		}
-	}, [])
-
-	return (
-		<div className="fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden bg-vscode-editor-background">
-			<BrowserSessionRow
-				messages={state.messages}
-				isLast={true}
-				lastModifiedMessage={state.messages.at(-1)}
-				isStreaming={false}
-				isExpanded={(messageTs: number) => expandedRows[messageTs] ?? false}
-				onToggleExpand={(messageTs: number) => {
-					setExpandedRows((prev: Record<number, boolean>) => ({
-						...prev,
-						[messageTs]: !prev[messageTs],
-					}))
-				}}
-				fullScreen={true}
-				browserViewportSizeProp={browserViewportSize}
-				isBrowserSessionActiveProp={isBrowserSessionActive}
-				navigateToPageIndex={navigateToStepIndex}
-			/>
-		</div>
-	)
-}
-
-const BrowserSessionPanel: React.FC = () => {
-	// Ensure the panel receives initial state and becomes "ready" without needing a second click
-	useEffect(() => {
-		try {
-			vscode.postMessage({ type: "webviewDidLaunch" })
-		} catch {
-			// Ignore errors during initial launch
-		}
-	}, [])
-
-	return (
-		<ErrorBoundary>
-			<ExtensionStateContextProvider>
-				<TooltipProvider>
-					<TranslationProvider>
-						<BrowserPanelStateProvider>
-							<BrowserSessionPanelContent />
-						</BrowserPanelStateProvider>
-					</TranslationProvider>
-				</TooltipProvider>
-			</ExtensionStateContextProvider>
-		</ErrorBoundary>
-	)
-}
-
-export default BrowserSessionPanel

+ 0 - 5
webview-ui/src/components/chat/AutoApproveDropdown.tsx

@@ -34,7 +34,6 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
 		setAlwaysAllowReadOnly,
 		setAlwaysAllowWrite,
 		setAlwaysAllowExecute,
-		setAlwaysAllowBrowser,
 		setAlwaysAllowMcp,
 		setAlwaysAllowModeSwitch,
 		setAlwaysAllowSubtasks,
@@ -57,9 +56,6 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
 				case "alwaysAllowExecute":
 					setAlwaysAllowExecute(value)
 					break
-				case "alwaysAllowBrowser":
-					setAlwaysAllowBrowser(value)
-					break
 				case "alwaysAllowMcp":
 					setAlwaysAllowMcp(value)
 					break
@@ -85,7 +81,6 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
 			setAlwaysAllowReadOnly,
 			setAlwaysAllowWrite,
 			setAlwaysAllowExecute,
-			setAlwaysAllowBrowser,
 			setAlwaysAllowMcp,
 			setAlwaysAllowModeSwitch,
 			setAlwaysAllowSubtasks,

+ 0 - 195
webview-ui/src/components/chat/BrowserActionRow.tsx

@@ -1,195 +0,0 @@
-import { memo, useMemo, useEffect, useRef } from "react"
-import { useTranslation } from "react-i18next"
-import {
-	MousePointer as MousePointerIcon,
-	Keyboard,
-	ArrowDown,
-	ArrowUp,
-	Pointer,
-	Play,
-	Check,
-	Maximize2,
-	Camera,
-} from "lucide-react"
-
-import type { ClineMessage, ClineSayBrowserAction } from "@roo-code/types"
-
-import { getViewportCoordinate as getViewportCoordinateShared, prettyKey } from "@roo/browserUtils"
-
-import { vscode } from "@src/utils/vscode"
-import { useExtensionState } from "@src/context/ExtensionStateContext"
-
-interface BrowserActionRowProps {
-	message: ClineMessage
-	nextMessage?: ClineMessage
-	actionIndex?: number
-	totalActions?: number
-}
-
-// Get icon for each action type
-const getActionIcon = (action: string) => {
-	switch (action) {
-		case "click":
-			return <MousePointerIcon className="w-3.5 h-3.5 opacity-70" />
-		case "type":
-		case "press":
-			return <Keyboard className="w-3.5 h-3.5 opacity-70" />
-		case "scroll_down":
-			return <ArrowDown className="w-3.5 h-3.5 opacity-70" />
-		case "scroll_up":
-			return <ArrowUp className="w-3.5 h-3.5 opacity-70" />
-		case "launch":
-			return <Play className="w-3.5 h-3.5 opacity-70" />
-		case "close":
-			return <Check className="w-3.5 h-3.5 opacity-70" />
-		case "resize":
-			return <Maximize2 className="w-3.5 h-3.5 opacity-70" />
-		case "screenshot":
-			return <Camera className="w-3.5 h-3.5 opacity-70" />
-		case "hover":
-		default:
-			return <Pointer className="w-3.5 h-3.5 opacity-70" />
-	}
-}
-
-const BrowserActionRow = memo(({ message, nextMessage, actionIndex, totalActions }: BrowserActionRowProps) => {
-	const { t } = useTranslation()
-	const { isBrowserSessionActive } = useExtensionState()
-	const hasHandledAutoOpenRef = useRef(false)
-
-	// Parse this specific browser action
-	const browserAction = useMemo<ClineSayBrowserAction | null>(() => {
-		try {
-			return JSON.parse(message.text || "{}") as ClineSayBrowserAction
-		} catch {
-			return null
-		}
-	}, [message.text])
-
-	// Get viewport dimensions from the result message if available
-	const viewportDimensions = useMemo(() => {
-		if (!nextMessage || nextMessage.say !== "browser_action_result") return null
-		try {
-			const result = JSON.parse(nextMessage.text || "{}")
-			return {
-				width: result.viewportWidth,
-				height: result.viewportHeight,
-			}
-		} catch {
-			return null
-		}
-	}, [nextMessage])
-
-	// Format action display text
-	const actionText = useMemo(() => {
-		if (!browserAction) return t("chat:browser.actions.title")
-
-		// Helper to scale coordinates from screenshot dimensions to viewport dimensions
-		// Matches the backend's scaleCoordinate function logic
-		const getViewportCoordinate = (coord?: string): string =>
-			getViewportCoordinateShared(coord, viewportDimensions?.width ?? 0, viewportDimensions?.height ?? 0)
-
-		switch (browserAction.action) {
-			case "launch":
-				return t("chat:browser.actions.launched")
-			case "click":
-				return t("chat:browser.actions.clicked", {
-					coordinate: browserAction.executedCoordinate || getViewportCoordinate(browserAction.coordinate),
-				})
-			case "type":
-				return t("chat:browser.actions.typed", { text: browserAction.text })
-			case "press":
-				return t("chat:browser.actions.pressed", { key: prettyKey(browserAction.text) })
-			case "hover":
-				return t("chat:browser.actions.hovered", {
-					coordinate: browserAction.executedCoordinate || getViewportCoordinate(browserAction.coordinate),
-				})
-			case "scroll_down":
-				return t("chat:browser.actions.scrolledDown")
-			case "scroll_up":
-				return t("chat:browser.actions.scrolledUp")
-			case "resize":
-				return t("chat:browser.actions.resized", { size: browserAction.size?.split(/[x,]/).join(" x ") })
-			case "screenshot":
-				return t("chat:browser.actions.screenshotSaved")
-			case "close":
-				return t("chat:browser.actions.closed")
-			default:
-				return browserAction.action
-		}
-	}, [browserAction, viewportDimensions, t])
-
-	// Auto-open Browser Session panel when:
-	// 1. This is a "launch" action (new browser session) - always opens and navigates to launch
-	// 2. Regular actions - only open panel if user hasn't manually closed it, let internal auto-advance logic handle step
-	// Only run this once per action to avoid re-sending messages when scrolling
-	useEffect(() => {
-		if (!isBrowserSessionActive || hasHandledAutoOpenRef.current) {
-			return
-		}
-
-		const isLaunchAction = browserAction?.action === "launch"
-
-		if (isLaunchAction) {
-			// Launch action: navigate to step 0 (the launch)
-			vscode.postMessage({
-				type: "showBrowserSessionPanelAtStep",
-				stepIndex: 0,
-				isLaunchAction: true,
-			})
-			hasHandledAutoOpenRef.current = true
-		} else {
-			// Regular actions: just show panel, don't navigate
-			// BrowserSessionRow's internal auto-advance logic will handle jumping to new steps
-			// only if user is currently on the most recent step
-			vscode.postMessage({
-				type: "showBrowserSessionPanelAtStep",
-				isLaunchAction: false,
-			})
-			hasHandledAutoOpenRef.current = true
-		}
-	}, [isBrowserSessionActive, browserAction])
-
-	const headerStyle: React.CSSProperties = {
-		display: "flex",
-		alignItems: "center",
-		gap: "10px",
-		marginBottom: "10px",
-		wordBreak: "break-word",
-	}
-
-	return (
-		<div className="px-[15px] py-[10px] pr-[6px]">
-			{/* Header with action description - clicking opens Browser Session panel at this step */}
-			<div
-				style={headerStyle}
-				className="cursor-pointer"
-				onClick={() => {
-					const idx = typeof actionIndex === "number" ? Math.max(0, actionIndex - 1) : 0
-					vscode.postMessage({ type: "showBrowserSessionPanelAtStep", stepIndex: idx, forceShow: true })
-				}}>
-				<span
-					className="codicon codicon-globe text-vscode-testing-iconPassed shrink-0"
-					style={{ marginBottom: "-1.5px" }}
-				/>
-				<span style={{ fontWeight: "bold" }}>{t("chat:browser.actions.title")}</span>
-				{actionIndex !== undefined && totalActions !== undefined && (
-					<span style={{ fontWeight: "bold" }}>
-						{" "}
-						- {actionIndex}/{totalActions} -{" "}
-					</span>
-				)}
-				{browserAction && (
-					<>
-						<span className="shrink-0">{getActionIcon(browserAction.action)}</span>
-						<span className="flex-1 truncate">{actionText}</span>
-					</>
-				)}
-			</div>
-		</div>
-	)
-})
-
-BrowserActionRow.displayName = "BrowserActionRow"
-
-export default BrowserActionRow

+ 0 - 1137
webview-ui/src/components/chat/BrowserSessionRow.tsx

@@ -1,1137 +0,0 @@
-import React, { memo, useEffect, useMemo, useRef, useState } from "react"
-import deepEqual from "fast-deep-equal"
-import { useTranslation } from "react-i18next"
-import type { TFunction } from "i18next"
-
-import type { ClineMessage, BrowserAction, BrowserActionResult, ClineSayBrowserAction } from "@roo-code/types"
-
-import { vscode } from "@src/utils/vscode"
-import { useExtensionState } from "@src/context/ExtensionStateContext"
-
-import CodeBlock from "../common/CodeBlock"
-import { ProgressIndicator } from "./ProgressIndicator"
-import { Button, StandardTooltip } from "@src/components/ui"
-import { getViewportCoordinate as getViewportCoordinateShared, prettyKey } from "@roo/browserUtils"
-import {
-	Globe,
-	Pointer,
-	SquareTerminal,
-	MousePointer as MousePointerIcon,
-	Keyboard,
-	ArrowDown,
-	ArrowUp,
-	Play,
-	Check,
-	Maximize2,
-	OctagonX,
-	ArrowLeft,
-	ArrowRight,
-	ChevronsLeft,
-	ChevronsRight,
-	ExternalLink,
-	Copy,
-	Camera,
-} from "lucide-react"
-
-const getBrowserActionText = (
-	t: TFunction,
-	action: BrowserAction,
-	executedCoordinate?: string,
-	coordinate?: string,
-	text?: string,
-	size?: string,
-	viewportWidth?: number,
-	viewportHeight?: number,
-) => {
-	// Helper to scale coordinates from screenshot dimensions to viewport dimensions
-	// Matches the backend's scaleCoordinate function logic
-	const getViewportCoordinate = (coord?: string): string =>
-		getViewportCoordinateShared(coord, viewportWidth ?? 0, viewportHeight ?? 0)
-
-	switch (action) {
-		case "launch":
-			return t("chat:browser.actions.launched")
-		case "click":
-			return t("chat:browser.actions.clicked", {
-				coordinate: executedCoordinate || getViewportCoordinate(coordinate),
-			})
-		case "type":
-			return t("chat:browser.actions.typed", { text })
-		case "press":
-			return t("chat:browser.actions.pressed", { key: prettyKey(text) })
-		case "scroll_down":
-			return t("chat:browser.actions.scrolledDown")
-		case "scroll_up":
-			return t("chat:browser.actions.scrolledUp")
-		case "hover":
-			return t("chat:browser.actions.hovered", {
-				coordinate: executedCoordinate || getViewportCoordinate(coordinate),
-			})
-		case "resize":
-			return t("chat:browser.actions.resized", { size: size?.split(/[x,]/).join(" x ") })
-		case "screenshot":
-			return t("chat:browser.actions.screenshotSaved")
-		case "close":
-			return t("chat:browser.actions.closed")
-		default:
-			return action
-	}
-}
-
-const getActionIcon = (action: BrowserAction) => {
-	switch (action) {
-		case "click":
-			return <MousePointerIcon className="w-4 h-4 opacity-80" />
-		case "type":
-		case "press":
-			return <Keyboard className="w-4 h-4 opacity-80" />
-		case "scroll_down":
-			return <ArrowDown className="w-4 h-4 opacity-80" />
-		case "scroll_up":
-			return <ArrowUp className="w-4 h-4 opacity-80" />
-		case "launch":
-			return <Play className="w-4 h-4 opacity-80" />
-		case "close":
-			return <Check className="w-4 h-4 opacity-80" />
-		case "resize":
-			return <Maximize2 className="w-4 h-4 opacity-80" />
-		case "screenshot":
-			return <Camera className="w-4 h-4 opacity-80" />
-		case "hover":
-		default:
-			return <Pointer className="w-4 h-4 opacity-80" />
-	}
-}
-
-interface BrowserSessionRowProps {
-	messages: ClineMessage[]
-	isExpanded: (messageTs: number) => boolean
-	onToggleExpand: (messageTs: number) => void
-	lastModifiedMessage?: ClineMessage
-	isLast: boolean
-	onHeightChange?: (isTaller: boolean) => void
-	isStreaming: boolean
-	onExpandChange?: (expanded: boolean) => void
-	fullScreen?: boolean
-	// Optional props for standalone panel (when not using ExtensionStateContext)
-	browserViewportSizeProp?: string
-	isBrowserSessionActiveProp?: boolean
-	// Optional: navigate to a specific page index (used by Browser Session panel)
-	navigateToPageIndex?: number
-}
-
-const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
-	const { messages, isLast, onHeightChange, lastModifiedMessage, onExpandChange, fullScreen } = props
-	const { t } = useTranslation()
-	const prevHeightRef = useRef(0)
-	const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false)
-	const [nextActionsExpanded, setNextActionsExpanded] = useState(false)
-	const [logFilter, setLogFilter] = useState<"all" | "debug" | "info" | "warn" | "error" | "log">("all")
-	// Track screenshot container size for precise cursor positioning with object-fit: contain
-	const screenshotRef = useRef<HTMLDivElement>(null)
-	const [sW, setSW] = useState(0)
-	const [sH, setSH] = useState(0)
-
-	// Auto-expand drawer when in fullScreen takeover mode so content is visible immediately
-	useEffect(() => {
-		if (fullScreen) {
-			setNextActionsExpanded(true)
-		}
-	}, [fullScreen])
-
-	// Observe screenshot container size to align cursor correctly with letterboxing
-	useEffect(() => {
-		const el = screenshotRef.current
-		if (!el) return
-		const update = () => {
-			const r = el.getBoundingClientRect()
-			setSW(r.width)
-			setSH(r.height)
-		}
-		update()
-		const ro =
-			typeof window !== "undefined" && "ResizeObserver" in window ? new ResizeObserver(() => update()) : null
-		if (ro) ro.observe(el)
-		return () => {
-			if (ro) ro.disconnect()
-		}
-	}, [])
-
-	// Try to use ExtensionStateContext if available, otherwise use props
-	let browserViewportSize = props.browserViewportSizeProp || "900x600"
-	let isBrowserSessionActive = props.isBrowserSessionActiveProp || false
-
-	try {
-		const extensionState = useExtensionState()
-		browserViewportSize = extensionState.browserViewportSize || "900x600"
-		isBrowserSessionActive = extensionState.isBrowserSessionActive || false
-	} catch (_e) {
-		// Not in ExtensionStateContext, use props
-	}
-
-	const [viewportWidth, viewportHeight] = browserViewportSize.split("x").map(Number)
-	const defaultMousePosition = `${Math.round(viewportWidth / 2)},${Math.round(viewportHeight / 2)}`
-
-	const isLastApiReqInterrupted = useMemo(() => {
-		// Check if last api_req_started is cancelled
-		const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started")
-		if (lastApiReqStarted?.text) {
-			const info = JSON.parse(lastApiReqStarted.text) as { cancelReason: string | null }
-			if (info && info.cancelReason !== null) {
-				return true
-			}
-		}
-		const lastApiReqFailed = isLast && lastModifiedMessage?.ask === "api_req_failed"
-		if (lastApiReqFailed) {
-			return true
-		}
-		return false
-	}, [messages, lastModifiedMessage, isLast])
-
-	const isBrowsing = useMemo(() => {
-		return isLast && messages.some((m) => m.say === "browser_action_result") && !isLastApiReqInterrupted // after user approves, browser_action_result with "" is sent to indicate that the session has started
-	}, [isLast, messages, isLastApiReqInterrupted])
-
-	// Organize messages into pages based on ALL browser actions (including those without screenshots)
-	const pages = useMemo(() => {
-		const result: {
-			url?: string
-			screenshot?: string
-			mousePosition?: string
-			consoleLogs?: string
-			action?: ClineSayBrowserAction
-			size?: string
-			viewportWidth?: number
-			viewportHeight?: number
-		}[] = []
-
-		// Build pages from browser_action messages and pair with results
-		messages.forEach((message) => {
-			if (message.say === "browser_action") {
-				try {
-					const action = JSON.parse(message.text || "{}") as ClineSayBrowserAction
-					// Find the corresponding result message
-					const resultMessage = messages.find(
-						(m) => m.say === "browser_action_result" && m.ts > message.ts && m.text !== "",
-					)
-
-					if (resultMessage) {
-						const resultData = JSON.parse(resultMessage.text || "{}") as BrowserActionResult
-						result.push({
-							url: resultData.currentUrl,
-							screenshot: resultData.screenshot,
-							mousePosition: resultData.currentMousePosition,
-							consoleLogs: resultData.logs,
-							action,
-							size: action.size,
-							viewportWidth: resultData.viewportWidth,
-							viewportHeight: resultData.viewportHeight,
-						})
-					} else {
-						// For actions without results (like close), add a page without screenshot
-						result.push({ action, size: action.size })
-					}
-				} catch {
-					// ignore parse errors
-				}
-			}
-		})
-
-		// Add placeholder page if no actions yet
-		if (result.length === 0) {
-			result.push({})
-		}
-
-		return result
-	}, [messages])
-
-	// Page index + user navigation guard (don't auto-jump while exploring history)
-	const [currentPageIndex, setCurrentPageIndex] = useState(0)
-	const hasUserNavigatedRef = useRef(false)
-	const didInitIndexRef = useRef(false)
-	const prevPagesLengthRef = useRef(0)
-
-	useEffect(() => {
-		// Initialize to last page on mount
-		if (!didInitIndexRef.current && pages.length > 0) {
-			didInitIndexRef.current = true
-			setCurrentPageIndex(pages.length - 1)
-			prevPagesLengthRef.current = pages.length
-			return
-		}
-
-		// Auto-advance if user is on the most recent step and a new step arrives
-		if (pages.length > prevPagesLengthRef.current) {
-			const wasOnLastPage = currentPageIndex === prevPagesLengthRef.current - 1
-			if (wasOnLastPage && !hasUserNavigatedRef.current) {
-				// User was on the most recent step, auto-advance to the new step
-				setCurrentPageIndex(pages.length - 1)
-			}
-			prevPagesLengthRef.current = pages.length
-		}
-	}, [pages.length, currentPageIndex])
-
-	// External navigation request (from panel host)
-	// Only navigate when navigateToPageIndex actually changes, not when pages.length changes
-	const prevNavigateToPageIndexRef = useRef<number | undefined>()
-	useEffect(() => {
-		if (
-			typeof props.navigateToPageIndex === "number" &&
-			props.navigateToPageIndex !== prevNavigateToPageIndexRef.current &&
-			pages.length > 0
-		) {
-			const idx = Math.max(0, Math.min(pages.length - 1, props.navigateToPageIndex))
-			setCurrentPageIndex(idx)
-			// Only reset manual navigation guard if navigating to the last page
-			// This allows auto-advance to work when clicking to the most recent step
-			// but prevents unwanted auto-advance when viewing historical steps
-			if (idx === pages.length - 1) {
-				hasUserNavigatedRef.current = false
-			}
-			prevNavigateToPageIndexRef.current = props.navigateToPageIndex
-		}
-		// eslint-disable-next-line react-hooks/exhaustive-deps
-	}, [props.navigateToPageIndex])
-
-	// Get initial URL from launch message
-	const initialUrl = useMemo(() => {
-		const launchMessage = messages.find((m) => m.ask === "browser_action_launch")
-		return launchMessage?.text || ""
-	}, [messages])
-
-	const currentPage = pages[currentPageIndex]
-
-	// Use actual viewport dimensions from result if available, otherwise fall back to settings
-
-	// Find the last available screenshot and its associated data to use as placeholders
-	const lastPageWithScreenshot = useMemo(() => {
-		for (let i = pages.length - 1; i >= 0; i--) {
-			if (pages[i].screenshot) {
-				return pages[i]
-			}
-		}
-		return undefined
-	}, [pages])
-
-	// Find last mouse position up to current page (not from future pages)
-	const lastPageWithMousePositionUpToCurrent = useMemo(() => {
-		for (let i = currentPageIndex; i >= 0; i--) {
-			if (pages[i].mousePosition) {
-				return pages[i]
-			}
-		}
-		return undefined
-	}, [pages, currentPageIndex])
-
-	// Display state from current page, with smart fallbacks
-	const displayState = {
-		url: currentPage?.url || initialUrl,
-		mousePosition:
-			currentPage?.mousePosition || lastPageWithMousePositionUpToCurrent?.mousePosition || defaultMousePosition,
-		consoleLogs: currentPage?.consoleLogs,
-		screenshot: currentPage?.screenshot || lastPageWithScreenshot?.screenshot,
-	}
-
-	// Parse logs for counts and filtering
-	const parsedLogs = useMemo(() => {
-		const counts = { debug: 0, info: 0, warn: 0, error: 0, log: 0 }
-		const byType: Record<"debug" | "info" | "warn" | "error" | "log", string[]> = {
-			debug: [],
-			info: [],
-			warn: [],
-			error: [],
-			log: [],
-		}
-		const raw = displayState.consoleLogs || ""
-		raw.split(/\r?\n/).forEach((line) => {
-			const trimmed = line.trim()
-			if (!trimmed) return
-			const m = /^\[([^\]]+)\]\s*/i.exec(trimmed)
-			let type = (m?.[1] || "").toLowerCase()
-			if (type === "warning") type = "warn"
-			if (!["debug", "info", "warn", "error", "log"].includes(type)) type = "log"
-			counts[type as keyof typeof counts]++
-			byType[type as keyof typeof byType].push(line)
-		})
-		return { counts, byType }
-	}, [displayState.consoleLogs])
-
-	const logsToShow = useMemo(() => {
-		if (!displayState.consoleLogs) return t("chat:browser.noNewLogs") as string
-		if (logFilter === "all") return displayState.consoleLogs
-		const arr = parsedLogs.byType[logFilter]
-		return arr.length ? arr.join("\n") : (t("chat:browser.noNewLogs") as string)
-	}, [displayState.consoleLogs, logFilter, parsedLogs, t])
-
-	// Meta for log badges (include "All" first)
-	const logTypeMeta = [
-		{ key: "all", label: "All" },
-		{ key: "debug", label: "Debug" },
-		{ key: "info", label: "Info" },
-		{ key: "warn", label: "Warn" },
-		{ key: "error", label: "Error" },
-		{ key: "log", label: "Log" },
-	] as const
-
-	// Use a fixed standard aspect ratio and dimensions for the drawer to prevent flickering
-	// Even if viewport changes, the drawer maintains consistent size
-	const fixedDrawerWidth = 900
-	const fixedDrawerHeight = 600
-	const drawerAspectRatio = (fixedDrawerHeight / fixedDrawerWidth) * 100
-
-	// For cursor positioning, use the viewport dimensions from the same page as the data we're displaying
-	// This ensures cursor position matches the screenshot/mouse position being shown
-	let cursorViewportWidth: number
-	let cursorViewportHeight: number
-
-	if (currentPage?.screenshot) {
-		// Current page has screenshot - use its dimensions
-		cursorViewportWidth = currentPage.viewportWidth ?? viewportWidth
-		cursorViewportHeight = currentPage.viewportHeight ?? viewportHeight
-	} else if (lastPageWithScreenshot) {
-		// Using placeholder screenshot - use dimensions from that page
-		cursorViewportWidth = lastPageWithScreenshot.viewportWidth ?? viewportWidth
-		cursorViewportHeight = lastPageWithScreenshot.viewportHeight ?? viewportHeight
-	} else {
-		// No screenshot available - use default settings
-		cursorViewportWidth = viewportWidth
-		cursorViewportHeight = viewportHeight
-	}
-
-	// Get browser action for current page (now stored in pages array)
-	const currentPageAction = useMemo(() => {
-		return pages[currentPageIndex]?.action
-	}, [pages, currentPageIndex])
-
-	// Latest non-close browser_action for header summary (fallback)
-
-	const lastBrowserActionOverall = useMemo(() => {
-		const all = messages.filter((m) => m.say === "browser_action")
-		return all.at(-1)
-	}, [messages])
-
-	// Use actual Playwright session state from extension (not message parsing)
-	const isBrowserSessionOpen = isBrowserSessionActive
-
-	// Check if a browser action is currently in flight (for spinner)
-	const isActionRunning = useMemo(() => {
-		if (!lastBrowserActionOverall || isLastApiReqInterrupted) {
-			return false
-		}
-
-		// Find the last browser_action_result (including empty text) to detect completion
-		const lastBrowserActionResult = [...messages].reverse().find((m) => m.say === "browser_action_result")
-
-		if (!lastBrowserActionResult) {
-			// We have at least one action, but haven't seen any result yet
-			return true
-		}
-
-		// If the last action happened after the last result, it's still running
-		return lastBrowserActionOverall.ts > lastBrowserActionResult.ts
-	}, [messages, lastBrowserActionOverall, isLastApiReqInterrupted])
-
-	// Browser session drawer never auto-expands - user must manually toggle it
-
-	// Calculate total API cost for the browser session
-	const totalApiCost = useMemo(() => {
-		let total = 0
-		messages.forEach((message) => {
-			if (message.say === "api_req_started" && message.text) {
-				try {
-					const data = JSON.parse(message.text)
-					if (data.cost && typeof data.cost === "number") {
-						total += data.cost
-					}
-				} catch {
-					// Ignore parsing errors
-				}
-			}
-		})
-		return total
-	}, [messages])
-
-	// Local size tracking without react-use to avoid timers after unmount in tests
-	const containerRef = useRef<HTMLDivElement>(null)
-	const [rowHeight, setRowHeight] = useState(0)
-	useEffect(() => {
-		const el = containerRef.current
-		if (!el) return
-		let mounted = true
-		const setH = (h: number) => {
-			if (mounted) setRowHeight(h)
-		}
-		const ro =
-			typeof window !== "undefined" && "ResizeObserver" in window
-				? new ResizeObserver((entries) => {
-						const entry = entries[0]
-						setH(entry?.contentRect?.height ?? el.getBoundingClientRect().height)
-					})
-				: null
-		// initial
-		setH(el.getBoundingClientRect().height)
-		if (ro) ro.observe(el)
-		return () => {
-			mounted = false
-			if (ro) ro.disconnect()
-		}
-	}, [])
-
-	const BrowserSessionHeader: React.FC = () => (
-		<div
-			style={{
-				display: "flex",
-				alignItems: "center",
-				gap: 8,
-				marginBottom: 0,
-				userSelect: "none",
-			}}>
-			{/* Globe icon - green when browser session is open */}
-			<Globe
-				className="w-4 h-4 shrink-0"
-				style={{
-					opacity: 0.7,
-					color: isBrowserSessionOpen ? "#4ade80" : undefined, // green-400 when session is open
-					cursor: fullScreen ? "default" : "pointer",
-				}}
-				aria-label="Browser interaction"
-				{...(fullScreen
-					? {}
-					: {
-							onClick: () =>
-								setNextActionsExpanded((v) => {
-									const nv = !v
-									onExpandChange?.(nv)
-									return nv
-								}),
-						})}
-			/>
-
-			{/* Simple text: "Browser Session" with step counter */}
-			<span
-				{...(fullScreen
-					? {}
-					: {
-							onClick: () =>
-								setNextActionsExpanded((v) => {
-									const nv = !v
-									onExpandChange?.(nv)
-									return nv
-								}),
-						})}
-				style={{
-					flex: 1,
-					fontSize: 13,
-					fontWeight: 500,
-					lineHeight: "22px",
-					color: "var(--vscode-editor-foreground)",
-					cursor: fullScreen ? "default" : "pointer",
-					display: "flex",
-					alignItems: "center",
-					gap: 8,
-				}}>
-				{t("chat:browser.session")}
-				{isActionRunning && (
-					<span className="ml-1 flex items-center" aria-hidden="true">
-						<ProgressIndicator />
-					</span>
-				)}
-				{pages.length > 0 && (
-					<span
-						style={{
-							fontSize: 11,
-							opacity: 0.6,
-							fontWeight: 400,
-						}}>
-						{currentPageIndex + 1}/{pages.length}
-					</span>
-				)}
-				{/* Inline action summary to the right, similar to ChatView */}
-				<span
-					style={{
-						display: "inline-flex",
-						alignItems: "center",
-						gap: 6,
-						fontSize: 12,
-						color: "var(--vscode-descriptionForeground)",
-						fontWeight: 400,
-					}}>
-					{(() => {
-						const action = currentPageAction
-						const pageSize = pages[currentPageIndex]?.size
-						const pageViewportWidth = pages[currentPageIndex]?.viewportWidth
-						const pageViewportHeight = pages[currentPageIndex]?.viewportHeight
-						if (action) {
-							return (
-								<>
-									{getActionIcon(action.action)}
-									<span>
-										{getBrowserActionText(
-											t,
-											action.action,
-											action.executedCoordinate,
-											action.coordinate,
-											action.text,
-											pageSize,
-											pageViewportWidth,
-											pageViewportHeight,
-										)}
-									</span>
-								</>
-							)
-						} else if (initialUrl) {
-							return (
-								<>
-									{getActionIcon("launch" as any)}
-									<span>{getBrowserActionText(t, "launch", undefined, initialUrl, undefined)}</span>
-								</>
-							)
-						}
-						return null
-					})()}
-				</span>
-			</span>
-
-			{/* Right side: cost badge and chevron */}
-			{totalApiCost > 0 && (
-				<div
-					className="text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg"
-					style={{
-						opacity: 0.4,
-						height: "22px",
-						display: "flex",
-						alignItems: "center",
-					}}>
-					${totalApiCost.toFixed(4)}
-				</div>
-			)}
-
-			{/* Chevron toggle hidden in fullScreen */}
-			{!fullScreen && (
-				<span
-					onClick={() =>
-						setNextActionsExpanded((v) => {
-							const nv = !v
-							onExpandChange?.(nv)
-							return nv
-						})
-					}
-					className={`codicon ${nextActionsExpanded ? "codicon-chevron-up" : "codicon-chevron-down"}`}
-					style={{
-						fontSize: 13,
-						fontWeight: 500,
-						lineHeight: "22px",
-						color: "var(--vscode-editor-foreground)",
-						cursor: "pointer",
-						display: "inline-block",
-						transition: "transform 150ms ease",
-					}}
-				/>
-			)}
-
-			{/* Kill browser button hidden from header in fullScreen; kept in toolbar */}
-			{isBrowserSessionOpen && !fullScreen && (
-				<StandardTooltip content="Disconnect session">
-					<Button
-						variant="ghost"
-						size="icon"
-						onClick={(e) => {
-							e.stopPropagation()
-							vscode.postMessage({ type: "killBrowserSession" })
-						}}
-						aria-label="Disconnect session">
-						<OctagonX className="size-4" />
-					</Button>
-				</StandardTooltip>
-			)}
-		</div>
-	)
-
-	const BrowserSessionDrawer: React.FC = () => {
-		if (!nextActionsExpanded) return null
-
-		return (
-			<div
-				style={{
-					marginTop: fullScreen ? 0 : 6,
-					background: "var(--vscode-editor-background)",
-					border: "1px solid var(--vscode-panel-border)",
-					borderRadius: fullScreen ? 0 : 6,
-					overflow: "hidden",
-					height: fullScreen ? "100%" : undefined,
-					display: fullScreen ? "flex" : undefined,
-					flexDirection: fullScreen ? "column" : undefined,
-				}}>
-				{/* Browser-like Toolbar */}
-				<div
-					style={{
-						padding: "6px 8px",
-						display: "flex",
-						alignItems: "center",
-						gap: "8px",
-						borderBottom: "1px solid var(--vscode-panel-border)",
-						background: "var(--vscode-editor-background)",
-					}}>
-					{/* Go to beginning */}
-					<StandardTooltip content="Go to beginning">
-						<button
-							onClick={(e) => {
-								e.stopPropagation()
-								hasUserNavigatedRef.current = true
-								setCurrentPageIndex(0)
-							}}
-							disabled={currentPageIndex === 0 || isBrowsing}
-							style={{
-								background: "none",
-								border: "1px solid var(--vscode-panel-border)",
-								borderRadius: 4,
-								cursor: currentPageIndex === 0 || isBrowsing ? "not-allowed" : "pointer",
-								opacity: currentPageIndex === 0 || isBrowsing ? 0.4 : 0.85,
-								padding: "4px",
-								display: "flex",
-								alignItems: "center",
-								color: "var(--vscode-foreground)",
-							}}
-							aria-label="Go to beginning">
-							<ChevronsLeft className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-
-					{/* Back */}
-					<StandardTooltip content="Back">
-						<button
-							onClick={(e) => {
-								e.stopPropagation()
-								hasUserNavigatedRef.current = true
-								setCurrentPageIndex((i) => Math.max(0, i - 1))
-							}}
-							disabled={currentPageIndex === 0 || isBrowsing}
-							style={{
-								background: "none",
-								border: "1px solid var(--vscode-panel-border)",
-								borderRadius: 4,
-								cursor: currentPageIndex === 0 || isBrowsing ? "not-allowed" : "pointer",
-								opacity: currentPageIndex === 0 || isBrowsing ? 0.4 : 0.85,
-								padding: "4px",
-								display: "flex",
-								alignItems: "center",
-								color: "var(--vscode-foreground)",
-							}}
-							aria-label="Back">
-							<ArrowLeft className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-
-					{/* Forward */}
-					<StandardTooltip content="Forward">
-						<button
-							onClick={(e) => {
-								e.stopPropagation()
-								const nextIndex = Math.min(pages.length - 1, currentPageIndex + 1)
-								// Reset user navigation flag if going to the last page
-								hasUserNavigatedRef.current = nextIndex !== pages.length - 1
-								setCurrentPageIndex(nextIndex)
-							}}
-							disabled={currentPageIndex === pages.length - 1 || isBrowsing}
-							style={{
-								background: "none",
-								border: "1px solid var(--vscode-panel-border)",
-								borderRadius: 4,
-								cursor: currentPageIndex === pages.length - 1 || isBrowsing ? "not-allowed" : "pointer",
-								opacity: currentPageIndex === pages.length - 1 || isBrowsing ? 0.4 : 0.85,
-								padding: "4px",
-								display: "flex",
-								alignItems: "center",
-								color: "var(--vscode-foreground)",
-							}}
-							aria-label="Forward">
-							<ArrowRight className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-
-					{/* Go to end */}
-					<StandardTooltip content="Go to end">
-						<button
-							onClick={(e) => {
-								e.stopPropagation()
-								// Reset user navigation flag since we're going to the most recent page
-								hasUserNavigatedRef.current = false
-								setCurrentPageIndex(pages.length - 1)
-							}}
-							disabled={currentPageIndex === pages.length - 1 || isBrowsing}
-							style={{
-								background: "none",
-								border: "1px solid var(--vscode-panel-border)",
-								borderRadius: 4,
-								cursor: currentPageIndex === pages.length - 1 || isBrowsing ? "not-allowed" : "pointer",
-								opacity: currentPageIndex === pages.length - 1 || isBrowsing ? 0.4 : 0.85,
-								padding: "4px",
-								display: "flex",
-								alignItems: "center",
-								color: "var(--vscode-foreground)",
-							}}
-							aria-label="Go to end">
-							<ChevronsRight className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-
-					{/* Address Bar */}
-					<div
-						role="group"
-						aria-label="Address bar"
-						style={{
-							flex: 1,
-							display: "flex",
-							alignItems: "center",
-							gap: 8,
-							border: "1px solid var(--vscode-panel-border)",
-							borderRadius: 999,
-							padding: "4px 10px",
-							background: "var(--vscode-input-background)",
-							color: "var(--vscode-descriptionForeground)",
-							minHeight: 26,
-							overflow: "hidden",
-						}}>
-						<Globe className="w-3 h-3 shrink-0 opacity-60" />
-						<span
-							style={{
-								fontSize: 12,
-								lineHeight: "18px",
-								textOverflow: "ellipsis",
-								overflow: "hidden",
-								whiteSpace: "nowrap",
-								color: "var(--vscode-foreground)",
-							}}>
-							{displayState.url || "about:blank"}
-						</span>
-						{/* Step counter removed */}
-					</div>
-
-					{/* Kill (Disconnect) replaces Reload */}
-					<StandardTooltip content="Disconnect session">
-						<button
-							onClick={(e) => {
-								e.stopPropagation()
-								vscode.postMessage({ type: "killBrowserSession" })
-							}}
-							style={{
-								background: "none",
-								border: "1px solid var(--vscode-panel-border)",
-								borderRadius: 4,
-								cursor: "pointer",
-								opacity: 0.85,
-								padding: "4px",
-								display: "flex",
-								alignItems: "center",
-								color: "var(--vscode-foreground)",
-							}}
-							aria-label="Disconnect session">
-							<OctagonX className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-
-					{/* Open External */}
-					<StandardTooltip content="Open in external browser">
-						<button
-							onClick={(e) => {
-								e.stopPropagation()
-								if (displayState.url) {
-									vscode.postMessage({ type: "openExternal", url: displayState.url })
-								}
-							}}
-							style={{
-								background: "none",
-								border: "1px solid var(--vscode-panel-border)",
-								borderRadius: 4,
-								cursor: displayState.url ? "pointer" : "not-allowed",
-								opacity: displayState.url ? 0.85 : 0.4,
-								padding: "4px",
-								display: "flex",
-								alignItems: "center",
-								color: "var(--vscode-foreground)",
-							}}
-							aria-label="Open external"
-							disabled={!displayState.url}>
-							<ExternalLink className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-
-					{/* Copy URL */}
-					<StandardTooltip content="Copy URL">
-						<button
-							onClick={async (e) => {
-								e.stopPropagation()
-								try {
-									await navigator.clipboard.writeText(displayState.url || "")
-								} catch {
-									// ignore
-								}
-							}}
-							style={{
-								background: "none",
-								border: "1px solid var(--vscode-panel-border)",
-								borderRadius: 4,
-								cursor: "pointer",
-								opacity: 0.85,
-								padding: "4px",
-								display: "flex",
-								alignItems: "center",
-								color: "var(--vscode-foreground)",
-							}}
-							aria-label="Copy URL">
-							<Copy className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-				</div>
-				{/* Screenshot Area */}
-				<div
-					data-testid="screenshot-container"
-					ref={screenshotRef}
-					style={{
-						width: "100%",
-						position: "relative",
-						backgroundColor: "var(--vscode-input-background)",
-						borderBottom: "1px solid var(--vscode-panel-border)",
-						...(fullScreen
-							? { flex: 1, minHeight: 0 }
-							: { paddingBottom: `${drawerAspectRatio.toFixed(2)}%` }),
-					}}>
-					{displayState.screenshot ? (
-						<img
-							src={displayState.screenshot}
-							alt={t("chat:browser.screenshot")}
-							style={{
-								position: "absolute",
-								top: 0,
-								left: 0,
-								width: "100%",
-								height: "100%",
-								objectFit: "contain",
-								objectPosition: "top center",
-								cursor: "pointer",
-							}}
-							onClick={() =>
-								vscode.postMessage({
-									type: "openImage",
-									text: displayState.screenshot,
-								})
-							}
-						/>
-					) : (
-						<div
-							style={{
-								position: "absolute",
-								top: "50%",
-								left: "50%",
-								transform: "translate(-50%, -50%)",
-							}}>
-							<span
-								className="codicon codicon-globe"
-								style={{ fontSize: "80px", color: "var(--vscode-descriptionForeground)" }}
-							/>
-						</div>
-					)}
-					{displayState.mousePosition &&
-						(() => {
-							// Use measured size if available; otherwise fall back to current client size so cursor remains visible
-							const containerW = sW || (screenshotRef.current?.clientWidth ?? 0)
-							const containerH = sH || (screenshotRef.current?.clientHeight ?? 0)
-							if (containerW <= 0 || containerH <= 0) {
-								// Minimal fallback to keep cursor visible before first measurement
-								return (
-									<BrowserCursor
-										style={{
-											position: "absolute",
-											top: `0px`,
-											left: `0px`,
-											zIndex: 2,
-											pointerEvents: "none",
-										}}
-									/>
-								)
-							}
-
-							// Compute displayed image box within the container for object-fit: contain; objectPosition: top center
-							const imgAspect = cursorViewportWidth / cursorViewportHeight
-							const containerAspect = containerW / containerH
-							let displayW = containerW
-							let displayH = containerH
-							let offsetX = 0
-							let offsetY = 0
-							if (containerAspect > imgAspect) {
-								// Full height, letterboxed left/right; top aligned
-								displayH = containerH
-								displayW = containerH * imgAspect
-								offsetX = (containerW - displayW) / 2
-								offsetY = 0
-							} else {
-								// Full width, potential space below; top aligned
-								displayW = containerW
-								displayH = containerW / imgAspect
-								offsetX = 0
-								offsetY = 0
-							}
-
-							// Parse "x,y" or "x,y@widthxheight" for original basis
-							const m = /^\s*(\d+)\s*,\s*(\d+)(?:\s*@\s*(\d+)\s*[x,]\s*(\d+))?\s*$/.exec(
-								displayState.mousePosition || "",
-							)
-							const mx = parseInt(m?.[1] || "0", 10)
-							const my = parseInt(m?.[2] || "0", 10)
-							const baseW = m?.[3] ? parseInt(m[3], 10) : cursorViewportWidth
-							const baseH = m?.[4] ? parseInt(m[4], 10) : cursorViewportHeight
-
-							const leftPx = offsetX + (baseW > 0 ? (mx / baseW) * displayW : 0)
-							const topPx = offsetY + (baseH > 0 ? (my / baseH) * displayH : 0)
-
-							return (
-								<BrowserCursor
-									style={{
-										position: "absolute",
-										top: `${topPx}px`,
-										left: `${leftPx}px`,
-										zIndex: 2,
-										pointerEvents: "none",
-										transition: "top 0.15s ease-out, left 0.15s ease-out",
-									}}
-								/>
-							)
-						})()}
-				</div>
-
-				{/* Browser Action summary moved inline to header; row removed */}
-
-				{/* Console Logs Section (collapsible, default collapsed) */}
-				<div
-					style={{
-						padding: "8px 10px",
-						// Pin logs to bottom of the fullscreen drawer
-						marginTop: fullScreen ? "auto" : undefined,
-					}}>
-					<div
-						onClick={(e) => {
-							e.stopPropagation()
-							setConsoleLogsExpanded((v) => !v)
-						}}
-						className="text-vscode-editor-foreground/70 hover:text-vscode-editor-foreground transition-colors"
-						style={{
-							display: "flex",
-							alignItems: "center",
-							gap: "8px",
-							marginBottom: consoleLogsExpanded ? "6px" : 0,
-							cursor: "pointer",
-						}}>
-						<SquareTerminal className="w-3" />
-						<span className="text-xs" style={{ fontWeight: 500 }}>
-							{t("chat:browser.consoleLogs")}
-						</span>
-
-						{/* Log type indicators */}
-						<div
-							onClick={(e) => e.stopPropagation()}
-							style={{ display: "flex", alignItems: "center", gap: 6, marginLeft: "auto" }}>
-							{logTypeMeta.map(({ key, label }) => {
-								const isAll = key === "all"
-								const count = isAll
-									? (Object.values(parsedLogs.counts) as number[]).reduce((a, b) => a + b, 0)
-									: parsedLogs.counts[key as "debug" | "info" | "warn" | "error" | "log"]
-								const isActive = logFilter === (key as any)
-								const disabled = count === 0
-								return (
-									<button
-										key={key}
-										onClick={() => {
-											setConsoleLogsExpanded(true)
-											setLogFilter(
-												isAll
-													? "all"
-													: (prev) => (prev === (key as any) ? "all" : (key as any)),
-											)
-										}}
-										disabled={disabled}
-										title={`${label}: ${count}`}
-										style={{
-											border: "1px solid var(--vscode-panel-border)",
-											borderRadius: 999,
-											padding: "0 6px",
-											height: 18,
-											lineHeight: "16px",
-											fontSize: 10,
-											color: "var(--vscode-foreground)",
-											background: isActive
-												? "var(--vscode-editor-selectionBackground)"
-												: "transparent",
-											opacity: disabled ? 0.35 : 0.85,
-											cursor: disabled ? "not-allowed" : "pointer",
-										}}>
-										{label}: {count}
-									</button>
-								)
-							})}
-							<span
-								onClick={() => setConsoleLogsExpanded((v) => !v)}
-								className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}
-								style={{ marginLeft: 6 }}
-							/>
-						</div>
-					</div>
-					{consoleLogsExpanded && (
-						<div style={{ marginTop: "6px" }}>
-							<CodeBlock source={logsToShow} language="shell" />
-						</div>
-					)}
-				</div>
-			</div>
-		)
-	}
-
-	const browserSessionRow = (
-		<div
-			ref={containerRef}
-			style={{
-				padding: "6px 10px",
-				background: "var(--vscode-editor-background,transparent)",
-				height: "100%",
-			}}>
-			<BrowserSessionHeader />
-
-			{/* Expanded drawer content - inline/fullscreen */}
-			<BrowserSessionDrawer />
-		</div>
-	)
-
-	// Height change effect
-	useEffect(() => {
-		const isInitialRender = prevHeightRef.current === 0
-		if (isLast && rowHeight !== 0 && rowHeight !== Infinity && rowHeight !== prevHeightRef.current) {
-			if (!isInitialRender) {
-				onHeightChange?.(rowHeight > prevHeightRef.current)
-			}
-			prevHeightRef.current = rowHeight
-		}
-	}, [rowHeight, isLast, onHeightChange])
-
-	return browserSessionRow
-}, deepEqual)
-
-const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
-	const { t } = useTranslation()
-	// (can't use svgs in vsc extensions)
-	const cursorBase64 =
-		"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAYCAYAAAAVibZIAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAFaADAAQAAAABAAAAGAAAAADwi9a/AAADGElEQVQ4EZ2VbUiTURTH772be/PxZdsz3cZwC4RVaB8SAjMpxQwSWZbQG/TFkN7oW1Df+h6IRV9C+hCpKUSIZUXOfGM5tAKViijFFEyfZ7Ol29S1Pbdzl8Uw9+aBu91zzv3/nt17zt2DEZjBYOAkKrtFMXIghAWM8U2vMN/FctsxGRMpM7NbEEYNMM2CYUSInlJx3OpawO9i+XSNQYkmk2uFb9njzkcfVSr1p/GJiQKMULVaw2WuBv296UKRxWJR6wxGCmM1EAhSNppv33GBH9qI32cPTAtss9lUm6EM3N7R+RbigT+5/CeosFCZKpjEW+iorS1pb30wDUXzQfHqtD/9L3ieZ2ee1OJCmbL8QHnRs+4uj0wmW4QzrpCwvJ8zGg3JqAmhTLynuLiwv8/5KyND8Q3cEkUEDWu15oJE4KRQJt5hs1rcriGNRqP+DK4dyyWXXm/aFQ+cEpSJ8/LyDGPuEZNOmzsOroUSOqzXG/dtBU4ZysTZYKNut91sNo2Cq6cE9enz86s2g9OCMrFSqVC5hgb32u072W3jKMU90Hb1seC0oUwsB+t92bO/rKx0EFGkgFCnjjc1/gVvC8rE0L+4o63t4InjxwbAJQjTe3qD8QrLkXA4DC24fWtuajp06cLFYSBIFKGmXKPRRmAnME9sPt+yLwIWb9WN69fKoTneQz4Dh2mpPNkvfeV0jjecb9wNAkwIEVQq5VJOds4Kb+DXoAsiVquVwI1Dougpij6UyGYx+5cKroeDEFibm5lWRRMbH1+npmYrq6qhwlQHIbajZEf1fElcqGGFpGg9HMuKzpfBjhytCTMgkJ56RX09zy/ysENTBElmjIgJnmNChJqohDVQqpEfwkILE8v/o0GAnV9F1eEvofVQCbiTBEXOIPQh5PGgefDZeAcjrpGZjULBr/m3tZOnz7oEQWRAQZLjWlEU/XEJWySiILgRc5Cz1DkcAyuBFcnpfF0JiXWKpcolQXizhS5hKAqFpr0MVbgbuxJ6+5xX+P4wNpbqPPrugZfbmIbLmgQR3Aw8QSi66hUXulOFbF73GxqjE5BNXWNeAAAAAElFTkSuQmCC"
-
-	return (
-		<img
-			src={cursorBase64}
-			style={{
-				width: "17px",
-				height: "22px",
-				...style,
-			}}
-			alt={t("chat:browser.cursor")}
-			aria-label={t("chat:browser.cursor")}
-		/>
-	)
-}
-
-export default BrowserSessionRow

+ 0 - 34
webview-ui/src/components/chat/BrowserSessionStatusRow.tsx

@@ -1,34 +0,0 @@
-import { memo } from "react"
-import { Globe } from "lucide-react"
-import { ClineMessage } from "@roo-code/types"
-
-interface BrowserSessionStatusRowProps {
-	message: ClineMessage
-}
-
-const BrowserSessionStatusRow = memo(({ message }: BrowserSessionStatusRowProps) => {
-	const isOpened = message.text?.includes("opened")
-
-	return (
-		<div className="flex items-center gap-2 py-2 px-[15px] text-sm">
-			<Globe
-				className="w-4 h-4 shrink-0"
-				style={{
-					opacity: 0.7,
-					color: isOpened ? "#4ade80" : "#9ca3af", // green when opened, gray when closed
-				}}
-			/>
-			<span
-				style={{
-					color: isOpened ? "var(--vscode-testing-iconPassed)" : "var(--vscode-descriptionForeground)",
-					fontWeight: 500,
-				}}>
-				{message.text}
-			</span>
-		</div>
-	)
-})
-
-BrowserSessionStatusRow.displayName = "BrowserSessionStatusRow"
-
-export default BrowserSessionStatusRow

+ 0 - 4
webview-ui/src/components/chat/ChatRow.tsx

@@ -1558,10 +1558,6 @@ export const ChatRowContent = ({
 							<ImageBlock imageUri={imageInfo.imageUri} imagePath={imageInfo.imagePath} />
 						</div>
 					)
-				case "browser_action":
-				case "browser_action_result":
-					// Handled by BrowserSessionRow; prevent raw JSON (action/result) from rendering here
-					return null
 				case "too_many_tools_warning": {
 					const warningData = safeJsonParse<{
 						toolCount: number

+ 0 - 11
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -52,9 +52,6 @@ interface ChatTextAreaProps {
 	// Edit mode props
 	isEditMode?: boolean
 	onCancel?: () => void
-	// Browser session status
-	isBrowserSessionActive?: boolean
-	showBrowserDockToggle?: boolean
 	// Stop/Queue functionality
 	isStreaming?: boolean
 	onStop?: () => void
@@ -79,8 +76,6 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			modeShortcutText,
 			isEditMode = false,
 			onCancel,
-			isBrowserSessionActive = false,
-			showBrowserDockToggle = false,
 			isStreaming = false,
 			onStop,
 			onEnqueueMessage,
@@ -1354,12 +1349,6 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						)}
 						{!isEditMode ? <IndexingStatusBadge /> : null}
 						{!isEditMode && cloudUserInfo && <CloudAccountSwitcher />}
-						{/* keep props referenced after moving browser button */}
-						<div
-							className="hidden"
-							data-browser-session-active={isBrowserSessionActive}
-							data-show-browser-dock-toggle={showBrowserDockToggle}
-						/>
 					</div>
 				</div>
 			</div>

+ 2 - 82
webview-ui/src/components/chat/ChatView.tsx

@@ -38,8 +38,6 @@ import TelemetryBanner from "../common/TelemetryBanner"
 import VersionIndicator from "../common/VersionIndicator"
 import HistoryPreview from "../history/HistoryPreview"
 import Announcement from "./Announcement"
-import BrowserActionRow from "./BrowserActionRow"
-import BrowserSessionStatusRow from "./BrowserSessionStatusRow"
 import ChatRow from "./ChatRow"
 import WarningRow from "./WarningRow"
 import { ChatTextArea } from "./ChatTextArea"
@@ -95,7 +93,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		soundVolume,
 		cloudIsAuthenticated,
 		messageQueue = [],
-		isBrowserSessionActive,
 		showWorktreesInHomeScreen,
 	} = useExtensionState()
 
@@ -370,13 +367,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 									break
 							}
 							break
-						case "browser_action_launch":
-							setSendingDisabled(isPartial)
-							setClineAsk("browser_action_launch")
-							setEnableButtons(!isPartial)
-							setPrimaryButtonText(t("chat:approve.title"))
-							setSecondaryButtonText(t("chat:reject.title"))
-							break
 						case "command":
 							setSendingDisabled(isPartial)
 							setClineAsk("command")
@@ -467,8 +457,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 						case "api_req_finished":
 						case "error":
 						case "text":
-						case "browser_action":
-						case "browser_action_result":
 						case "command_output":
 						case "mcp_server_request_started":
 						case "mcp_server_response":
@@ -713,7 +701,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					) {
 						case "followup":
 						case "tool":
-						case "browser_action_launch":
 						case "command": // User can provide feedback to a tool or command use.
 						case "use_mcp_server":
 						case "completion_result": // If this happens then the user has feedback for the completion result.
@@ -801,7 +788,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				case "api_req_failed":
 				case "command":
 				case "tool":
-				case "browser_action_launch":
 				case "use_mcp_server":
 				case "mistake_limit_reached":
 					// Only send text/images if they exist
@@ -886,7 +872,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					break
 				case "command":
 				case "tool":
-				case "browser_action_launch":
 				case "use_mcp_server":
 					// Only send text/images if they exist
 					if (trimmedInput || (images && images.length > 0)) {
@@ -1179,43 +1164,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		setWasStreaming(isStreaming)
 	}, [isStreaming, lastMessage, wasStreaming, messages.length])
 
-	// Compute current browser session messages for the top banner (not grouped into chat stream)
-	// Find the FIRST browser session from the beginning to show ALL sessions
-	const browserSessionStartIndex = useMemo(() => {
-		for (let i = 0; i < messages.length; i++) {
-			if (messages[i].ask === "browser_action_launch") {
-				return i
-			}
-		}
-		return -1
-	}, [messages])
-
-	const _browserSessionMessages = useMemo<ClineMessage[]>(() => {
-		if (browserSessionStartIndex === -1) return []
-		return messages.slice(browserSessionStartIndex)
-	}, [browserSessionStartIndex, messages])
-
-	// Show globe toggle only when in a task that has a browser session (active or inactive)
-	const showBrowserDockToggle = useMemo(
-		() => Boolean(task && (browserSessionStartIndex !== -1 || isBrowserSessionActive)),
-		[task, browserSessionStartIndex, isBrowserSessionActive],
-	)
-
-	const isBrowserSessionMessage = useCallback((message: ClineMessage): boolean => {
-		// Only the launch ask should be hidden from chat (it's shown in the drawer header)
-		if (message.type === "ask" && message.ask === "browser_action_launch") {
-			return true
-		}
-		// browser_action_result messages are paired with browser_action and should not appear independently
-		if (message.type === "say" && message.say === "browser_action_result") {
-			return true
-		}
-		return false
-	}, [])
-
 	const groupedMessages = useMemo(() => {
-		// Only filter out the launch ask and result messages - browser actions appear in chat
-		const filtered: ClineMessage[] = visibleMessages.filter((msg) => !isBrowserSessionMessage(msg))
+		const filtered: ClineMessage[] = visibleMessages
 
 		// Helper to check if a message is a read_file ask that should be batched
 		const isReadFileAsk = (msg: ClineMessage): boolean => {
@@ -1361,7 +1311,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 			} as ClineMessage)
 		}
 		return result
-	}, [isCondensing, visibleMessages, isBrowserSessionMessage])
+	}, [isCondensing, visibleMessages])
 
 	// scrolling
 
@@ -1496,34 +1446,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		(index: number, messageOrGroup: ClineMessage) => {
 			const hasCheckpoint = modifiedMessages.some((message) => message.say === "checkpoint_saved")
 
-			// Check if this is a browser action message
-			if (messageOrGroup.type === "say" && messageOrGroup.say === "browser_action") {
-				// Find the corresponding result message by looking for the next browser_action_result after this action's timestamp
-				const nextMessage = modifiedMessages.find(
-					(m) => m.ts > messageOrGroup.ts && m.say === "browser_action_result",
-				)
-
-				// Calculate action index and total count
-				const browserActions = modifiedMessages.filter((m) => m.say === "browser_action")
-				const actionIndex = browserActions.findIndex((m) => m.ts === messageOrGroup.ts) + 1
-				const totalActions = browserActions.length
-
-				return (
-					<BrowserActionRow
-						key={messageOrGroup.ts}
-						message={messageOrGroup}
-						nextMessage={nextMessage}
-						actionIndex={actionIndex}
-						totalActions={totalActions}
-					/>
-				)
-			}
-
-			// Check if this is a browser session status message
-			if (messageOrGroup.type === "say" && messageOrGroup.say === "browser_session_status") {
-				return <BrowserSessionStatusRow key={messageOrGroup.ts} message={messageOrGroup} />
-			}
-
 			// regular message
 			return (
 				<ChatRow
@@ -1900,8 +1822,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				mode={mode}
 				setMode={setMode}
 				modeShortcutText={modeShortcutText}
-				isBrowserSessionActive={!!isBrowserSessionActive}
-				showBrowserDockToggle={showBrowserDockToggle}
 				isStreaming={isStreaming}
 				onStop={handleStopTask}
 				onEnqueueMessage={handleEnqueueCurrentMessage}

+ 2 - 55
webview-ui/src/components/chat/TaskHeader.tsx

@@ -3,15 +3,7 @@ import { useTranslation } from "react-i18next"
 import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
 import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
 import DismissibleUpsell from "@src/components/common/DismissibleUpsell"
-import {
-	ChevronUp,
-	ChevronDown,
-	HardDriveDownload,
-	HardDriveUpload,
-	FoldVertical,
-	Globe,
-	ArrowLeft,
-} from "lucide-react"
+import { ChevronUp, ChevronDown, HardDriveDownload, HardDriveUpload, FoldVertical, ArrowLeft } from "lucide-react"
 import prettyBytes from "pretty-bytes"
 
 import type { ClineMessage } from "@roo-code/types"
@@ -68,7 +60,7 @@ const TaskHeader = ({
 	todos,
 }: TaskHeaderProps) => {
 	const { t } = useTranslation()
-	const { apiConfiguration, currentTaskItem, clineMessages, isBrowserSessionActive } = useExtensionState()
+	const { apiConfiguration, currentTaskItem, clineMessages } = useExtensionState()
 	const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
 	const [isTaskExpanded, setIsTaskExpanded] = useState(false)
 	const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false)
@@ -118,18 +110,6 @@ const TaskHeader = ({
 	)
 	const reservedForOutput = maxTokens || 0
 
-	// Detect if this task had any browser session activity so we can show a grey globe when inactive
-	const browserSessionStartIndex = useMemo(() => {
-		const msgs = clineMessages || []
-		for (let i = 0; i < msgs.length; i++) {
-			const m = msgs[i] as any
-			if (m?.ask === "browser_action_launch") return i
-		}
-		return -1
-	}, [clineMessages])
-
-	const showBrowserGlobe = browserSessionStartIndex !== -1 || !!isBrowserSessionActive
-
 	const condenseButton = (
 		<LucideIconButton
 			title={t("chat:task.condenseContext")}
@@ -336,39 +316,6 @@ const TaskHeader = ({
 								</>
 							)}
 						</div>
-						{showBrowserGlobe && (
-							<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
-								<StandardTooltip content={t("chat:browser.session")}>
-									<Button
-										variant="ghost"
-										size="sm"
-										aria-label={t("chat:browser.session")}
-										onClick={() => vscode.postMessage({ type: "openBrowserSessionPanel" } as any)}
-										className={cn(
-											"relative h-5 w-5 p-0",
-											"text-vscode-foreground opacity-85",
-											"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
-											"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-										)}>
-										<Globe
-											className="w-4 h-4"
-											style={{
-												color: isBrowserSessionActive
-													? "#4ade80"
-													: "var(--vscode-descriptionForeground)",
-											}}
-										/>
-									</Button>
-								</StandardTooltip>
-								{isBrowserSessionActive && (
-									<span
-										className="text-sm font-medium"
-										style={{ color: "var(--vscode-testing-iconPassed)" }}>
-										{t("chat:browser.active")}
-									</span>
-								)}
-							</div>
-						)}
 					</div>
 				)}
 				{/* Expanded state: Show task text and images */}

+ 0 - 55
webview-ui/src/components/chat/__tests__/BrowserSessionRow.aspect-ratio.spec.tsx

@@ -1,55 +0,0 @@
-import { render, screen, fireEvent } from "@testing-library/react"
-import React from "react"
-import BrowserSessionRow from "../BrowserSessionRow"
-import { ExtensionStateContext } from "@src/context/ExtensionStateContext"
-import { TooltipProvider } from "@src/components/ui/tooltip"
-
-describe("BrowserSessionRow - screenshot area", () => {
-	const renderRow = (messages: any[]) => {
-		const mockExtState: any = {
-			// Ensure known viewport so expected aspect ratio is deterministic (600/900 = 66.67%)
-			browserViewportSize: "900x600",
-			isBrowserSessionActive: false,
-		}
-
-		return render(
-			<TooltipProvider>
-				<ExtensionStateContext.Provider value={mockExtState}>
-					<BrowserSessionRow
-						messages={messages as any}
-						isExpanded={() => true}
-						onToggleExpand={() => {}}
-						lastModifiedMessage={undefined as any}
-						isLast={true}
-						onHeightChange={() => {}}
-						isStreaming={false}
-					/>
-				</ExtensionStateContext.Provider>
-			</TooltipProvider>,
-		)
-	}
-
-	it("reserves height while screenshot is loading (no layout collapse)", () => {
-		// Only a launch action, no corresponding browser_action_result yet (no screenshot)
-		const messages = [
-			{
-				ts: 1,
-				say: "browser_action",
-				text: JSON.stringify({ action: "launch", url: "http://localhost:3000" }),
-			},
-		]
-
-		renderRow(messages)
-
-		// Open the browser session drawer
-		const globe = screen.getByLabelText("Browser interaction")
-		fireEvent.click(globe)
-
-		const container = screen.getByTestId("screenshot-container") as HTMLDivElement
-		// padding-bottom should reflect aspect ratio (600/900 * 100) even without an image
-		const pb = parseFloat(container.style.paddingBottom || "0")
-		expect(pb).toBeGreaterThan(0)
-		// Be tolerant of rounding
-		expect(Math.round(pb)).toBe(67)
-	})
-})

+ 0 - 42
webview-ui/src/components/chat/__tests__/BrowserSessionRow.disconnect-button.spec.tsx

@@ -1,42 +0,0 @@
-import React from "react"
-import { render, screen } from "@testing-library/react"
-import BrowserSessionRow from "../BrowserSessionRow"
-import { ExtensionStateContext } from "@src/context/ExtensionStateContext"
-import { TooltipProvider } from "@radix-ui/react-tooltip"
-
-describe("BrowserSessionRow - Disconnect session button", () => {
-	const renderRow = (isActive: boolean) => {
-		const mockExtState: any = {
-			browserViewportSize: "900x600",
-			isBrowserSessionActive: isActive,
-		}
-
-		return render(
-			<TooltipProvider>
-				<ExtensionStateContext.Provider value={mockExtState}>
-					<BrowserSessionRow
-						messages={[] as any}
-						isExpanded={() => false}
-						onToggleExpand={() => {}}
-						lastModifiedMessage={undefined as any}
-						isLast={true}
-						onHeightChange={() => {}}
-						isStreaming={false}
-					/>
-				</ExtensionStateContext.Provider>
-			</TooltipProvider>,
-		)
-	}
-
-	it("shows the Disconnect session button when a session is active", () => {
-		renderRow(true)
-		const btn = screen.getByLabelText("Disconnect session")
-		expect(btn).toBeInTheDocument()
-	})
-
-	it("does not render the button when no session is active", () => {
-		renderRow(false)
-		const btn = screen.queryByLabelText("Disconnect session")
-		expect(btn).toBeNull()
-	})
-})

+ 0 - 126
webview-ui/src/components/chat/__tests__/BrowserSessionRow.spec.tsx

@@ -1,126 +0,0 @@
-import React from "react"
-import { describe, it, expect, vi } from "vitest"
-import { render, screen } from "@testing-library/react"
-
-import BrowserSessionRow from "../BrowserSessionRow"
-
-// Mock ExtensionStateContext so BrowserSessionRow falls back to props
-vi.mock("@src/context/ExtensionStateContext", () => ({
-	useExtensionState: () => {
-		throw new Error("No ExtensionStateContext in test environment")
-	},
-}))
-
-// Simplify i18n usage and provide initReactI18next for i18n setup
-vi.mock("react-i18next", () => ({
-	useTranslation: () => ({
-		t: (key: string) => key,
-	}),
-	initReactI18next: {
-		type: "3rdParty",
-		init: () => {},
-	},
-}))
-
-// Replace ProgressIndicator with a simple test marker
-vi.mock("../ProgressIndicator", () => ({
-	ProgressIndicator: () => <div data-testid="browser-session-spinner" />,
-}))
-
-const baseProps = {
-	isExpanded: () => false,
-	onToggleExpand: () => {},
-	lastModifiedMessage: undefined,
-	isLast: true,
-	onHeightChange: () => {},
-	isStreaming: false,
-}
-
-describe("BrowserSessionRow - action spinner", () => {
-	it("does not show spinner when there are no browser actions", () => {
-		const messages = [
-			{
-				type: "say",
-				say: "task",
-				ts: 1,
-				text: "Task started",
-			} as any,
-		]
-
-		render(<BrowserSessionRow {...baseProps} messages={messages} />)
-
-		expect(screen.queryByTestId("browser-session-spinner")).toBeNull()
-	})
-
-	it("shows spinner while the latest browser action is still running", () => {
-		const messages = [
-			{
-				type: "say",
-				say: "task",
-				ts: 1,
-				text: "Task started",
-			} as any,
-			{
-				type: "say",
-				say: "browser_action",
-				ts: 2,
-				text: JSON.stringify({ action: "click" }),
-			} as any,
-			{
-				type: "say",
-				say: "browser_action_result",
-				ts: 3,
-				text: JSON.stringify({ currentUrl: "https://example.com" }),
-			} as any,
-			{
-				type: "say",
-				say: "browser_action",
-				ts: 4,
-				text: JSON.stringify({ action: "scroll_down" }),
-			} as any,
-		]
-
-		render(<BrowserSessionRow {...baseProps} messages={messages} />)
-
-		expect(screen.getByTestId("browser-session-spinner")).toBeInTheDocument()
-	})
-
-	it("hides spinner once the latest browser action has a result", () => {
-		const messages = [
-			{
-				type: "say",
-				say: "task",
-				ts: 1,
-				text: "Task started",
-			} as any,
-			{
-				type: "say",
-				say: "browser_action",
-				ts: 2,
-				text: JSON.stringify({ action: "click" }),
-			} as any,
-			{
-				type: "say",
-				say: "browser_action_result",
-				ts: 3,
-				text: JSON.stringify({ currentUrl: "https://example.com" }),
-			} as any,
-			{
-				type: "say",
-				say: "browser_action",
-				ts: 4,
-				text: JSON.stringify({ action: "scroll_down" }),
-			} as any,
-			{
-				type: "say",
-				say: "browser_action_result",
-				ts: 5,
-				text: JSON.stringify({ currentUrl: "https://example.com/page2" }),
-			} as any,
-		]
-
-		render(<BrowserSessionRow {...baseProps} messages={messages} />)
-
-		expect(screen.queryByTestId("browser-session-spinner")).toBeNull()
-	})
-})

+ 0 - 4
webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx

@@ -24,10 +24,6 @@ vi.mock("use-sound", () => ({
 }))
 
 // Mock components
-vi.mock("../BrowserSessionRow", () => ({
-	default: () => null,
-}))
-
 vi.mock("../ChatRow", () => ({
 	default: () => null,
 }))

+ 0 - 6
webview-ui/src/components/chat/__tests__/ChatView.notification-sound.spec.tsx

@@ -49,12 +49,6 @@ vi.mock("use-sound", () => ({
 }))
 
 // Mock components that use ESM dependencies
-vi.mock("../BrowserSessionRow", () => ({
-	default: function MockBrowserSessionRow({ messages }: { messages: ClineMessage[] }) {
-		return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
-	},
-}))
-
 vi.mock("../ChatRow", () => ({
 	default: function MockChatRow({ message }: { message: ClineMessage }) {
 		return <div data-testid="chat-row">{JSON.stringify(message)}</div>

+ 0 - 6
webview-ui/src/components/chat/__tests__/ChatView.preserve-images.spec.tsx

@@ -44,12 +44,6 @@ vi.mock("use-sound", () => ({
 }))
 
 // Mock components that use ESM dependencies
-vi.mock("../BrowserSessionRow", () => ({
-	default: function MockBrowserSessionRow({ messages }: { messages: ClineMessage[] }) {
-		return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
-	},
-}))
-
 vi.mock("../ChatRow", () => ({
 	default: function MockChatRow({ message }: { message: ClineMessage }) {
 		return <div data-testid="chat-row">{JSON.stringify(message)}</div>

+ 0 - 6
webview-ui/src/components/chat/__tests__/ChatView.spec.tsx

@@ -45,12 +45,6 @@ vi.mock("use-sound", () => ({
 }))
 
 // Mock components that use ESM dependencies
-vi.mock("../BrowserSessionRow", () => ({
-	default: function MockBrowserSessionRow({ messages }: { messages: ClineMessage[] }) {
-		return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
-	},
-}))
-
 vi.mock("../ChatRow", () => ({
 	default: function MockChatRow({ message }: { message: ClineMessage }) {
 		return <div data-testid="chat-row">{JSON.stringify(message)}</div>

+ 0 - 4
webview-ui/src/components/settings/AutoApproveSettings.tsx

@@ -24,7 +24,6 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 	alwaysAllowWrite?: boolean
 	alwaysAllowWriteOutsideWorkspace?: boolean
 	alwaysAllowWriteProtected?: boolean
-	alwaysAllowBrowser?: boolean
 	alwaysAllowMcp?: boolean
 	alwaysAllowModeSwitch?: boolean
 	alwaysAllowSubtasks?: boolean
@@ -41,7 +40,6 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 		| "alwaysAllowWrite"
 		| "alwaysAllowWriteOutsideWorkspace"
 		| "alwaysAllowWriteProtected"
-		| "alwaysAllowBrowser"
 		| "alwaysAllowMcp"
 		| "alwaysAllowModeSwitch"
 		| "alwaysAllowSubtasks"
@@ -61,7 +59,6 @@ export const AutoApproveSettings = ({
 	alwaysAllowWrite,
 	alwaysAllowWriteOutsideWorkspace,
 	alwaysAllowWriteProtected,
-	alwaysAllowBrowser,
 	alwaysAllowMcp,
 	alwaysAllowModeSwitch,
 	alwaysAllowSubtasks,
@@ -155,7 +152,6 @@ export const AutoApproveSettings = ({
 					<AutoApproveToggle
 						alwaysAllowReadOnly={alwaysAllowReadOnly}
 						alwaysAllowWrite={alwaysAllowWrite}
-						alwaysAllowBrowser={alwaysAllowBrowser}
 						alwaysAllowMcp={alwaysAllowMcp}
 						alwaysAllowModeSwitch={alwaysAllowModeSwitch}
 						alwaysAllowSubtasks={alwaysAllowSubtasks}

+ 0 - 8
webview-ui/src/components/settings/AutoApproveToggle.tsx

@@ -8,7 +8,6 @@ type AutoApproveToggles = Pick<
 	GlobalSettings,
 	| "alwaysAllowReadOnly"
 	| "alwaysAllowWrite"
-	| "alwaysAllowBrowser"
 	| "alwaysAllowMcp"
 	| "alwaysAllowModeSwitch"
 	| "alwaysAllowSubtasks"
@@ -41,13 +40,6 @@ export const autoApproveSettingsConfig: Record<AutoApproveSetting, AutoApproveCo
 		icon: "edit",
 		testId: "always-allow-write-toggle",
 	},
-	alwaysAllowBrowser: {
-		key: "alwaysAllowBrowser",
-		labelKey: "settings:autoApprove.browser.label",
-		descriptionKey: "settings:autoApprove.browser.description",
-		icon: "globe",
-		testId: "always-allow-browser-toggle",
-	},
 	alwaysAllowMcp: {
 		key: "alwaysAllowMcp",
 		labelKey: "settings:autoApprove.mcp.label",

+ 0 - 243
webview-ui/src/components/settings/BrowserSettings.tsx

@@ -1,243 +0,0 @@
-import { VSCodeCheckbox, VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { HTMLAttributes, useEffect, useMemo, useState } from "react"
-import { Trans } from "react-i18next"
-
-import {
-	Select,
-	SelectContent,
-	SelectGroup,
-	SelectItem,
-	SelectTrigger,
-	SelectValue,
-	Slider,
-	Button,
-} from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
-import { vscode } from "@/utils/vscode"
-import { buildDocLink } from "@src/utils/docLinks"
-
-import { SearchableSetting } from "./SearchableSetting"
-import { Section } from "./Section"
-import { SectionHeader } from "./SectionHeader"
-import { SetCachedStateField } from "./types"
-
-type BrowserSettingsProps = HTMLAttributes<HTMLDivElement> & {
-	browserToolEnabled?: boolean
-	browserViewportSize?: string
-	screenshotQuality?: number
-	remoteBrowserHost?: string
-	remoteBrowserEnabled?: boolean
-	setCachedStateField: SetCachedStateField<
-		| "browserToolEnabled"
-		| "browserViewportSize"
-		| "screenshotQuality"
-		| "remoteBrowserHost"
-		| "remoteBrowserEnabled"
-	>
-}
-
-export const BrowserSettings = ({
-	browserToolEnabled,
-	browserViewportSize,
-	screenshotQuality,
-	remoteBrowserHost,
-	remoteBrowserEnabled,
-	setCachedStateField,
-	...props
-}: BrowserSettingsProps) => {
-	const { t } = useAppTranslation()
-
-	const [testingConnection, setTestingConnection] = useState(false)
-	const [testResult, setTestResult] = useState<{ success: boolean; text: string } | null>(null)
-	const [discovering, setDiscovering] = useState(false)
-
-	// We don't need a local state for useRemoteBrowser since we're using the
-	// `enableRemoteBrowser` prop directly. This ensures the checkbox always
-	// reflects the current global state.
-
-	// Set up message listener for browser connection results.
-	useEffect(() => {
-		const handleMessage = (event: MessageEvent) => {
-			const message = event.data
-
-			if (message.type === "browserConnectionResult") {
-				setTestResult({ success: message.success, text: message.text })
-				setTestingConnection(false)
-				setDiscovering(false)
-			}
-		}
-
-		window.addEventListener("message", handleMessage)
-
-		return () => {
-			window.removeEventListener("message", handleMessage)
-		}
-	}, [])
-
-	const testConnection = async () => {
-		setTestingConnection(true)
-		setTestResult(null)
-
-		try {
-			// Send a message to the extension to test the connection.
-			vscode.postMessage({ type: "testBrowserConnection", text: remoteBrowserHost })
-		} catch (error) {
-			setTestResult({
-				success: false,
-				text: `Error: ${error instanceof Error ? error.message : String(error)}`,
-			})
-			setTestingConnection(false)
-		}
-	}
-
-	const options = useMemo(
-		() => [
-			{
-				value: "1280x800",
-				label: t("settings:browser.viewport.options.largeDesktop"),
-			},
-			{
-				value: "900x600",
-				label: t("settings:browser.viewport.options.smallDesktop"),
-			},
-			{ value: "768x1024", label: t("settings:browser.viewport.options.tablet") },
-			{ value: "360x640", label: t("settings:browser.viewport.options.mobile") },
-		],
-		[t],
-	)
-
-	return (
-		<div {...props}>
-			<SectionHeader>{t("settings:sections.browser")}</SectionHeader>
-
-			<Section>
-				<SearchableSetting
-					settingId="browser-enable"
-					section="browser"
-					label={t("settings:browser.enable.label")}>
-					<VSCodeCheckbox
-						checked={browserToolEnabled}
-						onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
-						<span className="font-medium">{t("settings:browser.enable.label")}</span>
-					</VSCodeCheckbox>
-					<div className="text-vscode-descriptionForeground text-sm mt-1">
-						<Trans i18nKey="settings:browser.enable.description">
-							<VSCodeLink
-								href={buildDocLink("features/browser-use", "settings_browser_tool")}
-								style={{ display: "inline" }}>
-								{" "}
-							</VSCodeLink>
-						</Trans>
-					</div>
-				</SearchableSetting>
-
-				{browserToolEnabled && (
-					<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
-						<SearchableSetting
-							settingId="browser-viewport"
-							section="browser"
-							label={t("settings:browser.viewport.label")}>
-							<label className="block font-medium mb-1">{t("settings:browser.viewport.label")}</label>
-							<Select
-								value={browserViewportSize}
-								onValueChange={(value) => setCachedStateField("browserViewportSize", value)}>
-								<SelectTrigger className="w-full">
-									<SelectValue placeholder={t("settings:common.select")} />
-								</SelectTrigger>
-								<SelectContent>
-									<SelectGroup>
-										{options.map(({ value, label }) => (
-											<SelectItem key={value} value={value}>
-												{label}
-											</SelectItem>
-										))}
-									</SelectGroup>
-								</SelectContent>
-							</Select>
-							<div className="text-vscode-descriptionForeground text-sm mt-1">
-								{t("settings:browser.viewport.description")}
-							</div>
-						</SearchableSetting>
-
-						<SearchableSetting
-							settingId="browser-screenshot-quality"
-							section="browser"
-							label={t("settings:browser.screenshotQuality.label")}>
-							<label className="block font-medium mb-1">
-								{t("settings:browser.screenshotQuality.label")}
-							</label>
-							<div className="flex items-center gap-2">
-								<Slider
-									min={1}
-									max={100}
-									step={1}
-									value={[screenshotQuality ?? 75]}
-									onValueChange={([value]) => setCachedStateField("screenshotQuality", value)}
-								/>
-								<span className="w-10">{screenshotQuality ?? 75}%</span>
-							</div>
-							<div className="text-vscode-descriptionForeground text-sm mt-1">
-								{t("settings:browser.screenshotQuality.description")}
-							</div>
-						</SearchableSetting>
-
-						<SearchableSetting
-							settingId="browser-remote"
-							section="browser"
-							label={t("settings:browser.remote.label")}>
-							<VSCodeCheckbox
-								checked={remoteBrowserEnabled}
-								onChange={(e: any) => {
-									// Update the global state - remoteBrowserEnabled now means "enable remote browser connection".
-									setCachedStateField("remoteBrowserEnabled", e.target.checked)
-
-									if (!e.target.checked) {
-										// If disabling remote browser, clear the custom URL.
-										setCachedStateField("remoteBrowserHost", undefined)
-									}
-								}}>
-								<label className="block font-medium mb-1">{t("settings:browser.remote.label")}</label>
-							</VSCodeCheckbox>
-							<div className="text-vscode-descriptionForeground text-sm mt-1">
-								{t("settings:browser.remote.description")}
-							</div>
-						</SearchableSetting>
-
-						{remoteBrowserEnabled && (
-							<>
-								<div className="flex items-center gap-2">
-									<VSCodeTextField
-										value={remoteBrowserHost ?? ""}
-										onChange={(e: any) =>
-											setCachedStateField("remoteBrowserHost", e.target.value || undefined)
-										}
-										placeholder={t("settings:browser.remote.urlPlaceholder")}
-										style={{ flexGrow: 1 }}
-									/>
-									<Button disabled={testingConnection} onClick={testConnection}>
-										{testingConnection || discovering
-											? t("settings:browser.remote.testingButton")
-											: t("settings:browser.remote.testButton")}
-									</Button>
-								</div>
-								{testResult && (
-									<div
-										className={`p-2 rounded-xs text-sm ${
-											testResult.success
-												? "bg-green-800/20 text-green-400"
-												: "bg-red-800/20 text-red-400"
-										}`}>
-										{testResult.text}
-									</div>
-								)}
-								<div className="text-vscode-descriptionForeground text-sm mt-1">
-									{t("settings:browser.remote.instructions")}
-								</div>
-							</>
-						)}
-					</div>
-				)}
-			</Section>
-		</div>
-	)
-}

Некоторые файлы не были показаны из-за большого количества измененных файлов