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

  1. Pass a string selector, e.g. "aria-ref=e12" or a CSS selector
  2. If you have an ElementHandle, call its method directly: (await tab.id(n)).click()
  3. Add await before an expression that resolves to a selector string
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/c74c45576190032d. Report an issue: GitHub.