From 75cdf22152defb397f83d774f42fbbb338ab6dc7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 16 Jun 2026 22:50:53 +0800 Subject: [PATCH] fix(browser): accept top-level act fields with nested requests (#93674) * fix(browser): accept top-level act fields with nested requests Co-authored-by: Capivariano <11271294+angelusbr@users.noreply.github.com> * fix(clownfish): address review for gitcrawl-416-autonomous-terminal-gap (1) Co-authored-by: Capivariano <11271294+angelusbr@users.noreply.github.com> --------- Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com> Co-authored-by: Capivariano <11271294+angelusbr@users.noreply.github.com> --- extensions/browser/src/browser-tool.test.ts | 67 +++++++++++++++++++++ extensions/browser/src/browser-tool.ts | 26 +++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/extensions/browser/src/browser-tool.test.ts b/extensions/browser/src/browser-tool.test.ts index 21ad76d9f891..a612e45485fd 100644 --- a/extensions/browser/src/browser-tool.test.ts +++ b/extensions/browser/src/browser-tool.test.ts @@ -1435,6 +1435,73 @@ describe("browser tool act compatibility", () => { expect(opts.profile).toBeUndefined(); }); + it("backfills missing flattened fields into nested act requests", async () => { + const tool = createBrowserTool(); + await tool.execute?.("call-1", { + action: "act", + kind: "click", + ref: "f1e3", + selector: "#title", + targetId: "tab-1", + timeoutMs: 5000, + request: { + kind: "click", + doubleClick: true, + }, + }); + + const request = lastMockCallArg<{ + kind?: string; + ref?: string; + selector?: string; + targetId?: string; + timeoutMs?: number; + doubleClick?: boolean; + }>(browserActionsMocks.browserAct, 1); + expect(request).toEqual({ + kind: "click", + ref: "f1e3", + selector: "#title", + targetId: "tab-1", + timeoutMs: 5000, + doubleClick: true, + }); + }); + + it("keeps nested act request fields authoritative when flattened fields differ", async () => { + const tool = createBrowserTool(); + await tool.execute?.("call-1", { + action: "act", + kind: "click", + ref: "legacy-ref", + selector: "#legacy", + targetId: "legacy-tab", + timeoutMs: 5000, + request: { + kind: "click", + ref: "nested-ref", + selector: "#nested", + targetId: "nested-tab", + timeoutMs: 7000, + }, + }); + + const request = lastMockCallArg<{ + kind?: string; + ref?: string; + selector?: string; + targetId?: string; + timeoutMs?: number; + }>(browserActionsMocks.browserAct, 1); + expect(request).toEqual({ + kind: "click", + ref: "nested-ref", + selector: "#nested", + targetId: "nested-tab", + timeoutMs: 7000, + }); + }); + it("applies configured browser action timeout when act timeout is omitted", async () => { configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } }); diff --git a/extensions/browser/src/browser-tool.ts b/extensions/browser/src/browser-tool.ts index 98efe6975500..330a0b6f3ca4 100644 --- a/extensions/browser/src/browser-tool.ts +++ b/extensions/browser/src/browser-tool.ts @@ -163,6 +163,7 @@ function readTargetUrlParam(params: Record) { } const LEGACY_BROWSER_ACT_REQUEST_KEYS = [ + "kind", "targetId", "ref", "doubleClick", @@ -190,10 +191,31 @@ const LEGACY_BROWSER_ACT_REQUEST_KEYS = [ "timeoutMs", ] as const; +const LEGACY_BROWSER_ACT_SHARED_REQUEST_KEYS = new Set< + (typeof LEGACY_BROWSER_ACT_REQUEST_KEYS)[number] +>(["targetId"]); + function readActRequestParam(params: Record) { const requestParam = params.request; if (requestParam && typeof requestParam === "object") { - return requestParam as Parameters[1]; + const request = { ...(requestParam as Record) }; + const hasMismatchedKind = + typeof request.kind === "string" && + typeof params.kind === "string" && + request.kind !== params.kind; + for (const key of LEGACY_BROWSER_ACT_REQUEST_KEYS) { + if (Object.hasOwn(request, key) || !Object.hasOwn(params, key)) { + continue; + } + // Flattened act fields are legacy shape repair. Only the tab scope is + // safe across kind mismatches; action-specific fields can corrupt the + // explicit nested request. + if (hasMismatchedKind && !LEGACY_BROWSER_ACT_SHARED_REQUEST_KEYS.has(key)) { + continue; + } + request[key] = params[key]; + } + return request as Parameters[1]; } const kind = readStringParam(params, "kind"); @@ -201,7 +223,7 @@ function readActRequestParam(params: Record) { return undefined; } - const request: Record = { kind }; + const request: Record = {}; for (const key of LEGACY_BROWSER_ACT_REQUEST_KEYS) { if (!Object.hasOwn(params, key)) { continue;