Add controls inventory and truncate long data: URIs in dumps.
CI / automation-js (vitest + real Chromium) (pull_request) Successful in 1m13s
CI / Lint + tests (pull_request) Successful in 10m24s

Agents hitting Outline-style SPAs need visible vs hidden control lists,
and inlined base64 images were blowing up AI prompts and network copy.
This commit is contained in:
2026-08-09 19:13:54 -04:00
parent f45833ff07
commit ee520fc391
12 changed files with 297 additions and 12 deletions
+6
View File
@@ -200,6 +200,12 @@ so no `main_world_eval` workaround is needed there either.
example, its DOM without this filter is 700k+ characters, almost all of example, its DOM without this filter is 700k+ characters, almost all of
it invisible config junk). Without this check you'd be feeding an LLM it invisible config junk). Without this check you'd be feeding an LLM
chameleon experiment payloads instead of page content. chameleon experiment payloads instead of page content.
- Long `data:` URLs (e.g. inlined images) are truncated in markdown links and
in AI-prompt network lines via `truncateDataUri`, so dumps stay usable.
- `inventoryControls()` / `inventory_controls()` lists buttons, menuitems, and
`aria-label` nodes with a **visible vs hidden** flag (capped). `buildAiPrompt`
includes an **Interesting Controls** section so agents can spot hidden Radix
twins without dumping the whole DOM.
- `build_ai_prompt()` / the extension **Copy** button cap page content at **20,000 chars** - `build_ai_prompt()` / the extension **Copy** button cap page content at **20,000 chars**
by default (`maxChars` in JS, `max_chars=`/`--max-chars` in Python/CLI). A by default (`maxChars` in JS, `max_chars=`/`--max-chars` in Python/CLI). A
full `<body>` extraction on a JS-heavy SPA can still be enormous even after full `<body>` extraction on a JS-heavy SPA can still be enormous even after
+1
View File
@@ -34,6 +34,7 @@ const session = new ExtractorSession(page); // attach BEFORE navigating
await page.goto('https://example.com', { waitUntil: 'networkidle' }); await page.goto('https://example.com', { waitUntil: 'networkidle' });
console.log(await session.buildAiPrompt()); // or session.extractMarkdown('#main') console.log(await session.buildAiPrompt()); // or session.extractMarkdown('#main')
console.log(await session.inventoryControls('#main')); // visible/hidden buttons & aria-labels
console.log(session.getStore()); // { console, errors, network } console.log(session.getStore()); // { console, errors, network }
``` ```
+1
View File
@@ -5,5 +5,6 @@ export {
type ErrorEntry, type ErrorEntry,
type NetworkEntry, type NetworkEntry,
type ExtractedMarkdown, type ExtractedMarkdown,
type InterestingControl,
} from './session.js'; } from './session.js';
export { readSharedJs } from './loadJs.js'; export { readSharedJs } from './loadJs.js';
+41 -2
View File
@@ -118,18 +118,44 @@ return extractMarkdown(el);
`; `;
} }
function buildInventoryScript(selector: string | null | undefined): string {
const domJs = readSharedJs('dom.js');
const selJson = JSON.stringify(selector || '');
return `
(() => {
${domJs}
let el = null;
const sel = ${selJson};
if (sel) { try { el = document.querySelector(sel); } catch (_) { el = null; } }
if (!el) el = document.body;
return inventoryInterestingControls(el);
})()
`;
}
export interface InterestingControl {
tag: string;
role: string;
ariaLabel: string;
text: string;
visible: boolean;
selector: string;
}
function buildPromptScript( function buildPromptScript(
meta: Pick<ExtractedMarkdown, 'url' | 'title' | 'ts' | 'selector'>, meta: Pick<ExtractedMarkdown, 'url' | 'title' | 'ts' | 'selector'>,
markdown: string, markdown: string,
store: CaptureStore, store: CaptureStore,
maxChars: number | undefined, maxChars: number | undefined,
controls?: InterestingControl[],
): string { ): string {
const promptJs = readSharedJs('prompt.js'); const promptJs = readSharedJs('prompt.js');
const maxCharsJs = maxChars === undefined ? 'undefined' : JSON.stringify(maxChars); const maxCharsJs = maxChars === undefined ? 'undefined' : JSON.stringify(maxChars);
const controlsJs = JSON.stringify(controls ?? []);
return ` return `
(() => { (() => {
${promptJs} ${promptJs}
return buildAIPrompt(${JSON.stringify(meta)}, ${JSON.stringify(markdown)}, ${JSON.stringify(store)}, ${maxCharsJs}); return buildAIPrompt(${JSON.stringify(meta)}, ${JSON.stringify(markdown)}, ${JSON.stringify(store)}, ${maxCharsJs}, ${controlsJs});
})() })()
`; `;
} }
@@ -191,15 +217,28 @@ export class ExtractorSession {
}; };
} }
/** Inventory buttons / menuitems / aria-labels under selector (default body). */
async inventoryControls(selector?: string | null): Promise<InterestingControl[]> {
const list = (await this.page.evaluate(buildInventoryScript(selector))) as InterestingControl[];
return Array.isArray(list) ? list : [];
}
async buildAiPrompt(selector?: string | null, maxChars?: number): Promise<string> { async buildAiPrompt(selector?: string | null, maxChars?: number): Promise<string> {
const extracted = await this.extractMarkdown(selector); const extracted = await this.extractMarkdown(selector);
const controls = await this.inventoryControls(selector);
const meta = { const meta = {
url: extracted.url, url: extracted.url,
title: extracted.title, title: extracted.title,
ts: extracted.ts, ts: extracted.ts,
selector: extracted.selector, selector: extracted.selector,
}; };
const script = buildPromptScript(meta, extracted.markdown, this.getStore(), maxChars); const script = buildPromptScript(
meta,
extracted.markdown,
this.getStore(),
maxChars,
controls,
);
return (await this.page.evaluate(script)) as string; return (await this.page.evaluate(script)) as string;
} }
+20
View File
@@ -63,6 +63,9 @@ describe('ExtractorSession', () => {
expect(full.markdown).toContain('**world**'); expect(full.markdown).toContain('**world**');
expect(full.markdown).toContain('[docs](/docs)'); expect(full.markdown).toContain('[docs](/docs)');
expect(full.markdown).toContain('- Alpha'); expect(full.markdown).toContain('- Alpha');
expect(full.markdown).toContain('[tiny-jpeg](data:image/jpeg;base64,…[');
expect(full.markdown).toContain('bytes truncated])');
expect(full.markdown).not.toMatch(/4AAQSkZJRgABAQAAAQABAAD/);
const scoped = await session.extractMarkdown('#main-content'); const scoped = await session.extractMarkdown('#main-content');
expect(scoped.selector).toBe('#main-content'); expect(scoped.selector).toBe('#main-content');
@@ -95,11 +98,28 @@ describe('ExtractorSession', () => {
expect(prompt.startsWith('# Page Context')).toBe(true); expect(prompt.startsWith('# Page Context')).toBe(true);
expect(prompt).toContain('## Page Content'); expect(prompt).toContain('## Page Content');
expect(prompt).toContain('Hello **world**'); expect(prompt).toContain('Hello **world**');
expect(prompt).toContain('## Interesting Controls');
expect(prompt).toContain('[visible]');
expect(prompt).toContain('Document options');
expect(prompt).toContain('### Hidden');
expect(/JavaScript Errors|Console Errors/.test(prompt)).toBe(true); expect(/JavaScript Errors|Console Errors/.test(prompt)).toBe(true);
expect(prompt).toContain('_Extracted by Context Extractor_'); expect(prompt).toContain('_Extracted by Context Extractor_');
}); });
}); });
it('inventories visible and hidden controls', async () => {
await withPage(async (page) => {
const session = new ExtractorSession(page);
await page.goto(FIXTURE_URL, { waitUntil: 'domcontentloaded' });
const controls = await session.inventoryControls('#main-content');
const docs = controls.filter((c) => c.ariaLabel === 'Document options');
expect(docs.length).toBe(2);
expect(docs.some((c) => c.visible)).toBe(true);
expect(docs.some((c) => !c.visible)).toBe(true);
expect(controls.some((c) => c.ariaLabel === 'Upload file' && c.visible)).toBe(true);
});
});
it('clears the capture store, selectively and fully', async () => { it('clears the capture store, selectively and fully', async () => {
await withPage(async (page) => { await withPage(async (page) => {
const session = new ExtractorSession(page); const session = new ExtractorSession(page);
+38 -5
View File
@@ -94,15 +94,35 @@ return extractMarkdown(el);
""" """
def _build_inventory_script(selector: Optional[str]) -> str:
dom_js = _read_js("dom.js")
sel_json = json.dumps(selector or "")
return f"""
(() => {{
{dom_js}
let el = null;
const sel = {sel_json};
if (sel) {{ try {{ el = document.querySelector(sel); }} catch (_) {{ el = null; }} }}
if (!el) el = document.body;
return inventoryInterestingControls(el);
}})()
"""
def _build_prompt_script( def _build_prompt_script(
meta: dict[str, Any], markdown: str, store: dict[str, Any], max_chars: Optional[int] = None meta: dict[str, Any],
markdown: str,
store: dict[str, Any],
max_chars: Optional[int] = None,
controls: Optional[list[Any]] = None,
) -> str: ) -> str:
prompt_js = _read_js("prompt.js") prompt_js = _read_js("prompt.js")
max_chars_js = json.dumps(max_chars) if max_chars is not None else "undefined" max_chars_js = json.dumps(max_chars) if max_chars is not None else "undefined"
controls_js = json.dumps(controls if controls is not None else [])
return f""" return f"""
(() => {{ (() => {{
{prompt_js} {prompt_js}
return buildAIPrompt({json.dumps(meta)}, {json.dumps(markdown)}, {json.dumps(store)}, {max_chars_js}); return buildAIPrompt({json.dumps(meta)}, {json.dumps(markdown)}, {json.dumps(store)}, {max_chars_js}, {controls_js});
}})() }})()
""" """
@@ -196,13 +216,19 @@ class ExtractorSession(_CaptureMixin):
"markdown": markdown or "", "markdown": markdown or "",
} }
def inventory_controls(self, selector: Optional[str] = None) -> list[Any]:
result = self.page.evaluate(_build_inventory_script(selector))
return list(result) if result else []
def build_ai_prompt(self, selector: Optional[str] = None, max_chars: Optional[int] = None) -> str: def build_ai_prompt(self, selector: Optional[str] = None, max_chars: Optional[int] = None) -> str:
extracted = self.extract_markdown(selector) extracted = self.extract_markdown(selector)
controls = self.inventory_controls(selector)
meta = {k: extracted[k] for k in ("url", "title", "ts", "selector")} meta = {k: extracted[k] for k in ("url", "title", "ts", "selector")}
script = _build_prompt_script(meta, extracted["markdown"], self.get_store(), max_chars) script = _build_prompt_script(
meta, extracted["markdown"], self.get_store(), max_chars, controls
)
return self.page.evaluate(script) return self.page.evaluate(script)
class AsyncExtractorSession(_CaptureMixin): class AsyncExtractorSession(_CaptureMixin):
"""Async API. Use with `playwright.async_api` or `camoufox.AsyncCamoufox`. """Async API. Use with `playwright.async_api` or `camoufox.AsyncCamoufox`.
@@ -236,8 +262,15 @@ class AsyncExtractorSession(_CaptureMixin):
"markdown": markdown or "", "markdown": markdown or "",
} }
async def inventory_controls(self, selector: Optional[str] = None) -> list[Any]:
result = await self.page.evaluate(_build_inventory_script(selector))
return list(result) if result else []
async def build_ai_prompt(self, selector: Optional[str] = None, max_chars: Optional[int] = None) -> str: async def build_ai_prompt(self, selector: Optional[str] = None, max_chars: Optional[int] = None) -> str:
extracted = await self.extract_markdown(selector) extracted = await self.extract_markdown(selector)
controls = await self.inventory_controls(selector)
meta = {k: extracted[k] for k in ("url", "title", "ts", "selector")} meta = {k: extracted[k] for k in ("url", "title", "ts", "selector")}
script = _build_prompt_script(meta, extracted["markdown"], self.get_store(), max_chars) script = _build_prompt_script(
meta, extracted["markdown"], self.get_store(), max_chars, controls
)
return await self.page.evaluate(script) return await self.page.evaluate(script)
+4
View File
@@ -12,6 +12,10 @@
<main id="main-content"> <main id="main-content">
<h2>Section</h2> <h2>Section</h2>
<p>Hello <strong>world</strong>, visit <a href="/docs">docs</a>.</p> <p>Hello <strong>world</strong>, visit <a href="/docs">docs</a>.</p>
<p>Big inline art: <a href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxISEhUQEhIWFhUVFRUVFRUVFRUVFRUWFxUXFhUYHSggGBolGxUVITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OGxAQGy0lHyUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAAEAAQMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAADBAECBQYAB//EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAGfAP/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Bf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Bf//Z">tiny-jpeg</a>.</p>
<button type="button" aria-label="Document options" id="doc-opts-visible"></button>
<button type="button" aria-label="Document options" id="doc-opts-hidden" style="display:none"></button>
<button type="button" aria-label="Upload file" id="upload-btn">Upload file</button>
<ul> <ul>
<li>Alpha</li> <li>Alpha</li>
<li>Beta</li> <li>Beta</li>
+55
View File
@@ -14,7 +14,62 @@ def test_shared_js_files_exist_and_match_package():
assert (CORE / "prompt.js").is_file() assert (CORE / "prompt.js").is_file()
# package-side copies/symlinks must resolve to the same source # package-side copies/symlinks must resolve to the same source
assert "function extractMarkdown" in _read_js("dom.js") assert "function extractMarkdown" in _read_js("dom.js")
assert "function truncateDataUri" in _read_js("dom.js")
assert "function inventoryInterestingControls" in _read_js("dom.js")
assert "function buildAIPrompt" in _read_js("prompt.js") assert "function buildAIPrompt" in _read_js("prompt.js")
assert "function truncateDataUri" in _read_js("prompt.js")
def test_build_ai_prompt_truncates_data_uris_in_network(page):
page.goto("about:blank")
meta = {
"url": "https://example.test/page",
"title": "T",
"ts": 1_700_000_000_000,
"selector": "body",
}
long_data = "data:image/jpeg;base64," + ("A" * 500)
store = {
"console": [],
"errors": [],
"network": [{
"type": "fetch", "method": "GET", "url": long_data,
"status": 200, "ts": 3, "duration": 12, "error": None,
}],
}
out = page.evaluate(_build_prompt_script(meta, "# Hello", store))
assert "bytes truncated" in out
assert ("A" * 100) not in out
def test_build_ai_prompt_includes_interesting_controls(page):
page.goto("about:blank")
meta = {"url": "https://example.test/page", "title": "T", "ts": 1, "selector": "body"}
controls = [
{
"tag": "button",
"role": "",
"ariaLabel": "Document options",
"text": "",
"visible": True,
"selector": "#vis",
},
{
"tag": "button",
"role": "",
"ariaLabel": "Document options",
"text": "",
"visible": False,
"selector": "#hid",
},
]
out = page.evaluate(
_build_prompt_script(meta, "# Hello", {"console": [], "errors": [], "network": []}, None, controls)
)
assert "## Interesting Controls" in out
assert "[visible]" in out
assert "### Hidden" in out
assert "Document options" in out
def test_build_ai_prompt_script_formats_errors_and_failures(page): def test_build_ai_prompt_script_formats_errors_and_failures(page):
+15
View File
@@ -31,6 +31,8 @@ def test_extract_markdown_body_and_selector(page, fixture_url):
assert "**world**" in full["markdown"] assert "**world**" in full["markdown"]
assert "[docs](/docs)" in full["markdown"] assert "[docs](/docs)" in full["markdown"]
assert "- Alpha" in full["markdown"] assert "- Alpha" in full["markdown"]
assert "bytes truncated" in full["markdown"]
assert "4AAQSkZJRgABAQAAAQABAAD" not in full["markdown"]
scoped = session.extract_markdown("#main-content") scoped = session.extract_markdown("#main-content")
assert scoped["selector"] == "#main-content" assert scoped["selector"] == "#main-content"
@@ -64,10 +66,23 @@ def test_build_ai_prompt_has_sections(page, fixture_url):
assert prompt.startswith("# Page Context") assert prompt.startswith("# Page Context")
assert "## Page Content" in prompt assert "## Page Content" in prompt
assert "Hello **world**" in prompt assert "Hello **world**" in prompt
assert "## Interesting Controls" in prompt
assert "[visible]" in prompt
assert "Document options" in prompt
assert "JavaScript Errors" in prompt or "Console Errors" in prompt assert "JavaScript Errors" in prompt or "Console Errors" in prompt
assert "_Extracted by Context Extractor_" in prompt assert "_Extracted by Context Extractor_" in prompt
def test_inventory_controls_visible_and_hidden(page, fixture_url):
session = ExtractorSession(page)
page.goto(fixture_url, wait_until="domcontentloaded")
controls = session.inventory_controls("#main-content")
docs = [c for c in controls if c.get("ariaLabel") == "Document options"]
assert len(docs) == 2
assert any(c.get("visible") for c in docs)
assert any(not c.get("visible") for c in docs)
def test_clear_store(page, fixture_url): def test_clear_store(page, fixture_url):
session = ExtractorSession(page) session = ExtractorSession(page)
page.goto(fixture_url, wait_until="domcontentloaded") page.goto(fixture_url, wait_until="domcontentloaded")
+63 -1
View File
@@ -62,6 +62,24 @@ function getSelector(el) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const NEVER_RENDERED_TAGS = new Set(["script", "style", "noscript", "template", "svg", "iframe", "canvas"]); const NEVER_RENDERED_TAGS = new Set(["script", "style", "noscript", "template", "svg", "iframe", "canvas"]);
/** Max length before a data: URL is collapsed in dumps (keeps prompts usable). */
const DATA_URI_SOFT_MAX = 120;
/**
* Collapse long data: URIs so markdown / network dumps don't blow up (e.g. Outline
* inlining base64 images). Non-data URLs are returned unchanged.
* Keep in sync with truncateDataUri in core/prompt.js.
*/
function truncateDataUri(url, softMax) {
if (typeof url !== "string" || !url.startsWith("data:")) return url || "";
const limit = softMax || DATA_URI_SOFT_MAX;
if (url.length <= limit) return url;
const comma = url.indexOf(",");
const header = comma >= 0 ? url.slice(0, Math.min(comma, 64)) : url.slice(0, 64);
const payloadLen = comma >= 0 ? url.length - comma - 1 : url.length;
return header + ",…[" + payloadLen + " bytes truncated]";
}
function isHidden(el) { function isHidden(el) {
if (!el || el.nodeType !== 1) return false; if (!el || el.nodeType !== 1) return false;
if (el.hidden) return true; if (el.hidden) return true;
@@ -95,7 +113,7 @@ function extractMarkdown(node) {
const kids = walkInline(el); const kids = walkInline(el);
switch (tag) { switch (tag) {
case "a": { case "a": {
const href = el.getAttribute("href") || ""; const href = truncateDataUri(el.getAttribute("href") || "");
return "[" + kids + "](" + href + ")"; return "[" + kids + "](" + href + ")";
} }
case "strong": case "strong":
@@ -184,3 +202,47 @@ function extractMarkdown(node) {
md = md.replace(/\n{3,}/g, "\n\n"); md = md.replace(/\n{3,}/g, "\n\n");
return md.trim(); return md.trim();
} }
// ---------------------------------------------------------------------------
// Interesting controls inventory (agent / SPA debugging)
// ---------------------------------------------------------------------------
const CONTROLS_CAP = 80;
const CONTROL_SELECTOR =
'button, [role="button"], [role="menuitem"], [role="option"], [aria-label]';
/**
* List interactive / labeled controls under root with visibility flags.
* Helps agents find hidden Radix twins ("Document options") without dumping
* the whole DOM.
*/
function inventoryInterestingControls(root, cap) {
const base = root && root.nodeType === 1 ? root : document.body;
if (!base || !base.querySelectorAll) return [];
const limit = cap || CONTROLS_CAP;
const nodes = base.querySelectorAll(CONTROL_SELECTOR);
const seen = new Set();
const out = [];
for (let i = 0; i < nodes.length && out.length < limit; i++) {
const el = nodes[i];
if (seen.has(el)) continue;
seen.add(el);
const tag = el.tagName ? el.tagName.toLowerCase() : "";
const role = (el.getAttribute && el.getAttribute("role")) || "";
const ariaLabel = (el.getAttribute && el.getAttribute("aria-label")) || "";
let text = "";
try {
text = (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
} catch (_) {
text = "";
}
out.push({
tag: tag,
role: role,
ariaLabel: ariaLabel,
text: text,
visible: !isHidden(el),
selector: getSelector(el),
});
}
return out;
}
+52 -3
View File
@@ -16,8 +16,33 @@
// Pass a tighter selector (or a larger maxChars) instead of relying on this // Pass a tighter selector (or a larger maxChars) instead of relying on this
// cap for real content; it exists as a safety net, not a summarizer. // cap for real content; it exists as a safety net, not a summarizer.
const DEFAULT_MAX_MARKDOWN_CHARS = 20000; const DEFAULT_MAX_MARKDOWN_CHARS = 20000;
const DATA_URI_SOFT_MAX = 120;
function buildAIPrompt(meta, markdown, store, maxChars) { /**
* Collapse long data: URIs in network/prompt lines.
* Keep in sync with truncateDataUri in core/dom.js.
*/
function truncateDataUri(url, softMax) {
if (typeof url !== "string" || !url.startsWith("data:")) return url || "";
const limit = softMax || DATA_URI_SOFT_MAX;
if (url.length <= limit) return url;
const comma = url.indexOf(",");
const header = comma >= 0 ? url.slice(0, Math.min(comma, 64)) : url.slice(0, 64);
const payloadLen = comma >= 0 ? url.length - comma - 1 : url.length;
return header + ",…[" + payloadLen + " bytes truncated]";
}
function formatControlLine(c) {
const bits = [];
if (c.tag) bits.push(c.tag);
if (c.role) bits.push("role=" + c.role);
if (c.ariaLabel) bits.push('aria-label="' + c.ariaLabel + '"');
if (c.text) bits.push('"' + c.text + '"');
if (c.selector) bits.push("`" + c.selector + "`");
return (c.visible ? "[visible] " : "[hidden] ") + bits.join(" ");
}
function buildAIPrompt(meta, markdown, store, maxChars, controls) {
const limit = maxChars || DEFAULT_MAX_MARKDOWN_CHARS; const limit = maxChars || DEFAULT_MAX_MARKDOWN_CHARS;
const parts = []; const parts = [];
@@ -42,6 +67,28 @@ function buildAIPrompt(meta, markdown, store, maxChars) {
parts.push(""); parts.push("");
} }
const controlList = Array.isArray(controls) ? controls : [];
if (controlList.length) {
const visible = controlList.filter((c) => c && c.visible);
const hiddenCount = controlList.length - visible.length;
parts.push("## Interesting Controls");
parts.push("");
parts.push(
`_Showing ${visible.length} visible` +
(hiddenCount ? `, ${hiddenCount} hidden` : "") +
` (of ${controlList.length} inventoried)._`,
);
parts.push("");
visible.forEach((c) => parts.push("- " + formatControlLine(c)));
if (hiddenCount) {
parts.push("");
parts.push("### Hidden");
parts.push("");
controlList.filter((c) => c && !c.visible).forEach((c) => parts.push("- " + formatControlLine(c)));
}
parts.push("");
}
const errors = (store && store.errors) || []; const errors = (store && store.errors) || [];
if (errors.length) { if (errors.length) {
parts.push("## JavaScript Errors"); parts.push("## JavaScript Errors");
@@ -80,14 +127,16 @@ function buildAIPrompt(meta, markdown, store, maxChars) {
parts.push(""); parts.push("");
failed.forEach((n) => { failed.forEach((n) => {
const status = n.error ? "ERR" : n.status; const status = n.error ? "ERR" : n.status;
parts.push(`- ${n.method || "GET"} ${status} ${n.url}${n.duration ? " (" + n.duration + "ms)" : ""}${n.error ? " — " + n.error : ""}`); const url = truncateDataUri(n.url || "");
parts.push(`- ${n.method || "GET"} ${status} ${url}${n.duration ? " (" + n.duration + "ms)" : ""}${n.error ? " — " + n.error : ""}`);
}); });
parts.push(""); parts.push("");
} else if (network.length) { } else if (network.length) {
parts.push("## Recent Requests"); parts.push("## Recent Requests");
parts.push(""); parts.push("");
network.slice(-15).forEach((n) => { network.slice(-15).forEach((n) => {
parts.push(`- ${n.method || "GET"} ${n.status || ""} ${n.url}${n.duration ? " (" + n.duration + "ms)" : ""}`); const url = truncateDataUri(n.url || "");
parts.push(`- ${n.method || "GET"} ${n.status || "—"} ${url}${n.duration ? " (" + n.duration + "ms)" : ""}`);
}); });
parts.push(""); parts.push("");
} }
+1 -1
View File
@@ -412,7 +412,7 @@ function wire() {
out.push(""); out.push("");
state.store.network.forEach((n) => { state.store.network.forEach((n) => {
const status = n.error ? "ERR" : (n.status || "—"); const status = n.error ? "ERR" : (n.status || "—");
out.push(`- ${n.method || "GET"} ${status} ${n.url}${n.duration ? " (" + n.duration + "ms)" : ""}${n.error ? " — " + n.error : ""}`); out.push(`- ${n.method || "GET"} ${status} ${truncateDataUri(n.url || "")}${n.duration ? " (" + n.duration + "ms)" : ""}${n.error ? " — " + n.error : ""}`);
}); });
out.push(""); out.push("");
out.push("---"); out.push("---");