@@ -57,6 +57,34 @@ const cleanReasoning = (content: string) => {
5757 . trim ( )
5858}
5959
60+ // Helper function to extract content within <think> tags and strip all auxiliary tags from the final output
61+ const extractContentAndClean = (
62+ rawText : string
63+ ) : { reasoningText : string ; finalOutput : string } => {
64+ // Regex to match content within <think>...</think> tags
65+ const thinkTagRegex = / < t h i n k > ( [ \s \S ] * ?) < \/ t h i n k > / g
66+
67+ let reasoningText = ''
68+ let finalOutput = rawText
69+
70+ // Extract content within <think> tags for streamedReasoningText
71+ const thinkMatches = [ ...rawText . matchAll ( thinkTagRegex ) ]
72+ if ( thinkMatches . length > 0 ) {
73+ // Join all reasoning parts separated by newlines
74+ reasoningText = thinkMatches
75+ . map ( ( match ) => match [ 1 ] )
76+ . join ( '\n\n' )
77+ . trim ( )
78+ }
79+
80+ // 2. Strip ALL auxiliary tags from finalOutput
81+ finalOutput = finalOutput
82+ . replace ( thinkTagRegex , '' ) // Remove <think> tags and content
83+ . trim ( )
84+
85+ return { reasoningText, finalOutput }
86+ }
87+
6088const CopyButton = ( { text } : { text : string } ) => {
6189 const [ copied , setCopied ] = useState ( false )
6290 const { t } = useTranslation ( )
@@ -140,15 +168,38 @@ export const ThreadContent = memo(
140168 return { files : [ ] , cleanPrompt : text }
141169 } , [ text , item . role ] )
142170
171+ type StreamEvent = {
172+ timestamp : number
173+ type : 'reasoning_chunk' | 'tool_call' | 'tool_output'
174+ data : any
175+ }
176+
143177 const {
144178 finalOutputText,
145179 streamedReasoningText,
146180 isReasoningActiveLoading,
147181 hasReasoningSteps,
148182 } = useMemo ( ( ) => {
149- // With the streaming functions updated, the text variable now only contains the final output.
150- const currentFinalText = text . trim ( )
151- const currentReasoning = '' // Reasoning is now only derived from streamEvents/allSteps
183+ let currentFinalText = text . trim ( )
184+ let currentReasoning = '' // Reasoning is now only derived from streamEvents/allSteps
185+
186+ // Extract raw streamEvents and check for finalized state
187+ const streamEvents = ( item . metadata ?. streamEvents as StreamEvent [ ] ) || [ ]
188+ const isMessageFinalized = ! isStreamingThisThread
189+
190+ // If the message is finalized AND there are no streamEvents,
191+ // we assume the 'text' contains the full dump (reasoning + output + tool tags)
192+ if ( isMessageFinalized && streamEvents . length === 0 ) {
193+ // Use the new helper to separate reasoning (from <think>) and clean the final output
194+ const { reasoningText, finalOutput } = extractContentAndClean ( text )
195+ currentFinalText = finalOutput
196+ currentReasoning = reasoningText
197+ } else {
198+ // Otherwise, trust the streamEvents path (if present) or the current text is the final output
199+ // We clean the current text just in case, but it should be clean in streaming mode
200+ const { finalOutput } = extractContentAndClean ( text )
201+ currentFinalText = finalOutput
202+ }
152203
153204 // Check for tool calls or reasoning events in metadata to determine steps/loading
154205 const isToolCallsPresent = ! ! (
@@ -158,15 +209,10 @@ export const ThreadContent = memo(
158209 item . metadata . tool_calls . length > 0
159210 )
160211
161- // Check for any reasoning chunks in the streamEvents
162- const hasReasoningEvents = ! ! (
163- item . metadata &&
164- 'streamEvents' in item . metadata &&
165- Array . isArray ( item . metadata . streamEvents ) &&
166- item . metadata . streamEvents . some (
167- ( e : StreamEvent ) => e . type === 'reasoning_chunk'
168- )
169- )
212+ // Check for any reasoning chunks in the streamEvents OR if we extracted reasoning from text
213+ const hasReasoningEvents =
214+ streamEvents . some ( ( e : StreamEvent ) => e . type === 'reasoning_chunk' ) ||
215+ currentReasoning . length > 0 // Added check for extracted reasoning
170216
171217 const hasSteps = isToolCallsPresent || hasReasoningEvents
172218
@@ -262,12 +308,6 @@ export const ThreadContent = memo(
262308 | { avatar ?: React . ReactNode ; name ?: React . ReactNode }
263309 | undefined
264310
265- type StreamEvent = {
266- timestamp : number
267- type : 'reasoning_chunk' | 'tool_call' | 'tool_output'
268- data : any
269- }
270-
271311 // Constructing allSteps for ThinkingBlock - CHRONOLOGICAL approach
272312 const allSteps : ReActStep [ ] = useMemo ( ( ) => {
273313 const steps : ReActStep [ ] = [ ]
@@ -376,7 +416,7 @@ export const ThreadContent = memo(
376416
377417 const rawReasoningContent = streamedReasoningText || ''
378418 const reasoningParagraphs = rawReasoningContent
379- ? rawReasoningContent
419+ ? rawReasoningContent // streamedReasoningText is now populated from <think> tags if present
380420 . split ( / \n \s * \n / )
381421 . filter ( ( s ) => s . trim ( ) . length > 0 )
382422 . map ( ( content ) => content . trim ( ) )
@@ -653,7 +693,7 @@ export const ThreadContent = memo(
653693 />
654694 ) }
655695
656- { (
696+ {
657697 < div className = "flex items-center gap-2 text-main-view-fg/60 text-xs" >
658698 < div className = { cn ( 'flex items-center gap-2' ) } >
659699 < div
@@ -668,10 +708,10 @@ export const ThreadContent = memo(
668708 item . updateMessage && item . updateMessage ( item , message )
669709 }
670710 />
671- < CopyButton text = { item . content ?. [ 0 ] ?. text . value || '' } />
711+ < CopyButton text = { finalOutputText || '' } /> { ' ' }
712+ { /* Use finalOutputText for copy */ }
672713 < DeleteMessageDialog onDelete = { removeMessage } />
673714 < MessageMetadataDialog metadata = { item . metadata } />
674-
675715 { item . isLastMessage && selectedModel && (
676716 < Tooltip >
677717 < TooltipTrigger asChild >
@@ -697,7 +737,7 @@ export const ThreadContent = memo(
697737 />
698738 </ div >
699739 </ div >
700- ) }
740+ }
701741 </ >
702742 ) }
703743
0 commit comments