can1357/oh-my-pi · error · ToolError
Browser selector must be a string; got ${kind}. tab.click/ty
Error message
Browser selector must be a string; got ${kind}. tab.click/type/fill/waitFor take string selectors only — call the handle method directly (e.g. (await tab.id(n)).click()) or pass a string like "aria-ref=eN". What it means
assertSelectorString validates that selector-taking browser APIs (tab.click/type/fill/waitFor) receive a plain string. Passing a non-string — a Promise (forgotten await), an ElementHandle, a number, etc. — throws this ToolError with a message describing what type was actually received and how to fix it. The aria-ref handle workflow requires either string selectors or direct handle method calls.
Source
Thrown at packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts:88
const ARIA_REF_PREFIXES = ["aria-ref=", "aria-ref/", "ariaref/"];
/**
* Guard the selector funnels: `tab.click`/`type`/`fill`/`waitFor*`/`scrollIntoView`
* take string selectors only, but user `run` code routinely passes the ElementHandle
* from `tab.id(n)`/`tab.ref(...)` (or an un-awaited Promise of one) straight in.
* Without this the value reaches `.trim()`/`.startsWith()` and throws the opaque,
* minified `A.trim is not a function` instead of a recovery-naming ToolError.
*/
export function assertSelectorString(selector: unknown): asserts selector is string {
if (typeof selector === "string") return;
let kind: string;
if (selector !== null && typeof selector === "object") {
kind =
"then" in selector && typeof selector.then === "function" ? "a Promise (missing await?)" : "an ElementHandle";
} else {
kind = `a ${typeof selector}`;
}
throw new ToolError(
`Browser selector must be a string; got ${kind}. ` +
"tab.click/type/fill/waitFor take string selectors only — " +
'call the handle method directly (e.g. (await tab.id(n)).click()) or pass a string like "aria-ref=eN".',
);
}
/**
* Recognize a snapshot-ref selector and return the bare ref id, else null.
* Accepts `aria-ref=e5` (Playwright-MCP style), `aria-ref/e5`, `ariaref/e5`,
* and bare `e5`/`@e5`: agents copy ids straight out of the snapshot YAML
* (`[ref=e5]`), so `tab.click("e5")` must act on the ref instead of falling
* through to a CSS tag selector that can never match. Bare ids are safe to
* claim here — an eN tag name is not real HTML, and the tab-worker backend's
* observe ids are numeric (`tab.id(7)`), so refs are its only eN namespace.
* (The cmux backend parses selectors itself and routes bare `eN` to its own
* observe ids; either way `eN` means "the id from the last page dump".)
*/
export function parseAriaRefSelector(selector: string): string | null {View on GitHub (pinned to 9690622007)
Solutions
- Pass a string selector, e.g. "aria-ref=e12" or a CSS selector
- If you have an ElementHandle, call its method directly: (await tab.id(n)).click()
- Add await before an expression that resolves to a selector string
- Fix the argument type at the callsite flagged by the message's `got <kind>` clause
Example fix
// before
const ref = await tab.id(3);
await tab.click(ref); // throws: got an ElementHandle
// after
await tab.click("aria-ref=e12");
// or
(await tab.id(3)).click(); Defensive patterns
Strategy: type-guard
Validate before calling
function assertStringSelector(sel: unknown): string {
if (typeof sel !== "string") throw new TypeError(`selector must be a string, got ${typeof sel}`);
return sel;
}
await tab.click(assertStringSelector(mySelector)); Type guard
function isSelectorString(v: unknown): v is string { return typeof v === "string"; } Try / catch
try {
await tab.click(sel);
} catch (e) {
if (e instanceof ToolError && e.message.startsWith("Browser selector must be a string")) {
// fix callsite: await promise or use handle method directly
} else throw e;
} Prevention
- Type selector params as string so TS rejects ElementHandle/Promise at compile time
- Await promises before passing — check for Promise-typed values
- Use (await tab.id(n)).click() for handles instead of tab.click(handle)
- Keep aria-ref selectors as strings like "aria-ref=eN"
When it happens
Trigger: Calling tab.click(ariaRef) where ariaRef is an ElementHandle; calling tab.click(await something()) incorrectly ordered so a Promise is passed; passing a number instead of the string "aria-ref=eN".
Common situations: Forgotten await on a promise returning a selector/handle; conflating ElementHandle methods with tab-level selector methods; porting code from Puppeteer where element handles are passed to click directly.
Understand the failure class
Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.
Related errors
- Destination option ${key} must be a string
- tab.ariaSnapshot: selector ${__sel} matched no element
- No page target matched ${JSON.stringify(options.matcher)}. A
- Element handle selector no longer resolves
- No element matched ${spec.raw}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/c74c45576190032d.
Report an issue: GitHub.