diff --git a/CHANGELOG.md b/CHANGELOG.md index 3267a86be..0cf0f3dbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake) - Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs. - Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus) +- Browser tools: treat explicit `maxChars: 0` as unlimited while keeping the default limit only when omitted. (#796 — thanks @gabriel-trigo) - Tools: allow Claude/Gemini tool param aliases (`file_path`, `old_string`, `new_string`) while enforcing required params at runtime. (#793 — thanks @hsrvc) - Gemini: downgrade tool-call history missing `thought_signature` to avoid INVALID_ARGUMENT errors. (#793 — thanks @hsrvc) - Messaging: enforce context isolation for message tool sends across providers (normalized targets + tests). (#793 — thanks @hsrvc) diff --git a/src/agents/tools/browser-tool.test.ts b/src/agents/tools/browser-tool.test.ts index 8f34447c5..dc3439bfc 100644 --- a/src/agents/tools/browser-tool.test.ts +++ b/src/agents/tools/browser-tool.test.ts @@ -89,4 +89,17 @@ describe("browser tool snapshot maxChars", () => { }), ); }); + + it("skips the default when maxChars is explicitly zero", async () => { + const tool = createBrowserTool(); + await tool.execute?.(null, { + action: "snapshot", + format: "ai", + maxChars: 0, + }); + + expect(browserClientMocks.browserSnapshot).toHaveBeenCalled(); + const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? []; + expect(Object.hasOwn(opts ?? {}, "maxChars")).toBe(false); + }); }); diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index 2fcc0ad2b..0f0bcb433 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -322,6 +322,7 @@ export function createBrowserTool(opts?: { params.format === "ai" || params.format === "aria" ? (params.format as "ai" | "aria") : "ai"; + const hasMaxChars = Object.hasOwn(params, "maxChars"); const targetId = typeof params.targetId === "string" ? params.targetId.trim() @@ -338,7 +339,9 @@ export function createBrowserTool(opts?: { : undefined; const resolvedMaxChars = format === "ai" - ? (maxChars ?? DEFAULT_AI_SNAPSHOT_MAX_CHARS) + ? hasMaxChars + ? maxChars + : DEFAULT_AI_SNAPSHOT_MAX_CHARS : undefined; const interactive = typeof params.interactive === "boolean" @@ -360,7 +363,9 @@ export function createBrowserTool(opts?: { format, targetId, limit, - ...(resolvedMaxChars ? { maxChars: resolvedMaxChars } : {}), + ...(typeof resolvedMaxChars === "number" + ? { maxChars: resolvedMaxChars } + : {}), interactive, compact, depth, diff --git a/src/browser/routes/agent.ts b/src/browser/routes/agent.ts index 3439937ec..fd960ca99 100644 --- a/src/browser/routes/agent.ts +++ b/src/browser/routes/agent.ts @@ -1195,6 +1195,7 @@ export function registerBrowserAgentRoutes( : "aria"; const limitRaw = typeof req.query.limit === "string" ? Number(req.query.limit) : undefined; + const hasMaxChars = Object.hasOwn(req.query, "maxChars"); const maxCharsRaw = typeof req.query.maxChars === "string" ? Number(req.query.maxChars) @@ -1207,7 +1208,11 @@ export function registerBrowserAgentRoutes( ? Math.floor(maxCharsRaw) : undefined; const resolvedMaxChars = - format === "ai" ? (maxChars ?? DEFAULT_AI_SNAPSHOT_MAX_CHARS) : undefined; + format === "ai" + ? hasMaxChars + ? maxChars + : DEFAULT_AI_SNAPSHOT_MAX_CHARS + : undefined; const interactive = toBoolean(req.query.interactive); const compact = toBoolean(req.query.compact); const depth = toNumber(req.query.depth); diff --git a/src/browser/server.test.ts b/src/browser/server.test.ts index 85b2ecf15..9488b4331 100644 --- a/src/browser/server.test.ts +++ b/src/browser/server.test.ts @@ -687,6 +687,25 @@ describe("browser control server", () => { expect(stopped.stopped).toBe(true); }); + it("skips default maxChars when explicitly set to zero", async () => { + const { startBrowserControlServerFromConfig } = await import("./server.js"); + await startBrowserControlServerFromConfig(); + const base = `http://127.0.0.1:${testPort}`; + await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json()); + + const snapAi = (await realFetch( + `${base}/snapshot?format=ai&maxChars=0`, + ).then((r) => r.json())) as { ok: boolean; format?: string }; + expect(snapAi.ok).toBe(true); + expect(snapAi.format).toBe("ai"); + + const [call] = pwMocks.snapshotAiViaPlaywright.mock.calls.at(-1) ?? []; + expect(call).toEqual({ + cdpUrl: cdpBaseUrl, + targetId: "abcd1234", + }); + }); + it("validates agent inputs (agent routes)", async () => { const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig();