Show Qwen3 thinking output in chat as collapsible Reasoning block

Ollama streams message.thinking tokens alongside message.content when
think=True — previously silently dropped. Now forwarded end-to-end.

Backend:
- llm.py: ChatChunk type gains "thinking" variant; stream_chat_with_tools
  yields ChatChunk(type="thinking") for msg.thinking chunks before content
- generation_task.py: thinking chunks emit "thinking_chunk" SSE events
  (not added to content_so_far — not persisted to DB)

Frontend:
- types/chat.ts: Message.thinking?: string (session-only, not from DB)
- stores/chat.ts: streamingThinking ref; thinking_chunk handler accumulates
  chunks; on done, thinking carried into committed Message object then cleared
- ChatMessage.vue: collapsible <details class="thinking-block"> shown for
  messages that have .thinking content (collapsed by default)
- ChatView.vue + ChatPanel.vue: live thinking block in streaming bubble —
  open while only thinking is flowing, auto-collapses when content arrives;
  typing indicator hidden while thinking is active

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 23:16:59 -05:00
parent 5e83c8a56d
commit 432e0bd2a0
7 changed files with 171 additions and 5 deletions
@@ -442,7 +442,10 @@ async def run_generation(
cancelled = True
break
if chunk.type == "content":
if chunk.type == "thinking":
buf.append_event("thinking_chunk", {"chunk": chunk.content})
elif chunk.type == "content":
if timing["ttft_ms"] is None:
timing["ttft_ms"] = int((time.monotonic() - t_start) * 1000)
buf.content_so_far += chunk.content
+6 -1
View File
@@ -131,7 +131,7 @@ async def stream_chat(
@dataclass
class ChatChunk:
"""A chunk yielded by stream_chat_with_tools."""
type: Literal["content", "tool_calls", "done"]
type: Literal["content", "thinking", "tool_calls", "done"]
content: str = ""
tool_calls: list[dict] | None = None
@@ -178,6 +178,11 @@ async def stream_chat_with_tools(
data = json.loads(line)
msg = data.get("message", {})
# Thinking chunks (qwen3 chain-of-thought, only when think=True)
thinking = msg.get("thinking", "")
if thinking:
yield ChatChunk(type="thinking", content=thinking)
# Content chunks
chunk = msg.get("content", "")
if chunk: