From 263763e15a7bfc512789c05333c9aaa95c1cb3c8 Mon Sep 17 00:00:00 2001 From: Susheel Daswani Date: Tue, 28 Jul 2026 21:58:46 -0700 Subject: [PATCH] fix: stop Thinking chip from trapping answers in reasoning middleware - model.startWithReasoning means show/enable thinking; middleware startWithReasoning means treat content as reasoning until - Ollama thinking models emit a native reasoning field and plain answer content with no closing tag, so wiring the chip flag through left the spinner stuck with the reply in the muted Thinking pane - show a Thinking label while streaming with no active tool Co-authored-by: Cursor --- src/ai/fetch.ts | 11 +++++- src/components/chat/reasoning-group-title.tsx | 36 +++++++++---------- src/components/chat/reasoning-group.test.tsx | 3 ++ 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/ai/fetch.ts b/src/ai/fetch.ts index 2453ab56b..c5a4044d7 100644 --- a/src/ai/fetch.ts +++ b/src/ai/fetch.ts @@ -708,9 +708,18 @@ export const aiFetchStreamingResponse = async ({ providerId: model.provider, model: baseModel, middleware: [ + // Extract `...` from content when present. Do NOT pass + // `startWithReasoning` from `model.startWithReasoning` — that DB flag + // means “model can think / show Thinking chip”, while the middleware + // option means “treat all content as reasoning until ”. + // Ollama thinking models emit a native `reasoning` field and plain + // answer `content` with no closing tag; wiring the chip flag through + // here classifies the answer as reasoning forever and never emits + // reasoning-end (stuck spinner with the reply stuck in the muted + // Thinking pane). extractReasoningMiddleware({ tagName: 'think', - startWithReasoning: Boolean(model.startWithReasoning), + startWithReasoning: false, }), ], }) diff --git a/src/components/chat/reasoning-group-title.tsx b/src/components/chat/reasoning-group-title.tsx index 5372f5e19..886d23c12 100644 --- a/src/components/chat/reasoning-group-title.tsx +++ b/src/components/chat/reasoning-group-title.tsx @@ -34,30 +34,30 @@ const activeToolLabel = (tool: ToolOrDynamicToolUIPart, mcpTools?: UIMessageMeta export const ReasoningGroupTitle = ({ totalDuration, isGroupReasoning, tools, mcpTools }: ReasoningGroupTitleProps) => { const activeIndex = tools.length - 1 const activeTool = tools[activeIndex] - const loadingLabel = activeTool ? activeToolLabel(activeTool, mcpTools) : null + // While streaming with no tool yet, still show a label — an empty title next + // to the spinner reads as a hung chat (common for thinking-only turns). + const loadingLabel = activeTool ? activeToolLabel(activeTool, mcpTools) : 'Thinking' return (
{isGroupReasoning ? ( - loadingLabel ? ( - + - - {loadingLabel} - - - ) : null + {loadingLabel} + + ) : ( { // ReasoningDisplay should render the reasoning text when streaming // It might be in a specific container, so check the container expect(container.textContent).toContain('Let me think about this...') + // Thinking-only turns must keep a visible status label (empty title + spinner + // reads as a hung chat). + expect(screen.getByTestId('tool-status')).toHaveTextContent('Thinking') }) it('should not render ReasoningDisplay when hasTextPart is true', () => {