Repository navigation
Description
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 theScrollFade, and thewhitespace-pre-wrapinviewportClassName, which markdown makes redundant.- Reuse what the app already has.
studio/viewers/summary-viewer.tsxrenders markdown withStreamdownfromstreamdownand the shared plugin config instreamdown-config.ts. That is the pattern here; no new dependency and no new renderer. message.tsx—MarkdownTextis 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.
MarkdownTextpasseslinkSafety,allowedProtocolslimited to http, https and mailto, an emptyallowedImagePrefixesandallowDataImages: 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.tsxnamesmode="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 andtabIndex={0}. - Delete the Known gaps line from
docs/architecture/chat.mdif 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.pyBefore 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
ybai08 commentedon Sep 29, 2026
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.
feat(local): render the thinking trace as markdown
MODSetter commentedon Sep 29, 2026
Fixed by #2049,
feat(local): render the thinking trace as markdown, by @ybai08. Merged intodev.Closing by hand rather than by keyword:
Fixes #1949only auto-closes on a merge to the default branch, and this repo merges throughdev(CONTRIBUTING), so this would have stayed open and claimable while the work was already done. It reachesmainwith the next release.Nothing left to pick up here.