feat: add support for displaying thinking/reasoning blocks in chat - #181
feat: add support for displaying thinking/reasoning blocks in chat#181TaraTheStar wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Full Review: PR #181 — thinking/reasoning block display
Thanks @TaraTheStar — nice continuation of the reasoning display work. This cleanly covers the inline-tag formats that PR #169 didn't handle.
Security Audit
Clean. Streaming path strips tags before passing to renderMd() (which escapes HTML). History path extracts thinking text into thinkingText which is rendered via ${esc(thinkingText)} in the existing <pre> block. "Thinking..." placeholder is hardcoded with no user data. No XSS vectors.
Code Review
Both paths are well-implemented:
- Streaming (
_streamDisplay()): correctly buffers, detects open/closed thinking blocks, shows placeholder while thinking, strips completed blocks. ThestartsWithcheck is the right conservative choice — avoids false positives on<think>in code examples. - History (
renderMessages()):^-anchored regex is consistent with streaming path.slice()to remove matched block is correct.
The three reasoning formats are now fully covered:
- Structured content arrays (Claude, o3) — existing
- Top-level
m.reasoning(Hermes) — PR #169 - Inline
<think>/<|channel>thoughttags (DeepSeek, QwQ, Gemma 4) — this PR
Tests
474 passed, 0 regressions.
Verdict
Approved. Clean, focused, safe. Ready to merge.
nesquena-hermes
left a comment
There was a problem hiding this comment.
Thanks for this PR, @TaraTheStar! Adding inline <think> tag support is a meaningful feature — DeepSeek, QwQ, and Gemma 4 users all benefit. The approach is solid and fits naturally alongside the existing structured thinking block support (Claude extended thinking, o3 reasoning). I reviewed the diff carefully against current master and ran the full test suite.
Test results
499/499 pass on this branch. ✅
What works well
Streaming path (messages.js)
The _thinkPairs array and _streamDisplay() function are clean. The logic — show empty string while inside an open thinking block, strip the block once closed and show the remainder — is exactly right for the streaming case. The isThinking flag driving the Thinking… placeholder is a nice touch and matches the visual style already in the codebase.
History/reload path (ui.js)
Adding the inline tag parse after the existing structured block checks (Array.isArray(content) + m.reasoning) is the right order of precedence. Structured blocks (from Claude's API) should take priority, and they do.
The regex ^<think>([\s\S]*?)<\/think>\s* correctly anchors to the start of the string, uses non-greedy match for the content, and strips trailing whitespace before the answer. The content = content.slice(thinkMatch[0].length) correctly advances past the block so the answer renders clean.
Issues to address
1. Streaming: partial <think> tag briefly visible during streaming
The _streamDisplay() function only hides content once it startsWith(open) with the full opening tag. If tokens arrive one or two characters at a time, the user will briefly see <thi, <thin, <think rendered in the message bubble before the full <think> token arrives and the function switches to hiding mode.
This is a minor UX issue — it resolves itself within a few tokens — but it can look odd. A simple fix is to also hide content when it starts with a prefix of the open tag:
function _streamDisplay(){
let txt=assistantText;
for(const {open,close} of _thinkPairs){
if(txt.startsWith(open)){
const ci=txt.indexOf(close,open.length);
if(ci!==-1) return txt.slice(ci+close.length).replace(/^\s+/,'');
return ''; // inside open block
}
// Partial open tag — hide until we know what this is
if(open.startsWith(txt)) return '';
}
return txt;
}The added line if(open.startsWith(txt)) return '' suppresses display whenever the accumulated text is a valid prefix of any open tag (e.g. <, <t, <th, etc.). Since these are highly specific tokens (<think>, <|channel>), false positives in normal text would be vanishingly rare and resolve within one frame anyway.
2. Forward-compatibility with #177 (i18n)
Once PR #177 (i18n/Chinese localization) merges, i18n.js will define a global t() function. Two things in this PR will need minor fixes at that point:
-
function _streamDisplay()useslet t = assistantText— this local variable shadows the globalt(). It works fine today (no globalton master), but it's a latent footgun. Renaming tolet txt = assistantTextorlet raw = assistantTextavoids the collision entirely. -
The
Thinking\u2026placeholder string in_scheduleRenderis hardcoded in English. After #177 merges, it should uset('thinking')consistent with how the thinking card label inui.jsuses it. (The i18n branch already hast('thinking')defined.)
These don't need to block this PR if it merges first — but flagging so whoever merges doesn't forget the follow-up.
3. No test coverage for the new parsing logic
The JS logic runs client-side so the pytest suite doesn't cover it, which is expected. But it's worth noting that both the _streamDisplay() function and the renderMessages() inline parser have no automated tests. If you wanted to add any, the tests/ directory already has a pattern for JS behavior assertions via the test server. Not a blocker, just a suggestion for future work.
Summary
The core feature is correct and well-implemented. The one concrete code issue worth fixing before merge is the partial tag flicker (#1). The let t rename (#2) is low-effort and worth doing now to avoid the merge headache later. No security concerns, no regressions, tests pass.
Hide partial <think> tag prefixes during streaming and rename the local display variable for clarity. References #181.
PR #181 Review — feat: add support for displaying thinking/reasoning blocks in chatVerdict: CHANGES REQUESTED (minor — most issues are already addressed by the follow-up #182, but two ui.js edge cases remain unaddressed) SummaryThis PR adds client-side rendering of thinking/reasoning blocks for models that emit inline think tags (
The feature is broadly correct, well-scoped, and the approach (extending the existing thinking-card infrastructure) is the right one. Security handling is sound for the static render path. The main issues are one unfixed streaming edge case in File-by-file findings
|
|
Merged, thanks! |
Hide partial <think> tag prefixes during streaming and rename the local display variable for clarity. References nesquena#181.
Hide partial <think> tag prefixes during streaming and rename the local display variable for clarity. References nesquena#181.
This PR adds support for displaying thinking/reasoning blocks in the chat interface. It handles both standard tags and Gemma 4-style channel tokens, providing a clean 'Thinking...' placeholder during streaming and stripping/displaying the reasoning once completed.