From de3261d2d91fc8d89a6c81e8c47db01dec07d594 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 15 Jul 2026 23:06:02 -0700 Subject: [PATCH] [eric] browser: a navigate landing on a raw JSON/API URL hands data to the agent and recovers the card off the wall --- frontend/src/shared/browserCommandHandler.ts | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index f4953f82..c7970d91 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -312,10 +312,63 @@ async function handleNavigate(wv: BrowserWebview, params: Record): } finally { removeReady(); } + // A navigate that lands on a raw JSON/API document (Instagram's topsearch, any /api/... GET) + // paints an unreadable wall in the card and reads as a crash to the user. Hand the data to the + // agent as the result instead, and quietly get the card off the wall, so a person never sees + // raw JSON where a page should be. + const data = await readDataDocument(wv); + if (data) { + recoverCardOffDataWall(wv, url); + return { + text: `Fetched ${data.contentType} data from ${url} (this URL is a raw API endpoint, not a page):\n${data.body}`, + url: wv.getURL(), + data_document: true, + }; + } // Route-count is sampled on the next READ (handleGetText), not here: at navigate-return the SPA's XHRs haven't fired yet, so this would always be ~0. return { text: `Navigated to ${url}`, url }; } +// Chromium renders any JSON (or JSON-shaped text) response with its built-in viewer, so a navigate +// to an API endpoint leaves the card showing a wall of raw data. contentType is the reliable tell; +// re-fetch same-origin (the just-loaded GET, idempotent) to hand the agent the exact bytes. +async function readDataDocument(wv: BrowserWebview): Promise<{ contentType: string; body: string } | null> { + const code = `(async () => { + const ct = String(document.contentType || '').toLowerCase(); + const isJson = ct.startsWith('application/json'); + const maybePlain = ct.startsWith('text/plain'); + if (!isJson && !maybePlain) return null; + try { + const r = await fetch(location.href, { credentials: 'include', signal: AbortSignal.timeout(2500) }); + const body = await r.text(); + if (maybePlain && !isJson) { try { JSON.parse(body.trim()); } catch (e) { return null; } } + return { contentType: ct, body: body.slice(0, 15000) }; + } catch (e) { return null; } + })()`; + try { + const res = await evalInPage(wv, code); + if (res && !res.error && typeof res.body === 'string') return res; + return null; + } catch { + return null; + } +} + +// Get the card off a raw-data wall: step back to the real page the agent came from, or, if it opened +// straight onto the data URL, fall back to the site homepage. Fire-and-forget, the agent already has +// the data; this is purely so a human sees a page instead of JSON. +function recoverCardOffDataWall(wv: BrowserWebview, dataUrl: string): void { + try { + if (wv.canGoBack()) { + wv.goBack(); + return; + } + void wv.loadURL(new URL(dataUrl).origin).catch(() => {}); + } catch { + /* leave the card as-is if recovery fails; the data still reached the agent */ + } +} + async function handleClick(wv: BrowserWebview, params: Record): Promise> { const selector = params.selector as string; if (!selector) return { error: 'selector parameter is required' };