ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-05-26Intermediate

Why Antigravity's Browser Sub-Agent Reads SPAs as Empty Pages — and Three Wait Strategies That Stuck for Me

When you hand an SPA dashboard to Antigravity's Browser Sub-Agent, get_page_text often returns before the real content is rendered, and the agent reports an empty page. Here is how I diagnose the symptom and the three wait strategies that have stabilized my routine.

antigravity429browser-sub-agent3spatroubleshooting105agents123wait-strategy

One morning I asked Antigravity's Browser Sub-Agent to read my AdMob console for me. The return value of get_page_text was just "Sign in" and "Loading…". The browser address bar had clearly moved on to the dashboard, but from the agent's point of view the body of the page was still empty. The instruction was "find the networks whose eCPM has dropped this week," and the agent — taking the empty text at face value — replied "none found."

I have been running a portfolio of mobile apps solo since 2014, now at about fifty million cumulative downloads, and Browser Sub-Agents reading SPA dashboards like AdMob, App Store Connect, and Stripe Dashboard have become a regular part of my workflow. The same misread happened to me several times a week at first, but once I broke the symptom apart it became clear that the problem was a mismatch in wait strategy. The same approach now powers the four sites I run under Dolice Labs, so I want to write down the diagnostic flow I have settled on and the three wait patterns that have proven reliable.

Three different states can look like "an empty page"

The first thing that matters is that "empty" is actually three different states. If you treat them as one and just turn up the wait time, the behaviour never quite stabilizes.

The first state is the SPA shell. The HTML contains only <div id="root"></div> and a handful of meta tags; the body content is injected by client JavaScript later. When the result of get_page_text is unusually short and contains only generic strings like "Loading…" or "Sign in to continue," you are in this state.

The second state is being bounced back to a login page by an auth guard. document.title may still read like the app's title, but the body is just a sign-in form. This often happens when last week's session cookie has expired — AdMob expires in about seven days, App Store Connect in roughly eight hours.

The third state is when the DOM is fully built but the data rows are still waiting on an API response. Table headers are visible, rows show "No data" or empty cells. This is purely network-bound and goes away if you wait a bit longer.

The first step in diagnosis is always to look at the HTML structure. Pairing get_page_text with get_page_html lets you tell the three apart in a few seconds.

The root cause is the gap between DOMContentLoaded and hydration

By default, a Browser Sub-Agent treats a page as "loaded" right after navigate returns. That judgment is roughly based on the load event, when the HTML and external resources have all been fetched. SPAs built with React, Vue, or the Next.js App Router only evaluate their JS bundle, run hydration, and finish the initial data fetch after load. The screen does not show meaningful text until that pipeline completes.

In other words, the moment the Sub-Agent says "ready to read" and the moment you, as a human, see the dashboard appear are separated by two to six seconds on an SPA. On the AdMob dashboard on my setup the median was about 3.2 seconds and the 95th percentile was 5.8 seconds. Covering 95% of cases with a fixed sleep would cost six seconds every time and burn execution time and tokens for nothing.

To avoid that, you need a structural way to detect that the page is actually ready. Here are the three patterns that stabilized for me, in order.

Pattern 1: Wait for a known selector to render

The most reliable and least false-positive pattern is to declare "the body is ready when this selector appears." Inside an Antigravity Browser Sub-Agent you can either install a MutationObserver via javascript_tool or, more simply, poll. Polling is usually enough.

// Run via javascript_tool from the Browser Sub-Agent
async function waitForSelector(selector, timeoutMs = 10000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const el = document.querySelector(selector);
    if (el && el.offsetParent !== null) return true;
    await new Promise(r => setTimeout(r, 200));
  }
  throw new Error(`Selector not visible: ${selector}`);
}
 
// Wait for the AdMob mediation table to render
await waitForSelector('table[data-test="mediation-waterfall"] tbody tr');

The detail that matters is offsetParent !== null — it confirms not only that the element exists in the DOM but also that it is not display:none. SPAs often insert elements first and reveal them afterwards, so skipping this check will cause false-positive successes. For AdMob I look for tbody tr, but when I want to distinguish "render complete with zero rows" from "still rendering," I add an empty-state selector in an OR clause, such as tbody tr, [data-empty-state].

Pattern 2: Network idle plus structural verification

For pages where you cannot pin down a selector in advance — for example when the agent is given an open-ended instruction like "grab the latest error visible on the screen" — wait for the network to settle, then verify the structure.

