From a5d8f89b53348a4a2d131579cc1828126ba7962a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 16 Jan 2026 03:24:53 +0000 Subject: [PATCH] feat(browser): prefer Chrome default + add Brave/Edge fallbacks Co-authored-by: Christoph Nakazawa --- CHANGELOG.md | 1 + docs/cli/index.md | 2 +- docs/gateway/configuration.md | 9 +- docs/start/faq.md | 6 ++ docs/tools/browser-linux-troubleshooting.md | 6 +- docs/tools/browser.md | 64 +++++++++---- src/agents/subagent-announce.format.test.ts | 4 +- .../subagent-registry.persistence.test.ts | 3 +- src/agents/subagent-registry.store.ts | 4 +- src/agents/subagent-registry.ts | 6 +- src/agents/system-prompt.ts | 3 +- src/browser/chrome.executables.ts | 90 +++++++++++++++---- src/browser/chrome.test.ts | 23 +++-- src/browser/chrome.ts | 4 +- src/config/types.browser.ts | 2 +- src/gateway/server-methods/chat.ts | 12 ++- 16 files changed, 172 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5e2b9de..ed76e0ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` → `act`. - Browser: `profile="chrome"` now defaults to host control and returns clearer “attach a tab” errors. - Browser: extension mode recovers when only one tab is attached (stale targetId fallback). +- Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer. - Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page). - Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi. - Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) — thanks @azade-c. diff --git a/docs/cli/index.md b/docs/cli/index.md index 782d33be8..7837d4fe9 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -785,7 +785,7 @@ Location: ## Browser -Browser control CLI (dedicated Chrome/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser). +Browser control CLI (dedicated Chrome/Brave/Edge/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser). Common options: - `--url ` diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 0e3ac6967..a7efb838e 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -2376,10 +2376,10 @@ Example: } ``` -### `browser` (clawd-managed Chrome) +### `browser` (clawd-managed browser) -Clawdbot can start a **dedicated, isolated** Chrome/Chromium instance for clawd and expose a small loopback control server. -Profiles can point at a **remote** Chrome via `profiles..cdpUrl`. Remote +Clawdbot can start a **dedicated, isolated** Chrome/Brave/Edge/Chromium instance for clawd and expose a small loopback control server. +Profiles can point at a **remote** Chromium-based browser via `profiles..cdpUrl`. Remote profiles are attach-only (start/stop/reset are disabled). `browser.cdpUrl` remains for legacy single-profile configs and as the base @@ -2391,6 +2391,7 @@ Defaults: - CDP URL: `http://127.0.0.1:18792` (control URL + 1, legacy single-profile) - profile color: `#FF4500` (lobster-orange) - Note: the control server is started by the running gateway (Clawdbot.app menubar, or `clawdbot gateway`). +- Auto-detect order: Chrome → Brave → Edge → Chromium → Chrome Canary. ```json5 { @@ -2408,7 +2409,7 @@ Defaults: // Advanced: // headless: false, // noSandbox: false, - // executablePath: "/usr/bin/chromium", + // executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", // attachOnly: false, // set true when tunneling a remote CDP to localhost } } diff --git a/docs/start/faq.md b/docs/start/faq.md index 0b4be33b8..89526d8c4 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -55,6 +55,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [How do I enable web search (and web fetch)?](#how-do-i-enable-web-search-and-web-fetch) - [How do I run a central Gateway with specialized workers across devices?](#how-do-i-run-a-central-gateway-with-specialized-workers-across-devices) - [Can the Clawdbot browser run headless?](#can-the-clawdbot-browser-run-headless) + - [How do I use Brave for browser control?](#how-do-i-use-brave-for-browser-control) - [Remote gateways + nodes](#remote-gateways-nodes) - [How do commands propagate between Telegram, the gateway, and nodes?](#how-do-commands-propagate-between-telegram-the-gateway-and-nodes) - [Do nodes run a gateway daemon?](#do-nodes-run-a-gateway-daemon) @@ -663,6 +664,11 @@ Headless uses the **same Chromium engine** and works for most automation (forms, - Some sites are stricter about automation in headless mode (CAPTCHAs, anti‑bot). For example, X/Twitter often blocks headless sessions. +### How do I use Brave for browser control? + +Set `browser.executablePath` to your Brave binary (or any Chromium-based browser) and restart the Gateway. +See the full config examples in [Browser](/tools/browser#use-brave-or-another-chromium-based-browser). + ## Remote gateways + nodes ### How do commands propagate between Telegram, the gateway, and nodes? diff --git a/docs/tools/browser-linux-troubleshooting.md b/docs/tools/browser-linux-troubleshooting.md index 3cbd4dbe1..0d8da8cf5 100644 --- a/docs/tools/browser-linux-troubleshooting.md +++ b/docs/tools/browser-linux-troubleshooting.md @@ -1,5 +1,5 @@ --- -summary: "Fix Chrome/Chromium CDP startup issues for Clawdbot browser control on Linux" +summary: "Fix Chrome/Brave/Edge/Chromium CDP startup issues for Clawdbot browser control on Linux" read_when: "Browser control fails on Linux, especially with snap Chromium" --- @@ -7,7 +7,7 @@ read_when: "Browser control fails on Linux, especially with snap Chromium" ## Problem: "Failed to start Chrome CDP on port 18800" -Clawdbot's browser control server fails to launch Chrome/Chromium with the error: +Clawdbot's browser control server fails to launch Chrome/Brave/Edge/Chromium with the error: ``` {"error":"Error: Failed to start Chrome CDP on port 18800 for profile \"clawd\"."} ``` @@ -107,7 +107,7 @@ curl -s http://127.0.0.1:18791/tabs | Option | Description | Default | |--------|-------------|---------| | `browser.enabled` | Enable browser control | `true` | -| `browser.executablePath` | Path to Chrome/Chromium binary | auto-detected | +| `browser.executablePath` | Path to a Chromium-based browser binary (Chrome/Brave/Edge/Chromium) | auto-detected | | `browser.headless` | Run without GUI | `false` | | `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` | | `browser.attachOnly` | Don't launch browser, only attach to existing | `false` | diff --git a/docs/tools/browser.md b/docs/tools/browser.md index 0f6bc2e5e..388bf9338 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -8,13 +8,13 @@ read_when: # Browser (clawd-managed) -Clawdbot can run a **dedicated Chrome/Chromium profile** that the agent controls. +Clawdbot can run a **dedicated Chrome/Brave/Edge/Chromium profile** that the agent controls. It is isolated from your personal browser and is managed through a small local control server. Beginner view: - Think of it as a **separate, agent-only browser**. -- It does **not** touch your personal Chrome profile. +- It does **not** touch your personal browser profile. - The agent can **open tabs, read pages, click, and type** in a safe lane. ## What you get @@ -54,7 +54,7 @@ Browser settings live in `~/.clawdbot/clawdbot.json`. headless: false, noSandbox: false, attachOnly: false, - executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium", + executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", profiles: { clawd: { cdpPort: 18800, color: "#FF4500" }, work: { cdpPort: 18801, color: "#0066CC" }, @@ -69,17 +69,45 @@ Notes: - If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`), the default browser ports shift to stay in the same “family” (control = gateway + 2). - `cdpUrl` defaults to `controlUrl + 1` when unset. -- `attachOnly: true` means “never launch Chrome; only attach if it is already running.” +- `attachOnly: true` means “never launch a local browser; only attach if it is already running.” - `color` + per-profile `color` tint the browser UI so you can see which profile is active. +- Auto-detect order: Chrome → Brave → Edge → Chromium → Chrome Canary. + +## Use Brave (or another Chromium-based browser) + +Set `browser.executablePath` to override auto-detection: + +```json5 +// macOS +{ + browser: { + executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" + } +} + +// Windows +{ + browser: { + executablePath: "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" + } +} + +// Linux +{ + browser: { + executablePath: "/usr/bin/brave-browser" + } +} +``` ## Local vs remote control - **Local control (default):** `controlUrl` is loopback (`127.0.0.1`/`localhost`). - The Gateway starts the control server and can launch Chrome. + The Gateway starts the control server and can launch a local browser. - **Remote control:** `controlUrl` is non-loopback. The Gateway **does not** start a local server; it assumes you are pointing at an existing server elsewhere. - **Remote CDP:** set `browser.profiles..cdpUrl` (or `browser.cdpUrl`) to - attach to a remote Chrome. In this case, Clawdbot will not launch a local browser. + attach to a remote Chromium-based browser. In this case, Clawdbot will not launch a local browser. ## Remote browser (control server) @@ -88,7 +116,7 @@ Gateway at it with a remote `controlUrl`. This lets the agent drive a browser outside the host (lab box, VM, remote desktop, etc.). Key points: -- The **control server** speaks to Chrome/Chromium via **CDP**. +- The **control server** speaks to Chromium-based browsers (Chrome/Brave/Edge/Chromium) via **CDP**. - The **Gateway** only needs the HTTP control URL. - Profiles are resolved on the **control server** side. @@ -104,14 +132,14 @@ Example: ``` Use `profiles..cdpUrl` for **remote CDP** if you want the Gateway to talk -directly to a Chrome instance without a remote control server. +directly to a Chromium-based browser instance without a remote control server. ### Running the control server on the browser machine Run a standalone browser control server (recommended when your Gateway is remote): ```bash -# on the machine that runs Chrome +# on the machine that runs Chrome/Brave/Edge clawdbot browser serve --bind --port 18791 --token ``` @@ -198,8 +226,8 @@ Notes: ## Profiles (multi-browser) Clawdbot supports multiple named profiles (routing configs). Profiles can be: -- **clawd-managed**: a dedicated Chrome instance with its own user data directory + CDP port -- **remote**: an explicit CDP URL (Chrome running elsewhere) +- **clawd-managed**: a dedicated Chromium-based browser instance with its own user data directory + CDP port +- **remote**: an explicit CDP URL (Chromium-based browser running elsewhere) - **extension relay**: your existing Chrome tab(s) via the local relay + Chrome extension Defaults: @@ -264,22 +292,24 @@ Notes: ## Isolation guarantees -- **Dedicated user data dir**: never touches your personal Chrome profile. +- **Dedicated user data dir**: never touches your personal browser profile. - **Dedicated ports**: avoids `9222` to prevent collisions with dev workflows. - **Deterministic tab control**: target tabs by `targetId`, not “last tab”. ## Browser selection When launching locally, Clawdbot picks the first available: -1. Chrome Canary -2. Chromium -3. Chrome +1. Chrome +2. Brave +3. Edge +4. Chromium +5. Chrome Canary You can override with `browser.executablePath`. Platforms: - macOS: checks `/Applications` and `~/Applications`. -- Linux: looks for `google-chrome`, `chromium`, etc. +- Linux: looks for `google-chrome`, `brave`, `microsoft-edge`, `chromium`, etc. - Windows: checks common install locations. ## Control API (optional) @@ -313,7 +343,7 @@ For the Chrome extension relay driver, ARIA snapshots and screenshots require Pl High-level flow: - A small **control server** accepts HTTP requests. -- It connects to Chrome/Chromium via **CDP**. +- It connects to Chromium-based browsers (Chrome/Brave/Edge/Chromium) via **CDP**. - For advanced actions (click/type/snapshot/PDF), it uses **Playwright** on top of CDP. - When Playwright is missing, only non-Playwright operations are available. diff --git a/src/agents/subagent-announce.format.test.ts b/src/agents/subagent-announce.format.test.ts index 515e39254..b8c2542fa 100644 --- a/src/agents/subagent-announce.format.test.ts +++ b/src/agents/subagent-announce.format.test.ts @@ -54,7 +54,9 @@ describe("subagent announce formatting", () => { }); expect(agentSpy).toHaveBeenCalled(); - const call = agentSpy.mock.calls[0]?.[0] as { params?: { message?: string; sessionKey?: string } }; + const call = agentSpy.mock.calls[0]?.[0] as { + params?: { message?: string; sessionKey?: string }; + }; const msg = call?.params?.message as string; expect(call?.params?.sessionKey).toBe("agent:main:main"); expect(msg).toContain("background task"); diff --git a/src/agents/subagent-registry.persistence.test.ts b/src/agents/subagent-registry.persistence.test.ts index ba861ccf4..27d145ba6 100644 --- a/src/agents/subagent-registry.persistence.test.ts +++ b/src/agents/subagent-registry.persistence.test.ts @@ -119,7 +119,8 @@ describe("subagent registry persistence", () => { // announce should NOT be called since cleanupHandled was true const calls = announceSpy.mock.calls.map((call) => call[0]); const match = calls.find( - (params) => (params as { childSessionKey?: string }).childSessionKey === "agent:main:subagent:two", + (params) => + (params as { childSessionKey?: string }).childSessionKey === "agent:main:subagent:two", ); expect(match).toBeFalsy(); }); diff --git a/src/agents/subagent-registry.store.ts b/src/agents/subagent-registry.store.ts index 9681738a5..40f689368 100644 --- a/src/agents/subagent-registry.store.ts +++ b/src/agents/subagent-registry.store.ts @@ -38,9 +38,7 @@ export function loadSubagentRegistryFromDisk(): Map { const announceCompletedAt = typeof typed.announceCompletedAt === "number" ? typed.announceCompletedAt : undefined; const cleanupCompletedAt = - typeof typed.cleanupCompletedAt === "number" - ? typed.cleanupCompletedAt - : announceCompletedAt; + typeof typed.cleanupCompletedAt === "number" ? typed.cleanupCompletedAt : announceCompletedAt; const cleanupHandled = typeof typed.cleanupHandled === "boolean" ? typed.cleanupHandled diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index 596bae930..73c4417d5 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -214,11 +214,7 @@ function ensureListener() { }); } -function finalizeSubagentCleanup( - runId: string, - cleanup: "delete" | "keep", - didAnnounce: boolean, -) { +function finalizeSubagentCleanup(runId: string, cleanup: "delete" | "keep", didAnnounce: boolean) { const entry = subagentRuns.get(runId); if (!entry) return; if (cleanup === "delete") { diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 962aee57c..d4447ed41 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -442,7 +442,8 @@ export function buildAgentSystemPrompt(params: { if (extraSystemPrompt) { // Use "Subagent Context" header for minimal mode (subagents), otherwise "Group Chat Context" - const contextHeader = promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context"; + const contextHeader = + promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context"; lines.push(contextHeader, extraSystemPrompt, ""); } if (params.reactionGuidance) { diff --git a/src/browser/chrome.executables.ts b/src/browser/chrome.executables.ts index 7407528b6..1dda2fc72 100644 --- a/src/browser/chrome.executables.ts +++ b/src/browser/chrome.executables.ts @@ -5,7 +5,7 @@ import path from "node:path"; import type { ResolvedBrowserConfig } from "./config.js"; export type BrowserExecutable = { - kind: "canary" | "chromium" | "chrome" | "custom"; + kind: "brave" | "canary" | "chromium" | "chrome" | "custom" | "edge"; path: string; }; @@ -28,15 +28,28 @@ function findFirstExecutable(candidates: Array): BrowserExecu export function findChromeExecutableMac(): BrowserExecutable | null { const candidates: Array = [ { - kind: "canary", - path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", + kind: "chrome", + path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", }, { - kind: "canary", - path: path.join( - os.homedir(), - "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", - ), + kind: "chrome", + path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"), + }, + { + kind: "brave", + path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + }, + { + kind: "brave", + path: path.join(os.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser"), + }, + { + kind: "edge", + path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + }, + { + kind: "edge", + path: path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"), }, { kind: "chromium", @@ -47,12 +60,15 @@ export function findChromeExecutableMac(): BrowserExecutable | null { path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium"), }, { - kind: "chrome", - path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + kind: "canary", + path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", }, { - kind: "chrome", - path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"), + kind: "canary", + path: path.join( + os.homedir(), + "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", + ), }, ]; @@ -63,10 +79,16 @@ export function findChromeExecutableLinux(): BrowserExecutable | null { const candidates: Array = [ { kind: "chrome", path: "/usr/bin/google-chrome" }, { kind: "chrome", path: "/usr/bin/google-chrome-stable" }, + { kind: "chrome", path: "/usr/bin/chrome" }, + { kind: "brave", path: "/usr/bin/brave-browser" }, + { kind: "brave", path: "/usr/bin/brave-browser-stable" }, + { kind: "brave", path: "/usr/bin/brave" }, + { kind: "brave", path: "/snap/bin/brave" }, + { kind: "edge", path: "/usr/bin/microsoft-edge" }, + { kind: "edge", path: "/usr/bin/microsoft-edge-stable" }, { kind: "chromium", path: "/usr/bin/chromium" }, { kind: "chromium", path: "/usr/bin/chromium-browser" }, { kind: "chromium", path: "/snap/bin/chromium" }, - { kind: "chrome", path: "/usr/bin/chrome" }, ]; return findFirstExecutable(candidates); @@ -82,20 +104,30 @@ export function findChromeExecutableWindows(): BrowserExecutable | null { const candidates: Array = []; if (localAppData) { - // Chrome Canary (user install) + // Chrome (user install) candidates.push({ - kind: "canary", - path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe"), + kind: "chrome", + path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe"), + }); + // Brave (user install) + candidates.push({ + kind: "brave", + path: joinWin(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), + }); + // Edge (user install) + candidates.push({ + kind: "edge", + path: joinWin(localAppData, "Microsoft", "Edge", "Application", "msedge.exe"), }); // Chromium (user install) candidates.push({ kind: "chromium", path: joinWin(localAppData, "Chromium", "Application", "chrome.exe"), }); - // Chrome (user install) + // Chrome Canary (user install) candidates.push({ - kind: "chrome", - path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe"), + kind: "canary", + path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe"), }); } @@ -109,6 +141,26 @@ export function findChromeExecutableWindows(): BrowserExecutable | null { kind: "chrome", path: joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"), }); + // Brave (system install, 64-bit) + candidates.push({ + kind: "brave", + path: joinWin(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), + }); + // Brave (system install, 32-bit on 64-bit Windows) + candidates.push({ + kind: "brave", + path: joinWin(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), + }); + // Edge (system install, 64-bit) + candidates.push({ + kind: "edge", + path: joinWin(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"), + }); + // Edge (system install, 32-bit on 64-bit Windows) + candidates.push({ + kind: "edge", + path: joinWin(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"), + }); return findFirstExecutable(candidates); } diff --git a/src/browser/chrome.test.ts b/src/browser/chrome.test.ts index afc3bcbc8..a8a42ae95 100644 --- a/src/browser/chrome.test.ts +++ b/src/browser/chrome.test.ts @@ -128,10 +128,12 @@ describe("browser chrome helpers", () => { it("picks the first existing Chrome candidate on macOS", () => { const exists = vi .spyOn(fs, "existsSync") - .mockImplementation((p) => String(p).includes("Google Chrome Canary")); + .mockImplementation((p) => + String(p).includes("Google Chrome.app/Contents/MacOS/Google Chrome"), + ); const exe = findChromeExecutableMac(); - expect(exe?.kind).toBe("canary"); - expect(exe?.path).toMatch(/Google Chrome Canary/); + expect(exe?.kind).toBe("chrome"); + expect(exe?.path).toMatch(/Google Chrome\.app/); exists.mockRestore(); }); @@ -143,12 +145,17 @@ describe("browser chrome helpers", () => { it("picks the first existing Chrome candidate on Windows", () => { vi.stubEnv("LOCALAPPDATA", "C:\\Users\\Test\\AppData\\Local"); - const exists = vi - .spyOn(fs, "existsSync") - .mockImplementation((p) => String(p).includes("Chrome SxS")); + const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => { + const pathStr = String(p); + return ( + pathStr.includes("Google\\Chrome\\Application\\chrome.exe") || + pathStr.includes("BraveSoftware\\Brave-Browser\\Application\\brave.exe") || + pathStr.includes("Microsoft\\Edge\\Application\\msedge.exe") + ); + }); const exe = findChromeExecutableWindows(); - expect(exe?.kind).toBe("canary"); - expect(exe?.path).toMatch(/Chrome SxS/); + expect(exe?.kind).toBe("chrome"); + expect(exe?.path).toMatch(/chrome\.exe$/); exists.mockRestore(); }); diff --git a/src/browser/chrome.ts b/src/browser/chrome.ts index 68ab1f055..6667d18c9 100644 --- a/src/browser/chrome.ts +++ b/src/browser/chrome.ts @@ -147,7 +147,9 @@ export async function launchClawdChrome( const exe = resolveBrowserExecutable(resolved); if (!exe) { - throw new Error("No supported browser found (Chrome/Chromium on macOS, Linux, or Windows)."); + throw new Error( + "No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows).", + ); } const userDataDir = resolveClawdUserDataDir(profile.name); diff --git a/src/config/types.browser.ts b/src/config/types.browser.ts index f287c8fdd..b89da49ee 100644 --- a/src/config/types.browser.ts +++ b/src/config/types.browser.ts @@ -23,7 +23,7 @@ export type BrowserConfig = { cdpUrl?: string; /** Accent color for the clawd browser profile (hex). Default: #FF4500 */ color?: string; - /** Override the browser executable path (macOS/Linux). */ + /** Override the browser executable path (all platforms). */ executablePath?: string; /** Start Chrome headless (best-effort). Default: false */ headless?: boolean; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 8543a09d8..96730af07 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -389,7 +389,11 @@ export const chatHandlers: GatewayRequestHandlers = { : path.join(path.dirname(storePath), `${sessionId}.jsonl`); if (!fs.existsSync(transcriptPath)) { - respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "transcript file not found")); + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, "transcript file not found"), + ); return; } @@ -416,7 +420,11 @@ export const chatHandlers: GatewayRequestHandlers = { fs.appendFileSync(transcriptPath, `${JSON.stringify(transcriptEntry)}\n`, "utf-8"); } catch (err) { const errMessage = err instanceof Error ? err.message : String(err); - respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `failed to write transcript: ${errMessage}`)); + respond( + false, + undefined, + errorShape(ErrorCodes.UNAVAILABLE, `failed to write transcript: ${errMessage}`), + ); return; }