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
+63 -1
View File
@@ -62,6 +62,24 @@ function getSelector(el) {
// ---------------------------------------------------------------------------
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) {
if (!el || el.nodeType !== 1) return false;
if (el.hidden) return true;
@@ -95,7 +113,7 @@ function extractMarkdown(node) {
const kids = walkInline(el);
switch (tag) {
case "a": {
const href = el.getAttribute("href") || "";
const href = truncateDataUri(el.getAttribute("href") || "");
return "[" + kids + "](" + href + ")";
}
case "strong":
@@ -184,3 +202,47 @@ function extractMarkdown(node) {
md = md.replace(/\n{3,}/g, "\n\n");
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
// cap for real content; it exists as a safety net, not a summarizer.
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 parts = [];
@@ -42,6 +67,28 @@ function buildAIPrompt(meta, markdown, store, maxChars) {
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) || [];
if (errors.length) {
parts.push("## JavaScript Errors");
@@ -80,14 +127,16 @@ function buildAIPrompt(meta, markdown, store, maxChars) {
parts.push("");
failed.forEach((n) => {
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("");
} else if (network.length) {
parts.push("## Recent Requests");
parts.push("");
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("");
}
+1 -1
View File
@@ -412,7 +412,7 @@ function wire() {
out.push("");
state.store.network.forEach((n) => {
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("---");