Explorar o código

Add a way to save screenshots from the browser tool (#9963)

* Add a way to save screenshots from the browser tool

* fix: use cross-platform paths in BrowserSession screenshot tests

* fix: validate screenshot paths to prevent filesystem escape

---------

Co-authored-by: Roo Code <[email protected]>
Matt Rubens hai 3 semanas
pai
achega
721b02e58c
Modificáronse 31 ficheiros con 579 adicións e 26 borrados
  1. 2 1
      packages/types/src/tool-params.ts
  2. 2 0
      src/core/assistant-message/NativeToolCallParser.ts
  3. 13 0
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap
  4. 13 0
      src/core/prompts/tools/browser-action.ts
  5. 18 1
      src/core/prompts/tools/native-tools/browser_action.ts
  6. 21 2
      src/core/tools/BrowserActionTool.ts
  7. 27 0
      src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts
  8. 49 0
      src/services/browser/BrowserSession.ts
  9. 174 0
      src/services/browser/__tests__/BrowserSession.spec.ts
  10. 1 0
      src/shared/ExtensionMessage.ts
  11. 1 1
      src/shared/tools.ts
  12. 20 11
      webview-ui/src/components/chat/BrowserActionRow.tsx
  13. 22 10
      webview-ui/src/components/chat/BrowserSessionRow.tsx
  14. 12 0
      webview-ui/src/i18n/locales/ca/chat.json
  15. 12 0
      webview-ui/src/i18n/locales/de/chat.json
  16. 12 0
      webview-ui/src/i18n/locales/en/chat.json
  17. 12 0
      webview-ui/src/i18n/locales/es/chat.json
  18. 12 0
      webview-ui/src/i18n/locales/fr/chat.json
  19. 12 0
      webview-ui/src/i18n/locales/hi/chat.json
  20. 12 0
      webview-ui/src/i18n/locales/id/chat.json
  21. 12 0
      webview-ui/src/i18n/locales/it/chat.json
  22. 12 0
      webview-ui/src/i18n/locales/ja/chat.json
  23. 12 0
      webview-ui/src/i18n/locales/ko/chat.json
  24. 12 0
      webview-ui/src/i18n/locales/nl/chat.json
  25. 12 0
      webview-ui/src/i18n/locales/pl/chat.json
  26. 12 0
      webview-ui/src/i18n/locales/pt-BR/chat.json
  27. 12 0
      webview-ui/src/i18n/locales/ru/chat.json
  28. 12 0
      webview-ui/src/i18n/locales/tr/chat.json
  29. 12 0
      webview-ui/src/i18n/locales/vi/chat.json
  30. 12 0
      webview-ui/src/i18n/locales/zh-CN/chat.json
  31. 12 0
      webview-ui/src/i18n/locales/zh-TW/chat.json

+ 2 - 1
packages/types/src/tool-params.ts

@@ -23,11 +23,12 @@ export interface Size {
 }
 
 export interface BrowserActionParams {
-	action: "launch" | "click" | "hover" | "type" | "scroll_down" | "scroll_up" | "resize" | "close"
+	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 {

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

@@ -406,6 +406,7 @@ export class NativeToolCallParser {
 						coordinate: partialArgs.coordinate,
 						size: partialArgs.size,
 						text: partialArgs.text,
+						path: partialArgs.path,
 					}
 				}
 				break
@@ -645,6 +646,7 @@ export class NativeToolCallParser {
 							coordinate: args.coordinate,
 							size: args.size,
 							text: args.text,
+							path: args.path,
 						} as NativeArgsFor<TName>
 					}
 					break

+ 13 - 0
src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap

@@ -247,6 +247,10 @@ Parameters:
         - Use with the `size` parameter to specify the new size.
     * scroll_down: Scroll down the page by one page height.
     * scroll_up: Scroll up the page by one page height.
+    * screenshot: Take a screenshot and save it to a file.
+        - Use with the `path` parameter to specify the destination file path.
+        - Supported formats: .png, .jpeg, .webp
+        - Example: `<action>screenshot</action>` with `<path>screenshots/result.png</path>`
     * close: Close the Puppeteer-controlled browser instance. This **must always be the final browser action**.
         - Example: `<action>close</action>`
 - url: (optional) Use this for providing the URL for the `launch` action.
@@ -264,6 +268,9 @@ Parameters:
     * Example: <size>1280,720</size>
 - text: (optional) Use this for providing the text for the `type` action.
     * Example: <text>Hello, world!</text>
+- path: (optional) File path for the `screenshot` action. Path is relative to the workspace.
+    * Supported formats: .png, .jpeg, .webp
+    * Example: <path>screenshots/my-screenshot.png</path>
 Usage:
 <browser_action>
 <action>Action to perform (e.g., launch, click, type, press, scroll_down, scroll_up, close)</action>
@@ -284,6 +291,12 @@ Example: Requesting to click on the element at coordinates 450,300 on a 1024x768
 <coordinate>450,300@1024x768</coordinate>
 </browser_action>
 
+Example: Taking a screenshot and saving it to a file
+<browser_action>
+<action>screenshot</action>
+<path>screenshots/result.png</path>
+</browser_action>
+
 ## ask_followup_question
 Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
 

+ 13 - 0
src/core/prompts/tools/browser-action.ts

@@ -39,6 +39,10 @@ Parameters:
         - Use with the \`size\` parameter to specify the new size.
     * scroll_down: Scroll down the page by one page height.
     * scroll_up: Scroll up the page by one page height.
+    * screenshot: Take a screenshot and save it to a file.
+        - Use with the \`path\` parameter to specify the destination file path.
+        - Supported formats: .png, .jpeg, .webp
+        - Example: \`<action>screenshot</action>\` with \`<path>screenshots/result.png</path>\`
     * close: Close the Puppeteer-controlled browser instance. This **must always be the final browser action**.
         - Example: \`<action>close</action>\`
 - url: (optional) Use this for providing the URL for the \`launch\` action.
@@ -56,6 +60,9 @@ Parameters:
     * Example: <size>1280,720</size>
 - text: (optional) Use this for providing the text for the \`type\` action.
     * Example: <text>Hello, world!</text>
+- path: (optional) File path for the \`screenshot\` action. Path is relative to the workspace.
+    * Supported formats: .png, .jpeg, .webp
+    * Example: <path>screenshots/my-screenshot.png</path>
 Usage:
 <browser_action>
 <action>Action to perform (e.g., launch, click, type, press, scroll_down, scroll_up, close)</action>
@@ -74,5 +81,11 @@ Example: Requesting to click on the element at coordinates 450,300 on a 1024x768
 <browser_action>
 <action>click</action>
 <coordinate>450,300@1024x768</coordinate>
+</browser_action>
+
+Example: Taking a screenshot and saving it to a file
+<browser_action>
+<action>screenshot</action>
+<path>screenshots/result.png</path>
 </browser_action>`
 }

