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
+9
View File
@@ -27,6 +27,7 @@ export const useChatStore = defineStore("chat", () => {
const loading = ref(false);
const streaming = ref(false);
const streamingContent = ref("");
const streamingThinking = ref("");
const streamingToolCalls = ref<ToolCallRecord[]>([]);
const streamingStatus = ref("");
const streamingPendingTool = ref<ToolPendingRecord | null>(null);
@@ -206,6 +207,9 @@ export const useChatStore = defineStore("chat", () => {
case "status":
streamingStatus.value = event.data.status as string;
break;
case "thinking_chunk":
streamingThinking.value += event.data.chunk as string;
break;
case "chunk":
streamingContent.value += event.data.chunk as string;
streamingStatus.value = "";
@@ -235,11 +239,13 @@ export const useChatStore = defineStore("chat", () => {
: null,
created_at: new Date().toISOString(),
timing: event.data.timing as GenerationTiming | undefined,
thinking: streamingThinking.value || undefined,
};
if (currentConversation.value?.id === convId) {
currentConversation.value.messages.push(assistantMsg);
}
streamingContent.value = "";
streamingThinking.value = "";
streamingToolCalls.value = [];
streamingStatus.value = "";
streamingPendingTool.value = null;
@@ -257,6 +263,7 @@ export const useChatStore = defineStore("chat", () => {
gotDone = true;
streaming.value = false;
streamingContent.value = "";
streamingThinking.value = "";
streamingToolCalls.value = [];
streamingStatus.value = "";
streamingPendingTool.value = null;
@@ -296,6 +303,7 @@ export const useChatStore = defineStore("chat", () => {
streaming.value = false;
streamingContent.value = "";
streamingThinking.value = "";
streamingToolCalls.value = [];
streamingStatus.value = "";
streamingPendingTool.value = null;
@@ -397,6 +405,7 @@ export const useChatStore = defineStore("chat", () => {
loading,
streaming,
streamingContent,
streamingThinking,
streamingToolCalls,
streamingStatus,
streamingPendingTool,