diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5313350d..aeff73191 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@
### Fixes
- Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias).
+- Agents/Browser: cap Playwright AI snapshots for tool calls (maxChars); CLI snapshots remain full. (#763) — thanks @thesash.
- Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete.
- CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete.
- Gateway/WebChat: include handshake validation details in the WebSocket close reason for easier debugging; preserve close codes.
diff --git a/README.md b/README.md
index 0d9c7f10f..365df020f 100644
--- a/README.md
+++ b/README.md
@@ -458,19 +458,20 @@ Special thanks to @andrewting19 for the Anthropic OAuth tool-name fix.
Thanks to all clawtributors:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/clawtributors-map.json b/scripts/clawtributors-map.json
index 5327ebbff..9dc6bb837 100644
--- a/scripts/clawtributors-map.json
+++ b/scripts/clawtributors-map.json
@@ -2,7 +2,8 @@
"ensureLogins": [
"jdrhyne",
"latitudeki5223",
- "manmal"
+ "manmal",
+ "thesash"
],
"seedCommit": "d6863f87",
"placeholderAvatar": "assets/avatar-placeholder.svg",
diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts
index a003d90fa..5ec8e84ac 100644
--- a/src/agents/tools/browser-tool.ts
+++ b/src/agents/tools/browser-tool.ts
@@ -44,6 +44,8 @@ const BROWSER_ACT_KINDS = [
type BrowserActKind = (typeof BROWSER_ACT_KINDS)[number];
+const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
+
// NOTE: Using a flattened object schema instead of Type.Union([Type.Object(...), ...])
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
// The discriminator (kind) determines which properties are relevant; runtime validates.
@@ -326,14 +328,19 @@ export function createBrowserTool(opts?: {
: undefined;
const maxChars =
typeof params.maxChars === "number" &&
- Number.isFinite(params.maxChars)
- ? params.maxChars
+ Number.isFinite(params.maxChars) &&
+ params.maxChars > 0
+ ? Math.floor(params.maxChars)
+ : undefined;
+ const resolvedMaxChars =
+ format === "ai"
+ ? (maxChars ?? DEFAULT_AI_SNAPSHOT_MAX_CHARS)
: undefined;
const snapshot = await browserSnapshot(baseUrl, {
format,
targetId,
limit,
- maxChars,
+ ...(resolvedMaxChars ? { maxChars: resolvedMaxChars } : {}),
profile,
});
if (snapshot.format === "ai") {
diff --git a/src/browser/pw-tools-core.ts b/src/browser/pw-tools-core.ts
index 5fb917ed8..2f22d1380 100644
--- a/src/browser/pw-tools-core.ts
+++ b/src/browser/pw-tools-core.ts
@@ -10,8 +10,6 @@ import {
let nextUploadArmId = 0;
let nextDialogArmId = 0;
-const MAX_SNAPSHOT_CHARS = 80_000;
-
function requireRef(value: unknown): string {
const ref = typeof value === "string" ? value.trim() : "";
if (!ref) throw new Error("ref is required");
@@ -44,18 +42,17 @@ export async function snapshotAiViaPlaywright(opts: {
),
track: "response",
});
+ let snapshot = String(result?.full ?? "");
const maxChars = opts.maxChars;
const limit =
typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0
? Math.floor(maxChars)
- : MAX_SNAPSHOT_CHARS;
- let snapshot = String(result?.full ?? "");
- let truncated = false;
- if (snapshot.length > limit) {
+ : undefined;
+ if (limit && snapshot.length > limit) {
snapshot = `${snapshot.slice(0, limit)}\n\n[...TRUNCATED - page too large]`;
- truncated = true;
+ return { snapshot, truncated: true };
}
- return truncated ? { snapshot, truncated } : { snapshot };
+ return { snapshot };
}
export async function clickViaPlaywright(opts: {
diff --git a/src/browser/routes/agent.ts b/src/browser/routes/agent.ts
index 052d9eab6..d5b8a675e 100644
--- a/src/browser/routes/agent.ts
+++ b/src/browser/routes/agent.ts
@@ -567,23 +567,22 @@ export function registerBrowserAgentRoutes(
? Number(req.query.maxChars)
: undefined;
const limit = Number.isFinite(limitRaw) ? limitRaw : undefined;
- const maxChars = Number.isFinite(maxCharsRaw) ? maxCharsRaw : undefined;
+ const maxChars =
+ typeof maxCharsRaw === "number" &&
+ Number.isFinite(maxCharsRaw) &&
+ maxCharsRaw > 0
+ ? Math.floor(maxCharsRaw)
+ : undefined;
try {
const tab = await profileCtx.ensureTabAvailable(targetId || undefined);
if (format === "ai") {
const pw = await requirePwAi(res, "ai snapshot");
if (!pw) return;
- const resolvedMaxChars =
- typeof maxChars === "number" && maxChars > 0
- ? maxChars
- : typeof limit === "number" && limit > 0
- ? limit
- : undefined;
const snap = await pw.snapshotAiViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl,
targetId: tab.targetId,
- ...(resolvedMaxChars ? { maxChars: resolvedMaxChars } : {}),
+ ...(maxChars ? { maxChars } : {}),
});
return res.json({
ok: true,