diff --git a/src/ai/fetch.ts b/src/ai/fetch.ts
index b7ddacfb8..af0bf043f 100644
--- a/src/ai/fetch.ts
+++ b/src/ai/fetch.ts
@@ -709,9 +709,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', () => {