@@ -98,10 +98,10 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) {
9898 currentStream := rc .stream
9999 rc .stream = nil
100100 rc .inToolPhase = true
101- rc .thinkingDone = false // allow new thinking in next iteration
102- rc .thinkingBuffer = "" // reset thinking buffer for new iteration
103- rc .hasThinking = false // new iteration starts fresh
104- rc .tagParseSkipped = false // re-enable tag parsing for next iteration
101+ rc .thinkingDone = false // allow new thinking in next iteration
102+ rc .thinkingBuffer = "" // reset thinking buffer for new iteration
103+ rc .hasThinking = false // new iteration starts fresh
104+ rc .tagParsePending = "" // discard any incomplete tag from the previous iteration
105105 rc .mu .Unlock ()
106106 if currentStream != nil {
107107 if err := currentStream .Stop (ctx ); err != nil {
@@ -135,19 +135,22 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) {
135135 // Fallback <think> tag parsing: for providers that embed thinking
136136 // in the content stream (DeepSeek-via-OpenRouter, Qwen, some Ollama models).
137137 // Only activates when no native ChatEventThinking was received.
138- if ! rc .hasThinking && ! rc .thinkingDone && ! rc .tagParseSkipped {
139- candidate := rc .streamBuffer + content
138+ if ! rc .hasThinking && ! rc .thinkingDone {
139+ previousAnswer := rc .streamBuffer
140+ candidate := rc .streamBuffer + rc .tagParsePending + content
140141 split := SplitThinkTags (candidate )
141- if split .Thinking != "" {
142+ if split .Found || split . Pending != "" {
142143 // Found think tags — commit to buffer and route or suppress reasoning
143144 // before any tagged content can leak into the answer lane.
144145 displayReasoningInStream := rc .ReasoningDelivery .ShowInChannel && ! rc .ReasoningDelivery .BubbleDelivery && sc .ReasoningStreamEnabled ()
145146 previousThinking := rc .thinkingBuffer
146- rc .streamBuffer = candidate
147147 rc .thinkingBuffer = split .Thinking
148148 thinkText := rc .thinkingBuffer
149149 currentStream := rc .stream
150- if split .Partial {
150+ if split .Partial || split .Pending != "" {
151+ rc .streamBuffer = split .Answer
152+ rc .tagParsePending = split .Pending
153+ answerText := rc .streamBuffer
151154 // Still inside <think> — wait for the close tag before streaming
152155 // answer content. Native thinking uses hasThinking; tag parsing
153156 // keeps it false until close so later chunks continue parsing.
@@ -158,9 +161,12 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) {
158161 }
159162 } else if displayReasoningInStream && currentStream != nil {
160163 currentStream .Update (ctx , formatReasoningPreview (thinkText ))
164+ } else if currentStream != nil {
165+ currentStream .Update (ctx , answerText )
161166 }
162167 break
163168 }
169+ rc .tagParsePending = ""
164170 // Tag closed — transition to answer, or strip reasoning entirely
165171 // when Show Reasoning is off.
166172 answerText := split .Answer
@@ -177,7 +183,7 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) {
177183 m .flushReasoningBubbles (runID )
178184 }
179185
180- if ! displayReasoningInStream {
186+ if previousAnswer != "" || ! displayReasoningInStream {
181187 if reasoningStream != nil && answerText != "" {
182188 reasoningStream .Update (ctx , answerText )
183189 }
@@ -209,9 +215,6 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) {
209215 }
210216 break
211217 }
212- // No think tags found — mark as skipped so we don't re-parse.
213- // Don't commit to streamBuffer here — the normal flow below appends content.
214- rc .tagParseSkipped = true
215218 }
216219
217220 // Reasoning→answer transition: first chunk after native thinking events.
0 commit comments