Files
my-pi/pi-chrome/extensions/chrome-profile-bridge/browser-extension/snapshot_injected.js
T

678 lines
32 KiB
JavaScript

// Static MAIN-world snapshot implementation injected by the MV3 service worker.
// Keep this file free of eval/new Function so `chrome_snapshot` works on strict-CSP pages.
(() => {
function getPiChromeState() {
const state = window.__PI_CHROME_STATE__ || {
nextElementUid: 1,
elements: {},
console: [],
network: [],
nextRequestId: 1,
instrumentationInstalled: false,
lastSnapshotDigest: null,
};
window.__PI_CHROME_STATE__ = state;
return state;
}
function rememberElement(element) {
const state = getPiChromeState();
if (!element.__piChromeUid) element.__piChromeUid = "el-" + state.nextElementUid++;
state.elements[element.__piChromeUid] = element;
return element.__piChromeUid;
}
function isElementVisible(element) {
if (!element || !element.getBoundingClientRect) return false;
const style = getComputedStyle(element);
if (style.visibility === "hidden" || style.display === "none") return false;
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return false;
if (rect.bottom < 0 || rect.right < 0) return false;
if (rect.top > innerHeight || rect.left > innerWidth) return false;
return true;
}
function occluderAt(x, y, expected) {
const top = document.elementFromPoint(x, y);
if (!top || top === expected) return null;
if (expected && expected.contains(top)) return null;
if (top.contains(expected)) return null;
return {
tag: top.tagName.toLowerCase(),
id: top.id || undefined,
className: typeof top.className === "string" ? top.className : undefined,
};
}
function textOf(element, max) {
return (element?.innerText || element?.textContent || "").replace(/\s+/g, " ").trim().slice(0, max || 500);
}
function accessibleLabel(element) {
if (!element) return "";
const labelledBy = element.getAttribute("aria-labelledby");
if (labelledBy) {
const text = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.innerText || "").join(" ").trim();
if (text) return text;
}
const id = element.id;
if (id) {
try {
const label = document.querySelector(`label[for="${cssEscape(id)}"]`);
if (label?.innerText) return label.innerText;
} catch {}
}
const wrappingLabel = element.closest?.("label");
return (
element.getAttribute("aria-label") ||
element.getAttribute("title") ||
element.getAttribute("placeholder") ||
wrappingLabel?.innerText ||
element.innerText ||
element.textContent ||
""
).trim().replace(/\s+/g, " ").slice(0, 180);
}
function cssEscape(value) {
return (window.CSS && CSS.escape) ? CSS.escape(value) : String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
}
function roleOf(element) {
const explicit = element.getAttribute("role");
if (explicit) return explicit.toLowerCase();
const tag = element.tagName.toLowerCase();
const type = (element.getAttribute("type") || "").toLowerCase();
if (tag === "a" && element.href) return "link";
if (tag === "button" || type === "button" || type === "submit" || type === "reset") return "button";
if (tag === "textarea") return "textbox";
if (tag === "select") return "combobox";
if (tag === "input") {
if (["checkbox", "radio", "range", "search", "email", "password", "tel", "url", "number"].includes(type)) return type === "checkbox" || type === "radio" || type === "range" ? type : "textbox";
return "textbox";
}
if (element.isContentEditable) return "textbox";
if (tag.match(/^h[1-6]$/)) return "heading";
return tag;
}
function isSensitiveField(element) {
if (!element) return false;
const tag = element.tagName?.toLowerCase?.() || "";
if (!/^(input|textarea|select)$/.test(tag) && !element.isContentEditable) return false;
const type = (element.getAttribute("type") || "").toLowerCase();
if (["password"].includes(type)) return true;
const haystack = [
type,
element.getAttribute("name"),
element.id,
element.getAttribute("autocomplete"),
element.getAttribute("aria-label"),
element.getAttribute("placeholder"),
element.getAttribute("data-testid"),
].filter(Boolean).join(" ").toLowerCase();
return /password|passwd|\bpwd\b|secret|token|bearer|api[-_ ]?key|access[-_ ]?key|auth[-_ ]?code|one[-_ ]?time|otp|2fa|mfa|verification[-_ ]?code|recovery[-_ ]?code|credit[-_ ]?card|card[-_ ]?number|cc-number|cc-csc|cvc|cvv|security[-_ ]?code|ssn|social[-_ ]?security/.test(haystack);
}
function installPiChromeInstrumentation() {
const state = getPiChromeState();
if (state.instrumentationInstalled) return;
state.instrumentationInstalled = true;
const pushConsole = (level, args) => {
state.console.push({
id: state.console.length + 1,
level,
timestamp: Date.now(),
url: location.href,
args: Array.from(args).map((arg) => {
try {
if (typeof arg === "string") return arg;
if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack };
return JSON.parse(JSON.stringify(arg));
} catch {
return String(arg);
}
}),
});
if (state.console.length > 500) state.console.splice(0, state.console.length - 500);
};
for (const level of ["debug", "log", "info", "warn", "error"]) {
const original = console[level];
if (typeof original !== "function" || original.__piChromeWrapped) continue;
const wrapped = function(...args) {
pushConsole(level, args);
return original.apply(this, args);
};
wrapped.__piChromeWrapped = true;
console[level] = wrapped;
}
window.addEventListener("error", (event) => pushConsole("pageerror", [event.message, event.filename + ":" + event.lineno + ":" + event.colno]));
window.addEventListener("unhandledrejection", (event) => pushConsole("unhandledrejection", [event.reason]));
const record = (entry) => {
state.network.push(entry);
if (state.network.length > 1000) state.network.splice(0, state.network.length - 1000);
return entry;
};
if (window.fetch && !window.fetch.__piChromeWrapped) {
const originalFetch = window.fetch.bind(window);
const wrappedFetch = async (...args) => {
const id = "req-" + state.nextRequestId++;
const startedAt = Date.now();
const input = args[0];
const init = args[1] || {};
const url = typeof input === "string" ? input : input?.url;
const method = (init.method || input?.method || "GET").toUpperCase();
const entry = record({ id, type: "fetch", method, url: String(url || ""), startedAt, pageUrl: location.href, status: "pending" });
try {
const response = await originalFetch(...args);
entry.status = response.status;
entry.statusText = response.statusText;
entry.ok = response.ok;
entry.responseUrl = response.url;
entry.durationMs = Date.now() - startedAt;
entry.responseHeaders = Array.from(response.headers.entries());
entry.responseBodyOmitted = "response body capture is disabled by default";
return response;
} catch (error) {
entry.error = error?.message || String(error);
entry.durationMs = Date.now() - startedAt;
throw error;
}
};
wrappedFetch.__piChromeWrapped = true;
window.fetch = wrappedFetch;
}
if (window.XMLHttpRequest && !XMLHttpRequest.prototype.open.__piChromeWrapped) {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
this.__piChromeRequest = { method: String(method || "GET").toUpperCase(), url: String(url || "") };
return originalOpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.open.__piChromeWrapped = true;
XMLHttpRequest.prototype.send = function(body) {
const id = "req-" + state.nextRequestId++;
const startedAt = Date.now();
const info = this.__piChromeRequest || {};
const entry = record({ id, type: "xhr", method: info.method || "GET", url: info.url || "", startedAt, pageUrl: location.href, status: "pending" });
this.addEventListener("loadend", () => {
entry.status = this.status;
entry.statusText = this.statusText;
entry.responseUrl = this.responseURL;
entry.durationMs = Date.now() - startedAt;
try { entry.responseHeadersText = this.getAllResponseHeaders(); } catch {}
entry.responseBodyOmitted = "response body capture is disabled by default";
});
this.addEventListener("error", () => { entry.error = "XMLHttpRequest error"; entry.durationMs = Date.now() - startedAt; });
return originalSend.call(this, body);
};
}
}
function hashString(text) {
let h = 0;
for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0;
return h;
}
function selectorFor(element) {
const unique = (selector) => {
try { return document.querySelectorAll(selector).length === 1; } catch { return false; }
};
if (element.id && unique("#" + cssEscape(element.id))) return "#" + cssEscape(element.id);
const attr = ["aria-label", "name", "placeholder", "data-testid", "role"].find((name) => element.getAttribute(name));
if (attr) {
const candidate = element.tagName.toLowerCase() + "[" + attr + "=" + JSON.stringify(element.getAttribute(attr)) + "]";
if (unique(candidate)) return candidate;
}
const parts = [];
let current = element;
while (current && current.nodeType === Node.ELEMENT_NODE && parts.length < 5) {
let part = current.tagName.toLowerCase();
if (current.classList.length > 0) part += "." + Array.from(current.classList).slice(0, 2).map(cssEscape).join(".");
const siblings = Array.from(current.parentElement?.children ?? []).filter((sibling) => sibling.tagName === current.tagName);
if (siblings.length > 1) part += ":nth-of-type(" + (siblings.indexOf(current) + 1) + ")";
parts.unshift(part);
const candidate = parts.join(" > ");
if (unique(candidate)) return candidate;
current = current.parentElement;
}
return parts.join(" > ");
}
function directHeadingText(element) {
const labelledBy = element.getAttribute?.("aria-labelledby");
if (labelledBy) {
const text = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.innerText || "").join(" ").replace(/\s+/g, " ").trim();
if (text) return text.slice(0, 180);
}
const aria = element.getAttribute?.("aria-label");
if (aria) return aria.trim().slice(0, 180);
const heading = Array.from(element.querySelectorAll?.("h1,h2,h3,h4,[role='heading']") || []).find(isElementVisible);
if (heading) return textOf(heading, 180);
return "";
}
function meaningfulContainerFor(element) {
let current = element.parentElement;
let fallback = current;
let depth = 0;
while (current && current !== document.body && depth++ < 8) {
if (!isElementVisible(current)) { current = current.parentElement; continue; }
const tag = current.tagName.toLowerCase();
const role = (current.getAttribute("role") || "").toLowerCase();
const cls = typeof current.className === "string" ? current.className : "";
const id = current.id || "";
const named = Boolean(current.getAttribute("aria-label") || current.getAttribute("aria-labelledby") || directHeadingText(current));
const semantic = /^(form|dialog|section|article|nav|header|main|aside|footer|li|tr|td|fieldset)$/.test(tag) ||
/^(dialog|alertdialog|region|group|listitem|row|cell|tabpanel|menu|toolbar|navigation|main|banner|contentinfo|complementary)$/.test(role);
const classHint = /card|panel|pane|modal|dialog|section|content|container|toolbar|menu|list|item|row|cell|header|footer|sidebar|drawer|popover|dropdown/i.test(`${id} ${cls}`);
const rect = current.getBoundingClientRect();
const childActions = current.querySelectorAll?.('a, button, input, textarea, select, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])').length || 0;
if ((semantic || classHint || named) && rect.width > 20 && rect.height > 20 && childActions <= 80) return current;
if (!fallback && rect.width > 20 && rect.height > 20) fallback = current;
current = current.parentElement;
}
return fallback || document.body;
}
function contextForElement(element) {
const container = meaningfulContainerFor(element);
if (!container || container === document.body || container === element) return undefined;
return {
uid: rememberElement(container),
tag: container.tagName.toLowerCase(),
role: roleOf(container),
label: directHeadingText(container) || accessibleLabel(container) || textOf(container, 140),
rect: rectSummary(container),
};
}
function summarizeElement(element, index) {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const occluded = occluderAt(cx, cy, element);
const role = roleOf(element);
const disabled = Boolean(element.disabled || element.getAttribute("aria-disabled") === "true");
const rawValue = "value" in element && typeof element.value === "string" ? element.value : undefined;
const sensitive = isSensitiveField(element);
const value = rawValue && !sensitive ? rawValue.slice(0, 120) : undefined;
const checked = "checked" in element ? Boolean(element.checked) : undefined;
return {
index,
uid: rememberElement(element),
tag: element.tagName.toLowerCase(),
role,
selector: selectorFor(element),
label: accessibleLabel(element),
href: element.href || undefined,
type: element.getAttribute("type") || undefined,
value: value || undefined,
hasValue: rawValue ? rawValue.length > 0 : undefined,
valueLength: rawValue && sensitive ? rawValue.length : undefined,
valueRedacted: sensitive && rawValue ? true : undefined,
checked,
disabled,
inert: Boolean(element.closest?.("[inert]")),
pointerEvents: style.pointerEvents,
occluded: occluded || undefined,
context: contextForElement(element),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
}
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= innerHeight && rect.left <= innerWidth;
}
function formSummaries() {
const fields = Array.from(document.querySelectorAll('input, textarea, select, [contenteditable="true"]'))
.filter(isElementVisible)
.slice(0, 80)
.map((element, index) => ({
...summarizeElement(element, index),
required: Boolean(element.required || element.getAttribute("aria-required") === "true"),
invalid: Boolean(element.matches?.(":invalid") || element.getAttribute("aria-invalid") === "true"),
autocomplete: element.getAttribute("autocomplete") || undefined,
}));
const submits = Array.from(document.querySelectorAll('button, input[type="submit"], [role="button"]'))
.filter(isElementVisible)
.filter((element) => /submit|save|continue|next|send|sign in|log in|create|update|done/i.test(accessibleLabel(element) + " " + (element.getAttribute("type") || "")))
.slice(0, 30)
.map((element, index) => summarizeElement(element, index));
return { fields, submits };
}
function pageMap() {
const landmarkSelectors = [
["header", 'header, [role="banner"]'],
["nav", 'nav, [role="navigation"]'],
["main", 'main, [role="main"]'],
["aside", 'aside, [role="complementary"]'],
["footer", 'footer, [role="contentinfo"]'],
["dialog", 'dialog, [role="dialog"], [aria-modal="true"]'],
["form", "form"],
];
const regions = [];
for (const [kind, selector] of landmarkSelectors) {
for (const element of Array.from(document.querySelectorAll(selector)).filter(isElementVisible).slice(0, 12)) {
const headings = Array.from(element.querySelectorAll("h1,h2,h3,[role='heading']")).filter(isElementVisible).slice(0, 6).map((h) => textOf(h, 120));
const actions = Array.from(element.querySelectorAll('a, button, input, textarea, select, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])')).filter(isElementVisible).slice(0, 8).map((a) => {
const summary = summarizeElement(a, 0);
return { uid: summary.uid, role: summary.role, label: summary.label || summary.selector, disabled: summary.disabled || undefined };
});
regions.push({ kind, uid: rememberElement(element), label: accessibleLabel(element) || headings[0] || textOf(element, 100), headings, actions });
}
}
const headings = Array.from(document.querySelectorAll("h1,h2,h3,[role='heading']")).filter(isElementVisible).slice(0, 30).map((element) => ({
uid: rememberElement(element),
level: Number(element.tagName?.slice(1)) || Number(element.getAttribute("aria-level")) || undefined,
text: textOf(element, 180),
}));
return { regions, headings };
}
function layoutSections(elements, forms) {
const byUid = new Map();
const addToSection = (summary, kind) => {
const source = getPiChromeState().elements[summary.uid];
const container = source ? meaningfulContainerFor(source) : null;
if (!container || container === document.body) return;
const uid = rememberElement(container);
let section = byUid.get(uid);
if (!section) {
const rect = rectSummary(container);
section = {
uid,
tag: container.tagName.toLowerCase(),
role: roleOf(container),
label: directHeadingText(container) || accessibleLabel(container) || textOf(container, 160),
text: textOf(container, 260),
rect,
actions: [],
fields: [],
};
byUid.set(uid, section);
}
const item = { uid: summary.uid, role: summary.role, label: summary.label || summary.selector, disabled: summary.disabled || undefined };
if (kind === "field") section.fields.push(item);
else section.actions.push(item);
};
for (const el of (elements || []).slice(0, 80)) addToSection(el, ["textbox", "checkbox", "radio", "combobox"].includes(el.role) ? "field" : "action");
for (const field of (forms?.fields || []).slice(0, 80)) addToSection(field, "field");
const sections = Array.from(byUid.values())
.filter((section) => section.actions.length || section.fields.length)
.sort((a, b) => a.rect.y - b.rect.y || a.rect.x - b.rect.x)
.slice(0, 18);
for (const section of sections) {
section.actions = section.actions.slice(0, 10);
section.fields = section.fields.slice(0, 10);
}
return sections;
}
function tokenScore(haystack, query) {
if (!query) return 0;
const hay = String(haystack || "").toLowerCase();
const tokens = String(query).toLowerCase().split(/\W+/).filter(Boolean);
if (!tokens.length) return 0;
let score = 0;
for (const token of tokens) {
if (hay.includes(token)) score += token.length <= 2 ? 1 : 3;
}
if (hay.includes(String(query).toLowerCase())) score += 8;
return score;
}
function queryMatches(query, elements, map) {
if (!query) return [];
const candidates = [];
for (const element of elements) {
const hay = [element.role, element.label, element.selector, element.type, element.href].filter(Boolean).join(" ");
const score = tokenScore(hay, query);
if (score > 0) candidates.push({ score, kind: "element", ...element });
}
const textNodes = [];
for (const block of Array.from(document.querySelectorAll("h1,h2,h3,h4,p,li,td,th,label,summary,[role='alert']")).filter(isElementVisible).slice(0, 300)) {
const text = textOf(block, 300);
const score = tokenScore(text, query);
if (score > 0) textNodes.push({ score, kind: "text", uid: rememberElement(block), tag: block.tagName.toLowerCase(), role: roleOf(block), text, rect: rectSummary(block) });
}
for (const region of map.regions || []) {
const score = tokenScore([region.kind, region.label, ...(region.headings || [])].join(" "), query);
if (score > 0) candidates.push({ score, kind: "region", ...region });
}
return candidates.concat(textNodes).sort((a, b) => b.score - a.score).slice(0, 20);
}
function rectSummary(element) {
const rect = element.getBoundingClientRect();
return { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) };
}
function activeElementSummary() {
const el = document.activeElement;
if (!el || el === document.body || el === document.documentElement) return null;
return summarizeElement(el, 0);
}
function modalSummary() {
const selectors = 'dialog[open], [role="dialog"], [aria-modal="true"], [role="alertdialog"]';
const modal = Array.from(document.querySelectorAll(selectors)).find(isElementVisible);
if (!modal) return null;
return { uid: rememberElement(modal), tag: modal.tagName.toLowerCase(), role: roleOf(modal), label: accessibleLabel(modal) || textOf(modal, 180), rect: rectSummary(modal) };
}
function digestFor(snapshot) {
return {
url: snapshot.url,
title: snapshot.title,
textHash: hashString(snapshot.text || ""),
focusedUid: snapshot.focused?.uid || null,
modalUid: snapshot.modal?.uid || null,
labels: (snapshot.elements || []).slice(0, 50).map((el) => ({ uid: el.uid, role: el.role, label: el.label, disabled: el.disabled, value: el.value, checked: el.checked })),
};
}
function diffSnapshot(previous, current) {
if (!previous) return { firstSnapshot: true };
const changes = [];
if (previous.url !== current.url) changes.push({ kind: "url", before: previous.url, after: current.url });
if (previous.title !== current.title) changes.push({ kind: "title", before: previous.title, after: current.title });
if (previous.textHash !== current.textHash) changes.push({ kind: "textChanged" });
if (previous.focusedUid !== current.focusedUid) changes.push({ kind: "focus", before: previous.focusedUid, after: current.focusedUid });
if (previous.modalUid !== current.modalUid) changes.push({ kind: "modal", before: previous.modalUid, after: current.modalUid });
const prevByUid = new Map((previous.labels || []).map((x) => [x.uid, x]));
const curByUid = new Map((current.labels || []).map((x) => [x.uid, x]));
const added = [];
const removed = [];
const updated = [];
for (const cur of current.labels || []) {
const prev = prevByUid.get(cur.uid);
if (!prev) added.push(cur);
else if (prev.label !== cur.label || prev.disabled !== cur.disabled || prev.value !== cur.value || prev.checked !== cur.checked) updated.push({ uid: cur.uid, before: prev, after: cur });
}
for (const prev of previous.labels || []) {
if (!curByUid.has(prev.uid)) removed.push(prev);
}
return { changes, added: added.slice(0, 12), removed: removed.slice(0, 12), updated: updated.slice(0, 12) };
}
function visibleTextSnippets(maxChars) {
const snippets = [];
const blocks = Array.from(document.querySelectorAll("h1,h2,h3,h4,p,li,td,th,label,summary,[role='alert']")).filter(isElementVisible);
let used = 0;
for (const block of blocks) {
if (!isInViewport(block) && snippets.length > 12) continue;
const text = textOf(block, 500);
if (!text || snippets.some((s) => s.text === text)) continue;
const next = { uid: rememberElement(block), tag: block.tagName.toLowerCase(), text, rect: rectSummary(block) };
snippets.push(next);
used += text.length;
if (used >= maxChars || snippets.length >= 40) break;
}
return snippets;
}
function snapshotPage(maxElements, containingText, roleFilter, nearUid, mode, query, maxTextChars) {
installPiChromeInstrumentation();
mode = ["auto", "interactive", "forms", "pageMap", "text", "changes", "full"].includes(mode) ? mode : "auto";
const fullTextLimit = Number(maxTextChars || (mode === "full" ? 30000 : mode === "text" ? 18000 : 6000));
let candidates = Array.from(document.querySelectorAll('a, button, input, textarea, select, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'));
if (containingText) {
const needle = String(containingText).toLowerCase();
candidates = candidates.filter((element) => accessibleLabel(element).toLowerCase().includes(needle));
}
if (roleFilter) {
const wanted = String(roleFilter).toLowerCase();
candidates = candidates.filter((element) => roleOf(element) === wanted || element.tagName.toLowerCase() === wanted);
}
let near;
if (nearUid) near = getPiChromeState().elements[nearUid];
if (near) {
const nearRect = near.getBoundingClientRect();
const cx = nearRect.left + nearRect.width / 2;
const cy = nearRect.top + nearRect.height / 2;
candidates.sort((a, b) => {
const ra = a.getBoundingClientRect();
const rb = b.getBoundingClientRect();
const da = Math.hypot(ra.left + ra.width / 2 - cx, ra.top + ra.height / 2 - cy);
const db = Math.hypot(rb.left + rb.width / 2 - cx, rb.top + rb.height / 2 - cy);
return da - db;
});
} else {
candidates.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
const avis = isInViewport(a) ? 0 : 1;
const bvis = isInViewport(b) ? 0 : 1;
return avis - bvis || ar.top - br.top || ar.left - br.left;
});
}
const visibleCandidates = candidates.filter(isElementVisible);
const elements = visibleCandidates.slice(0, maxElements).map((element, index) => summarizeElement(element, index));
const queryElements = query
? visibleCandidates.slice(0, Math.max(maxElements, 500)).map((element, index) => summarizeElement(element, index))
: elements;
const map = pageMap();
const forms = formSummaries();
const layout = layoutSections(elements, forms);
const focused = activeElementSummary();
const modal = modalSummary();
const bodyText = document.body ? document.body.innerText.replace(/\s+\n/g, "\n").trim() : "";
const text = bodyText.slice(0, fullTextLimit);
const snapshot = {
title: document.title,
url: location.href,
mode,
query: query || undefined,
viewport: { width: innerWidth, height: innerHeight, scrollX, scrollY },
summary: {
visibleText: textOf(document.body, 500),
visibleInteractiveCount: elements.filter((el) => el.rect.y >= 0 && el.rect.y <= innerHeight).length,
totalInteractiveSampled: elements.length,
totalInteractiveVisible: visibleCandidates.length,
focused: focused ? { uid: focused.uid, role: focused.role, label: focused.label } : undefined,
modal: modal ? { uid: modal.uid, label: modal.label } : undefined,
hints: [],
},
focused: focused || undefined,
modal: modal || undefined,
text,
textTruncated: bodyText.length > text.length,
textSnippets: visibleTextSnippets(mode === "text" ? 12000 : 3000),
elements,
forms,
layout,
pageMap: map,
matches: queryMatches(query, queryElements, map),
filter: { containingText: containingText || undefined, roleFilter: roleFilter || undefined, nearUid: nearUid || undefined },
};
if (snapshot.modal) snapshot.summary.hints.push("A modal/dialog is visible; interact with it before the underlying page.");
const disabledImportant = elements.find((el) => el.disabled && /submit|save|merge|continue|next|send|approve|login|sign in/i.test(el.label || ""));
if (disabledImportant) snapshot.summary.hints.push(`${disabledImportant.uid} '${disabledImportant.label}' is disabled.`);
const occluded = elements.find((el) => el.occluded);
if (occluded) snapshot.summary.hints.push(`${occluded.uid} '${occluded.label || occluded.role}' appears occluded by ${occluded.occluded.tag}.`);
const state = getPiChromeState();
const currentDigest = digestFor(snapshot);
snapshot.diff = diffSnapshot(state.lastSnapshotDigest, currentDigest);
state.lastSnapshotDigest = currentDigest;
if (mode === "interactive") {
delete snapshot.text;
delete snapshot.textSnippets;
delete snapshot.pageMap;
} else if (mode === "forms") {
delete snapshot.text;
delete snapshot.textSnippets;
snapshot.elements = elements.filter((el) => ["textbox", "checkbox", "radio", "combobox", "button"].includes(el.role));
} else if (mode === "pageMap") {
delete snapshot.text;
delete snapshot.textSnippets;
snapshot.elements = elements.slice(0, 20);
} else if (mode === "changes") {
delete snapshot.text;
delete snapshot.textSnippets;
delete snapshot.elements;
delete snapshot.forms;
delete snapshot.layout;
delete snapshot.pageMap;
} else if (mode === "text") {
snapshot.elements = elements.slice(0, 20);
} else if (mode !== "full") {
snapshot.elements = elements.slice(0, Math.min(maxElements, 40));
snapshot.text = text.slice(0, Math.min(text.length, 6000));
}
return snapshot;
}
function inspectTarget(uid, selector, shouldScrollIntoView) {
installPiChromeInstrumentation();
const state = getPiChromeState();
let element = null;
if (uid) element = state.elements[uid];
if (!element && selector) element = document.querySelector(selector);
if (!element || !element.isConnected) throw new Error(uid ? `No live element for uid: ${uid}. Take a fresh chrome_snapshot.` : `No element matches selector: ${selector}`);
if (shouldScrollIntoView) element.scrollIntoView?.({ block: "center", inline: "center", behavior: "instant" });
const summary = summarizeElement(element, 0);
const ancestors = [];
let current = element.parentElement;
while (current && current !== document.body && ancestors.length < 6) {
ancestors.push({ uid: rememberElement(current), tag: current.tagName.toLowerCase(), role: roleOf(current), label: accessibleLabel(current) || textOf(current, 100), selector: selectorFor(current) });
current = current.parentElement;
}
const container = element.closest?.('form, dialog, [role="dialog"], [aria-modal="true"], section, article, main, aside') || element.parentElement || document.body;
const nearbyText = Array.from(container.querySelectorAll("h1,h2,h3,h4,p,li,label,[role='alert']"))
.filter(isElementVisible)
.slice(0, 24)
.map((node) => ({ uid: rememberElement(node), tag: node.tagName.toLowerCase(), text: textOf(node, 240), rect: rectSummary(node) }))
.filter((entry) => entry.text);
const nearbyActions = Array.from(container.querySelectorAll('a, button, input, textarea, select, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])'))
.filter(isElementVisible)
.slice(0, 30)
.map((node, index) => summarizeElement(node, index));
const form = element.closest?.("form");
const formContext = form ? {
uid: rememberElement(form),
label: accessibleLabel(form) || textOf(form, 160),
fields: Array.from(form.querySelectorAll('input, textarea, select, [contenteditable="true"]')).filter(isElementVisible).slice(0, 30).map((node, index) => summarizeElement(node, index)),
actions: Array.from(form.querySelectorAll('button, input[type="submit"], [role="button"]')).filter(isElementVisible).slice(0, 12).map((node, index) => summarizeElement(node, index)),
} : undefined;
const rect = element.getBoundingClientRect();
const center = { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2) };
const clickSuggestion = summary.disabled || summary.inert || summary.pointerEvents === "none"
? undefined
: { uid: summary.uid, x: center.x, y: center.y };
return { target: summary, ancestors, nearbyText, nearbyActions, formContext, clickSuggestion };
}
globalThis.__piChromeSnapshotPage = snapshotPage;
globalThis.__piChromeInspectTarget = inspectTarget;
})();