Files
brain-of-reese/frontend/assets/verbatim.js
T
ducoterra fb17d15fbe
Build and Push Containers / build-and-push-app (push) Successful in 1m56s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 192_verbatim_inline_midturn
Phase 192 — final verification pass complete.

- **Verified (all 4 tasks already complete):** additive `verbatim_block` SSE frame (`VerbatimBlockPiece` → `ChatVerbatimBlockEvent` → chat dispatch, after the round's `tool`/`tool_result`, before the next round); `hydrateVerbatimNow` matched-only mid-turn pass + app.js branch; `dropPlacedTwin` per-locator done/restore dedupe; the dedicated E2E suite.
- **Root cause of the rejected attempt:** my own concurrent background `pytest` run deadlocked the harness validation run on the shared Postgres (`psycopg.errors.DeadlockDetected`). No code defect — re-ran every gate **exclusively**.
- `uv run pytest -q --cov=app --cov-report=term-missing` → **exit 0**, `TOTAL 7181 16 99%` (>90%)
- `uv run pytest tests/e2e/test_verbatim_inline_midturn.py --no-cov` → **6 passed** (isolated)
- `uv run pytest tests/e2e/test_verbatim_show_text.py tests/e2e/test_chat_history.py --no-cov` → **18 passed** (`test_chat_history.py` byte-unedited; the phase-182 suite's "no eighth type" pin was tightened, not loosened, to seven + `verbatim_block` with frame-order asserts)
- `uv run ruff check .` → All checks passed (exit 0) · `uv run pyright` → **0 errors**
- **Completion criteria:** single frame per success + zero per refusal ✅ · `BOR_ANSWER_TEXT=0` wire byte-identical (integration spelled baseline + E2E App B) ✅ · fence hydrates in place / unfenced lands at the call moment, before `done` ✅ · append+fence → 1 panel, two ranges → 2 panels ✅ · restore 1 panel/locator, `done.answer_text` + record key + `verbatim_blocks=N` unchanged ✅ · suite green, coverage 99%, lint/types clean ✅ · no-show_text turn byte-identical ✅ · commit → left to the harness (no git index/history commands run).
- **Docs:** README reviewed line-by-line (2399 lines) + AGENTS.md in full; mechanically verified all 86 `BOR_*` settings, every `.env.example` var, table defaults, internal anchors (0 broken), ports 8123/8901, tree depth 1–8, pagination 25/1–100, 17 palette colors, `0029`/`0030` migrations, and every cited test file. Fixed 1 inconsistency: the verbatim "Parity" bullet claimed the live turn/reload/shared page share "the same three globals" — now names the settle-path trio and states that `hydrateVerbatimNow` is the ONE chat-only addition (a shared page has no stream).
- **Notable:** `run_agent`'s four repeated piece unions were collapsed into the ONE exported `AgentPiece` alias, so a future piece type is spelled in one place; `test_frontend_edit_message.py`'s `runTurn` byte-pin was regenerated, not weakened.
- **Next pending phase:** `193_sync_remaining_count`.
2026-10-19 12:19:51 -04:00

631 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Brain of Reese — the phase-182 VERBATIM panel client (task 07).
*
* A ` ```bor <source/path>#12-40 ` locator fence is a CLAIM about a file,
* never its contents: `markdown.js` (task 06) drops the model's body and
* paints a `.verbatim.verbatim-pending` placeholder keyed by the canonical
* locator, and the SERVER (`app.rag.verbatim.resolve_answer_blocks`,
* task 05) puts the real bytes on the `done` frame as `answer_text`. This
* file is the other half of that handshake: it RESOLVES the placeholders
* against the frame's blocks (`hydrateVerbatim`), settles the ONE block a
* mid-turn `verbatim_block` frame carries (`hydrateVerbatimNow`, phase 192),
* places the blocks the answer never located (`appendUnlocatedVerbatim`),
* settles leftovers when a turn ends without a `done`
* (`settleVerbatimPlaceholders`), gives every settled answer code block a
* Copy button (`enhanceAnswerCodeBlocks`) and owns the ONE clipboard path
* (`copyTextWithFallback`).
*
* WHY IT IS A CLASSIC SCRIPT, NOT A MODULE (the `markdown.js` pattern):
* `index.html` and `shared.html` load it as a plain `<script>` right after
* `markdown.js` and before their page modules, so the top-level function
* declarations below are globals both module pages can call — the ONE
* implementation shared by the live turn, the reload restore and the
* anonymous shared page (parity is the phase's hard requirement). The
* Containerfile minifies it WITHOUT `--bundle` for the same reason.
*
* NO LOCATOR GRAMMAR LIVES HERE (the phase's main drift risk): the canonical
* string is produced in exactly ONE client place,
* `markdown.js::borLocator` (the mirror of Python
* `app.rag.verbatim.canonical_locator`). Hydration reads the already
* normalized `data-bor-locator` attribute and compares it to the server's
* `locator` with a plain string `===`. Nothing here re-parses a range,
* canonicalizes an en dash, or normalizes a path.
*
* THE CONTRACT WITH THE SERVER (LOCKED A2/A4/A7):
* • `block.text` is painted with `textContent` and copied with
* `navigator.clipboard` VERBATIM — no trim, no re-wrap, no join, no
* model byte ever substituted in, and the line numbers never enter the
* clipboard (they live in the `user-select: none` gutter, a separate
* node the copy path never reads);
* • the caption's numbers are the SERVER's resolved extent, so a locator
* whose range the model got wrong shows what the server actually read
* (A2 made visible);
* • a locator the server could not resolve says so — the unresolved node
* carries NO badge, NO body and NONE of the model's bytes;
*
* ONE CANONICAL LOCATOR, ONE PANEL (phase 192): the mid-turn frame can place
* a block BEFORE the answer fences it, and hydration drops that appended
* twin when the fence resolves (`dropPlacedTwin`) — the fence is where the
* model wanted the bytes. The scan is PER LOCATOR, so two honest ranges of
* one file stay two panels.
*
* The house frontend rules hold throughout: every node is built with
* `createElement` + `textContent` (no HTML string anywhere — the data here
* is model-written text and indexed file bytes), every handler is attached
* with `addEventListener` (the CSP is `default-src 'self'`: no inline
* handlers, no `onclick=`/`oncopy=` attribute), and the states are carried
* by TEXT and STRUCTURE, never by colour alone (WCAG 1.4.1).
*/
/* The one honest sentence for a locator nothing resolved (the pinned copy —
* the whole unresolved state is this sentence plus the document link, so a
* screen reader, a monochrome theme and a screenshot all agree on it). */
function verbatimUnresolvedCopy() {
return "I could not verify this against the knowledge base";
}
/* The badge's claim, spelled out for the pointer tooltip — the WORD is the
* signal (locked A5), the tooltip only explains it. */
function verbatimBadgeTitle() {
return "These bytes are copied from the knowledge base, not written by the model.";
}
/* ---------- the small guards (a malformed frame never throws) ----------
* `answer_text` crosses the wire, so every field is treated as untrusted
* shape: a block that is not a plain object with the three fields the panel
* cannot live without is DROPPED, exactly like the restore path drops a
* malformed timeline entry. */
function isVerbatimBlock(block) {
return !!(
block &&
typeof block === "object" &&
typeof block.source === "string" &&
block.source &&
typeof block.path === "string" &&
block.path &&
typeof block.text === "string"
);
}
function verbatimBlocks(blocks) {
return Array.isArray(blocks) ? blocks.filter(isVerbatimBlock) : [];
}
/* A positive line count, or null (a missing/garbage count never prints). */
function verbatimCount(value) {
return Number.isInteger(value) && value > 0 ? value : null;
}
/* The one `source/path` identity the whole app uses (the read/grep
* coordinate). NO normalization here — see the header: markdown.js owns the
* grammar, this only joins the two halves the server already resolved. */
function verbatimIdentity(source, path) {
return `${source}/${path}`;
}
/* Split a canonical locator into its identity half and its range suffix.
* The split is on the `#` the server PUT THERE (canonical_locator's own
* separator) — never a re-parse of the range. */
function verbatimSplitLocator(locator) {
const text = typeof locator === "string" ? locator : "";
const hash = text.indexOf("#");
return hash === -1
? { identity: text, hasRange: false }
: { identity: text.slice(0, hash), hasRange: true };
}
/* The plain viewer href (the shared page's whole affordance — that page has
* no modal, and the documents API is admin-gated so a guest lands on the
* sign-in page by design). app.js OVERRIDES this through
* `opts.documentHref` with `documentUrl(…, "/")`, the chip's own
* back-to-the-chat URL (phase 153). A rooted path built from
* encodeURIComponent — never a `javascript:`/`data:` target. */
function verbatimDocumentHref(source, path) {
return (
"/document.html?source=" +
encodeURIComponent(source) +
"&path=" +
encodeURIComponent(path)
);
}
/* ---------- the panel's text pieces ---------- */
/* `homelab/a.md · lines 12–40 of 210`, or `homelab/a.md · all 210 lines`
* when the block IS the whole document (the caption is the SERVER's extent:
* `start_line`/`end_line` describe exactly the lines `text` contains after
* any character cap, so a capped block reads as a range, never as "all"). */
function verbatimCaptionText(block) {
const identity = verbatimIdentity(block.source, block.path);
const start = verbatimCount(block.start_line);
const end = verbatimCount(block.end_line);
const total = verbatimCount(block.total_lines);
if (start === null || end === null || end < start) return identity;
if (!block.truncated && start === 1 && total !== null && end === total) {
return `${identity} · all ${total} lines`;
}
return total === null
? `${identity} · lines ${start}–${end}`
: `${identity} · lines ${start}–${end} of ${total}`;
}
/* `Verbatim text from homelab/a.md, lines 12–40` (the range clause only when
* the panel shows a range — a whole-document panel is named by its file). */
function verbatimPanelLabel(block) {
const identity = verbatimIdentity(block.source, block.path);
const start = verbatimCount(block.start_line);
const end = verbatimCount(block.end_line);
if (start === null || end === null || end < start) {
return `Verbatim text from ${identity}`;
}
if (start === end) return `Verbatim text from ${identity}, line ${start}`;
return `Verbatim text from ${identity}, lines ${start}–${end}`;
}
/* The lines the gutter numbers: one entry per LINE OF THE SERVER TEXT, each
* `{ shown, number }`. A truncated block's last line is the server's
* `[…truncated…]` marker — a real line of the panel, but NOT a line of the
* file, so it gets no number (an empty cell keeps the rows aligned; the
* phase's rule is that the gutter never claims a coordinate the file does
* not have). */
function verbatimGutter(block) {
const lines = String(block.text).split("\n");
const start = verbatimCount(block.start_line);
const end = verbatimCount(block.end_line);
const numbered = start !== null && end !== null && end >= start ? end - start + 1 : 0;
return lines.map((_, i) => ({
number: i < numbered ? String(start + i) : "\u00a0",
}));
}
/* ---------- the controls ---------- */
/* The select-on-focus of the clipboard fallback field (an `<a>` has no
* `.select()`, a range does the job). Best-effort: a selection failure only
* means the reader copies by hand — the field still shows the whole text.
* (app.js's `copyShareLinkWithFallback` used to own this; task 07 moved the
* pair into verbatim.js so the code blocks and the share link share ONE
* degradation.) */
function selectAllInFallbackField(el) {
try {
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} catch {
/* selection is best-effort — the field still carries the full text */
}
}
/* The phase-55 degradation, generalized (LOCKED: the owner-locked share
* strings and behaviour are unchanged — app.js delegates here).
* `navigator.clipboard` is unavailable on a non-secure (http) homelab origin
* and rejects outside a user gesture, so the failure path renders ONE
* transient link field inside `hostEl` that selects its text on focus
* (click or Tab, then Ctrl/Cmd+C). Returns true when the clipboard took it.
* NEVER reads the DOM — the caller hands over the exact string, which is
* what makes "the clipboard content equals the server text" true. */
async function copyTextWithFallback(text, hostEl, title) {
const host = hostEl || document.body;
document.querySelectorAll(".share-link-fallback").forEach((el) => el.remove());
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
const field = document.createElement("a");
field.className = "share-link-fallback";
// An <a> needs an href to focus; a code block has none, so it takes the
// tabindex instead and stays a select-to-copy field either way.
if (/^(?:https?:|mailto:|\/)/i.test(text)) field.href = text;
else field.tabIndex = 0;
field.textContent = text; // the text is data — textContent, never innerHTML
field.title = title || "Click, then copy (Ctrl/Cmd+C)";
field.addEventListener("focus", () => selectAllInFallbackField(field));
host.appendChild(field); // beside the control that failed, never elsewhere
field.focus({ preventScroll: true });
return false;
}
}
/* One pill: `Copy` → `Copied` (the TEXT carries the state; `.copied` only
* supports it — WCAG 1.4.1). The label rides a `<span>` because the clipped
* label is the CSS ellipsis target (the phase-156 rule) while the control's
* `aria-label` names the full path. Sticky by design: no timer, so a reader
* who tabs away and back finds the state the click left. */
function buildCopyButton(className, text, ariaLabel, hostEl) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = className;
btn.setAttribute("aria-label", ariaLabel);
const label = document.createElement("span");
label.textContent = "Copy";
btn.appendChild(label);
btn.addEventListener("click", () => {
copyTextWithFallback(text, hostEl || btn.parentElement, ariaLabel).then((ok) => {
if (!ok) return; // the field is the feedback (it is focused and selected)
btn.classList.add("copied");
label.textContent = "Copied";
});
});
return btn;
}
/* The panel's link to the document. `opts.onOpen(source, path, linkEl)` is
* the chat page's modal hook (phase 26/153 — left-click opens the viewer IN
* the page, the href stays the `/document.html` escape hatch for
* middle-click and copy-link); with no hook the href simply navigates,
* which is the shared page's whole affordance. */
function buildVerbatimOpenLink(source, path, opts, text) {
const link = document.createElement("a");
link.className = "verbatim-open";
const identity = verbatimIdentity(source, path);
const href =
opts && typeof opts.documentHref === "function"
? opts.documentHref(source, path)
: verbatimDocumentHref(source, path);
link.href = href;
link.textContent = text || identity;
link.title = identity; // the full path as the native tooltip (chip pattern)
if (opts && typeof opts.onOpen === "function") {
link.addEventListener("click", (event) => {
event.preventDefault(); // no new tab — the modal takes over
event.stopPropagation();
opts.onOpen(source, path, link);
});
}
return link;
}
/* ---------- the panel ---------- */
/* buildVerbatimPanel(block, opts) → the `.verbatim` node.
*
* .verbatim[role=group][aria-label][data-bor-locator]
* .verbatim-head sticky (PLAN §7.1)
* .verbatim-caption source/path · lines 12–40 of 210 (clipped)
* .verbatim-badge VERBATIM (a WORD)
* .verbatim-open → the document viewer (optional)
* .verbatim-copy Copy → Copied (aria-label
* names path)
* .verbatim-body the bounded scroll panel
* ol.verbatim-lines the SERVER's line numbers (user-select: none)
* pre.verbatim-text block.text, byte for byte
* .verbatim-more the truncation line + the link (optional)
*
* `opts = { documentHref, onOpen }` — both optional (see the two builders
* above). Every value reaches the DOM through `textContent`/`title`/a
* built `href`: the caption carries indexed text and the aria-labels carry
* model-written locators, so there is no HTML string to escape. */
function buildVerbatimPanel(block, opts) {
const identity = verbatimIdentity(block.source, block.path);
const root = document.createElement("div");
root.className = "verbatim";
root.setAttribute("role", "group");
root.setAttribute("aria-label", verbatimPanelLabel(block));
// The SERVER's canonical locator is the identity of the placed block (the
// pending placeholder's own attribute is replaced with it — that is how
// `appendUnlocatedVerbatim` knows this block already has a home).
root.dataset.borLocator =
typeof block.locator === "string" && block.locator
? block.locator
: identity;
const head = document.createElement("div");
head.className = "verbatim-head";
const caption = document.createElement("span");
caption.className = "verbatim-caption";
caption.textContent = verbatimCaptionText(block);
caption.title = verbatimCaptionText(block); // the clipped text stays reachable
const badge = document.createElement("span");
badge.className = "verbatim-badge";
badge.textContent = "VERBATIM";
badge.title = verbatimBadgeTitle();
head.append(caption, badge);
head.appendChild(buildVerbatimOpenLink(block.source, block.path, opts));
const gutter = verbatimGutter(block);
// The clipboard's line count (the aria-label's N): the SERVER's extent,
// not the marker line the cap appended.
const from = verbatimCount(block.start_line);
const to = verbatimCount(block.end_line);
const shown = from !== null && to !== null && to >= from ? to - from + 1 : gutter.length;
head.appendChild(
buildCopyButton(
"verbatim-copy",
block.text,
`Copy ${shown} lines from ${identity}`,
root
)
);
root.appendChild(head);
const body = document.createElement("div");
body.className = "verbatim-body";
const numbers = document.createElement("ol");
numbers.className = "verbatim-lines";
numbers.setAttribute("aria-hidden", "true"); // the range rides the panel's label
for (const row of gutter) {
const li = document.createElement("li");
li.textContent = row.number;
numbers.appendChild(li);
}
const pre = document.createElement("pre");
pre.className = "verbatim-text";
pre.textContent = block.text; // THE POINT OF THE PHASE: the server's bytes
body.append(numbers, pre);
root.appendChild(body);
if (block.truncated) {
// One muted line naming where the rest lives, plus the same viewer link
// — the panel never pretends the slice is the file.
const more = document.createElement("div");
more.className = "verbatim-more";
const note = document.createElement("span");
note.textContent = "Cut short by the size limit — the rest is in the document. ";
more.appendChild(note);
more.appendChild(
buildVerbatimOpenLink(block.source, block.path, opts, `open ${identity}`)
);
root.appendChild(more);
}
return root;
}
/* The honest state for a locator the server did not resolve (and for a
* placeholder no terminal frame ever settled): the sentence, plus the
* document link ONLY when the identity actually parsed into a
* `source/path`. NO badge, NO body, NONE of the model's bytes — the fence's
* own text is already gone (task 06 dropped it), so there is nothing here
* to leak even by accident. */
function buildVerbatimUnresolved(locator, opts) {
const root = document.createElement("div");
root.className = "verbatim verbatim-unresolved";
root.setAttribute("role", "note");
const key = typeof locator === "string" ? locator : "";
if (key) root.dataset.borLocator = key;
root.dataset.verbatimState = (opts && opts.kind) || "unverified";
const state = document.createElement("span");
state.className = "verbatim-state";
state.textContent = verbatimUnresolvedCopy();
root.appendChild(state);
const { identity } = verbatimSplitLocator(key);
const slash = identity.indexOf("/");
if (slash > 0 && slash < identity.length - 1) {
root.appendChild(document.createTextNode(" — open "));
root.appendChild(
buildVerbatimOpenLink(identity.slice(0, slash), identity.slice(slash + 1), opts)
);
}
return root;
}
/* ---------- hydration ---------- */
/* The ONE match rule (LOCKED A2 made visible):
* 1. the canonical locator, compared as PLAIN STRINGS against the
* server's `locator` — the attribute markdown.js already normalized;
* 2. failing that, exactly ONE block sharing the same `source/path`
* (the model's RANGE was wrong; the panel shows the server's extent);
* 3. failing that, null → the unresolved state. An ambiguous identity
* (two blocks, same file, different ranges) never guesses. */
function matchVerbatimBlock(blocks, key) {
for (const block of blocks) {
if (block.locator === key) return block;
}
const identity = verbatimSplitLocator(key).identity;
const samePath = blocks.filter(
(block) => verbatimIdentity(block.source, block.path) === identity
);
return samePath.length === 1 ? samePath[0] : null;
}
/* The key a PLACED panel carries for a block — `buildVerbatimPanel`'s own
* convention (the server's canonical locator, the `source/path` identity
* when the block carries none). The ONE place that key is derived, so the
* append-side placed-scan and the hydration-side twin-scan can never drift
* (phase 192: the two are the same dedupe seen from either end). */
function verbatimPlacedKey(block) {
return typeof block.locator === "string" && block.locator
? block.locator
: verbatimIdentity(block.source, block.path);
}
/* dropPlacedTwin(host, block, keep) — the phase-192 (A3) dedupe,
* the twin a MID-TURN frame left behind.
*
* `hydrateVerbatimNow` appends a block no fence named YET (the call moment —
* the model may fence it in a later round). When that fence finally hydrates,
* TWO panels would carry the same canonical locator. The IN-TEXT panel wins:
* the fence is where the model wanted the bytes, the appended panel already
* served its mid-turn purpose. So the placed twin is dropped as its
* placeholder resolves.
*
* PER LOCATOR, never per identity: two different ranges of one file are two
* honest panels (`shown_texts` dedupes per canonical locator too), and a
* pending or unresolved node is NOT a placement (the same exclusion
* `appendUnlocatedVerbatim` uses). `keep` is the placeholder about to
* hydrate — never the node this scan removes.
*
* A fence the model repeated for the SAME locator collapses the same way: the
* answer shows those bytes once, at the last fence, never twice. */
function dropPlacedTwin(host, block, keep) {
if (!host || typeof host.querySelectorAll !== "function") return 0;
const placedKey = verbatimPlacedKey(block);
let dropped = 0;
host.querySelectorAll(".verbatim[data-bor-locator]").forEach((el) => {
if (el === keep) return;
if (el.classList.contains("verbatim-pending")) return;
if (el.classList.contains("verbatim-unresolved")) return;
if (el.getAttribute("data-bor-locator") !== placedKey) return;
el.remove();
dropped += 1;
});
return dropped;
}
/* hydrateVerbatim(root, blocks, opts) — replace every PENDING placeholder
* under `root` with its panel, or with the unresolved state.
*
* Only `.verbatim-pending` is touched, so the pass is idempotent: a panel
* this function built (or one restored from storage) is skipped on a re-run,
* which keeps a `Copied` state and a scrolled body alive. Never throws —
* a missing/malformed `blocks` settles every placeholder to the honest
* state instead. Returns the number of placeholders settled.
*
* A MATCHED placeholder also drops the panel the mid-turn pass appended for
* the same canonical locator (dropPlacedTwin, phase 192 A3) — one locator,
* one panel, and it sits where the model fenced it. */
function hydrateVerbatim(root, blocks, opts) {
if (!root || typeof root.querySelectorAll !== "function") return 0;
const list = verbatimBlocks(blocks);
// The message wrap is the scan's bound: the mid-turn panel lives in
// `.msg-body` beside the bubble, so a bubble-local walk would miss it.
// With no `.msg` ancestor (a bare bubble, a fragment not mounted yet) the
// ROOT bounds the scan — a missing wrap must not switch the dedupe off.
const host =
(typeof root.closest === "function" ? root.closest(".msg") : null) || root;
let settled = 0;
// The NodeList is STATIC, so replacing a node never disturbs the walk.
root.querySelectorAll(".verbatim-pending[data-bor-locator]").forEach((node) => {
const key = node.getAttribute("data-bor-locator") || "";
const block = matchVerbatimBlock(list, key);
if (block) dropPlacedTwin(host, block, node);
node.replaceWith(
block ? buildVerbatimPanel(block, opts) : buildVerbatimUnresolved(key, opts)
);
settled += 1;
});
return settled;
}
/* hydrateVerbatimNow(root, block, opts) — the MID-TURN pass (phase 192,
* A2): settle the PENDING placeholders THIS ONE block matches, leave
* every other placeholder pending.
*
* The matching is the done path's semantics (matchVerbatimBlock with a
* ONE-block list): the exact canonical locator, or the identity
* fallback when the block is the only one of that identity — a fence
* for range B of a shown file A hydrates with A's server extent (the
* caption shows the SERVER's range, the phase-182 contract), so the
* mid-turn pass and the done pass can never disagree.
*
* ONLY matched nodes are replaced (buildVerbatimPanel); the unmatched
* pending nodes STAY .verbatim-pending — settling them is the
* done/failure path's job (settleVerbatimPlaceholders), and a mid-turn
* pass must never burn a placeholder for a not-yet-shown fence.
*
* Returns the number of placeholders settled (the caller appends the
* block — appendUnlocatedVerbatim — only when it is 0). Never throws:
* a missing root → 0, and a frame that is not a block settles nothing
* (the house never-throw contract — the guard is `isVerbatimBlock`, the
* done path's own). */
function hydrateVerbatimNow(root, block, opts) {
if (!root || typeof root.querySelectorAll !== "function") return 0;
// A malformed frame settles NOTHING (not even to the honest state — the
// `done` pass and the terminal settle still own this turn's placeholders).
if (!isVerbatimBlock(block)) return 0;
const list = [block];
let settled = 0;
root.querySelectorAll(".verbatim-pending[data-bor-locator]").forEach((node) => {
const key = node.getAttribute("data-bor-locator") || "";
const matched = matchVerbatimBlock(list, key);
if (!matched) return; // STAYS pending — the done frame may resolve it
node.replaceWith(buildVerbatimPanel(matched, opts));
settled += 1;
});
return settled;
}
/* appendUnlocatedVerbatim(wrap, blocks, opts) — the blocks NO placeholder
* matched (a `show_text` call whose answer never located the file), appended
* in the frame's order at the `answer_image` position: below the answer
* text, above the source clusters (the phase-124/130 DOM order — callers run
* this BEFORE `appendSources`).
*
* State-free idempotence: a block is already placed when the wrap holds a
* resolved panel carrying its canonical `locator`, so calling this twice
* (or after a hydration that matched) appends nothing. */
function appendUnlocatedVerbatim(wrap, blocks, opts) {
if (!wrap || typeof wrap.querySelectorAll !== "function") return 0;
const list = verbatimBlocks(blocks);
if (!list.length) return 0;
const host = wrap.querySelector(".msg-body") || wrap;
const placed = new Set();
wrap.querySelectorAll(".verbatim[data-bor-locator]").forEach((el) => {
// An unresolved placeholder is NOT a placement (it names a locator the
// server never returned, and the block belongs on screen anyway).
if (el.classList.contains("verbatim-unresolved")) return;
placed.add(el.getAttribute("data-bor-locator"));
});
let appended = 0;
for (const block of list) {
const key = verbatimPlacedKey(block);
if (placed.has(key)) continue;
host.appendChild(buildVerbatimPanel(block, opts));
placed.add(key);
appended += 1;
}
return appended;
}
/* settleVerbatimPlaceholders(root, kind) — the terminal-frame sweep the
* failure paths run (the `error` frame, the 300 s guard, the Stop action,
* the failed-turn funnel): a panel NEVER sits at "Retrieving the exact
* text…" once a turn is over. `kind` is recorded on the node
* (`data-verbatim-state`) for the next reader; every kind renders the same
* honest sentence, because "I could not verify this" is the truth for a
* turn that ended before `done` too. */
function settleVerbatimPlaceholders(root, kind) {
if (!root || typeof root.querySelectorAll !== "function") return 0;
let settled = 0;
root.querySelectorAll(".verbatim-pending").forEach((node) => {
node.replaceWith(
buildVerbatimUnresolved(node.getAttribute("data-bor-locator") || "", { kind })
);
settled += 1;
});
return settled;
}
/* ---------- the ordinary answer code block ---------- */
/* enhanceAnswerCodeBlocks(root) — every `<pre>` inside an answer bubble that
* is NOT inside a `.verbatim` gets the `.code-block` wrapper and a Copy
* button, ONCE the turn settled (the callers run it on the settled answer,
* never mid-stream). NO badge, NO caption: a `VERBATIM` claim belongs only
* to a server-resolved block (LOCKED A5) — plainly the model's, or verbatim
* and badged, never a hybrid.
*
* Idempotent on re-render: a `<pre>` already inside a `.code-block` is left
* alone (the live path re-runs the renderer per delta; the restore path runs
* this once). Returns the number of blocks wrapped. */
function enhanceAnswerCodeBlocks(root) {
if (!root || typeof root.querySelectorAll !== "function") return 0;
const bubbles = [];
if (root.classList && root.classList.contains("bubble")) bubbles.push(root);
root.querySelectorAll(".bubble").forEach((el) => bubbles.push(el));
let wrapped = 0;
for (const bubble of bubbles) {
bubble.querySelectorAll("pre").forEach((pre) => {
if (pre.closest(".verbatim")) return; // the panel owns its own Copy
const parent = pre.parentElement;
if (!parent || parent.classList.contains("code-block")) return;
const box = document.createElement("div");
box.className = "code-block";
parent.insertBefore(box, pre);
box.appendChild(pre);
box.appendChild(
buildCopyButton(
"code-block-copy",
pre.textContent || "",
"Copy this code block",
box
)
);
wrapped += 1;
});
}
return wrapped;
}