diff --git a/static/messages.js b/static/messages.js
index 284ca5b..0209c2f 100644
--- a/static/messages.js
+++ b/static/messages.js
@@ -84,6 +84,11 @@ async function send(){
let assistantText='';
let assistantRow=null;
let assistantBody=null;
+ // Thinking tag patterns for streaming display
+ const _thinkPairs=[
+ {open:'',close:''},
+ {open:'<|channel>thought\n',close:''}
+ ];
function ensureAssistantRow(){
if(assistantRow)return;
@@ -106,12 +111,36 @@ async function send(){
// rAF-throttled rendering: buffer tokens, render at most once per frame
let _renderPending=false;
+ // Extract display text from assistantText, stripping completed thinking blocks
+ // and hiding content still inside an open thinking block.
+ function _streamDisplay(){
+ const raw=assistantText;
+ for(const {open,close} of _thinkPairs){
+ if(raw.startsWith(open)){
+ const ci=raw.indexOf(close,open.length);
+ if(ci!==-1){
+ // Thinking block complete — strip it, show the rest
+ return raw.slice(ci+close.length).replace(/^\s+/,'');
+ }
+ // Still inside thinking block — show placeholder
+ return '';
+ }
+ // Hide partial tag prefixes while streaming so users don't see
+ // `{
_renderPending=false;
- if(assistantBody) assistantBody.innerHTML=renderMd(assistantText);
+ if(assistantBody){
+ const txt=_streamDisplay();
+ const isThinking=!txt&&assistantText.length>0;
+ assistantBody.innerHTML=txt?renderMd(txt):(isThinking?'Thinking\u2026':'');
+ }
scrollIfPinned();
});
}
diff --git a/static/ui.js b/static/ui.js
index f318a32..3b5ab78 100644
--- a/static/ui.js
+++ b/static/ui.js
@@ -503,6 +503,22 @@ function renderMessages(){
if(!thinkingText && m.reasoning){
thinkingText=m.reasoning;
}
+ // Parse inline thinking tags from plain text: ... (DeepSeek, QwQ, etc.)
+ // and Gemma 4 channel tokens: <|channel>thought\n...
+ if(!thinkingText && typeof content==='string'){
+ const thinkMatch=content.match(/^([\s\S]*?)<\/think>\s*/);
+ if(thinkMatch){
+ thinkingText=thinkMatch[1].trim();
+ content=content.slice(thinkMatch[0].length);
+ }
+ if(!thinkingText){
+ const gemmaMatch=content.match(/^<\|channel>thought\n([\s\S]*?)\s*/);
+ if(gemmaMatch){
+ thinkingText=gemmaMatch[1].trim();
+ content=content.slice(gemmaMatch[0].length);
+ }
+ }
+ }
const isUser=m.role==='user';
const isLastAssistant=!isUser&&vi===visWithIdx.length-1;
// Render thinking card before the assistant message (collapsed by default)