// Wait until network has been idle for N milliseconds
async function waitForNetworkIdle(idleMs = 800, timeoutMs = 12000) {
  return new Promise((resolve, reject) => {
    const start = Date.now();
    let lastActivity = Date.now();
    let pending = 0;
 
    const origFetch = window.fetch;
    window.fetch = async function (...args) {
      pending++;
      lastActivity = Date.now();
      try {
        return await origFetch.apply(this, args);
      } finally {
        pending--;
        lastActivity = Date.now();
      }
    };
 
    const tick = setInterval(() => {
      const idle = Date.now() - lastActivity;
      if (pending === 0 && idle >= idleMs) {
        clearInterval(tick);
        window.fetch = origFetch;
        resolve(true);
      } else if (Date.now() - start > timeoutMs) {
        clearInterval(tick);
        window.fetch = origFetch;
        reject(new Error("Network idle timeout"));
      }
    }, 100);
  });
}
 
await waitForNetworkIdle(800);
 
// Structural verification: a heading and a table at minimum
const ok = document.querySelectorAll('h1, h2, h3').length > 0
        && document.querySelectorAll('table, [role="grid"]').length > 0;
if (!ok) throw new Error("Page structure not ready");

Wrapping fetch will miss XHR traffic, so if the screen still relies on older XHR-based code, hook XMLHttpRequest.prototype.send in the same way. I added an XHR hook for a few App Store Connect screens, which is what finally made them reliable.

Pattern 3: A retry-first agent prompt

There is also the option of not over-engineering on the runtime side and instead baking the behaviour into the Sub-Agent's instructions: "if get_page_text returns something short or only contains keywords like 'Loading,' wait three seconds and retry. After three failures, summarize the situation and ask."

# Shared prompt fragment for the Sub-Agent
 
When reading a page, follow these rules.
 
1. Call get_page_text right after navigate.
2. If the result is shorter than 200 characters, or only contains "Loading" / "Sign in,"
   wait 3 seconds and call get_page_text again. Up to three retries.
3. If all three retries fail, report "page may not have rendered" with the current
   URL, title, and the first 500 characters, and ask for human judgment.
4. Treat the read as successful only when the expected keywords
   (for example "Mediation", "Waterfall") appear.

The implementation cost of this pattern is essentially zero, but token usage goes up. In practice I use Pattern 1 selector waits for high-frequency dashboards like AdMob, App Store Connect, and Stripe Dashboard, and Pattern 3 retries for admin screens I open only once a month.

Two habits that prevent this from coming back

Beyond the wait pattern itself, two operational habits raise the floor of stability noticeably. The first is to make the Sub-Agent itself responsible for "is this result obviously too short?" and "is the expected keyword completely missing?" Don't just look at the raw text length — define, per domain, a label that absolutely has to appear on this page, and key the success judgment off that. Misreads drop close to zero.

The second is to write session cookie lifetimes into your operational calendar. AdMob lasts about seven days, App Store Connect roughly eight hours, Stripe Dashboard via CSR about two weeks, and Cloudflare Dashboard around twelve hours with MFA enabled. Half of the "the Sub-Agent says it can't read it" complaints I get are really sessions, so reserving a single block of time on Monday to refresh the sessions you need that week makes a lot of mystery waits disappear.

That is the full picture of how I work the SPA-versus-Sub-Agent friction into my routine. Thank you for reading. As a concrete next step, pick the one dashboard you have the agent read most often and write down a single selector that would mean "this page is ready." Just that one line will visibly raise the trustworthiness of what the agent returns.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Agents & Manager2026-05-30
When the Antigravity Agent Says 'command not found' for node or python: Causes and Fixes
It works in your own terminal, but the Antigravity agent hits command not found. Starting from how PATH inheritance works, here are concrete fixes for nvm, pyenv, Homebrew, and WSL setups—plus how to confirm the fix actually took.
Agents & Manager2026-05-09
Why Antigravity Agents Can't Read Your .env File — Three Propagation Paths to Check First
When an Antigravity agent fails with Missing API_KEY but the same build works in your terminal, the cause is one of three env propagation paths. Diagnose each.
Agents & Manager2026-05-05
Debugging Antigravity AI Agents: A Systematic Diagnosis Guide
A systematic approach to diagnosing AI agent failures in Antigravity—covering startup failures, tool call errors, loop detection, and incorrect behavior—with practical debugging patterns for each.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →