feat(client): render chat replies as markdown with collapsible thinking #359

Merged
james merged 1 commit from 352-chat-markdown into main 2026-06-29 13:15:14 +00:00
Owner

Assistant chat replies now render as Markdown, and <think>…</think> reasoning renders as a collapsed-by-default, expandable section.

What's in it

  • lib/agent/parseAssistantContent.ts — pure, node-tested splitter carving an assistant string into ordered markdown/think segments. An unterminated <think> (still streaming) becomes a trailing thinking-in-progress block so the live bubble stays stable and settles cleanly when </think> arrives.
  • lib/markdown/Markdown.tsx — self-rendered Markdown → RN primitives (headings, bold/italic, inline + fenced code, links via Linking.openURL, blockquotes, lists). No dependency added — the mature RN markdown libs are stale against this React 19 / RN 0.85 / Expo 56 stack and carry old native transitives. No dangerouslySetInnerHTML → no XSS surface (semgrep clean). Colors via theme.tokens; tolerates partial markdown mid-stream.
  • lib/agent/AssistantMessage.tsx — composes segments + a collapsible ThinkBlock (Lucide ChevronRight/ChevronDown). chat.tsx uses it for assistant rows + the live streaming bubble; user/tool text stays plain <Text>.
  • i18nchat.reasoning.{show,hide} ("Show thinking" / "Hide thinking").

Verification (local, re-run in the worktree)

  • typecheck ✓ · lint ✓ · test ✓ 162 (+8 parseAssistantContent cases) · no package.json/lockfile change (no dep) · semgrep ✓ 0 findings on the 4 changed source files

⚠️ Needs maintainer verification (headless limit)

Client tests are node-env — the visual Markdown rendering and the think-block collapse/expand need a real check on both Android and the PWA: progressive rendering as tokens stream, link taps, code-block/list/quote styling in light + dark themes, and the collapse toggle. The pure parsing layer is unit-tested.

Closes #352

🤖 Generated with Claude Code

Assistant chat replies now render as **Markdown**, and `<think>…</think>` reasoning renders as a **collapsed-by-default, expandable** section. ## What's in it - **`lib/agent/parseAssistantContent.ts`** — pure, node-tested splitter carving an assistant string into ordered `markdown`/`think` segments. An unterminated `<think>` (still streaming) becomes a trailing thinking-in-progress block so the live bubble stays stable and settles cleanly when `</think>` arrives. - **`lib/markdown/Markdown.tsx`** — self-rendered Markdown → RN primitives (headings, bold/italic, inline + fenced code, links via `Linking.openURL`, blockquotes, lists). **No dependency added** — the mature RN markdown libs are stale against this React 19 / RN 0.85 / Expo 56 stack and carry old native transitives. No `dangerouslySetInnerHTML` → no XSS surface (semgrep clean). Colors via `theme.tokens`; tolerates partial markdown mid-stream. - **`lib/agent/AssistantMessage.tsx`** — composes segments + a collapsible `ThinkBlock` (Lucide `ChevronRight`/`ChevronDown`). `chat.tsx` uses it for assistant rows + the live streaming bubble; user/tool text stays plain `<Text>`. - **i18n** — `chat.reasoning.{show,hide}` ("Show thinking" / "Hide thinking"). ## Verification (local, re-run in the worktree) - `typecheck` ✓ · `lint` ✓ · `test` ✓ 162 (+8 `parseAssistantContent` cases) · no `package.json`/lockfile change (no dep) · semgrep ✓ 0 findings on the 4 changed source files ## ⚠️ Needs maintainer verification (headless limit) Client tests are `node`-env — the **visual** Markdown rendering and the think-block collapse/expand need a real check on **both** Android and the PWA: progressive rendering as tokens stream, link taps, code-block/list/quote styling in light + dark themes, and the collapse toggle. The pure parsing layer is unit-tested. Closes #352 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(client): render chat replies as markdown with collapsible thinking
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 26s
PR / Lint (pull_request) Successful in 2m23s
PR / Static analysis (pull_request) Successful in 2m8s
PR / OpenAPI (pull_request) Successful in 3m15s
PR / Client (web export smoke) (pull_request) Successful in 3m48s
PR / Typecheck (pull_request) Successful in 4m27s
PR / OSV-Scanner (pull_request) Successful in 2m9s
PR / Build (pull_request) Successful in 4m19s
PR / pnpm audit (pull_request) Successful in 2m37s
PR / Package age policy (soft) (pull_request) Successful in 46s
Secrets / gitleaks (pull_request) Successful in 55s
PR / E2E (Playwright) (pull_request) Successful in 5m11s
PR / Test (postgres) (pull_request) Successful in 4m22s
PR / Test (sqlite) (pull_request) Successful in 4m27s
PR / Trivy (image) (pull_request) Successful in 4m19s
PR / Coverage (soft) (pull_request) Successful in 3m27s
78a204f1d0
Assistant chat replies now render as Markdown, and `<think>…</think>`
reasoning renders as a collapsed-by-default, expandable section.

- lib/agent/parseAssistantContent.ts: pure, node-tested splitter that
  carves an assistant string into ordered markdown/think segments;
  handles an unterminated <think> (still streaming) as a trailing
  thinking-in-progress block so the live bubble stays stable.
- lib/markdown/Markdown.tsx: self-rendered Markdown → RN primitives
  (headings, bold/italic, inline + fenced code, links via Linking,
  blockquotes, lists). No dependency added — the mature RN markdown libs
  are stale against this React 19 / RN 0.85 / Expo 56 stack and carry old
  native transitives. No dangerouslySetInnerHTML, so no XSS surface.
  Colors via theme.tokens; tolerates partial markdown mid-stream.
- lib/agent/AssistantMessage.tsx: composes segments + a collapsible
  ThinkBlock (ChevronRight/ChevronDown). chat.tsx uses it for assistant
  rows + the live streaming bubble; user/tool text stays plain.
- i18n: chat.reasoning.{show,hide}.

Closes #352

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.3% ≥ 50%
Branches 71.1% ⚠️ ≥ 75%
Functions 80.5% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.3% ✅ | ≥ 50% | | Branches | 71.1% ⚠️ | ≥ 75% | | Functions | 80.5% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit aa55e08bfe into main 2026-06-29 13:15:14 +00:00
james deleted branch 352-chat-markdown 2026-06-29 13:15:14 +00:00
Sign in to join this conversation.
No description provided.