can1357/oh-my-pi · error · Error

tab.select() requires a <select> element

Error message

tab.select() requires a <select> element

What it means

tab.select() resolves the target selector to an element and then checks tagName === "SELECT" inside the page. Selecting options only makes sense on a <select>; on any other tag the in-page evaluate throws this error, which surfaces as the tool failure. It is thrown even if the element supports a role="combobox" ARIA pattern but is not a native <select>.

Source

Thrown at packages/coding-agent/src/tools/browser/tab-worker.ts:2033

		}
	}

	async #select(selector: string, values: string[], timeoutMs: number, signal: AbortSignal): Promise<string[]> {
		const handle = await this.#resolveActionHandle(selector, timeoutMs, signal);
		try {
			return (await untilAborted(signal, () =>
				handle.evaluate((el, vals) => {
					interface SelectOption {
						value: string;
						selected: boolean;
					}
					interface SelectLike {
						tagName: string;
						options: ArrayLike<SelectOption>;
						dispatchEvent: (event: unknown) => boolean;
					}
					const select = el as unknown as SelectLike;
					if (select?.tagName !== "SELECT") throw new Error("tab.select() requires a <select> element");
					const EventCtor = (
						globalThis as unknown as { Event: new (type: string, init?: { bubbles: boolean }) => unknown }
					).Event;
					const wanted = new Set(vals as string[]);
					// Assign the full selection first, then read back: on a single
					// <select>, un-selecting the current option mid-loop leaves the
					// browser reporting it selected until another option takes over,
					// which double-counted the old value in the returned list.
					for (let i = 0; i < select.options.length; i++) {
						const opt = select.options[i] as SelectOption;
						opt.selected = wanted.has(opt.value);
					}
					const selected: string[] = [];
					for (let i = 0; i < select.options.length; i++) {
						const opt = select.options[i] as SelectOption;
						if (opt.selected) selected.push(opt.value);
					}
					select.dispatchEvent(new EventCtor("input", { bubbles: true }));

View on GitHub (pinned to 9690622007)

Solutions

  1. Target the actual native <select> element if one exists in the DOM (inspect with DevTools).
  2. For custom dropdowns, click to open the list and click the option element instead of tab.select().
  3. Use the aria-ref of a real <select> from a fresh tab.observe().
  4. As a fallback, set the value via page evaluate and dispatch a change event if the site tolerates it.

Example fix

// before
await tab.select("#react-select-country", ["US"]); // div-based dropdown
// after
await tab.click("#react-select-country");
await tab.click("#react-select-country-option-us");
Defensive patterns

Strategy: validation

Validate before calling

const tag = await tab.evaluate((sel) => document.querySelector(sel)?.tagName ?? null, selector);
if (tag !== "SELECT") throw new Error(`${selector} is <${tag}>; use click-based flow for custom dropdowns`);

Type guard

function isNativeSelect(info: { tagName?: string }): boolean {
  return info?.tagName === "SELECT";
}

Try / catch

try {
  await tab.select(selector, values);
} catch (err) {
  if (err.message.includes("requires a <select> element")) {
    await tab.click(selector); // open custom dropdown
    return tab.click(optionSelectorFor(values[0]));
  }
  throw err;
}

Prevention

When it happens

Trigger: tab.select("#country", ["US"]) where #country is a div-based custom dropdown, an <input> with a datalist, or an ARIA listbox built from <ul>/<li>.

Common situations: Modern component libraries (React-Select, headless UI) render fake dropdowns from divs; targeting the wrapper element rather than the native select; a site replaced a native select with a custom widget in a redesign.

Related errors


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