(() => {
const data = JSON.parse(document.getElementById('debug-data').textContent);
const {groupNodes, imageSource, matchesRole, visibleRect} = DebugViewerFilter;
const byId = id => document.getElementById(id);
const ns = 'http://www.w3.org/2000/svg';
const roles = new Set();
let phase = data.before ? 'before' : 'after';
let selected = data.mode === 'click' ? 0 : null;
const explanations = {
skeleton: 'Skeleton is a shallow overview (logical depth at most 3), not just the parent window. Amber dashed boxes are drill anchors; blue boxes have action refs; gray boxes provide context. children_count marks children not expanded here.',
'drill-down': 'This tree starts at the requested root ref, inside its owning window. Only nodes returned by this drill-down are shown.',
snapshot: 'These are the nodes returned by this snapshot, with its filters applied. Blue boxes have action refs; gray boxes provide context.',
click: 'Before shows the strictly resolved target before command dispatch. After is a fresh screenshot of the same window. No post-click target box is drawn because the UI may have changed. This is not a live click recording.'
};
byId('explanation').textContent = explanations[data.mode];
byId('notice').textContent = data.notice;
byId('warning').textContent = [data.warning, data.error?.message].filter(Boolean).join('\n');
byId('result').textContent = JSON.stringify(data.result || data.error || {snapshot_id: data.snapshot_id, ok: data.ok}, null, 2);
byId('before').hidden = data.mode !== 'click';
byId('after').hidden = data.mode !== 'click';
byId('before').disabled = !data.before;
byId('after').disabled = !data.after;
byId('all').onclick = () => {
roles.clear();
selected = null;
byId('highlights').checked = true;
renderGroups();
draw();
};
byId('highlights').onchange = draw;
for (const name of ['before', 'after']) {
byId(name).onclick = () => {
phase = name;
selected = data.mode === 'click' && name === 'before' ? 0 : null;
render();
};
}
function select(index) {
selected = selected === index ? null : index;
byId('nodes').querySelectorAll('.node').forEach(button => {
button.setAttribute('aria-pressed', String(Number(button.dataset.index) === selected));
});
draw();
}
function draw() {
const frame = data[phase];
const svg = byId('overlay');
svg.replaceChildren();
if (!frame || !byId('highlights').checked) return;
const window = frame.window.bounds;
svg.setAttribute('viewBox', `0 0 ${window.width} ${window.height}`);
frame.nodes.forEach((node, index) => {
if (node.not_visible || !matchesRole(node, roles) || (selected !== null && selected !== index)) return;
const rect = visibleRect(node.bounds, window);
if (!rect) return;
const group = document.createElementNS(ns, 'g');
group.setAttribute('class', node.kind);
group.dataset.role = node.role;
const box = document.createElementNS(ns, 'rect');
for (const [name, value] of Object.entries(rect)) box.setAttribute(name, value);
const label = document.createElementNS(ns, 'text');
const alignRight = selected === index && rect.x > window.width / 2;
label.setAttribute('text-anchor', alignRight ? 'end' : 'start');
label.setAttribute('x', alignRight ? Math.min(window.width - 4, rect.x + rect.width) : Math.max(4, Math.min(rect.x + 4, window.width - 32)));
label.setAttribute('y', Math.max(15, rect.y + 15));
label.textContent = selected === index ? `${index + 1} ${node.ref_id || node.role}` : String(index + 1);
group.append(box, label);
svg.append(group);
});
}
function nodeButton(node, index, frame) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'node';
button.dataset.index = index;
button.disabled = !matchesRole(node, roles);
button.setAttribute('aria-pressed', String(selected === index));
const title = document.createElement('strong');
title.textContent = `${index + 1}. ${node.name || node.role}`;
const ref = document.createElement('code');
ref.textContent = node.ref_id || 'No ref · context only';
const reason = document.createElement('small');
reason.textContent = node.reason;
if (node.children_count !== undefined) reason.textContent += ` ${node.children_count} unexpanded direct children.`;
else if (node.subtree_truncated) reason.textContent += ' Descendants truncated; count unavailable.';
if (node.not_visible) reason.textContent += ' Reported hidden or offscreen; highlight omitted.';
else if (!visibleRect(node.bounds, frame.window.bounds)) reason.textContent += ' No drawable bounds inside this window.';
button.append(title, ref, reason);
button.onclick = () => select(index);
return button;
}
function updateFilterCount() {
const nodes = data[phase]?.nodes || [];
const count = nodes.filter(node => matchesRole(node, roles)).length;
byId('filter-status').textContent = roles.size ? `${count} of ${nodes.length} nodes in selected roles` : `All ${nodes.length} nodes · no role filter`;
}
function renderGroups() {
const frame = data[phase];
byId('nodes').replaceChildren();
updateFilterCount();
if (!frame) return;
for (const group of groupNodes(frame.nodes)) {
const section = document.createElement('section');
section.className = 'role-group';
section.dataset.role = group.role;
const header = document.createElement('div');
header.className = 'role-header';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = roles.has(group.role);
checkbox.setAttribute('aria-label', `Show only ${group.label.toLowerCase()}`);
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'role-toggle';
toggle.textContent = `${group.label} (${group.items.length})`;
const list = document.createElement('div');
list.className = 'role-items';
list.id = `role-items-${group.role}`;
const expand = open => { list.hidden = !open; toggle.setAttribute('aria-expanded', String(open)); };
expand(data.mode === 'click');
toggle.setAttribute('aria-controls', list.id);
toggle.onclick = () => expand(list.hidden);
for (const {node, index} of group.items) list.append(nodeButton(node, index, frame));
checkbox.onchange = () => {
if (checkbox.checked) roles.add(group.role); else roles.delete(group.role);
selected = null;
byId('highlights').checked = true;
for (const other of byId('nodes').querySelectorAll('.role-group')) {
if (roles.size && !roles.has(other.dataset.role)) {
other.querySelector('.role-items').hidden = true;
other.querySelector('.role-toggle').setAttribute('aria-expanded', 'false');
}
}
byId('nodes').querySelectorAll('.node').forEach(button => {
button.setAttribute('aria-pressed', 'false');
button.disabled = !matchesRole(frame.nodes[Number(button.dataset.index)], roles);
});
updateFilterCount();
draw();
};
header.append(checkbox, toggle);
section.append(header, list);
byId('nodes').append(section);
}
}
function render() {
const frame = data[phase];
byId('stage').hidden = !frame;
for (const name of ['before', 'after']) byId(name).setAttribute('aria-pressed', String(name === phase));
byId('phase').textContent = frame ? (phase === 'before' ? 'Before dispatch: requested target' : data.mode === 'click' ? 'After command: observed window' : 'Returned tree over captured window') : 'Screenshot unavailable';
if (frame) {
byId('summary').textContent = `${data.mode} · ${frame.window.app} · ${frame.window.title || frame.window.id} · ${data.ok ? 'command succeeded' : 'command failed'}`;
byId('capture').src = imageSource(frame.image);
byId('capture').alt = `${frame.window.app} window, ${phase} command capture`;
byId('list-title').textContent = `${frame.nodes.length} ${data.mode === 'click' ? 'target elements' : 'returned nodes'}`;
}
renderGroups();
draw();
}
render();
})();