Skip to content

Render the thinking trace as markdown #1949

Closed

Description

@MODSetter

The gap

docs/architecture/chat.md lists this under Known gaps:

The trace renders as plain text, not markdown, and there is no switch to turn thinking off for answers.

That line holds two independent tasks, so it has been split. This issue is the first half, rendering the trace as markdown. The switch is its own issue, and the gap line is deleted by whichever of the two merges second.

Why it matters

A thinking model writes its trace in markdown, because that is how models write. It arrives as literal ##, **, - and fenced code in a box that renders none of it, so the trace reads as a wall of syntax. The answer directly below it, from the same model in the same reply, is rendered properly — which makes the trace look broken rather than plain.

The trace was recently given a box, a fold, an accessible name and a keyboard-reachable scroll area. It is a surface people are now expected to read.

Where

  • surfsense_local/frontend/src/features/chat/reply-thinking.tsx — {reasoning.text} inside the ScrollFade, and the whitespace-pre-wrap in viewportClassName, which markdown makes redundant.
  • Reuse what the app already has. studio/viewers/summary-viewer.tsx renders markdown with Streamdown from streamdown and the shared plugin config in streamdown-config.ts. That is the pattern here; no new dependency and no new renderer.
  • message.tsx — MarkdownText is the answer's renderer. Do not reach for it directly: it is an assistant-ui text primitive bound to a message part, and the trace is not one. Read it for the settings it passes, not for the component.

What done looks like

  • Headings, lists, emphasis and code blocks in a trace render as markdown inside the existing box.
  • The trace carries the same restrictions the answer does. MarkdownText passes linkSafety, allowedProtocols limited to http, https and mailto, an empty allowedImagePrefixes and allowDataImages: false. A trace is model output from the same stream, so it gets the same treatment — copy those settings across rather than deciding them again. One difference: summary-viewer.tsx names mode="static" because its content is already complete, and a trace streams in, so leave the default mode.
  • The box keeps what it has: the fold, the fade, the follow-the-newest-line scrolling, role="region", its accessible name and tabIndex={0}.
  • Delete the Known gaps line from docs/architecture/chat.md if the other half of this split has already merged; if it has not, edit the line down to the switch alone.

What not to change

The trace rides in the message's metadata.custom, not as a content part, so Copy takes the answer alone. That is deliberate and this change must not move it.

How to verify

reply-thinking.test.tsx already renders ReplyThinking directly with a trace string. Write the failing test first: a trace containing markdown renders as elements, not as literal characters.

cd surfsense_local/frontend
pnpm test
# from the repo root, because the PR edits a doc
cd ../.. && python scripts/check_docs.py

Before you start

Read CONTRIBUTING.md and the chat doc. React work in surfsense_local/frontend follows the frontend-workflow skill described in AGENTS.md. Open your PR against dev, not main. Comment here to claim this.

Activity

  1. ybai08 commented on Sep 29, 2026

    @ybai08
    Contributor

    I'd like to take this. Plan: failing test first in reply-thinking.test.tsx (a trace with a heading, a list and a code fence renders as elements, not literal characters), then Streamdown with the shared streamdown-config plugins and the same link/image restrictions MarkdownText passes, default mode since the trace streams; box, fold, fade, scrolling and the region/name/tabIndex untouched; the chat doc's gap line edited down to the switch half unless #1950 has landed.

  2. added a commit that references this issue on Sep 29, 2026
    3db6b0a
  3. MODSetter commented on Sep 29, 2026

    @MODSetter
    OwnerAuthor

    Fixed by #2049, feat(local): render the thinking trace as markdown, by @ybai08. Merged into dev.

    Closing by hand rather than by keyword: Fixes #1949 only auto-closes on a merge to the default branch, and this repo merges through dev (CONTRIBUTING), so this would have stayed open and claimable while the work was already done. It reaches main with the next release.

    Nothing left to pick up here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions