@@ -9,8 +9,6 @@ interface SplitMessage {
99 content : PendingMessage [ 'content' ] ;
1010 thought ?: string ;
1111 isThinking ?: boolean ;
12- toolOutput ?: string ;
13- toolTitle ?: string ;
1412}
1513
1614export default function ChatMessage ( {
@@ -51,71 +49,40 @@ export default function ChatMessage({
5149 const prevSibling = siblingLeafNodeIds [ siblingCurrIdx - 1 ] ;
5250
5351 // for reasoning model, we split the message into content, thought, and tool output
54- const { content, thought, isThinking, toolOutput, toolTitle } : SplitMessage =
55- useMemo ( ( ) => {
56- if ( msg . content === null || msg . role !== 'assistant' ) {
57- return { content : msg . content } ;
58- }
59- let currentContent = msg . content ;
60- let extractedThought : string | undefined = undefined ;
61- let isCurrentlyThinking = false ;
62- let extractedToolOutput : string | undefined = undefined ;
63- let extractedToolTitle : string | undefined = 'Tool Output' ;
64-
65- // Process <think> tags
66- const thinkParts = currentContent . split ( '<think>' ) ;
67- currentContent = thinkParts [ 0 ] ;
68- if ( thinkParts . length > 1 ) {
69- isCurrentlyThinking = true ;
70- const tempThoughtArray : string [ ] = [ ] ;
71- for ( let i = 1 ; i < thinkParts . length ; i ++ ) {
72- const thinkSegment = thinkParts [ i ] . split ( '</think>' ) ;
73- tempThoughtArray . push ( thinkSegment [ 0 ] ) ;
74- if ( thinkSegment . length > 1 ) {
75- isCurrentlyThinking = false ; // Closing tag found
76- currentContent += thinkSegment [ 1 ] ;
77- }
78- }
79- extractedThought = tempThoughtArray . join ( '\n' ) ;
80- }
52+ const { content, thought, isThinking } : SplitMessage = useMemo ( ( ) => {
53+ if (
54+ msg . content === null ||
55+ ( msg . role !== 'assistant' && msg . role !== 'tool' )
56+ ) {
57+ return { content : msg . content } ;
58+ }
8159
82- // Process <tool> tags (after thoughts are processed)
83- const toolParts = currentContent . split ( '<tool>' ) ;
84- currentContent = toolParts [ 0 ] ;
85- if ( toolParts . length > 1 ) {
86- const tempToolOutputArray : string [ ] = [ ] ;
87- for ( let i = 1 ; i < toolParts . length ; i ++ ) {
88- const toolSegment = toolParts [ i ] . split ( '</tool>' ) ;
89- const toolContent = toolSegment [ 0 ] . trim ( ) ;
60+ let actualContent = '' ;
61+ let thought = '' ;
62+ let isThinking = false ;
63+ let thinkSplit = msg . content . split ( '<think>' , 2 ) ;
9064
91- const firstLineEnd = toolContent . indexOf ( '\n' ) ;
92- if ( firstLineEnd !== - 1 ) {
93- extractedToolTitle = toolContent . substring ( 0 , firstLineEnd ) ;
94- tempToolOutputArray . push (
95- toolContent . substring ( firstLineEnd + 1 ) . trim ( )
96- ) ;
97- } else {
98- // If no newline, extractedToolTitle keeps its default; toolContent is pushed as is.
99- tempToolOutputArray . push ( toolContent ) ;
100- }
65+ actualContent += thinkSplit [ 0 ] ;
10166
102- if ( toolSegment . length > 1 ) {
103- currentContent += toolSegment [ 1 ] ;
104- }
105- }
106- extractedToolOutput = tempToolOutputArray . join ( '\n\n' ) ;
67+ while ( thinkSplit [ 1 ] !== undefined ) {
68+ // <think> tag found
69+ thinkSplit = thinkSplit [ 1 ] . split ( '</think>' , 2 ) ;
70+ thought += thinkSplit [ 0 ] ;
71+ isThinking = true ;
72+ if ( thinkSplit [ 1 ] !== undefined ) {
73+ // </think> closing tag found
74+ isThinking = false ;
75+ thinkSplit = thinkSplit [ 1 ] . split ( '<think>' , 2 ) ;
76+ actualContent += thinkSplit [ 0 ] ;
10777 }
78+ }
10879
109- return {
110- content : currentContent . trim ( ) ,
111- thought : extractedThought ,
112- isThinking : isCurrentlyThinking ,
113- toolOutput : extractedToolOutput ,
114- toolTitle : extractedToolTitle ,
115- } ;
116- } , [ msg ] ) ;
80+ return { content : actualContent , thought, isThinking } ;
81+ } , [ msg ] ) ;
11782 if ( ! viewingChat ) return null ;
11883
84+ const toolCalls = msg . tool_calls ?? null ;
85+
11986 return (
12087 < div className = "group" id = { id } >
12188 < div
@@ -165,8 +132,12 @@ export default function ChatMessage({
165132 < >
166133 { content === null ? (
167134 < >
168- { /* show loading dots for pending message */ }
169- < span className = "loading loading-dots loading-md" > </ span >
135+ { toolCalls ? null : (
136+ < >
137+ { /* show loading dots for pending message */ }
138+ < span className = "loading loading-dots loading-md" > </ span >
139+ </ >
140+ ) }
170141 </ >
171142 ) : (
172143 < >
@@ -232,27 +203,32 @@ export default function ChatMessage({
232203 content = { content }
233204 isGenerating = { isPending }
234205 />
235-
236- { toolOutput && (
237- < details
238- className = "collapse bg-base-200 collapse-arrow mb-4"
239- open = { true } // todo: make this configurable like showThoughtInProgress
240- >
241- < summary className = "collapse-title" >
242- < b > { toolTitle || 'Tool Output' } </ b >
243- </ summary >
244- < div className = "collapse-content" >
245- < MarkdownDisplay
246- content = { toolOutput }
247- // Tool output is not "generating" in the same way
248- isGenerating = { false }
249- />
250- </ div >
251- </ details >
252- ) }
253206 </ div >
254207 </ >
255208 ) }
209+ { toolCalls &&
210+ toolCalls . map ( ( toolCall , i ) => (
211+ < details
212+ key = { i }
213+ className = "collapse bg-base-200 collapse-arrow mb-4"
214+ open = { false } // todo: make this configurable like showThoughtInProgress
215+ >
216+ < summary className = "collapse-title" >
217+ < b > Tool call:</ b > { toolCall . function . name }
218+ </ summary >
219+
220+ < div className = "collapse-content" >
221+ < div className = "font-bold mb-1" > Arguments:</ div >
222+ < pre className = "whitespace-pre-wrap bg-base-300 p-2 rounded" >
223+ { JSON . stringify (
224+ JSON . parse ( toolCall . function . arguments ) ,
225+ null ,
226+ 2
227+ ) }
228+ </ pre >
229+ </ div >
230+ </ details >
231+ ) ) }
256232 { /* render timings if enabled */ }
257233 { timings && config . showTokensPerSecond && (
258234 < div className = "dropdown dropdown-hover dropdown-top mt-2" >
0 commit comments