phase: 182_verbatim_show_text
**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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user