diff --git a/apps/desktop/src/components/assistant-ui/streaming.test.tsx b/apps/desktop/src/components/assistant-ui/streaming.test.tsx
index c15b4696a21..08dba733ae1 100644
--- a/apps/desktop/src/components/assistant-ui/streaming.test.tsx
+++ b/apps/desktop/src/components/assistant-ui/streaming.test.tsx
@@ -164,6 +164,27 @@ function assistantMultiReasoningMessage(texts: string[]): ThreadMessage {
} as ThreadMessage
}
+function assistantSeparatedReasoningMessage(): ThreadMessage {
+ return {
+ id: 'assistant-reasoning-separated-1',
+ role: 'assistant',
+ content: [
+ { type: 'reasoning', text: ' Complete first thought.', status: { type: 'complete' } },
+ { type: 'text', text: 'Interim answer.' },
+ { type: 'reasoning', text: ' Streaming second thought.', status: { type: 'running' } }
+ ],
+ status: { type: 'running' },
+ createdAt,
+ metadata: {
+ unstable_state: null,
+ unstable_annotations: [],
+ unstable_data: [],
+ steps: [],
+ custom: {}
+ }
+ } as ThreadMessage
+}
+
function assistantTodoMessage(
todos: Array<{ content: string; id: string; status: 'cancelled' | 'completed' | 'in_progress' | 'pending' }>,
running = true
@@ -685,6 +706,18 @@ describe('assistant-ui streaming renderer', () => {
expect(reasoningParts[1]?.textContent).toBe('Second thought.')
})
+ it('does not reopen an earlier completed thinking group when a later group is running', () => {
+ const { container } = render()
+
+ const disclosures = container.querySelectorAll('[data-slot="aui_thinking-disclosure"]')
+ expect(disclosures.length).toBe(2)
+
+ expect(disclosures[0].querySelector('button')?.getAttribute('aria-expanded')).toBe('false')
+ expect(disclosures[1].querySelector('button')?.getAttribute('aria-expanded')).toBe('true')
+ expect(container.textContent).not.toContain('Complete first thought.')
+ expect(container.textContent).toContain('Interim answer.')
+ })
+
it('renders live todo rows during a running turn', () => {
const { container } = render(
s.thread.isRunning &&
s.message.status?.type === 'running' &&
- s.message.parts.slice(Math.max(0, startIndex)).some(p => p?.type === 'reasoning' && p.status?.type !== 'complete')
+ s.message.parts
+ .slice(Math.max(0, startIndex), endIndex + 1)
+ .some(p => p?.type === 'reasoning' && p.status?.type !== 'complete')
)
// A reasoning group with no actual text is pure noise — drop the whole