{"record":{"id":"c74c45576190032d","repo":"can1357/oh-my-pi","slug":"browser-selector-must-be-a-string-got-kind-ta","errorCode":null,"errorMessage":"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\".","messagePattern":"Browser selector must be a string; got (.+?)\\. 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\"\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts","lineNumber":88,"sourceCode":"const ARIA_REF_PREFIXES = [\"aria-ref=\", \"aria-ref/\", \"ariaref/\"];\n\n/**\n * Guard the selector funnels: `tab.click`/`type`/`fill`/`waitFor*`/`scrollIntoView`\n * take string selectors only, but user `run` code routinely passes the ElementHandle\n * from `tab.id(n)`/`tab.ref(...)` (or an un-awaited Promise of one) straight in.\n * Without this the value reaches `.trim()`/`.startsWith()` and throws the opaque,\n * minified `A.trim is not a function` instead of a recovery-naming ToolError.\n */\nexport function assertSelectorString(selector: unknown): asserts selector is string {\n\tif (typeof selector === \"string\") return;\n\tlet kind: string;\n\tif (selector !== null && typeof selector === \"object\") {\n\t\tkind =\n\t\t\t\"then\" in selector && typeof selector.then === \"function\" ? \"a Promise (missing await?)\" : \"an ElementHandle\";\n\t} else {\n\t\tkind = `a ${typeof selector}`;\n\t}\n\tthrow new ToolError(\n\t\t`Browser selector must be a string; got ${kind}. ` +\n\t\t\t\"tab.click/type/fill/waitFor take string selectors only — \" +\n\t\t\t'call the handle method directly (e.g. (await tab.id(n)).click()) or pass a string like \"aria-ref=eN\".',\n\t);\n}\n\n/**\n * Recognize a snapshot-ref selector and return the bare ref id, else null.\n * Accepts `aria-ref=e5` (Playwright-MCP style), `aria-ref/e5`, `ariaref/e5`,\n * and bare `e5`/`@e5`: agents copy ids straight out of the snapshot YAML\n * (`[ref=e5]`), so `tab.click(\"e5\")` must act on the ref instead of falling\n * through to a CSS tag selector that can never match. Bare ids are safe to\n * claim here — an eN tag name is not real HTML, and the tab-worker backend's\n * observe ids are numeric (`tab.id(7)`), so refs are its only eN namespace.\n * (The cmux backend parses selectors itself and routes bare `eN` to its own\n * observe ids; either way `eN` means \"the id from the last page dump\".)\n */\nexport function parseAriaRefSelector(selector: string): string | null {","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts#L70-L106","documentation":"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.","triggerScenarios":"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\".","commonSituations":"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.","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"],"exampleFix":"// before\nconst ref = await tab.id(3);\nawait tab.click(ref); // throws: got an ElementHandle\n// after\nawait tab.click(\"aria-ref=e12\");\n// or\n(await tab.id(3)).click();","handlingStrategy":"type-guard","validationCode":"function assertStringSelector(sel: unknown): string {\n  if (typeof sel !== \"string\") throw new TypeError(`selector must be a string, got ${typeof sel}`);\n  return sel;\n}\nawait tab.click(assertStringSelector(mySelector));","typeGuard":"function isSelectorString(v: unknown): v is string { return typeof v === \"string\"; }","tryCatchPattern":"try {\n  await tab.click(sel);\n} catch (e) {\n  if (e instanceof ToolError && e.message.startsWith(\"Browser selector must be a string\")) {\n    // fix callsite: await promise or use handle method directly\n  } else throw e;\n}","preventionTips":["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\""],"tags":["browser","validation","typescript","selector"],"backgroundTag":"invalid-argument-type","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}