@@ -205,12 +205,21 @@ function TruncatedResult({
205205 t
206206} : DetailRow & { branch ?: TreeBranch ; rails ?: TreeRails ; t : Theme } ) {
207207 const [ expanded , setExpanded ] = useState ( false )
208- const text = typeof content === 'string' ? content : String ( content ?? '' )
208+ const text = typeof content === 'string' ? content : ''
209+ const isReactNode = ! text && content
209210 const lines = useMemo ( ( ) => text . split ( '\n' ) , [ text ] )
210- const truncated = lines . length > RESULT_PREVIEW_LINES
211+ const truncated = ! isReactNode && lines . length > RESULT_PREVIEW_LINES
211212 const visible = expanded ? lines : lines . slice ( 0 , RESULT_PREVIEW_LINES )
212213 const hidden = lines . length - RESULT_PREVIEW_LINES
213214
215+ if ( isReactNode ) {
216+ return (
217+ < TreeRow branch = { branch } rails = { rails } t = { t } >
218+ { content as ReactNode }
219+ </ TreeRow >
220+ )
221+ }
222+
214223 return (
215224 < Box flexDirection = "column" >
216225 { visible . map ( ( line , i ) => (
@@ -227,7 +236,7 @@ function TruncatedResult({
227236 { truncated && ! expanded ? (
228237 < TreeRow branch = { branch } rails = { rails } t = { t } >
229238 < Box onClick = { ( ) => setExpanded ( true ) } >
230- < Text color = { t . color . accent } > ... +{ hidden } lines (click to expand)</ Text >
239+ < Text color = { t . color . muted } dim > ... +{ hidden } lines (click to expand)</ Text >
231240 </ Box >
232241 </ TreeRow >
233242 ) : null }
@@ -297,12 +306,11 @@ function Chevron({
297306 title : string
298307 tone ?: 'dim' | 'error' | 'warn'
299308} ) {
300- const color = tone === 'error' ? t . color . error : tone === 'warn' ? t . color . warn : t . color . muted
309+ const color = tone === 'error' ? t . color . error : tone === 'warn' ? t . color . warn : t . color . text
301310
302311 return (
303312 < Box onClick = { ( e : any ) => onClick ( ! ! e ?. shiftKey || ! ! e ?. ctrlKey ) } >
304- < Text color = { color } dim = { tone === 'dim' } >
305- < Text color = { t . color . accent } > { open ? '▾ ' : '▸ ' } </ Text >
313+ < Text color = { color } >
306314 { title }
307315 { typeof count === 'number' ? ` (${ count } )` : '' }
308316 { suffix ? (
@@ -792,7 +800,7 @@ export const ToolTrail = memo(function ToolTrail({
792800 // sections (regression caught after #14968).
793801 const [ openThinking , setOpenThinking ] = useState ( visible . thinking === 'expanded' )
794802 const thinkingUserToggledRef = useRef ( false )
795- const [ openTools , setOpenTools ] = useState ( visible . tools === 'expanded' )
803+ const [ openTools , setOpenTools ] = useState ( true )
796804 const toolsUserToggledRef = useRef ( false )
797805 const [ openSubagents , setOpenSubagents ] = useState ( visible . subagents === 'expanded' )
798806 const [ deepSubagents , setDeepSubagents ] = useState ( visible . subagents === 'expanded' )
@@ -864,7 +872,7 @@ export const ToolTrail = memo(function ToolTrail({
864872
865873 if ( parsed . detail ) {
866874 pushDetail ( {
867- color : parsed . mark === '✗' ? t . color . error : t . color . muted ,
875+ color : parsed . mark === '✗' ? t . color . error : t . color . text ,
868876 content : parsed . detail ,
869877 dimColor : parsed . mark !== '✗' ,
870878 key : `tr-${ i } -d`
@@ -911,21 +919,32 @@ export const ToolTrail = memo(function ToolTrail({
911919 for ( const tool of tools ) {
912920 const args = parseToolArgs ( tool . verboseArgs ?? '' )
913921 const label = args ?. description ?? formatToolCall ( tool . name , tool . verboseArgs || tool . context || '' )
922+ const hasDescription = Boolean ( args ?. description )
914923 const details : DetailRow [ ] = [ ]
915924 if ( args ?. command ) {
916- details . push ( {
917- color : t . color . muted ,
918- content : formatToolCall ( tool . name , args . command ) ,
919- dimColor : true ,
920- key : `${ tool . id } -cmd`
921- } )
925+ if ( hasDescription ) {
926+ // Header shows the description — detail shows the tool call.
927+ const toolLine = formatToolCall ( tool . name , args . command )
928+ details . push ( {
929+ color : t . color . muted ,
930+ content : toolLine ,
931+ dimColor : true ,
932+ key : `${ tool . id } -cmd`
933+ } )
934+ }
935+ // Without a description the header already shows the tool call —
936+ // no need to duplicate it in the detail.
922937 } else if ( tool . verboseArgs ) {
923- details . push ( {
924- color : t . color . muted ,
925- content : `Args:\n${ boundedLiveRenderText ( tool . verboseArgs ) } ` ,
926- dimColor : true ,
927- key : `${ tool . id } -args`
928- } )
938+ if ( hasDescription ) {
939+ const toolLine = formatToolCall ( tool . name , tool . verboseArgs || tool . context || '' )
940+ details . push ( {
941+ color : t . color . muted ,
942+ content : `${ toolLine } \n${ boundedLiveRenderText ( tool . verboseArgs ) } ` ,
943+ dimColor : true ,
944+ key : `${ tool . id } -args`
945+ } )
946+ }
947+ // Without a description, header = tool call, skip the detail.
929948 }
930949
931950 groups . push ( {
@@ -955,22 +974,42 @@ export const ToolTrail = memo(function ToolTrail({
955974 const hasMeta = meta . length > 0
956975 const hasThinking = ! ! cot || reasoningActive || reasoningStreaming
957976 const thinkingLive = reasoningActive || reasoningStreaming
977+ const terminalCols = process . stdout . columns || 80
958978 const thoughtPreviewLines = useMemo ( ( ) => {
959- if ( thinkingLive ) return [ ]
960979 const raw = cleanThinkingText ( reasoning )
961- return raw . split ( '\n' ) . filter ( Boolean ) . slice ( 0 , THOUGHT_PREVIEW_LINES )
962- } , [ thinkingLive , reasoning ] )
980+ const sourceLines = raw . split ( '\n' ) . filter ( Boolean )
981+ // Collect source lines until they fill THOUGHT_PREVIEW_LINES visual lines
982+ const result : string [ ] = [ ]
983+ let visualLines = 0
984+ for ( const line of sourceLines ) {
985+ result . push ( line )
986+ visualLines += Math . max ( 1 , Math . ceil ( line . length / terminalCols ) )
987+ if ( visualLines >= THOUGHT_PREVIEW_LINES ) break
988+ }
989+ return result
990+ } , [ reasoning , terminalCols ] )
991+ const totalThoughtLines = useMemo ( ( ) => {
992+ const raw = cleanThinkingText ( reasoning )
993+ const sourceLines = raw . split ( '\n' ) . filter ( Boolean )
994+ return sourceLines . reduce ( ( sum , line ) => sum + Math . max ( 1 , Math . ceil ( line . length / terminalCols ) ) , 0 )
995+ } , [ reasoning , terminalCols ] )
963996
964997 // Auto-expand Thinking during streaming, revert to user preference when done.
965998 // User clicks take precedence for the remainder of the turn.
999+ // Collapse to preview once reasoning exceeds THOUGHT_PREVIEW_LINES to avoid
1000+ // flooding the screen during long streaming output.
9661001 useEffect ( ( ) => {
9671002 if ( thinkingLive ) {
9681003 thinkingUserToggledRef . current = false
969- setOpenThinking ( true )
1004+ if ( totalThoughtLines > THOUGHT_PREVIEW_LINES ) {
1005+ setOpenThinking ( false )
1006+ } else {
1007+ setOpenThinking ( true )
1008+ }
9701009 } else if ( ! thinkingUserToggledRef . current ) {
9711010 setOpenThinking ( visible . thinking === 'expanded' )
9721011 }
973- } , [ thinkingLive , visible . thinking ] )
1012+ } , [ thinkingLive , visible . thinking , totalThoughtLines ] )
9741013
9751014 // Auto-expand Tool calls while tools are running, revert to user preference when done.
9761015 // Same logic as Thinking: user clicks take precedence for the remainder of the turn.
@@ -981,7 +1020,7 @@ export const ToolTrail = memo(function ToolTrail({
9811020 toolsUserToggledRef . current = false
9821021 setOpenTools ( true )
9831022 } else if ( ! toolsUserToggledRef . current && hasTools ) {
984- setOpenTools ( visible . tools === 'expanded' )
1023+ setOpenTools ( true )
9851024 }
9861025 } , [ toolsActive , hasTools , visible . tools ] )
9871026
@@ -1086,7 +1125,7 @@ export const ToolTrail = memo(function ToolTrail({
10861125 } [ ] = [ ]
10871126
10881127 if ( hasThinking && visible . thinking !== 'hidden' ) {
1089- const showPreview = ! openThinking && ! thinkingLive && thoughtPreviewLines . length > 0
1128+ const showPreview = ! openThinking && thoughtPreviewLines . length > 0
10901129 panels . push ( {
10911130 header : (
10921131 < Box
@@ -1101,14 +1140,13 @@ export const ToolTrail = memo(function ToolTrail({
11011140 } }
11021141 >
11031142 < Box >
1104- < Text color = { t . color . muted } dim = { ! thinkingLive } >
1105- < Text color = { t . color . accent } > { openThinking ? '▾ ' : '▸ ' } </ Text >
1143+ < Text color = { t . color . text } >
11061144 { thinkingLive ? (
11071145 < Text bold color = { t . color . text } >
11081146 Thinking
11091147 </ Text >
11101148 ) : (
1111- < Text color = { t . color . muted } dim >
1149+ < Text bold color = { t . color . text } >
11121150 Thought
11131151 </ Text >
11141152 ) }
@@ -1121,26 +1159,44 @@ export const ToolTrail = memo(function ToolTrail({
11211159 </ Text >
11221160 </ Box >
11231161 { showPreview
1124- ? thoughtPreviewLines . map ( ( line , i ) => (
1125- < Text color = { t . color . muted } dim key = { i } wrap = "truncate-end" >
1126- { line || ' ' }
1127- </ Text >
1128- ) )
1162+ ? (
1163+ < >
1164+ { thoughtPreviewLines . map ( ( line , i ) => (
1165+ < Text color = { t . color . muted } key = { i } wrap = "wrap-trim" >
1166+ { line || ' ' }
1167+ </ Text >
1168+ ) ) }
1169+ { totalThoughtLines > THOUGHT_PREVIEW_LINES ? (
1170+ < Text color = { t . color . muted } dim >
1171+ ... +{ totalThoughtLines - THOUGHT_PREVIEW_LINES } lines (click to expand)
1172+ </ Text >
1173+ ) : null }
1174+ </ >
1175+ )
11291176 : null }
11301177 </ Box >
11311178 ) ,
11321179 key : 'thinking' ,
11331180 open : openThinking ,
11341181 render : rails => (
1135- < Thinking
1136- active = { reasoningActive }
1137- branch = "last"
1138- mode = "full"
1139- rails = { rails }
1140- reasoning = { busy ? reasoning : cot }
1141- streaming = { busy && reasoningStreaming }
1142- t = { t }
1143- />
1182+ < Box flexDirection = "column" >
1183+ < Thinking
1184+ active = { reasoningActive }
1185+ branch = { totalThoughtLines > THOUGHT_PREVIEW_LINES ? 'mid' : 'last' }
1186+ mode = "full"
1187+ rails = { rails }
1188+ reasoning = { busy ? reasoning : cot }
1189+ streaming = { busy && reasoningStreaming }
1190+ t = { t }
1191+ />
1192+ { ! thinkingLive && totalThoughtLines > THOUGHT_PREVIEW_LINES ? (
1193+ < TreeRow branch = "last" rails = { rails } t = { t } >
1194+ < Box onClick = { ( ) => setOpenThinking ( false ) } >
1195+ < Text color = { t . color . muted } dim > ... collapse</ Text >
1196+ </ Box >
1197+ </ TreeRow >
1198+ ) : null }
1199+ </ Box >
11441200 )
11451201 } )
11461202 }
@@ -1150,7 +1206,8 @@ export const ToolTrail = memo(function ToolTrail({
11501206 const { duration, label : toolHeaderLabel } = splitToolDuration ( group . label )
11511207 const tone : 'dim' | 'error' | 'warn' = group . color === t . color . error ? 'error' : 'dim'
11521208 const isLastGroup = groupIndex === groups . length - 1
1153- const suffix = [ duration , isLastGroup ? toolTokensLabel : undefined ] . filter ( Boolean ) . join ( ' ' ) || undefined
1209+ const suffix = [ duration , isLastGroup ? toolTokensLabel : undefined ]
1210+ . filter ( Boolean ) . join ( ' ' ) || undefined
11541211
11551212 panels . push ( {
11561213 header : (
@@ -1181,6 +1238,64 @@ export const ToolTrail = memo(function ToolTrail({
11811238 { group . details . map ( ( detail , detailIndex ) => {
11821239 const detailBranch : TreeBranch =
11831240 detailIndex === group . details . length - 1 && ! hasInlineSubagents ? 'last' : 'mid'
1241+ const text = typeof detail . content === 'string' ? detail . content : ''
1242+ const nl = text . indexOf ( '\n' )
1243+ // Bold the tool name in "Bash(command)" first line
1244+ const toolCallMatch = nl > 0 ? text . slice ( 0 , nl ) . match ( / ^ ( \w + ) \( ( .+ ) \) $ / ) : null
1245+
1246+ if ( toolCallMatch ) {
1247+ const toolName = toolCallMatch [ 1 ] !
1248+ const toolArg = toolCallMatch [ 2 ] !
1249+ const rest = text . slice ( nl + 1 )
1250+ const restNl = rest . indexOf ( '\n' )
1251+ const resultLabel = restNl > 0 && rest . startsWith ( 'Result:' ) ? 'Result:' : ''
1252+ const resultBody = resultLabel ? rest . slice ( restNl + 1 ) : rest
1253+ return (
1254+ < Box flexDirection = "column" key = { detail . key } >
1255+ < TreeTextRow
1256+ branch = { resultLabel || resultBody !== rest ? 'mid' : detailBranch }
1257+ color = { detail . color }
1258+ content = {
1259+ < Text >
1260+ < Text bold color = { detail . color } > { toolName } </ Text >
1261+ < Text color = { detail . color } > ({ toolArg } )</ Text >
1262+ </ Text >
1263+ }
1264+ rails = { rails }
1265+ t = { t }
1266+ />
1267+ { resultLabel ? (
1268+ < >
1269+ < TreeTextRow
1270+ branch = "mid"
1271+ color = { detail . color }
1272+ content = "Result:"
1273+ rails = { rails }
1274+ t = { t }
1275+ />
1276+ < TruncatedResult
1277+ { ...detail }
1278+ branch = { detailBranch }
1279+ color = { t . color . muted }
1280+ content = { resultBody }
1281+ dimColor
1282+ rails = { rails }
1283+ t = { t }
1284+ />
1285+ </ >
1286+ ) : (
1287+ < TruncatedResult
1288+ { ...detail }
1289+ branch = { detailBranch }
1290+ content = { rest }
1291+ rails = { rails }
1292+ t = { t }
1293+ />
1294+ ) }
1295+ </ Box >
1296+ )
1297+ }
1298+
11841299 return (
11851300 < TruncatedResult
11861301 { ...detail }
0 commit comments