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 <roomote@roocode.com>
This commit is contained in:
Matt Rubens 2025-12-09 16:12:36 -08:00 committed by GitHub
parent 1898848d95
commit 721b02e58c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 579 additions and 26 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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.

View file

@ -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>`
}

View file

@ -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,

View file

@ -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`

View file

@ -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)
})
})
})

View file

@ -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
*/

View file

@ -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 = {

View file

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

View file

@ -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"> {

View file

@ -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

View file

@ -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>
</>
)
}

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -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": "ब्राउज़र बंद करें"
}
},

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -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": "ブラウザを閉じる"
}
},

View file

@ -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": "브라우저 닫기"
}
},

View file

@ -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"
}
},

View file

@ -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ę"
}
},

View file

@ -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"
}
},

View file

@ -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": "Закрыть браузер"
}
},

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -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": "关闭浏览器"
}
},

View file

@ -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": "關閉瀏覽器"
}
},