**Phase 138 final verification pass — all green, no defects found.** - Verified: blockquote pass in `frontend/assets/markdown.js` (step 2b, between escape+inline and paragraphs), guard extended to `|blockquote`, docstring updated; `.md-quote` CSS (left border + padding + `--ink-soft`, no fill, palette vars only, contrast comments, user-bubble override); unit pins + node behavior; dedicated E2E (chat / viewer / restore); mock `QUOTE_TRIGGER`/`QUOTE_ANSWER`; fixture `tests/fixtures/quote_docs/`. Thinking block confirmed on the same shared renderer (app.js L2025/L3095). - `uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov` → 3 passed (isolation) - `uv run pytest --cov=app --cov-report=term-missing` → 3268 passed, 9 skipped; TOTAL **99%** (>90%) - `uv run ruff check . && uv run pyright` → All checks passed; 0 errors - Criteria: quotes render in chat/viewer/restore ✅ (E2E) + thinking ✅ (shared renderer + unscoped AA CSS); `>>` collapses, fence/table `>` untouched ✅ (unit+e2e); non-quote byte-identical ✅ (exact fixture; phase-44/renderer suites pass **unedited**, confirmed by empty `git diff --stat`) - No deviations; commit + phase-dir move left to the harness per pipeline rules (working tree clean of stray changes — only phase-138 diffs present) - Next pending phase: **139_read_range_tool_lines**
6.4 KiB
Phase 138 — Markdown blockquotes render: > quotes become styled <blockquote>s
Source: TODO.md L3 — "Markdown formatted ">" quotes aren't rendering"
Story: n/a (TODO-derived phase, Protocol B append).
Context: frontend/assets/markdown.js — the shared escape-first renderer (loaded by all three pages: index.html chat, document.html viewer, shared.html): step 1 fence protection (\u0000CODEn\u0000), step 1b table protection (\u0000TABLEn\u0000, phase 44), step 2 inline(escapeHtml(text)) (the inline chain: code → strong → em → h3/h4 → ul → ol), step 3 the paragraph pass (split on \n{2,}; the block-element guard /^<(h\d|ul|ol|pre|li)/), step 4 restore. Today a > line is escaped to > and renders as literal text in a <p> (verified: renderMarkdown("> quoted") → <p>> quoted</p>). frontend/assets/styles.css — the :root palette (--ink-soft 8.2:1 on --surface, --line, the --brand-* family; the :root-drift unit test parses the built-ins from this file — no second copy) and the chat-bubble / viewer typography. tests/unit/test_markdown_tables.py — the frontend source-pin + node-behavior unit pattern (HAVE_NODE skip); tests/e2e/test_markdown_tables.py — the phase-44 E2E pattern (mock flow with table output + the document viewer).
Objective
>-prefixed lines in any rendered markdown — a chat answer, the thinking block, a saved/shared chat (restore), the document viewer — render as a semantic <blockquote class="md-quote"> with theme-safe styling; every non-quote input renders byte-identical to today (the phase-44 table suite and the renderer-dependent unit suites stay green unedited).
Dependencies
137_pdf_image_context(complete) — pipeline order only; the code dependencies are the shared renderer +styles.cssonmain.
Design (shared by all tasks — the executor reads this, not the chat)
- The pass (task 01): a new line-block pass in
markdown.jsBETWEEN step 2 (inline(escapeHtml(text))) and step 3 (paragraphs) — at that point the text is escaped (a quote line starts with>) and already inline-transformed (bold/emphasis/code work inside quotes for free — the pass never re-transforms). Group the maximal run of consecutive lines matching^\s*>+ ?into ONE<blockquote class="md-quote">…</blockquote>: strip the leading>run plus ONE optional space from each member (nested>>/>>>collapses to one level — ASSUMPTION A1), join members with<br>(the paragraph convention). A BLANK line terminates the group (a re-quoted line starts a new<blockquote>— simple, predictable). The step-3 guard extends to/^<(h\d|ul|ol|pre|li|blockquote)/so the paragraph pass leaves the block alone (no<p>wrap). - Precedence is untouched: fences protect first (step 1 — a
>inside a code block never reaches the pass), tables protect second (step 1b — cell content is escaped inside the<td>markup and restored in step 4, AFTER the pass). No other pass changes. - In-quote structure (documented limit): a markdown STRUCTURE inside a quote (a
- listline, a# headingline) renders as plain text inside the blockquote — the inline chain runs before the pass, so its line-start transforms never see the post-strip text. Inline (code/strong/em) still works. Acceptable — the scope is quote rendering (owner L3). - The CSS (task 02):
.md-quoteinstyles.css— theme-safe from the EXISTING:rootpalette only: left border + horizontal padding + vertical margin + a muted text color carrying a ≥4.5:1 contrast comment on the bubble surface (the house comment convention, cf.--ink-soft's/* 8.2:1 on --surface */). ONE rule set serves all three contexts (chat.bubble,.thinking-text, the document viewer) — a scoped override only if the E2E proves a clash. - Byte-identity (task 03): any input without a quote line renders byte-identical to the pre-phase renderer — pinned by the node behavior check and by the unedited
test_markdown_tables.py/test_thinking_scroll.py/test_document_viewer.pysuites. - E2E (task 03): dedicated
tests/e2e/test_markdown_blockquotes.py, run in isolation (AGENTS.md rules 4/9), mock-only.
Tasks
01_blockquote_pass.md— the blockquote pass inmarkdown.js+ the paragraph-guard extension.02_blockquote_css.md— the.md-quotestyling instyles.css(theme-safe, WCAG AA contrast).03_unit_and_e2e.md— unit source-pins + node behavior + the dedicated Playwright E2E.
Testing & Quality
- Unit:
tests/unit/test_markdown_blockquotes.py(new — the phase-44 pattern): source pins (pass order fences → tables → escape+inline → blockquotes → paragraphs → restore; the guard regex includesblockquote; the exact<blockquote class="md-quote">marker; the.md-quoteCSS rule references palette vars only) + node behavior (single-line quote, multi-line group, nested collapse, XSS quote inert, fence-wins, a table cell containing>untouched, non-quote input byte-identical). - E2E:
tests/e2e/test_markdown_blockquotes.py(new), in isolation. - Coverage: >90% on
app/(the gate runs the full suite — this phase adds noapp/code, so the suite staying green IS the gate).
Completion Criteria
>quotes render as a styled<blockquote class="md-quote">in the chat answer, the thinking block, saved/shared chats (restore), and the document viewer- nested
>>collapses to one level; a>inside a code fence or a table cell is untouched - non-quote input renders byte-identical (the phase-44 / renderer-dependent suites pass unedited)
uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missingTOTAL >90%;uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-covgreen in isolation;uv run ruff check . && uv run pyrightclean- one
--no-gpg-signConventional Commit; phase dir moved to.agents/phases/complete/by the pipeline gate
Locked decisions
- A1 — nested quotes (
>>,>>>) collapse to a single<blockquote>level (owner-confirmed 2026-09-30, roadmap confirmation). - A2 — the quote style is a left border + padding + muted text color (NO background fill), built from the existing
:rootpalette only (owner-confirmed 2026-09-30, roadmap confirmation).
Commit
git add frontend/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "feat(ui): render markdown blockquotes in chat, viewer, and shared pages"