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/).
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# 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>`.
|
||||
Reference in New Issue
Block a user