can1357/oh-my-pi · error · ToolError

tab.waitForNavigation() timed out after ${timeoutMs}ms

Error message

tab.waitForNavigation() timed out after ${timeoutMs}ms

What it means

CmuxTab.waitForNavigation() snapshots the current URL and polls browser.url.get every 200ms until it differs from the baseline, throwing this ToolError when the timeout expires without any URL change. Because cmux has no native 'next navigation' wait, the URL must actually change — same-URL navigations or calls made after the navigation completed will time out.

Source

Thrown at packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts:688

				signal,
			)) as CmuxUrlGetResult;
			if (typeof result.url === "string" && result.url.length > 0) {
				this.#lastUrl = result.url;
				if (result.url !== startUrl) {
					if (opts?.waitUntil) {
						await this.#request(
							"browser.wait",
							{ load_state: mapWaitUntil(opts.waitUntil), timeout_ms: timeoutMs },
							timeoutMs,
							signal,
						);
					}
					return null;
				}
			}
			await untilAborted(signal, () => Bun.sleep(200));
		}
		throw new ToolError(`tab.waitForNavigation() timed out after ${timeoutMs}ms`);
	}

	async drag(from: DragTarget, to: DragTarget): Promise<void> {
		const start = await this.#dragPoint(from);
		const end = await this.#dragPoint(to);
		await this.#evalScript(
			`(() => {
				const points = ${JSON.stringify({ start, end })};
				const target = document.elementFromPoint(points.start.x, points.start.y) || document.body;
				const dispatch = (type, point) => target.dispatchEvent(new MouseEvent(type, {
					bubbles: true,
					cancelable: true,
					view: window,
					clientX: point.x,
					clientY: point.y,
					buttons: type === "mouseup" ? 0 : 1,
				}));
				dispatch("mousemove", points.start);

View on GitHub (pinned to 9690622007)

Solutions

  1. Start waitForNavigation() BEFORE the click/submit that triggers navigation, then await the click.
  2. Increase opts.timeout for slow pages.
  3. If the URL does not change (same-URL nav or SPA state change), replace with tab.waitForFunction() polling a DOM condition or tab.waitForSelector() on a post-nav element.
  4. Verify the click actually fired: use tab.waitFor(selector) first, scroll the element into view, and check that no overlay intercepts the pointer events.

Example fix

// before: wait after the click already completed
await tab.click("#submit");
await tab.waitForNavigation();
// after: arm the wait first, then act
const nav = tab.waitForNavigation({ timeout: 60_000 });
await tab.click("#submit");
await nav;
Defensive patterns

Strategy: try-catch

Validate before calling

// only meaningful if the URL can actually change
const before = await tab.evaluate(() => location.href);
// if your flow cannot change the URL, prefer waitForFunction instead

Try / catch

const nav = tab.waitForNavigation({ timeout: 30_000 });
await tab.click("#submit");
try {
  await nav;
} catch (err) {
  if (err instanceof ToolError && err.message.includes("waitForNavigation() timed out")) {
    await tab.waitForSelector("[data-loaded='true']", { timeout: 30_000 }); // same-URL fallback
  } else throw err;
}

Prevention

When it happens

Trigger: Calling waitForNavigation() after the navigation already finished (baseline taken post-nav, URL never changes again); the triggering click/submit failed or was intercepted; the page navigates to the same URL (e.g. form POST back to itself) so the URL comparison never differs; timeout too short for slow loads.

Common situations: Puppeteer-style code that clicks and waits, but the click was swallowed by an overlay or new-tab popup (cmux may not follow target=_blank); SPA route transitions that keep the same URL string; polling during a network stall; forgetting that waitForNavigation must be started BEFORE the action per the documented contract.

Understand the failure class

Related errors


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