phase: 182_verbatim_show_text
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s

**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**

- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
This commit is contained in:
2026-10-15 08:33:12 -04:00
parent 814a400e6d
commit b34b8ca672
104 changed files with 10852 additions and 203 deletions
+294
View File
@@ -701,6 +701,289 @@ body::before {
.doc-md h5, .msg .bubble h5, .thinking-text h5 { margin: 0.8rem 0 0.3rem; font-size: 0.95rem; }
.doc-md h6, .msg .bubble h6, .thinking-text h6 { margin: 0.7rem 0 0.3rem; font-size: 0.88rem; }
/* ---------- Verbatim panels (phase 182, task 06) — BEGIN ----------
A ` ```bor <source/path> <range> ` locator fence (markdown.js step 0) and
its hydrated panel (verbatim.js, task 07): the answer shows the EXACT
bytes of an indexed file, typed by the SERVER, with a caption whose
numbers are the server's resolved extent, a VERBATIM badge, a line-number
gutter and a Copy button whose clipboard content equals those bytes.
The phase's UI contract (PLAN §7.1 + AGENTS rule 5): an answer's code
block is EITHER verbatim-and-badged OR plainly the model's — never a
silently-substituted hybrid, which is why `.verbatim-badge` belongs to a
resolved panel alone and `.code-block` (the plain wrapper task 07 adds
around an ordinary `<pre>`) carries the Copy button and no badge.
PHASE-08 TOKENS ONLY — no hex/rgb/hsl literal anywhere in this block (the
phase-92 monochrome invariant, and the :root-drift test parses the
built-ins out of this file, so a literal would be a second source of
truth). A monochrome or inverted theme keeps every state readable because
the states are carried by TEXT and STRUCTURE, never by colour alone
(WCAG 1.4.1): `Copy` → `Copied` is a text change with the --ok family as
support, the badge is the WORD "VERBATIM", the unresolved state is a
sentence, and the line gutter is digits. Contrast of the pairs used here
(built-in palette, all AA ≥4.5:1):
--ink on --surface 15.3:1 (the code text)
--ink-soft on --surface 8.2:1 (caption, gutter, pending state, button label)
--ok-ink on --ok-bg 10.6:1 (the badge, the copied state's fill)
--ok-ink on --surface 12.2:1 (the copied state's label on hover-off)
--ink-soft on --err-bg 7.9:1 (the unresolved sentence)
A code surface is also readable in a LIGHT theme for the same reason: no
#fff/#000, no new hue — every token comes from the theme's own palette.
Static content: NOTHING in this block animates or transitions, so
prefers-reduced-motion has nothing to still (the pending state is a
`role="status"` live region — the announcement IS the feedback, a pulse
would add nothing but motion). */
.verbatim {
display: block;
min-width: 0;
margin: 0.5rem 0;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow: hidden;
}
/* The head row is the panel's sticky header — PLAN §7.1's bounded-panel
rule (the .table-wrap table thead th idiom, phase 156): it stays put
while the body scrolls, so the caption (source/path · lines 12–40 of 210)
is never off-screen in a 40-line slice. OPAQUE --surface background, so
scrolled bytes never show through it. */
.verbatim-head {
position: sticky;
top: 0;
z-index: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 0.5rem;
padding: 0.4rem 0.6rem;
background: var(--surface);
border-bottom: 1px solid var(--line);
}
/* The clipped label keeps the phase-156 `.cell-clip` ellipsis convention
(block/flex item that shrinks below its text width, hard cut → "…"); the
FULL text stays reachable through the control's aria-label / title, so
the clip is visual only. */
.verbatim-caption {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--mono);
font-size: 0.75rem;
color: var(--ink-soft); /* 8.2:1 on --surface */
}
/* The badge is a WORD, never an icon alone (locked A5: the VERBATIM claim
must survive a screen reader and a monochrome theme), and
`text-transform: none` keeps the lowercase letters the clipboard/AT
would otherwise have to reconcile (no uppercase surprises spelled out
letter-by-letter). --ok-ink on --ok-bg is the app's existing AA pair. */
.verbatim-badge {
flex: 0 0 auto;
padding: 0.1rem 0.45rem;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--ok-bg);
color: var(--ok-ink); /* 10.6:1 on --ok-bg */
font-family: var(--mono);
font-size: 0.68rem;
text-transform: none;
}
/* The bounded scroll panel (PLAN §7.1: a bounded panel, never a page-long
paste). CODE scrolls sideways and never wraps (phase 116 locked A1 —
wrapping breaks code readability), so overflow-wrap is pinned back to
normal against the .bubble's `overflow-wrap: anywhere` (the phase-165
prose/code split: prose wraps, code scrolls). min-width: 0 keeps the
long-line escape hatch that every other scroller in the app carries. */
.verbatim-body {
max-height: min(40rem, 70vh);
overflow: auto;
min-width: 0;
margin: 0;
padding: 0.5rem 0.75rem;
background: var(--surface);
color: var(--ink); /* 15.3:1 on --surface */
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
white-space: pre;
tab-size: 2;
overflow-wrap: normal;
}
/* No scrollbar-color here ON PURPOSE: phase 116 scoped the themed
scrollbar to `.doc-modal .doc-raw` alone (pinned by
tests/unit/test_prose_word_wrap.py — "no global scrollbar restyle"), and
the chat's other scrollers (.bubble pre, .md-table-wrap) keep the
browser-native bar. The verbatim panel joins THOSE, not the modal. */
/* The gutter: the SERVER's line numbers (the phase-131 coordinate system —
the same 1-based lines `read` and `grep` report), never the model's
claim. `user-select: none` is the load-bearing rule: a reader who
selects the text — or hits Copy on a panel whose selection the browser
picks up — never drags line numbers into the clipboard. The digits are
rendered as list-item TEXT (list-style is off) so the rule behaves the
same in every engine, tabular-nums keeps the column straight, and the
gutter sticks to the left edge while the code scrolls under it. */
.verbatim-lines {
list-style: none;
position: sticky;
left: 0;
flex: 0 0 auto;
min-width: 2.5rem;
background: var(--surface); /* opaque: scrolled code never shows THROUGH
the gutter (the .table-wrap thead rule) */
margin: 0;
padding: 0 0.6rem 0 0;
text-align: right;
color: var(--ink-soft); /* 8.2:1 on --surface — a gutter is still text */
font-variant-numeric: tabular-nums;
user-select: none;
-webkit-user-select: none;
}
.verbatim-lines li {
user-select: none;
-webkit-user-select: none;
}
/* Neutralize the .bubble pre treatment INSIDE the panel: the panel is its
own surface (its own padding, hairline, scroll), so a <pre> child must
not stack a second border/padding on top of it. */
.verbatim pre {
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
/* The streaming/pending state (markdown.js step 0 → the restore step) and
the settled-but-unresolvable state. The pending look is MUTED on purpose
— it is not an answer yet — and it never paints the model's bytes. */
.verbatim-pending {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.15rem 0.6rem;
padding: 0.45rem 0.6rem;
background: var(--surface);
}
.verbatim-state {
min-width: 0;
color: var(--ink-soft); /* 8.2:1 on --surface */
font-size: 0.78rem;
}
/* The honest failure state (locked A7): a locator the server could not
resolve says so in a sentence, carries NO badge and NO body, and shows
none of the model's text. --err-line is the app's existing UI boundary
token (decorative, 4.6:1 — the sentence, not the colour, carries the
meaning); the badge rule below is CSS-side enforcement of the claim that
an unresolved panel never badges, whatever DOM reaches it. */
.verbatim-unresolved {
padding: 0.45rem 0.6rem;
background: var(--err-bg);
border-color: var(--err-line);
color: var(--ink-soft); /* 7.9:1 on --err-bg */
font-size: 0.8rem;
}
.verbatim-unresolved .verbatim-badge { display: none; }
/* The two Copy controls — `.verbatim-copy` inside a resolved panel's head,
`.code-block-copy` under an ordinary answer code block (locked A5: every
fenced block gets a Copy button, only a resolved one gets the badge).
The house pill family (mono text, --line hairline, --surface fill,
ink-soft label 8.2:1); the label is the designated clip target
(text-overflow: ellipsis, the phase-156 convention) while the control's
aria-label names the full path and line count. `min-height: 2rem` is the
desktop pill; the ≤640px block below lifts both to the 44px touch floor,
and the global :focus-visible ring is restated with the SAME values
(explicit here because the panel clips with overflow: hidden). */
.code-block {
position: relative;
min-width: 0;
margin: 0.4rem 0;
}
.code-block pre { margin: 0; }
.code-block-copy,
.verbatim-copy {
display: inline-flex;
align-items: center;
gap: 0.3rem;
max-width: 100%;
min-width: 0;
min-height: 2rem;
padding: 0.25rem 0.65rem;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface);
color: var(--ink-soft); /* 8.2:1 on --surface */
font-family: var(--mono);
font-size: 0.72rem;
cursor: pointer;
}
.code-block-copy > span,
.verbatim-copy > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.code-block-copy:hover,
.verbatim-copy:hover { color: var(--ink); border-color: var(--ink-soft); }
.code-block-copy:focus-visible,
.verbatim-copy:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
}
/* The `.copied` state: the TEXT changes ("Copy" → "Copied", task 07 owns
the swap) and this rule SUPPORTS it with the --ok family — colour is
never the only signal (WCAG 1.4.1), so a monochrome theme still reads
"Copied". */
.code-block-copy.copied,
.verbatim-copy.copied {
color: var(--ok-ink); /* 10.0:1 on --surface */
border-color: var(--ok-ink);
background: var(--ok-bg);
}
/* A resolved panel's Copy pill sits at the END of the head row (the caption
already took the flexible space; the auto margin keeps it right even
when the caption clips). */
.verbatim-copy { margin-left: auto; flex: 0 0 auto; }
.code-block-copy { margin: 0.25rem 0 0; }
/* What task 07's REAL DOM needs on top of task 06's contract — palette
variables only, no new literal, nothing animated. The body is a ROW of
(gutter, text): the gutter's `flex: 0 0 auto` + `position: sticky;
left: 0` above are written for exactly this, and the text takes its own
width so a long line scrolls UNDER the sticky gutter instead of wrapping
(phase 116 locked A1). */
.verbatim-body { display: flex; align-items: flex-start; }
.verbatim-text { flex: 0 0 auto; }
/* The panel's link to the document viewer (the chat page opens the modal,
the shared page navigates — one affordance, one look). It is a TEXT link
(underlined, not colour-only — WCAG 1.4.1) in the mono family of the
caption it sits beside, and it restates the app-wide focus ring with the
SAME values because the panel clips with `overflow: hidden`. */
.verbatim-open {
flex: 0 0 auto;
color: var(--ink-soft); /* 8.2:1 on --surface */
font-family: var(--mono);
font-size: 0.72rem;
text-decoration: underline;
text-underline-offset: 2px;
}
.verbatim-open:hover { color: var(--ink); }
.verbatim-open:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
}
/* The truncation line of a capped block: one muted sentence naming that the
rest lives in the document, plus the same link. It sits OUTSIDE the scroll
body, so it stays readable without scrolling the bytes. */
.verbatim-more {
padding: 0.4rem 0.6rem;
border-top: 1px solid var(--line);
background: var(--surface);
color: var(--ink-soft); /* 8.2:1 on --surface */
font-size: 0.75rem;
}
/* ---------- Verbatim panels (phase 182, task 06) — END ---------- */
.msg.user { justify-content: flex-end; }
.msg.user .msg-body { align-items: flex-end; }
.msg.user .bubble {
@@ -6672,6 +6955,17 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
flex-shrink: 0 would keep each at 100%); the "Page N of M"
status sits between them (the 0.75rem gap goes vertical). */
.history-pager { flex-direction: column; align-items: center; }
/* Phase 182 (task 06): the two Copy controls of a verbatim panel and of
an ordinary answer code block reach the 44px touch floor on a phone
(the desktop pill is 32px tall — fine for a pointer, too small a tap
target for a thumb). The pill itself grows; its label keeps the
ellipsis escape, so a long "Copy 210 lines from …" never widens the
panel. */
.code-block-copy, .verbatim-copy { min-height: 44px; }
/* Phase 182 (task 07): the panel's viewer link is a control too — it
reaches the same touch floor (inline-flex so the line box grows without
moving the baseline of the caption row it sits in). */
.verbatim-open { min-height: 44px; display: inline-flex; align-items: center; }
}
/* Phase 46: prefers-reduced-motion stills the mobile menu — no