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"
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# Task 01 — The blockquote pass in the shared renderer
|
||||
|
||||
**Phase:** `138_markdown_blockquotes` · **Source:** `TODO.md:3` — "Markdown formatted ">" quotes aren't rendering"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
`renderMarkdown` turns runs of `>`-prefixed lines into `<blockquote class="md-quote">` elements — after the escape + inline step, before the paragraph pass — with fences and tables keeping their precedence and non-quote input byte-identical.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/markdown.js` — new pass between the step-2 `text = inline(escapeHtml(text))` and the step-3 paragraph `.map(...)`: split the text on `\n`; group the maximal run of consecutive lines matching `^\s*>+ ?` (the escaped quote marker); strip the `>` run + one optional space from each member (nested `>>`/`>>>` collapses to one level — ASSUMPTION A1); join members with `<br>`; emit `<blockquote class="md-quote">…</blockquote>`. A blank line terminates the group.
|
||||
2. `frontend/assets/markdown.js` — extend the step-3 block-element guard from `/^<(h\d|ul|ol|pre|li)/` to `/^<(h\d|ul|ol|pre|li|blockquote)/` so the paragraph pass leaves the block alone (no `<p>` wrap).
|
||||
3. `frontend/assets/markdown.js` — update the module docstring: pass order is now fences → tables → escape+inline → **blockquotes** → paragraphs → restore; document the group rule (blank line terminates; nested collapses) and the documented limit (a list/heading line INSIDE a quote renders as plain text — the inline chain ran first; inline code/strong/em work).
|
||||
- ASSUMPTION: a `>` inside a code fence is already protected by the step-1 placeholder (no handling needed); a `>` in a table cell is escaped inside the `<td>` markup and restored in step 4 — both are pinned in task 03's behavior checks.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: covered by task 03's file (the source pin + the node behavior listed there) — the frontend house pattern is one unit file per feature.
|
||||
- Coverage: n/a (frontend) — the suite + the pins are the gate.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] node repro: `renderMarkdown("> a\n> b")` → `<blockquote class="md-quote">a<br>b</blockquote>`; `renderMarkdown("> quoted")` no longer emits a literal `>`
|
||||
- [ ] a non-quote input (plain paragraph, list, table, fence) renders byte-identical to the pre-task renderer
|
||||
- [ ] `uv run pytest` green (the renderer-dependent unit suites unedited); `uv run ruff check .` clean
|
||||
@@ -0,0 +1,21 @@
|
||||
# Task 02 — `.md-quote` styling (theme-safe, WCAG AA)
|
||||
|
||||
**Phase:** `138_markdown_blockquotes` · **Source:** `TODO.md:3` — "Markdown formatted ">" quotes aren't rendering"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
The blockquote is visually distinct and readable in all three rendering contexts (chat bubble, thinking block, document viewer) with ONE rule set, using only the existing `:root` palette.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/styles.css` — add the `.md-quote` rule next to the other markdown element styles (the phase-44 `.md-table` / `.md-table-wrap` block is the anchor): `border-left: 3px solid <palette var>` (a visible-but-restrained border — pick from the existing `:root` palette, the executor's contrast judgment), `padding: 0.25rem 0.75rem`, `margin: 0.5rem 0`, `color: <muted palette var>` with a ≥4.5:1 contrast comment on the bubble surface per the house convention (`--ink-soft`'s `/* 8.2:1 on --surface */` qualifies — verify against the actual bubble background and document it).
|
||||
2. `frontend/assets/styles.css` — no background fill (ASSUMPTION A2); verify the rule renders acceptably in the chat `.bubble`, the `.thinking-text` block, and the document-viewer content region — add a scoped override only if the task-03 E2E proves a clash.
|
||||
- ASSUMPTION: the built-in dark palette is the single source of truth — the `:root`-drift unit test parses built-ins from this file, so no hex literal outside `:root` and no second copy.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_markdown_blockquotes.py` (task 03) source-pins the `.md-quote` rule's presence and its palette-var-only references (the phase-44 CSS-pin pattern).
|
||||
- Coverage: n/a (frontend).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] the `.md-quote` rule exists in `styles.css`, references only `:root` palette vars, and carries a ≥4.5:1 contrast comment
|
||||
- [ ] the quote is visually distinct in chat + viewer (the E2E screenshot, task 03)
|
||||
- [ ] the theme no-op (B4) is unaffected — the rule uses existing vars; no new `ui_settings` column
|
||||
@@ -0,0 +1,24 @@
|
||||
# Task 03 — Unit pins, node behavior, dedicated E2E
|
||||
|
||||
**Phase:** `138_markdown_blockquotes` · **Source:** `TODO.md:3` — "Markdown formatted ">" quotes aren't rendering"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
Pin the renderer contract at the source level, prove the browser behavior, and ship the dedicated Playwright file, run in isolation (AGENTS.md rules 4/9).
|
||||
|
||||
## Work
|
||||
1. `tests/unit/test_markdown_blockquotes.py` (new) — the phase-44 pattern (source pins + a `HAVE_NODE`-skipped node behavior):
|
||||
- Source pins: the blockquote pass runs after `inline(escapeHtml(text))` and before the paragraph split; the paragraph guard includes `blockquote`; the exact `<blockquote class="md-quote">` marker string; the `.md-quote` CSS rule references palette vars only (the task-02 pin).
|
||||
- Node behavior: single-line quote → one blockquote; a 3-line group → one blockquote with `<br>` joins; `>> nested` collapses to one level; `> <script>alert(1)</script>` renders inert (no live tag in the output); a `>` inside a ``` fence stays fence content; a table cell containing `> x` renders as table-cell text, not a blockquote; non-quote input (a paragraph + list + table) renders byte-identical to the pre-phase renderer (fixture string checked into the test, per the phase-44 shape).
|
||||
2. `tests/e2e/test_markdown_blockquotes.py` (new) — dedicated, run in isolation (`--no-cov`, DB up), mock-only, following `tests/e2e/test_markdown_tables.py`'s structure:
|
||||
- (a) the chat: a mock flow whose answer contains `>` quotes (the phase-44 mock-flow pattern — a new user-message marker, a deterministic canned answer) → assert the DOM: the `.bubble` contains a `blockquote.md-quote` with the quote text and no literal `>` text at the block's start;
|
||||
- (b) the viewer: a KB fixture with a `.md` document containing a quote block → open the document → assert the viewer content renders the `blockquote.md-quote`;
|
||||
- (c) the restore: save the chat (or reload `/?chat=<id>`) → the restored bubble keeps the blockquote (the restore path runs the same renderer — a cheap assertion if the suite structure allows it).
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: as above. E2E: as above. Coverage: **>90%** on `app/` (the gate runs the full suite; frontend changes add no `app/` code — the suite must stay green).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/unit/test_markdown_blockquotes.py -v` green (node available → the behavior check runs)
|
||||
- [ ] `uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov` green in isolation
|
||||
- [ ] `uv run pytest` green, coverage TOTAL >90%, `uv run ruff check . && uv run pyright` clean
|
||||
Reference in New Issue
Block a user