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**
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# 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.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 `>`) 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 `- 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
|
||||
```bash
|
||||
git add frontend/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "feat(ui): render markdown blockquotes in chat, viewer, and shared pages"
|
||||
```
|
||||
Reference in New Issue
Block a user