can1357/oh-my-pi · error · ToolError

Timed out clicking ${selector} (seen ${lastSeen} matches; la

Error message

Timed out clicking ${selector} (seen ${lastSeen} matches; last reason: ${lastReason ?? "unknown"}). If there are multiple matching elements, use observe + tab.id() or a more specific selector.

What it means

The click loop resolves candidate handles for the selector and retries until an overall timeout; each attempt records why the click was skipped. If the loop exhausts the timeout without a successful click, this error reports how many matches were seen and the last skip reason, and suggests observe + tab.id() or a more specific selector.

Source

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

			}
			const actionability = await isClickActionable(target);
			if (!actionability.ok) {
				lastReason = actionability.reason;
				await untilAborted(clickSignal, () => Bun.sleep(100));
				continue;
			}
			try {
				await untilAborted(clickSignal, () => target.click());
				return;
			} catch (err) {
				lastReason = err instanceof Error ? err.message : String(err);
				await untilAborted(clickSignal, () => Bun.sleep(100));
			}
		} finally {
			await Promise.all(handles.map(async handle => handle.dispose().catch(() => undefined)));
		}
	}
	throw new ToolError(
		`Timed out clicking ${selector} (seen ${lastSeen} matches; last reason: ${lastReason ?? "unknown"}). ` +
			"If there are multiple matching elements, use observe + tab.id() or a more specific selector.",
	);
}

/**
 * Hint appended to a selector op's fail-fast timeout, given the selector's current
 * match count: a missing element (consent wall, wrong page) reads differently from
 * a present-but-unactionable one.
 */
export function formatSelectorMatchHint(count: number): string {
	return count === 0
		? "; selector currently matches no elements — run tab.observe() or tab.ariaSnapshot() to inspect the page"
		: `; selector currently matches ${count} element(s) but the action never became possible — the element may be hidden or covered (try tab.scrollIntoView() or a more specific selector)`;
}

export interface InflightOp {
	label: string;

View on GitHub (pinned to 9690622007)

Solutions

  1. Read the `last reason` in the message (e.g. not visible, intercepted, detached) and address that specific condition.
  2. Use a more specific selector: prefer `aria/<name>` or `text/<exact label>` over broad CSS.
  3. Run `tab.observe()` and click by handle id (`tab.id()`) to disambiguate multiple matches.
  4. Wait for the element to be actionable before clicking, or increase the action timeout.

Example fix

// before
tab.click("text/Submit"); // matches 3 buttons, none clickable
// after
const obs = await tab.observe();
const btn = obs.find(el => el.name === "Submit order");
tab.click(`aria/${btn.name}`);
Defensive patterns

Strategy: validation

Validate before calling

const matches = await tab.handles(selector);
if (matches.length > 1) {
  const obs = await tab.observe();
  throw new Error(`selector '${selector}' matched ${matches.length} elements; pick one via observe`);
}

Try / catch

try {
  await tab.click(selector);
} catch (err) {
  if (err instanceof ToolError && err.message.startsWith("Timed out clicking")) {
    const obs = await tab.observe();
    const target = obs.find(el => el.clickable && el.name === expectedLabel);
    await tab.click(target.id);
  } else throw err;
}

Prevention

When it happens

Trigger: Selector matching multiple elements where earlier matches are non-clickable (hidden, covered, disabled, detached) so every attempt is skipped; selector matching zero elements; element becomes visible only after the timeout.

Common situations: Cookie banners with several similarly-named buttons; elements behind overlays; SPA where the target element mounts slowly; ambiguous selectors like `text/OK` matching many buttons.

Understand the failure class

Related errors


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