+ 18 - 1
src/core/prompts/tools/native-tools/browser_action.ts

@@ -21,6 +21,8 @@ const SIZE_PARAMETER_DESCRIPTION = `Viewport dimensions for the resize action in
 
 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: {
@@ -33,7 +35,18 @@ export default {
 				action: {
 					type: "string",
 					description: ACTION_PARAMETER_DESCRIPTION,
-					enum: ["launch", "click", "hover", "type", "press", "scroll_down", "scroll_up", "resize", "close"],
+					enum: [
+						"launch",
+						"click",
+						"hover",
+						"type",
+						"press",
+						"scroll_down",
+						"scroll_up",
+						"resize",
+						"close",
+						"screenshot",
+					],
 				},
 				url: {
 					type: ["string", "null"],
@@ -51,6 +64,10 @@ export default {
 					type: ["string", "null"],
 					description: TEXT_PARAMETER_DESCRIPTION,
 				},
+				path: {
+					type: ["string", "null"],
+					description: PATH_PARAMETER_DESCRIPTION,
+				},
 			},
 			required: ["action"],
 			additionalProperties: false,

+ 21 - 2
src/core/tools/BrowserActionTool.ts

@@ -23,6 +23,7 @@ export async function browserActionTool(
 	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
@@ -155,6 +156,17 @@ export async function browserActionTool(
 					}
 				}
 
+				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
@@ -191,6 +203,9 @@ export async function browserActionTool(
 					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
@@ -205,12 +220,16 @@ export async function browserActionTool(
 				case "press":
 				case "scroll_down":
 				case "scroll_up":
-				case "resize": {
+				case "resize":
+				case "screenshot": {
 					await cline.say("browser_action_result", JSON.stringify(browserActionResult))
 
 					const images = browserActionResult?.screenshot ? [browserActionResult.screenshot] : []
 
-					let messageText = `The browser action has been executed.`
+					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`

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

@@ -0,0 +1,27 @@
+// Test screenshot action functionality in browser actions
+import { describe, it, expect } from "vitest"
+import { browserActions } from "../../../shared/ExtensionMessage"
+
+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)
+		})
+	})
+})

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

@@ -756,6 +756,55 @@ export class BrowserSession {
 		})
 	}
 
+	/**
+	 * 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
 	 */

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

@@ -1,5 +1,6 @@
 // npx vitest services/browser/__tests__/BrowserSession.spec.ts
 
+import * as path from "path"
 import { BrowserSession } from "../BrowserSession"
 import { discoverChromeHostUrl, tryChromeHostUrl } from "../browserDiscovery"
 
