Files
brain-of-reese/.agents/phases/complete/138_markdown_blockquotes/00_phase.md
T
ducoterra 1a657ce4ca
Build and Push Containers / build-and-push-app (push) Successful in 2m36s
Build and Push Containers / build-and-push-db (push) Successful in 11s
phase: 138_markdown_blockquotes
**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**
2026-09-30 10:49:40 -04:00

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 &gt; and renders as literal text in a <p> (verified: renderMarkdown("> quoted") → <p>&gt; 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.css on main.

Design (shared by all tasks — the executor reads this, not the chat)

  • The pass (task 01): a new line-block pass in markdown.js BETWEEN step 2 (inline(escapeHtml(text))) and step 3 (paragraphs) — at that point the text is escaped (a quote line starts with &gt;) 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*&gt;+ ? into ONE <blockquote class="md-quote">…</blockquote>: strip the leading &gt; 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 - list line, a # heading line) 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-quote in styles.css — theme-safe from the EXISTING :root palette 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.py suites.
  • E2E (task 03): dedicated tests/e2e/test_markdown_blockquotes.py, run in isolation (AGENTS.md rules 4/9), mock-only.

Tasks

  1. 01_blockquote_pass.md — the blockquote pass in markdown.js + the paragraph-guard extension.
  2. 02_blockquote_css.md — the .md-quote styling in styles.css (theme-safe, WCAG AA contrast).
  3. 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 includes blockquote; the exact <blockquote class="md-quote"> marker; the .md-quote CSS 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 no app/ 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 pytest green; uv run pytest --cov=app --cov-report=term-missing TOTAL >90%; uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov green in isolation; uv run ruff check . && uv run pyright clean
  • one --no-gpg-sign Conventional 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 :root palette 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"