feat(assistant): readable answers: GFM markdown and presentation blocks #111

Merged
KofTwentyTwo merged 2 commits from feature/assistant-answer-presentation into main 2026-10-14 01:29:52 +00:00
KofTwentyTwo commented 2026-10-14 00:50:56 +00:00 (Migrated from github.com)

Readable assistant answers: GFM markdown and presentation blocks

Owner priority (2026-10-13). An answer's table rendered on ONE line ("| Type | Count | |---|---| | JPEG ... | 81 | ..."), and the reply said "You can open the lists filtered by JPEG or PNG" with no links.

Root cause

AssistantMarkdown rendered react-markdown without remark-gfm. GFM tables never parsed, so their lines collapsed into one paragraph through soft line breaks. The backend keeps \n: events are JSON, and the turn's text buffer is untouched.

Fix

  • Markdown:
    • remark-gfm and remark-breaks are added (deps remark-gfm@4.0.1, remark-breaks@4.0.0).
    • The sanitize schema and the link and image rules are unchanged.
    • Tables are styled for light and dark, and numeric columns align right.
    • SSE data: fields that span several lines keep their newlines.
  • Presentation blocks: a new presentation message part and SSE event, following the qbit-assistant contract (the server side is the companion qbit-assistant PR).
    • summary: the one-line answer first.
    • metrics: big-number tiles.
    • table: uses the server-formatted display values, with per-row links.
    • chart: bar, pie or line with recharts, lazy-loaded. It is accessible: an aria summary and a hidden data list.
    • filterLinks and recordLinks.
    • actions: a prompt sends the next message; an href navigates.
    • Hrefs become links only when internalAppHref accepts them (same-origin /app), the same rule as markdown links.
    • Unknown kinds are skipped. A part with no renderable blocks falls back to its markdown.
  • Storybook: the owner's example, built from blocks.

Tests

  • The owner's exact answer text:
    • it renders a 2-column table with 3 rows;
    • lists, headings and line breaks render;
    • splitting the text across many text.delta events gives the same table.
  • Blocks: every block kind; href safety (javascript:, protocol-relative and external hrefs never become links); action prompts; unknown kinds; the reducer presentation event.
  • Results: pnpm typecheck passes and pnpm lint has 0 errors. The assistant and API suites pass (371 tests). In the full run, two standalone-build tests (tests/standalone/acceptance-stage, tests/acceptance/support/standalone-variant) fail on Windows on main too; that is environment-only, and CI is the gate.

Bundle

+44.7 KiB gzip (+174 KiB raw) across compiled chunks, mostly recharts, which loads only when a chart block renders. Size limits are report-only.

Screenshots of the owner's example (light and dark, 390 and 1440 px) follow once the qbit side lands, from the demo server.

🤖 Generated with Claude Code

## Readable assistant answers: GFM markdown and presentation blocks **Owner priority (2026-10-13).** An answer's table rendered on ONE line ("| Type | Count | |---|---| | JPEG ... | 81 | ..."), and the reply said "You can open the lists filtered by JPEG or PNG" with no links. ### Root cause `AssistantMarkdown` rendered `react-markdown` **without `remark-gfm`**. GFM tables never parsed, so their lines collapsed into one paragraph through soft line breaks. The backend keeps `\n`: events are JSON, and the turn's text buffer is untouched. ### Fix - **Markdown:** - `remark-gfm` and `remark-breaks` are added (deps `remark-gfm@4.0.1`, `remark-breaks@4.0.0`). - The sanitize schema and the link and image rules are unchanged. - Tables are styled for light and dark, and numeric columns align right. - SSE `data:` fields that span several lines keep their newlines. - **Presentation blocks:** a new `presentation` message part and SSE event, following the qbit-assistant contract (the server side is the companion qbit-assistant PR). - `summary`: the one-line answer first. - `metrics`: big-number tiles. - `table`: uses the server-formatted `display` values, with per-row links. - `chart`: bar, pie or line with recharts, **lazy-loaded**. It is accessible: an aria summary and a hidden data list. - `filterLinks` and `recordLinks`. - `actions`: a `prompt` sends the next message; an `href` navigates. - Hrefs become links only when `internalAppHref` accepts them (same-origin `/app`), the same rule as markdown links. - Unknown kinds are skipped. A part with no renderable blocks falls back to its `markdown`. - **Storybook:** the owner's example, built from blocks. ### Tests - **The owner's exact answer text:** - it renders a 2-column table with 3 rows; - lists, headings and line breaks render; - splitting the text across many `text.delta` events gives the same table. - **Blocks:** every block kind; href safety (`javascript:`, protocol-relative and external hrefs never become links); action prompts; unknown kinds; the reducer `presentation` event. - **Results:** `pnpm typecheck` passes and `pnpm lint` has 0 errors. The assistant and API suites pass (371 tests). In the full run, two standalone-build tests (`tests/standalone/acceptance-stage`, `tests/acceptance/support/standalone-variant`) fail on Windows on `main` too; that is environment-only, and CI is the gate. ### Bundle +44.7 KiB gzip (+174 KiB raw) across compiled chunks, mostly recharts, which loads only when a chart block renders. Size limits are report-only. Screenshots of the owner's example (light and dark, 390 and 1440 px) follow once the qbit side lands, from the demo server. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.