Add controls inventory and truncate long data: URIs in dumps.
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:
+63
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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("---");
|
||||
|
||||
Reference in New Issue
Block a user