feat(browser): prefer Chrome default + add Brave/Edge fallbacks

Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-16 03:24:53 +00:00
parent a0d2a7232e
commit a5d8f89b53
16 changed files with 172 additions and 67 deletions

View File

@@ -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");

View File

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

View File

@@ -38,9 +38,7 @@ export function loadSubagentRegistryFromDisk(): Map<string, SubagentRunRecord> {
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

View File

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

View File

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

View File

@@ -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<BrowserExecutable>): BrowserExecu
export function findChromeExecutableMac(): BrowserExecutable | null {
const candidates: Array<BrowserExecutable> = [
{
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<BrowserExecutable> = [
{ 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<BrowserExecutable> = [];
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);
}

View File

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

View File

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

View File

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

View File

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