Files
brain-of-reese/.agent/user_stories/markdown-tables.md
T
ducoterra 02c76ad328 chore(agent): phase roadmap from TODO.md — 8 phases (40–47), 24 tasks
Converts the 9 TODO items into an executable phase roadmap (Protocol B,
appended after phase 39):

- 40 tuning toggle anonymous flash (TODO L3)
- 41 sync fail-fast + modal when a model is down (TODO L4)
- 42 no reply autoscroll (TODO L5)
- 43 thinking scroll back — user scroll + gated autoscroll (TODO L7)
- 44 markdown tables (TODO L6)
- 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8)
- 46 mobile hamburger nav (TODO L9)
- 47 quadlet + jinja import formats, A9 revision (TODO L10–L11)

Each phase carries a user story, a dedicated Playwright E2E suite plan,
and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget
revision, A1–A5 scope decisions) confirmed 2026-08-27.

Also records the completed phases 30–39 todo/ -> complete/ moves that
were pending in the working tree. TODO.md is cleared (items now live in
.agent/phases/todo/).
2026-08-27 18:25:53 -04:00

101 lines
5.1 KiB
Markdown

# Story: Markdown tables in chat (and everywhere the renderer runs)
**Phase:** `44_markdown_tables` · **Source:** `TODO.md` L6 ·
**E2E:** `tests/e2e/test_markdown_tables.py`
## Bug report (verbatim, `TODO.md` L6)
> "Certain markdown formatting isn't working - tables for example don't
> get rendered as tables in the chat response."
## Narrative
As **a user**, when Brain answers with a markdown table (services and
ports, versions, schedules — the concrete specifics the persona is built
around), I expect a real table: aligned columns, borders, readable.
Today the shared renderer (`frontend/assets/markdown.js`, ~60 lines,
no-CDN by A11) has no table support — a pipe table renders as one
paragraph of raw `|` text.
- **Given** Brain's answer (or a document / thinking block) contains a
GFM pipe table
- **When** the renderer runs
- **Then** it renders a semantic `<table>` with a `<thead>` header row
and `<tbody>` body rows, XSS-safe (escape-first, as the rest of the
renderer).
## Acceptance criteria
1. **Pipe tables render:** header row + `|---|` separator row + body
rows → `<table class="md-table">` with `<th scope="col">` header
cells; leading/trailing pipes and in-cell whitespace are handled;
cells keep their inline markdown (bold/em/code).
2. **XSS-safe:** cell content is escaped before any transform — a cell
containing `<img onerror=…>` renders inert (the escape-first
guarantee, same as all other content).
3. **Code fences win:** a `|`-heavy block inside a ``` fence is never
parsed as a table (fence protection runs first, as today).
4. **Non-tables stay put:** a single `|` in prose, a lone separator
without a header, or a 1-line "table" is left as text.
5. **Wide tables:** the table sits in an `overflow-x: auto` wrapper so a
wide table scrolls horizontally instead of breaking the 46rem chat
column (PLAN §7.1).
6. **Shared everywhere:** the same renderer serves the chat answer, the
document viewer, and the thinking block — all three render tables.
7. **Style:** `.md-table` uses the existing dark-tech palette
(PLAN §7.2 tokens, contrast ≥4.5:1); alignment colons in the
separator are parsed but all cells render left-aligned (owner
decision).
## Owner-confirmed (2026-08-27, roadmap A3)
1. **Scope = GFM pipe tables** (header + separator + body rows). Links,
blockquotes, and hr are **not** in scope for this story.
2. **Alignment colons parsed, rendered left.**
3. **Wide tables get a horizontal scroll wrapper** inside the bubble.
## UI Visualization & Structure
- **Renderer** (`frontend/assets/markdown.js`): a table-protection pass
between the existing fence pass and the escape pass — consecutive
lines forming a table (every line contains `|`; line 2 matches the
separator `^\s*\|?(\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$`-style
rule with ≥1 cell) are pulled out, each cell is escaped + inline-
transformed, and the block is reinserted as protected HTML (same
`\u0000CODE…\u0000` placeholder mechanism as code fences, or a
sibling placeholder — the existing restore step is the only place
placeholders are re-expanded). Output shape:
`<div class="md-table-wrap"><table class="md-table"><thead><tr>
<th scope="col">…` / `</table></div>`.
- **CSS** (`frontend/assets/styles.css`): `.md-table-wrap
{ overflow-x: auto; }` (the wrapper is the scroller — the table keeps
natural width); `.md-table` border-collapse, `th`/`td` borders from
`--line`, padding ≈0.4rem 0.6rem, `thead` tinted from the surface
palette; fits the 46rem column without a new container.
- **Mock** (`tests/e2e/mock_llm.py`): new `TABLE_TRIGGER` (a substring
like `"show me a table"`) → `compose_answer` returns a fixed
deterministic GFM table answer (checked before the default tail-echo
branch), including one deliberately wide table for the overflow
assertion.
- **Non-goals:** no new library (A11), no CDN, no change to the escape-
first architecture, no table editing in the steering/tuning UI.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_markdown_tables.py`** (mock LLM; DB
up):
1. `test_chat_table_renders` — ask a question containing `TABLE_TRIGGER`;
the brain bubble contains `<table class="md-table">` with a
`<thead>` (header cells as `<th scope="col">`) and the expected
body-cell texts; no raw `|---|` separator text in the bubble.
2. `test_wide_table_scrolls` — the mock's wide table: the bubble's
`.md-table-wrap` has `scrollWidth > clientWidth` and horizontal
wheel/scroll moves it; the 46rem column itself does not overflow the
page.
3. `test_table_xss_safe` — a table whose cell contains an HTML tag
(mock variant or document content) renders the tag as text
(no injected element).
4. `test_viewer_table_renders` (shared renderer) — an indexed fixture
document containing a pipe table opens in the document modal and
renders the same `<table class="md-table">`.
5. `test_fence_not_a_table` (regression) — a fenced code block full of
`|` pipes renders as `<pre><code>`, no `<table>`.
6. `test_plain_pipe_stays_text` (regression) — a prose answer with a
single `|` renders as text, no `<table>`.