@@ -395,6 +396,179 @@ describe("cursor visualization", () => {
 		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 = {

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -432,6 +432,7 @@ export const browserActions = [
 	"scroll_up",
 	"resize",
 	"close",
+	"screenshot",
 ] as const
 
 export type BrowserAction = (typeof browserActions)[number]

+ 1 - 1
src/shared/tools.ts

@@ -191,7 +191,7 @@ export interface ListCodeDefinitionNamesToolUse extends ToolUse<"list_code_defin
 
 export interface BrowserActionToolUse extends ToolUse<"browser_action"> {
 	name: "browser_action"
-	params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text" | "size">>
+	params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text" | "size" | "path">>
 }
 
 export interface UseMcpToolToolUse extends ToolUse<"use_mcp_tool"> {

+ 20 - 11
webview-ui/src/components/chat/BrowserActionRow.tsx

@@ -12,6 +12,7 @@ import {
 	Play,
 	Check,
 	Maximize2,
+	Camera,
 } from "lucide-react"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useTranslation } from "react-i18next"
@@ -41,6 +42,8 @@ const getActionIcon = (action: string) => {
 			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" />
@@ -77,7 +80,7 @@ const BrowserActionRow = memo(({ message, nextMessage, actionIndex, totalActions
 
 	// Format action display text
 	const actionText = useMemo(() => {
-		if (!browserAction) return "Browser action"
+		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
@@ -86,27 +89,33 @@ const BrowserActionRow = memo(({ message, nextMessage, actionIndex, totalActions
 
 		switch (browserAction.action) {
 			case "launch":
-				return `Launched browser`
+				return t("chat:browser.actions.launched")
 			case "click":
-				return `Clicked at: ${browserAction.executedCoordinate || getViewportCoordinate(browserAction.coordinate)}`
+				return t("chat:browser.actions.clicked", {
+					coordinate: browserAction.executedCoordinate || getViewportCoordinate(browserAction.coordinate),
+				})
 			case "type":
-				return `Typed: ${browserAction.text}`
+				return t("chat:browser.actions.typed", { text: browserAction.text })
 			case "press":
-				return `Pressed key: ${prettyKey(browserAction.text)}`
+				return t("chat:browser.actions.pressed", { key: prettyKey(browserAction.text) })
 			case "hover":
-				return `Hovered at: ${browserAction.executedCoordinate || getViewportCoordinate(browserAction.coordinate)}`
+				return t("chat:browser.actions.hovered", {
+					coordinate: browserAction.executedCoordinate || getViewportCoordinate(browserAction.coordinate),
+				})
 			case "scroll_down":
-				return "Scrolled down"
+				return t("chat:browser.actions.scrolledDown")
 			case "scroll_up":
-				return "Scrolled up"
+				return t("chat:browser.actions.scrolledUp")
 			case "resize":
-				return `Resized to: ${browserAction.size?.split(/[x,]/).join(" x ")}`
+				return t("chat:browser.actions.resized", { size: browserAction.size?.split(/[x,]/).join(" x ") })
+			case "screenshot":
+				return t("chat:browser.actions.screenshotSaved")
 			case "close":
-				return "Closed browser"
+				return t("chat:browser.actions.closed")
 			default:
 				return browserAction.action
 		}
-	}, [browserAction, viewportDimensions])
+	}, [browserAction, viewportDimensions, t])
 
 	// Auto-open Browser Session panel when:
 	// 1. This is a "launch" action (new browser session) - always opens and navigates to launch

+ 22 - 10
webview-ui/src/components/chat/BrowserSessionRow.tsx

@@ -1,6 +1,7 @@
 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 } from "@roo-code/types"
 
 import { BrowserAction, BrowserActionResult, ClineSayBrowserAction } from "@roo/ExtensionMessage"
@@ -30,9 +31,11 @@ import {
 	ChevronsRight,
 	ExternalLink,
 	Copy,
+	Camera,
 } from "lucide-react"
 
 const getBrowserActionText = (
+	t: TFunction,
 	action: BrowserAction,
 	executedCoordinate?: string,
 	coordinate?: string,
@@ -48,23 +51,29 @@ const getBrowserActionText = (
 
 	switch (action) {
 		case "launch":
-			return `Launched browser`
+			return t("chat:browser.actions.launched")
 		case "click":
-			return `Clicked at: ${executedCoordinate || getViewportCoordinate(coordinate)}`
+			return t("chat:browser.actions.clicked", {
+				coordinate: executedCoordinate || getViewportCoordinate(coordinate),
+			})
 		case "type":
-			return `Typed: ${text}`
+			return t("chat:browser.actions.typed", { text })
 		case "press":
-			return `Pressed key: ${prettyKey(text)}`
+			return t("chat:browser.actions.pressed", { key: prettyKey(text) })
 		case "scroll_down":
-			return "Scrolled down"
+			return t("chat:browser.actions.scrolledDown")
 		case "scroll_up":
-			return "Scrolled up"
+			return t("chat:browser.actions.scrolledUp")
 		case "hover":
-			return `Hovered at: ${executedCoordinate || getViewportCoordinate(coordinate)}`
+			return t("chat:browser.actions.hovered", {
+				coordinate: executedCoordinate || getViewportCoordinate(coordinate),
+			})
 		case "resize":
-			return `Resized to: ${size?.split(/[x,]/).join(" x ")}`
+			return t("chat:browser.actions.resized", { size: size?.split(/[x,]/).join(" x ") })
+		case "screenshot":
+			return t("chat:browser.actions.screenshotSaved")
 		case "close":
-			return "Closed browser"
+			return t("chat:browser.actions.closed")
 		default:
 			return action
 	}
@@ -87,6 +96,8 @@ const getActionIcon = (action: BrowserAction) => {
 			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" />
@@ -557,6 +568,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
 									{getActionIcon(action.action)}
 									<span>
 										{getBrowserActionText(
+											t,
 											action.action,
 											action.executedCoordinate,
 											action.coordinate,
@@ -572,7 +584,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
 							return (
 								<>
 									{getActionIcon("launch" as any)}
-									<span>{getBrowserActionText("launch", undefined, initialUrl, undefined)}</span>
+									<span>{getBrowserActionText(t, "launch", undefined, initialUrl, undefined)}</span>
 								</>
 							)
 						}

+ 12 - 0
webview-ui/src/i18n/locales/ca/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Sessió de navegador iniciada",
 		"actions": {
 			"title": "Acció del navegador: ",
+			"launched": "Navegador iniciat",
 			"launch": "Iniciar navegador a {{url}}",
+			"clicked": "Fet clic ({{coordinate}})",
 			"click": "Clic ({{coordinate}})",
+			"typed": "Escrit: {{text}}",
 			"type": "Escriure \"{{text}}\"",
+			"pressed": "Premut: {{key}}",
 			"press": "Prem {{key}}",
+			"scrolledDown": "Desplaçat avall",
 			"scrollDown": "Desplaçar avall",
+			"scrolledUp": "Desplaçat amunt",
 			"scrollUp": "Desplaçar amunt",
+			"hovered": "Planat sobre ({{coordinate}})",
 			"hover": "Plana sobre ({{coordinate}})",
+			"resized": "Mida canviada a: {{size}}",
+			"resize": "Canvia la mida a {{size}}",
+			"screenshotSaved": "Captura de pantalla guardada",
+			"screenshot": "Guarda la captura de pantalla a {{path}}",
+			"closed": "Navegador tancat",
 			"close": "Tancar navegador"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/de/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Browser-Sitzung gestartet",
 		"actions": {
 			"title": "Browser-Aktion: ",
+			"launched": "Browser gestartet",
 			"launch": "Browser starten auf {{url}}",
+			"clicked": "Geklickt auf: {{coordinate}}",
 			"click": "Klicken ({{coordinate}})",
+			"typed": "Eingegeben: {{text}}",
 			"type": "Eingeben \"{{text}}\"",
+			"pressed": "{{key}} gedrückt",
 			"press": "{{key}} drücken",
+			"scrolledDown": "Nach unten gescrollt",
 			"scrollDown": "Nach unten scrollen",
+			"scrolledUp": "Nach oben gescrollt",
 			"scrollUp": "Nach oben scrollen",
+			"hovered": "Gehovered auf: {{coordinate}}",
 			"hover": "Hover ({{coordinate}})",
+			"resized": "Größe geändert auf: {{size}}",
+			"resize": "Größe ändern auf {{size}}",
+			"screenshotSaved": "Screenshot gespeichert",
+			"screenshot": "Screenshot speichern unter {{path}}",
+			"closed": "Browser geschlossen",
 			"close": "Browser schließen"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/en/chat.json

@@ -356,13 +356,25 @@
 		"sessionStarted": "Browser Session Started",
 		"actions": {
 			"title": "Browser Action: ",
+			"launched": "Launched browser",
 			"launch": "Launch browser at {{url}}",
+			"clicked": "Clicked at: {{coordinate}}",
 			"click": "Click ({{coordinate}})",
+			"typed": "Typed: {{text}}",
 			"type": "Type \"{{text}}\"",
+			"pressed": "Pressed key: {{key}}",
 			"press": "Press {{key}}",
+			"scrolledDown": "Scrolled down",
 			"scrollDown": "Scroll down",
+			"scrolledUp": "Scrolled up",
 			"scrollUp": "Scroll up",
+			"hovered": "Hovered at: {{coordinate}}",
 			"hover": "Hover ({{coordinate}})",
+			"resized": "Resized to: {{size}}",
+			"resize": "Resize to {{size}}",
+			"screenshotSaved": "Screenshot saved",
+			"screenshot": "Save screenshot to {{path}}",
+			"closed": "Closed browser",
 			"close": "Close browser"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/es/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Sesión de navegador iniciada",
 		"actions": {
 			"title": "Acción del navegador: ",
+			"launched": "Navegador iniciado",
 			"launch": "Iniciar navegador en {{url}}",
+			"clicked": "Clic en: {{coordinate}}",
 			"click": "Clic ({{coordinate}})",
+			"typed": "Escrito: {{text}}",
 			"type": "Escribir \"{{text}}\"",
+			"pressed": "Pulsado: {{key}}",
 			"press": "Pulsar {{key}}",
+			"scrolledDown": "Desplazado hacia abajo",
 			"scrollDown": "Desplazar hacia abajo",
+			"scrolledUp": "Desplazado hacia arriba",
 			"scrollUp": "Desplazar hacia arriba",
+			"hovered": "Flotado en: {{coordinate}}",
 			"hover": "Flotar ({{coordinate}})",
+			"resized": "Tamaño cambiado a: {{size}}",
+			"resize": "Cambiar tamaño a {{size}}",
+			"screenshotSaved": "Captura de pantalla guardada",
+			"screenshot": "Guardar captura de pantalla en {{path}}",
+			"closed": "Navegador cerrado",
 			"close": "Cerrar navegador"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/fr/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Session de navigateur démarrée",
 		"actions": {
 			"title": "Action du navigateur : ",
+			"launched": "Navigateur lancé",
 			"launch": "Lancer le navigateur sur {{url}}",
+			"clicked": "Cliqué sur : {{coordinate}}",
 			"click": "Cliquer ({{coordinate}})",
+			"typed": "Saisi : {{text}}",
 			"type": "Saisir \"{{text}}\"",
+			"pressed": "Appuyé sur : {{key}}",
 			"press": "Appuyer sur {{key}}",
+			"scrolledDown": "Défilé vers le bas",
 			"scrollDown": "Défiler vers le bas",
+			"scrolledUp": "Défilé vers le haut",
 			"scrollUp": "Défiler vers le haut",
+			"hovered": "Survolé : {{coordinate}}",
 			"hover": "Survoler ({{coordinate}})",
+			"resized": "Redimensionné à : {{size}}",
+			"resize": "Redimensionner à {{size}}",
+			"screenshotSaved": "Capture d'écran enregistrée",
+			"screenshot": "Enregistrer une capture d'écran dans {{path}}",
+			"closed": "Navigateur fermé",
 			"close": "Fermer le navigateur"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/hi/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "ब्राउज़र सत्र शुरू हुआ",
 		"actions": {
 			"title": "ब्राउज़र क्रिया: ",
+			"launched": "ब्राउज़र लॉन्च हुआ",
 			"launch": "{{url}} पर ब्राउज़र लॉन्च करें",
+			"clicked": "क्लिक किया गया ({{coordinate}})",
 			"click": "क्लिक करें ({{coordinate}})",
+			"typed": "टाइप किया गया: {{text}}",
 			"type": "टाइप करें \"{{text}}\"",
+			"pressed": "दबाया गया: {{key}}",
 			"press": "{{key}} दबाएँ",
+			"scrolledDown": "नीचे स्क्रॉल किया गया",
 			"scrollDown": "नीचे स्क्रॉल करें",
+			"scrolledUp": "ऊपर स्क्रॉल किया गया",
 			"scrollUp": "ऊपर स्क्रॉल करें",
+			"hovered": "होवर किया गया ({{coordinate}})",
 			"hover": "होवर करें ({{coordinate}})",
+			"resized": "आकार बदला गया: {{size}}",
+			"resize": "आकार बदलें {{size}}",
+			"screenshotSaved": "स्क्रीनशॉट सहेजा गया",
+			"screenshot": "स्क्रीनशॉट को {{path}} में सहेजें",
+			"closed": "ब्राउज़र बंद किया गया",
 			"close": "ब्राउज़र बंद करें"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/id/chat.json

@@ -362,13 +362,25 @@
 		"sessionStarted": "Sesi Browser Dimulai",
 		"actions": {
 			"title": "Aksi Browser: ",
+			"launched": "Browser diluncurkan",
 			"launch": "Luncurkan browser di {{url}}",
+			"clicked": "Diklik ({{coordinate}})",
 			"click": "Klik ({{coordinate}})",
+			"typed": "Diketik: {{text}}",
 			"type": "Ketik \"{{text}}\"",
+			"pressed": "Ditekan: {{key}}",
 			"press": "Tekan {{key}}",
+			"scrolledDown": "Digulir ke bawah",
 			"scrollDown": "Gulir ke bawah",
+			"scrolledUp": "Digulir ke atas",
 			"scrollUp": "Gulir ke atas",
+			"hovered": "Diarahkan ({{coordinate}})",
 			"hover": "Arahkan ({{coordinate}})",
+			"resized": "Ukuran diubah ke: {{size}}",
+			"resize": "Ubah ukuran ke {{size}}",
+			"screenshotSaved": "Screenshot disimpan",
+			"screenshot": "Simpan screenshot ke {{path}}",
+			"closed": "Browser ditutup",
 			"close": "Tutup browser"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/it/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Sessione browser avviata",
 		"actions": {
 			"title": "Azione browser: ",
+			"launched": "Browser avviato",
 			"launch": "Avvia browser su {{url}}",
+			"clicked": "Cliccato su: {{coordinate}}",
 			"click": "Clic ({{coordinate}})",
+			"typed": "Digitato: {{text}}",
 			"type": "Digita \"{{text}}\"",
+			"pressed": "Premuto: {{key}}",
 			"press": "Premi {{key}}",
+			"scrolledDown": "Scorso verso il basso",
 			"scrollDown": "Scorri verso il basso",
+			"scrolledUp": "Scorso verso l'alto",
 			"scrollUp": "Scorri verso l'alto",
+			"hovered": "Passato il mouse su: {{coordinate}}",
 			"hover": "Passa il mouse ({{coordinate}})",
+			"resized": "Ridimensionato a: {{size}}",
+			"resize": "Ridimensiona a {{size}}",
+			"screenshotSaved": "Screenshot salvato",
+			"screenshot": "Salva screenshot in {{path}}",
+			"closed": "Browser chiuso",
 			"close": "Chiudi browser"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/ja/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "ブラウザセッション開始",
 		"actions": {
 			"title": "ブラウザ操作: ",
+			"launched": "ブラウザを起動しました",
 			"launch": "{{url}} でブラウザを起動",
+			"clicked": "クリック: {{coordinate}}",
 			"click": "クリック ({{coordinate}})",
+			"typed": "入力: {{text}}",
 			"type": "入力 \"{{text}}\"",
+			"pressed": "{{key}}を押しました",
 			"press": "{{key}}を押す",
+			"scrolledDown": "下にスクロールしました",
 			"scrollDown": "下にスクロール",
+			"scrolledUp": "上にスクロールしました",
 			"scrollUp": "上にスクロール",
+			"hovered": "ホバー: {{coordinate}}",
 			"hover": "ホバー ({{coordinate}})",
+			"resized": "サイズを変更しました: {{size}}",
+			"resize": "サイズを {{size}} に変更",
+			"screenshotSaved": "スクリーンショットを保存しました",
+			"screenshot": "スクリーンショットを {{path}} に保存",
+			"closed": "ブラウザを閉じました",
 			"close": "ブラウザを閉じる"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/ko/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "브라우저 세션 시작됨",
 		"actions": {
 			"title": "브라우저 작업: ",
+			"launched": "브라우저 실행됨",
 			"launch": "{{url}}에서 브라우저 실행",
+			"clicked": "클릭함: {{coordinate}}",
 			"click": "클릭 ({{coordinate}})",
+			"typed": "입력함: {{text}}",
 			"type": "입력 \"{{text}}\"",
+			"pressed": "{{key}} 누름",
 			"press": "{{key}} 누르기",
+			"scrolledDown": "아래로 스크롤됨",
 			"scrollDown": "아래로 스크롤",
+			"scrolledUp": "위로 스크롤됨",
 			"scrollUp": "위로 스크롤",
+			"hovered": "가리킴: {{coordinate}}",
 			"hover": "가리키기 ({{coordinate}})",
+			"resized": "크기 조정됨: {{size}}",
+			"resize": "크기를 {{size}}로 조정",
+			"screenshotSaved": "스크린샷 저장됨",
+			"screenshot": "스크린샷을 {{path}}에 저장",
+			"closed": "브라우저 닫음",
 			"close": "브라우저 닫기"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/nl/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Browsersessie gestart",
 		"actions": {
 			"title": "Browseractie: ",
+			"launched": "Browser gestart",
 			"launch": "Browser starten op {{url}}",
+			"clicked": "Geklikt ({{coordinate}})",
 			"click": "Klik ({{coordinate}})",
+			"typed": "Getypt: {{text}}",
 			"type": "Typ \"{{text}}\"",
+			"pressed": "Ingedrukt: {{key}}",
 			"press": "Druk op {{key}}",
+			"scrolledDown": "Naar beneden gescrolld",
 			"scrollDown": "Scroll naar beneden",
+			"scrolledUp": "Naar boven gescrolld",
 			"scrollUp": "Scroll naar boven",
+			"hovered": "Zweefde ({{coordinate}})",
 			"hover": "Zweven ({{coordinate}})",
+			"resized": "Grootte gewijzigd naar: {{size}}",
+			"resize": "Grootte wijzigen naar {{size}}",
+			"screenshotSaved": "Schermopname opgeslagen",
+			"screenshot": "Schermopname opslaan naar {{path}}",
+			"closed": "Browser gesloten",
 			"close": "Browser sluiten"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/pl/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Sesja przeglądarki rozpoczęta",
 		"actions": {
 			"title": "Akcja przeglądarki: ",
+			"launched": "Przeglądarka uruchomiona",
 			"launch": "Uruchom przeglądarkę na {{url}}",
+			"clicked": "Kliknięto ({{coordinate}})",
 			"click": "Kliknij ({{coordinate}})",
+			"typed": "Wpisano: {{text}}",
 			"type": "Wpisz \"{{text}}\"",
+			"pressed": "Naciśnięto: {{key}}",
 			"press": "Naciśnij {{key}}",
+			"scrolledDown": "Przewinięto w dół",
 			"scrollDown": "Przewiń w dół",
+			"scrolledUp": "Przewinięto w górę",
 			"scrollUp": "Przewiń w górę",
+			"hovered": "Najechano ({{coordinate}})",
 			"hover": "Najedź ({{coordinate}})",
+			"resized": "Zmieniono rozmiar na: {{size}}",
+			"resize": "Zmień rozmiar na {{size}}",
+			"screenshotSaved": "Zrzut ekranu zapisany",
+			"screenshot": "Zapisz zrzut ekranu do {{path}}",
+			"closed": "Przeglądarka zamknięta",
 			"close": "Zamknij przeglądarkę"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -341,13 +341,25 @@
 		"sessionStarted": "Sessão do navegador iniciada",
 		"actions": {
 			"title": "Ação do navegador: ",
+			"launched": "Navegador iniciado",
 			"launch": "Iniciar navegador em {{url}}",
+			"clicked": "Clicado em: {{coordinate}}",
 			"click": "Clique ({{coordinate}})",
+			"typed": "Digitado: {{text}}",
 			"type": "Digitar \"{{text}}\"",
+			"pressed": "Pressionado: {{key}}",
 			"press": "Pressione {{key}}",
+			"scrolledDown": "Rolado para baixo",
 			"scrollDown": "Rolar para baixo",
+			"scrolledUp": "Rolado para cima",
 			"scrollUp": "Rolar para cima",
+			"hovered": "Pairado em: {{coordinate}}",
 			"hover": "Pairar ({{coordinate}})",
+			"resized": "Redimensionado para: {{size}}",
+			"resize": "Redimensionar para {{size}}",
+			"screenshotSaved": "Captura de tela salva",
+			"screenshot": "Salvar captura de tela em {{path}}",
+			"closed": "Navegador fechado",
 			"close": "Fechar navegador"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/ru/chat.json

@@ -342,13 +342,25 @@
 		"sessionStarted": "Сессия браузера запущена",
 		"actions": {
 			"title": "Действие браузера: ",
+			"launched": "Браузер открыт",
 			"launch": "Открыть браузер по адресу {{url}}",
+			"clicked": "Клик в: {{coordinate}}",
 			"click": "Клик ({{coordinate}})",
+			"typed": "Введено: {{text}}",
 			"type": "Ввести \"{{text}}\"",
+			"pressed": "Нажато: {{key}}",
 			"press": "Нажать {{key}}",
+			"scrolledDown": "Прокручено вниз",
 			"scrollDown": "Прокрутить вниз",
+			"scrolledUp": "Прокручено вверх",
 			"scrollUp": "Прокрутить вверх",
+			"hovered": "Наведено в: {{coordinate}}",
 			"hover": "Навести ({{coordinate}})",
+			"resized": "Размер изменен на: {{size}}",
+			"resize": "Изменить размер на {{size}}",
+			"screenshotSaved": "Снимок экрана сохранён",
+			"screenshot": "Сохранить снимок экрана в {{path}}",
+			"closed": "Браузер закрыт",
 			"close": "Закрыть браузер"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/tr/chat.json

@@ -342,13 +342,25 @@
 		"sessionStarted": "Tarayıcı Oturumu Başlatıldı",
 		"actions": {
 			"title": "Tarayıcı Eylemi: ",
+			"launched": "Tarayıcı başlatıldı",
 			"launch": "{{url}} adresinde tarayıcı başlat",
+			"clicked": "Tıklandı ({{coordinate}})",
 			"click": "Tıkla ({{coordinate}})",
+			"typed": "Yazıldı: {{text}}",
 			"type": "Yaz \"{{text}}\"",
+			"pressed": "Basıldı: {{key}}",
 			"press": "{{key}} tuşuna bas",
+			"scrolledDown": "Aşağı kaydırıldı",
 			"scrollDown": "Aşağı kaydır",
+			"scrolledUp": "Yukarı kaydırıldı",
 			"scrollUp": "Yukarı kaydır",
+			"hovered": "Üzerine gelinildi ({{coordinate}})",
 			"hover": "Üzerine gel ({{coordinate}})",
+			"resized": "Boyut değiştirildi: {{size}}",
+			"resize": "Boyutu değiştir {{size}}",
+			"screenshotSaved": "Ekran görüntüsü kaydedildi",
+			"screenshot": "Ekran görüntüsünü {{path}} yoluna kaydet",
+			"closed": "Tarayıcı kapatıldı",
 			"close": "Tarayıcıyı kapat"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/vi/chat.json

@@ -342,13 +342,25 @@
 		"sessionStarted": "Phiên trình duyệt đã bắt đầu",
 		"actions": {
 			"title": "Hành động trình duyệt: ",
+			"launched": "Trình duyệt đã khởi chạy",
 			"launch": "Khởi chạy trình duyệt tại {{url}}",
+			"clicked": "Đã nhấp ({{coordinate}})",
 			"click": "Nhấp ({{coordinate}})",
+			"typed": "Đã gõ: {{text}}",
 			"type": "Gõ \"{{text}}\"",
+			"pressed": "Đã nhấn: {{key}}",
 			"press": "Nhấn {{key}}",
+			"scrolledDown": "Đã cuộn xuống",
 			"scrollDown": "Cuộn xuống",
+			"scrolledUp": "Đã cuộn lên",
 			"scrollUp": "Cuộn lên",
+			"hovered": "Đã di chuột ({{coordinate}})",
 			"hover": "Di chuột ({{coordinate}})",
+			"resized": "Đã thay đổi kích thước: {{size}}",
+			"resize": "Thay đổi kích thước thành {{size}}",
+			"screenshotSaved": "Ảnh chụp màn hình đã được lưu",
+			"screenshot": "Lưu ảnh chụp màn hình vào {{path}}",
+			"closed": "Trình duyệt đã đóng",
 			"close": "Đóng trình duyệt"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -342,13 +342,25 @@
 		"sessionStarted": "浏览器会话已启动",
 		"actions": {
 			"title": "浏览器操作: ",
+			"launched": "浏览器已启动",
 			"launch": "访问 {{url}}",
+			"clicked": "点击位置: {{coordinate}}",
 			"click": "点击 ({{coordinate}})",
+			"typed": "输入: {{text}}",
 			"type": "输入 \"{{text}}\"",
+			"pressed": "已按 {{key}}",
 			"press": "按 {{key}}",
+			"scrolledDown": "已向下滚动",
 			"scrollDown": "向下滚动",
+			"scrolledUp": "已向上滚动",
 			"scrollUp": "向上滚动",
+			"hovered": "悬停位置: {{coordinate}}",
 			"hover": "悬停 ({{coordinate}})",
+			"resized": "大小已更改: {{size}}",
+			"resize": "调整大小为 {{size}}",
+			"screenshotSaved": "截图已保存",
+			"screenshot": "将截图保存到 {{path}}",
+			"closed": "浏览器已关闭",
 			"close": "关闭浏览器"
 		}
 	},

+ 12 - 0
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -360,13 +360,25 @@
 		"sessionStarted": "瀏覽器工作階段已啟動",
 		"actions": {
 			"title": "瀏覽器動作:",
+			"launched": "瀏覽器已啟動",
 			"launch": "在 {{url}} 啟動瀏覽器",
+			"clicked": "點選位置: {{coordinate}}",
 			"click": "點選 ({{coordinate}})",
+			"typed": "輸入: {{text}}",
 			"type": "輸入「{{text}}」",
+			"pressed": "已按下 {{key}}",
 			"press": "按下 {{key}}",
+			"scrolledDown": "已向下捲動",
 			"scrollDown": "向下捲動",
+			"scrolledUp": "已向上捲動",
 			"scrollUp": "向上捲動",
+			"hovered": "懸停位置: {{coordinate}}",
 			"hover": "懸停 ({{coordinate}})",
+			"resized": "大小已變更: {{size}}",
+			"resize": "調整大小為 {{size}}",
+			"screenshotSaved": "螢幕擷圖已儲存",
+			"screenshot": "將螢幕擷圖儲存至 {{path}}",
+			"closed": "瀏覽器已關閉",
 			"close": "關閉瀏覽器"
 		}
 	},