aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-07-05 13:15:22 +0530
committerGitHub <noreply@github.com>2026-07-05 13:15:22 +0530
commit26daf87896d6645fe9be71460d2868755a4f939f (patch)
treef81876c314ee78c742e9351ceea6b53a6852ab7b /src
parentcf770d148082c58cf13102c9ea57030e0e404ef4 (diff)
fix: make dispatch_key fire legacy keyCode handlers and tighten execute_js IIFE wrap (#9)
- browser_dispatch_key now sets keyCode/which on the synthesized KeyboardEvent (both ref and selector paths) so handlers checking e.keyCode === 13 fire. - browser_execute_js IIFE auto-wrap triggers only when the source starts with a return statement, not on any substring "return " (which silently produced undefined for bare expressions). - browser_fill guidelines point at dispatch_key for tag-input submission and the snapshot+click recipe for custom dropdowns. Bump to 0.8.1. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/client.ts14
-rw-r--r--src/domains/form.ts2
-rw-r--r--src/domains/keyboard.ts11
3 files changed, 18 insertions, 9 deletions
diff --git a/src/client.ts b/src/client.ts
index 3c79f1a..724a711 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -170,12 +170,14 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
const evaluateJs = async (expression: string, sessionId?: string): Promise<Result<unknown, CdpError>> => {
// Heuristic IIFE wrap: legacy convenience that lets agents write
- // `return foo` instead of `(() => foo)()`. False-positive on substring
- // `"return "` inside string literals or comments — agents that hit this
- // can pass an explicit IIFE instead.
- const wrapped = expression.includes("return ") && !expression.trim().startsWith("(")
- ? `(function(){${expression}})()`
- : expression;
+ // `return foo` instead of `(() => foo)()`. Only wrap when the trimmed
+ // source *starts with* a return statement — a bare expression, or one that
+ // merely mentions `return` inside a string literal or comment, is passed
+ // through untouched (previously any substring `"return "` triggered a wrap,
+ // silently turning such expressions into `undefined`).
+ const trimmed = expression.trim();
+ const isReturnStatement = /^return[\s(]/.test(trimmed);
+ const wrapped = isReturnStatement ? `(function(){${expression}})()` : expression;
const r = sessionId
? await session.callOnTarget("Runtime.evaluate", { expression: wrapped, returnByValue: true, awaitPromise: true }, sessionId)
: await session.call("Runtime.evaluate", { expression: wrapped, returnByValue: true, awaitPromise: true });
diff --git a/src/domains/form.ts b/src/domains/form.ts
index a10646c..063cfaf 100644
--- a/src/domains/form.ts
+++ b/src/domains/form.ts
@@ -119,6 +119,8 @@ export const fillTool = defineBrowserTool({
"Pass the desired value; no browser_click is needed first.",
"A 'ref is stale' error means the page changed — re-run browser_snapshot to get fresh refs.",
"Use browser_type instead only for keystroke-sensitive widgets (autocomplete, masked/segmented inputs).",
+ "Fill sets the value but does not focus or submit. To submit a tag/autocomplete input, follow with browser_dispatch_key({ ref, key: 'Enter' }) — not browser_press_key, which targets the focused element and may miss this field.",
+ "For a custom (div-based) dropdown, browser_fill won't validate against its option list — open it, run browser_snapshot, then browser_click the option's ref.",
"The result reports the field's value after writing and an appended page-changes diff — confirm both match what you intended before moving on.",
],
parameters: FillArgs,
diff --git a/src/domains/keyboard.ts b/src/domains/keyboard.ts
index 69a7396..24b7833 100644
--- a/src/domains/keyboard.ts
+++ b/src/domains/keyboard.ts
@@ -129,12 +129,17 @@ export const dispatchKeyTool = defineBrowserTool({
"PREFER `ref` from browser_snapshot over a CSS selector — survives re-renders.",
"Dispatches a synthetic DOM KeyboardEvent — for React/Vue synthetic event listeners. Does NOT insert text (use browser_type or browser_press_key for actual typing).",
"Try browser_press_key first; only use browser_dispatch_key when the page ignores raw CDP key events.",
+ "The event carries keyCode/which (e.g. 13 for Enter) for legacy handlers, but is untrusted (isTrusted === false) — a few libraries may still ignore it.",
"eventType defaults to 'keydown'.",
],
parameters: DispatchKeyArgs,
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
const eventType = args.eventType ?? "keydown";
const target = args.ref ?? args.selector ?? "";
+ // Legacy handlers commonly branch on e.keyCode/e.which (e.g. === 13 for
+ // Enter) rather than e.key, so populate both. virtualKeyCode maps named
+ // keys and single chars; 0 for anything unknown.
+ const code = virtualKeyCode(args.key);
// Ref path: dispatch on the single resolved node. Selector path: dispatch on
// all matches (preserves the original multi-match behavior).
if (args.ref !== undefined) {
@@ -142,8 +147,8 @@ export const dispatchKeyTool = defineBrowserTool({
if (!objectId.success) return objectId;
const r = await client.session().call("Runtime.callFunctionOn", {
objectId: objectId.data,
- functionDeclaration: `function (type, key) { this.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true, cancelable: true })); return 1; }`,
- arguments: [{ value: eventType }, { value: args.key }],
+ functionDeclaration: `function (type, key, keyCode) { this.dispatchEvent(new KeyboardEvent(type, { key, keyCode, which: keyCode, bubbles: true, cancelable: true })); return 1; }`,
+ arguments: [{ value: eventType }, { value: args.key }, { value: code }],
returnByValue: true,
});
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
@@ -160,7 +165,7 @@ export const dispatchKeyTool = defineBrowserTool({
const els = document.querySelectorAll(${args.selector});
if (els.length === 0) return 0;
for (const el of els) {
- el.dispatchEvent(new KeyboardEvent(${eventType}, { key: ${args.key}, bubbles: true, cancelable: true }));
+ el.dispatchEvent(new KeyboardEvent(${eventType}, { key: ${args.key}, keyCode: ${code}, which: ${code}, bubbles: true, cancelable: true }));
}
return els.length;
})()