refactor(ui): renderVerdictBadge returns DocumentFragment, callers use appendChild

Rewrite the verdict-badge HTML builder from string-concat into DOM
construction (createElement + textContent + setAttribute + append).
The helper now returns a DocumentFragment of two top-level siblings
(.verdict-badge and .verdict-detail), which appendChild expands into
the parent — preserving the sibling-traversal invariants relied on by
Pane.updateVerdictBadge, toggleVerdictDetail, and the d-key keyboard
shortcut.

Inline onclick="toggleVerdictDetail(this)" replaced with an
addEventListener click handler; the non-arrow callback keeps the
`this`→button binding the old inline form had.

Both call sites (replayHistory + the live approval flow) swap from
el.insertAdjacentHTML("beforeend", X) to el.appendChild(X).

This is the last unsafe-write site in the DOM-cleanup arc started in
#532; commit 2 broadens the test_app_js.py lint regex to forbid the
insertAdjacent-HTML sink across all 8 tracked JS bundles.
This commit is contained in:
Patrick Buckley
2026-05-21 23:39:30 -07:00
parent dac541d304
commit c3ddcd0b24
+74 -58
View File
@@ -1437,10 +1437,7 @@ class Pane {
// judgePending=false because any verdict on replay is
// final — no spinner.
if (tc.verdict) {
div.insertAdjacentHTML(
"beforeend",
renderVerdictBadge(tc.verdict, false),
);
div.appendChild(renderVerdictBadge(tc.verdict, false));
}
block.appendChild(div);
// Output-guard finding — defer insertion until the tool
@@ -1652,10 +1649,7 @@ class Pane {
// key in case a stale SSE payload arrives mid-deploy.
const heuristic = item.heuristic_verdict || item.verdict;
if (heuristic) {
block.insertAdjacentHTML(
"beforeend",
renderVerdictBadge(heuristic, judgePending),
);
block.appendChild(renderVerdictBadge(heuristic, judgePending));
const rec = heuristic.recommendation || "review";
if (
!glowRec ||
@@ -4838,61 +4832,83 @@ function buildToolDiv(item) {
}
function renderVerdictBadge(verdict, judgePending) {
if (!verdict) return "";
if (!verdict) return document.createDocumentFragment();
const risk = verdict.risk_level || "medium";
const rec = verdict.recommendation || "review";
const conf = Math.round((verdict.confidence || 0) * 100);
const summary = verdict.intent_summary || "";
let spinnerHtml = "";
const badge = document.createElement("div");
badge.className = "verdict-badge verdict-" + risk + " ts-verdict-badge";
badge.setAttribute("data-risk", risk);
if (verdict.call_id) badge.setAttribute("data-call-id", verdict.call_id);
const riskSpan = document.createElement("span");
riskSpan.className = "verdict-risk";
riskSpan.textContent = risk.toUpperCase();
const recSpan = document.createElement("span");
recSpan.className = "verdict-rec";
recSpan.textContent = rec;
const confSpan = document.createElement("span");
confSpan.className = "verdict-conf";
confSpan.textContent = conf + "%";
badge.append(riskSpan, recSpan, confSpan);
if (judgePending) {
spinnerHtml =
'<span class="verdict-judge-spinner">' +
'<span class="judge-spinner-dot"></span> judge analyzing\u2026</span>';
const spinner = document.createElement("span");
spinner.className = "verdict-judge-spinner";
const dot = document.createElement("span");
dot.className = "judge-spinner-dot";
spinner.append(dot, " judge analyzing\u2026");
badge.appendChild(spinner);
}
const callId = escapeHtml(verdict.call_id || "");
return (
'<div class="verdict-badge verdict-' +
escapeHtml(risk) +
' ts-verdict-badge" data-risk="' +
escapeHtml(risk) +
'" data-call-id="' +
callId +
'">' +
'<span class="verdict-risk">' +
escapeHtml(risk.toUpperCase()) +
"</span>" +
'<span class="verdict-rec">' +
escapeHtml(rec) +
"</span>" +
'<span class="verdict-conf">' +
conf +
"%</span>" +
spinnerHtml +
'<button class="verdict-expand" onclick="toggleVerdictDetail(this)">details</button>' +
"</div>" +
'<div class="verdict-detail" style="display:none">' +
'<div class="verdict-summary">' +
escapeHtml(summary) +
"</div>" +
'<div class="verdict-reasoning">' +
escapeHtml(verdict.reasoning || "") +
"</div>" +
((verdict.evidence || []).length
? '<div class="verdict-evidence">' +
(verdict.evidence || [])
.map(function (e) {
return "<div>\u2022 " + escapeHtml(e) + "</div>";
})
.join("") +
"</div>"
: "") +
'<div class="verdict-tier">' +
escapeHtml(verdict.tier || "heuristic") +
" tier" +
(verdict.judge_model ? " | " + escapeHtml(verdict.judge_model) : "") +
"</div>" +
"</div>"
);
const expand = document.createElement("button");
expand.className = "verdict-expand";
expand.textContent = "details";
expand.addEventListener("click", function () {
toggleVerdictDetail(this);
});
badge.appendChild(expand);
const detail = document.createElement("div");
detail.className = "verdict-detail";
detail.style.display = "none";
const summaryEl = document.createElement("div");
summaryEl.className = "verdict-summary";
summaryEl.textContent = verdict.intent_summary || "";
const reasoningEl = document.createElement("div");
reasoningEl.className = "verdict-reasoning";
reasoningEl.textContent = verdict.reasoning || "";
detail.append(summaryEl, reasoningEl);
const evidence = verdict.evidence || [];
if (evidence.length) {
const evEl = document.createElement("div");
evEl.className = "verdict-evidence";
for (const ev of evidence) {
const row = document.createElement("div");
row.textContent = "\u2022 " + ev;
evEl.appendChild(row);
}
detail.appendChild(evEl);
}
const tierEl = document.createElement("div");
tierEl.className = "verdict-tier";
let tierText = (verdict.tier || "heuristic") + " tier";
if (verdict.judge_model) tierText += " | " + verdict.judge_model;
tierEl.textContent = tierText;
detail.appendChild(tierEl);
const frag = document.createDocumentFragment();
frag.append(badge, detail);
return frag;
}
function toggleVerdictDetail(btn) {