Skip to content

feat(local): render the thinking trace as markdown - #2049

Merged
MODSetter merged 1 commit into
MODSetter:devfrom
ybai08:feat/thinking-trace-markdown
Sep 29, 2026
Merged

MODSetter merged 1 commit into
MODSetter:devfrom
ybai08:feat/thinking-trace-markdown

Conversation

@ybai08

@ybai08 ybai08 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What

The thinking trace in reply-thinking.tsx renders as markdown: headings, lists, emphasis and fenced code inside the existing box. It uses Streamdown with the shared streamdownPlugins (code and math), linkSafety on, and the answer's restrictions from message.tsx: http, https and mailto links only, no images, no data images. Default mode, since a trace streams in. whitespace-pre-wrap is dropped from the viewport class; markdown handles line breaks.

One thing worth knowing: the plain Streamdown component has no security prop; that belongs to the assistant-ui primitive, which turns it into a rehype plugin list. So the trace passes rehypePlugins built from Streamdown's exported defaultRehypePlugins: its raw and sanitize passes as they are, and its harden pass re-run with the answer's options. Same passes, same order, stricter limits.

The box keeps everything it had: the fold, the fade, following the newest line, role="region", its accessible name and tabIndex={0}. The trace still rides in metadata.custom, so Copy still takes the answer alone.

docs/architecture/chat.md: the Known gaps line is cut down to the thinking switch, since #1950 has not landed, and the trace paragraph says it renders as markdown with the answer's restrictions.

Why

The answer directly below the trace is rendered, so a trace full of literal ## and ** read as broken rather than plain. Now the same model's output gets the same treatment on both sides of the fold.

Fixes #1949

How to test

cd surfsense_local/frontend
pnpm test
cd ../.. && python scripts/check_docs.py

Two new tests in reply-thinking.test.tsx, written first: a trace with a heading, a list, emphasis and a code fence renders as elements with no literal syntax left, and a javascript: link, a data-URI image and an https link come out as blocked text, blocked image and the one link. The two scroll-following tests now find the viewport by its region role, since the text no longer sits directly in it. Locally: chat feature 27/27, whole frontend suite green, eslint, prettier --check, pnpm typecheck and check_docs.py clean.

High-level PR Summary

This PR enhances the thinking trace display in the chat interface by rendering it as markdown instead of plain text. The trace now supports headings, lists, emphasis, and code blocks using the Streamdown component with the same security restrictions as the answer (allowing only http, https, and mailto links, no images). The implementation reuses the shared streamdownPlugins and carefully reconstructs the rehype plugin chain to apply the answer's stricter link and image policies while maintaining all existing functionality like scroll-following, folding, and accessibility features.

⏱️ Estimated Review Time: 5-15 minutes

💡 Review Order Suggestion
Order File Path
1 surfsense_local/frontend/src/features/chat/reply-thinking.test.tsx
2 surfsense_local/frontend/src/features/chat/reply-thinking.tsx
3 docs/architecture/chat.md

Need help? Join our Discord

A thinking model writes its trace in markdown, and the box showed it as
literal ##, ** and fences under an answer that renders properly. The trace
now goes through Streamdown with the shared code and math plugins, the same
link safety as the answer, and the answer's harden limits: http, https and
mailto links only, no images, no data images. Streamdown's own prop is a
rehype plugin list, so the limits are its default raw and sanitize passes
plus its harden pass re-run with those options, the way the assistant-ui
primitive builds them for the answer. Default mode, since the trace streams.

The box keeps its fold, fade, follow-the-newest-line scrolling, region role,
name and tabIndex; whitespace-pre-wrap goes, markdown handles it. The scroll
tests find the viewport by its role now that the text sits inside elements.
The chat doc's gap line is cut down to the thinking switch (MODSetter#1950).

Fixes MODSetter#1949
@vercel

vercel Bot commented Sep 29, 2026

Copy link
Copy Markdown

@ybai08 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 6d04d083-a247-433a-8205-26a6393e9850

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MODSetter
MODSetter merged commit d87b692 into MODSetter:dev Sep 29, 2026
17 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants