pi-browser-harness 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to pi-browser-harness will be documented in this file.
4
4
 
5
+ ## 0.8.1 — 2026-07-05
6
+
7
+ ### Fixed
8
+
9
+ - **`browser_dispatch_key` now populates `keyCode`/`which`** on the synthesized `KeyboardEvent` (both the ref and selector paths). Legacy React/Vue key handlers commonly branch on `e.keyCode === 13` rather than `e.key`, so Enter on tag/autocomplete inputs previously did nothing. The event remains untrusted (`isTrusted === false`), so a few libraries may still ignore it.
10
+ - **`browser_execute_js` IIFE auto-wrap no longer false-positives.** The wrap now triggers only when the trimmed source *starts with* a `return` statement, instead of whenever the string merely contained the substring `"return "` (which silently turned bare expressions — including ones mentioning `return` inside a string literal or comment — into `undefined`).
11
+
12
+ ### Changed
13
+
14
+ - **`browser_fill` guidelines** now point at `browser_dispatch_key({ ref, key: 'Enter' })` for submitting tag/autocomplete inputs (not `browser_press_key`, which targets the focused element), and at the open → `browser_snapshot` → `browser_click` recipe for custom (div-based) dropdowns.
15
+
5
16
  ## 0.6.0 — 2026-06-21
6
17
 
7
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-browser-harness",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Browser control extension for pi — navigate, click, type, screenshot, and extract data from real Chrome via CDP",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/client.ts CHANGED
@@ -170,12 +170,14 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
170
170
 
171
171
  const evaluateJs = async (expression: string, sessionId?: string): Promise<Result<unknown, CdpError>> => {
172
172
  // Heuristic IIFE wrap: legacy convenience that lets agents write
173
- // `return foo` instead of `(() => foo)()`. False-positive on substring
174
- // `"return "` inside string literals or comments — agents that hit this
175
- // can pass an explicit IIFE instead.
176
- const wrapped = expression.includes("return ") && !expression.trim().startsWith("(")
177
- ? `(function(){${expression}})()`
178
- : expression;
173
+ // `return foo` instead of `(() => foo)()`. Only wrap when the trimmed
174
+ // source *starts with* a return statement — a bare expression, or one that
175
+ // merely mentions `return` inside a string literal or comment, is passed
176
+ // through untouched (previously any substring `"return "` triggered a wrap,
177
+ // silently turning such expressions into `undefined`).
178
+ const trimmed = expression.trim();
179
+ const isReturnStatement = /^return[\s(]/.test(trimmed);
180
+ const wrapped = isReturnStatement ? `(function(){${expression}})()` : expression;
179
181
  const r = sessionId
180
182
  ? await session.callOnTarget("Runtime.evaluate", { expression: wrapped, returnByValue: true, awaitPromise: true }, sessionId)
181
183
  : await session.call("Runtime.evaluate", { expression: wrapped, returnByValue: true, awaitPromise: true });
@@ -119,6 +119,8 @@ export const fillTool = defineBrowserTool({
119
119
  "Pass the desired value; no browser_click is needed first.",
120
120
  "A 'ref is stale' error means the page changed — re-run browser_snapshot to get fresh refs.",
121
121
  "Use browser_type instead only for keystroke-sensitive widgets (autocomplete, masked/segmented inputs).",
122
+ "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.",
123
+ "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.",
122
124
  "The result reports the field's value after writing and an appended page-changes diff — confirm both match what you intended before moving on.",
123
125
  ],
124
126
  parameters: FillArgs,
@@ -129,12 +129,17 @@ export const dispatchKeyTool = defineBrowserTool({
129
129
  "PREFER `ref` from browser_snapshot over a CSS selector — survives re-renders.",
130
130
  "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).",
131
131
  "Try browser_press_key first; only use browser_dispatch_key when the page ignores raw CDP key events.",
132
+ "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.",
132
133
  "eventType defaults to 'keydown'.",
133
134
  ],
134
135
  parameters: DispatchKeyArgs,
135
136
  async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
136
137
  const eventType = args.eventType ?? "keydown";
137
138
  const target = args.ref ?? args.selector ?? "";
139
+ // Legacy handlers commonly branch on e.keyCode/e.which (e.g. === 13 for
140
+ // Enter) rather than e.key, so populate both. virtualKeyCode maps named
141
+ // keys and single chars; 0 for anything unknown.
142
+ const code = virtualKeyCode(args.key);
138
143
  // Ref path: dispatch on the single resolved node. Selector path: dispatch on
139
144
  // all matches (preserves the original multi-match behavior).
140
145
  if (args.ref !== undefined) {
@@ -142,8 +147,8 @@ export const dispatchKeyTool = defineBrowserTool({
142
147
  if (!objectId.success) return objectId;
143
148
  const r = await client.session().call("Runtime.callFunctionOn", {
144
149
  objectId: objectId.data,
145
- functionDeclaration: `function (type, key) { this.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true, cancelable: true })); return 1; }`,
146
- arguments: [{ value: eventType }, { value: args.key }],
150
+ functionDeclaration: `function (type, key, keyCode) { this.dispatchEvent(new KeyboardEvent(type, { key, keyCode, which: keyCode, bubbles: true, cancelable: true })); return 1; }`,
151
+ arguments: [{ value: eventType }, { value: args.key }, { value: code }],
147
152
  returnByValue: true,
148
153
  });
149
154
  if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
@@ -160,7 +165,7 @@ export const dispatchKeyTool = defineBrowserTool({
160
165
  const els = document.querySelectorAll(${args.selector});
161
166
  if (els.length === 0) return 0;
162
167
  for (const el of els) {
163
- el.dispatchEvent(new KeyboardEvent(${eventType}, { key: ${args.key}, bubbles: true, cancelable: true }));
168
+ el.dispatchEvent(new KeyboardEvent(${eventType}, { key: ${args.key}, keyCode: ${code}, which: ${code}, bubbles: true, cancelable: true }));
164
169
  }
165
170
  return els.length;
166
171
  })()