@@ -1911,7 +1911,10 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
19111911 < text fg = { theme . textMuted } > { errorMessage ( props . message . error ) } </ text >
19121912 < Show when = { ctx . footer ( "time" , "error" , props . message . id ) } >
19131913 < text fg = { theme . textMuted } >
1914- { Locale . todayTimeOrDateFirst ( props . message . time . completed ?? props . message . time . created , ctx . tui . datetime_format ) }
1914+ { Locale . todayTimeOrDateFirst (
1915+ props . message . time . completed ?? props . message . time . created ,
1916+ ctx . tui . datetime_format ,
1917+ ) }
19151918 </ text >
19161919 </ Show >
19171920 </ box >
@@ -2194,9 +2197,9 @@ function GenericTool(props: ToolProps) {
21942197 const ctx = use ( )
21952198 const output = createMemo ( ( ) => props . output ?. trim ( ) ?? "" )
21962199 const [ expanded , setExpanded ] = createSignal ( false )
2197- const maxLines = 3
2198- const maxChars = createMemo ( ( ) => maxLines * Math . max ( 20 , ctx . width - 6 ) )
2199- const collapsed = createMemo ( ( ) => collapseToolOutput ( output ( ) , maxLines , maxChars ( ) ) )
2200+ const collapsed = createMemo ( ( ) =>
2201+ collapseOutput ( output ( ) , TuiConfig . toolOutputLimit ( ctx . tui . tool_output , props . tool , "generic" ) , ctx . width ) ,
2202+ )
22002203 const limited = createMemo ( ( ) => {
22012204 if ( expanded ( ) || ! collapsed ( ) . overflow ) return output ( )
22022205 return collapsed ( ) . output
@@ -2223,9 +2226,11 @@ function GenericTool(props: ToolProps) {
22232226 hint = { expandHint ( collapsed ( ) . overflow , expanded ( ) ) }
22242227 onClick = { collapsed ( ) . overflow ? ( ) => setExpanded ( ( prev ) => ! prev ) : undefined }
22252228 >
2226- < box gap = { 1 } >
2227- < text fg = { theme . text } > { limited ( ) } </ text >
2228- </ box >
2229+ < Show when = { ! collapsed ( ) . overflow || expanded ( ) || limited ( ) } >
2230+ < box gap = { 1 } >
2231+ < text fg = { theme . text } > { limited ( ) } </ text >
2232+ </ box >
2233+ </ Show >
22292234 </ BlockTool >
22302235 </ Show >
22312236 )
@@ -2269,9 +2274,7 @@ function InlineTool(props: {
22692274 )
22702275
22712276 const failed = createMemo ( ( ) => Boolean ( error ( ) && ! denied ( ) ) )
2272- const timestamp = createMemo ( ( ) =>
2273- formatToolTimestamp ( props . part . state , toolFooter ( ctx , props . part ) ) ,
2274- )
2277+ const timestamp = createMemo ( ( ) => formatToolTimestamp ( props . part . state , toolFooter ( ctx , props . part ) ) )
22752278 const clickable = createMemo ( ( ) => Boolean ( props . onClick || failed ( ) ) )
22762279 const fg = createMemo ( ( ) => {
22772280 if ( props . color ) return props . color
@@ -2499,9 +2502,9 @@ function Shell(props: ToolProps) {
24992502 const isRunning = createMemo ( ( ) => props . part . state . status === "running" )
25002503 const output = createMemo ( ( ) => stripAnsi ( stringValue ( props . metadata . output ) ?. trim ( ) ?? "" ) )
25012504 const [ expanded , setExpanded ] = createSignal ( false )
2502- const maxLines = 10
2503- const maxChars = createMemo ( ( ) => maxLines * Math . max ( 20 , ctx . width - 6 ) )
2504- const collapsed = createMemo ( ( ) => collapseToolOutput ( output ( ) , maxLines , maxChars ( ) ) )
2505+ const collapsed = createMemo ( ( ) =>
2506+ collapseOutput ( output ( ) , TuiConfig . toolOutputLimit ( ctx . tui . tool_output , props . tool , "bash" ) , ctx . width ) ,
2507+ )
25052508 const limited = createMemo ( ( ) => {
25062509 if ( expanded ( ) || ! collapsed ( ) . overflow ) return output ( )
25072510 return collapsed ( ) . output
@@ -2534,7 +2537,7 @@ function Shell(props: ToolProps) {
25342537 < Show when = { isRunning ( ) } fallback = { < text fg = { theme . text } > $ { stringValue ( props . input . command ) } </ text > } >
25352538 < Spinner color = { theme . text } > { stringValue ( props . input . command ) } </ Spinner >
25362539 </ Show >
2537- < Show when = { output ( ) } >
2540+ < Show when = { limited ( ) } >
25382541 < text fg = { theme . text } > { limited ( ) } </ text >
25392542 </ Show >
25402543 </ box >
@@ -2555,20 +2558,32 @@ function Write(props: ToolProps) {
25552558 const code = createMemo ( ( ) => {
25562559 return stringValue ( props . input . content ) ?? ""
25572560 } )
2561+ const collapse = useBlockCollapse (
2562+ ( ) => props . tool ,
2563+ "write" ,
2564+ ( ) => code ( ) . length > 0 ,
2565+ )
25582566
25592567 return (
25602568 < Switch >
25612569 < Match when = { props . metadata . diagnostics !== undefined } >
2562- < BlockTool title = { "# Wrote " + pathFormatter . format ( stringValue ( props . input . filePath ) ) } part = { props . part } >
2563- < line_number fg = { theme . textMuted } minWidth = { 3 } paddingRight = { 1 } >
2564- < code
2565- conceal = { false }
2566- fg = { theme . text }
2567- filetype = { filetype ( stringValue ( props . input . filePath ) ) }
2568- syntaxStyle = { syntax ( ) }
2569- content = { code ( ) }
2570- />
2571- </ line_number >
2570+ < BlockTool
2571+ title = { "# Wrote " + pathFormatter . format ( stringValue ( props . input . filePath ) ) }
2572+ part = { props . part }
2573+ hint = { collapse . hint ( ) }
2574+ onClick = { collapse . onClick ( ) }
2575+ >
2576+ < ClippedRows limit = { collapse . limit ( ) } expanded = { collapse . expanded ( ) } onClip = { collapse . setClipped } >
2577+ < line_number fg = { theme . textMuted } minWidth = { 3 } paddingRight = { 1 } >
2578+ < code
2579+ conceal = { false }
2580+ fg = { theme . text }
2581+ filetype = { filetype ( stringValue ( props . input . filePath ) ) }
2582+ syntaxStyle = { syntax ( ) }
2583+ content = { code ( ) }
2584+ />
2585+ </ line_number >
2586+ </ ClippedRows >
25722587 < Diagnostics diagnostics = { props . metadata . diagnostics } filePath = { stringValue ( props . input . filePath ) ?? "" } />
25732588 </ BlockTool >
25742589 </ Match >
@@ -2773,6 +2788,53 @@ function toolFooter(ctx: ReturnType<typeof use>, part: ToolPart) {
27732788 }
27742789}
27752790
2791+ // A tool's text output cut to `limit` lines and that many lines' worth of characters; undefined never cuts.
2792+ export function collapseOutput ( output : string , limit : number | undefined , width : number ) {
2793+ if ( limit === undefined ) return { output, overflow : false }
2794+ return collapseToolOutput ( output , limit , limit * Math . max ( 20 , width - 6 ) )
2795+ }
2796+
2797+ // Collapsing for a tool block drawn by a component (code, diff) rather than as text: shows the
2798+ // first `limit` rows of what it renders, and reports whether anything is hidden.
2799+ function useBlockCollapse ( tool : ( ) => string , group : TuiConfig . ToolOutputGroup , content : ( ) => boolean ) {
2800+ const ctx = use ( )
2801+ const limit = createMemo ( ( ) => TuiConfig . toolOutputLimit ( ctx . tui . tool_output , tool ( ) , group ) )
2802+ const [ expanded , setExpanded ] = createSignal ( false )
2803+ const [ clipped , setClipped ] = createSignal ( false )
2804+ const overflow = ( ) => ( limit ( ) === 0 ? content ( ) : clipped ( ) )
2805+ return {
2806+ limit,
2807+ expanded,
2808+ setClipped,
2809+ hint : ( ) => expandHint ( overflow ( ) , expanded ( ) ) ,
2810+ onClick : ( ) => ( overflow ( ) ? ( ) => setExpanded ( ( value ) => ! value ) : undefined ) ,
2811+ }
2812+ }
2813+
2814+ export function ClippedRows ( props : {
2815+ limit : number | undefined
2816+ expanded : boolean
2817+ onClip : ( clipped : boolean ) => void
2818+ children : JSX . Element
2819+ } ) {
2820+ return (
2821+ < Show when = { props . limit !== undefined && ! props . expanded } fallback = { props . children } >
2822+ < Show when = { props . limit ! > 0 } >
2823+ < box maxHeight = { props . limit } overflow = "hidden" flexShrink = { 0 } >
2824+ < box
2825+ flexShrink = { 0 }
2826+ onSizeChange = { function ( ) {
2827+ props . onClip ( this . height > props . limit ! )
2828+ } }
2829+ >
2830+ { props . children }
2831+ </ box >
2832+ </ box >
2833+ </ Show >
2834+ </ Show >
2835+ )
2836+ }
2837+
27762838function expandHint ( overflow : boolean , expanded : boolean ) {
27772839 if ( ! overflow ) return
27782840 return expanded ? "Click to collapse" : "Click to expand"
@@ -2908,32 +2970,44 @@ function Edit(props: ToolProps) {
29082970 const ft = createMemo ( ( ) => filetype ( stringValue ( props . input . filePath ) ) )
29092971
29102972 const diffContent = createMemo ( ( ) => stringValue ( props . metadata . diff ) ?? "" )
2973+ const collapse = useBlockCollapse (
2974+ ( ) => props . tool ,
2975+ "edit" ,
2976+ ( ) => diffContent ( ) . length > 0 ,
2977+ )
29112978
29122979 return (
29132980 < Switch >
29142981 < Match when = { stringValue ( props . metadata . diff ) !== undefined } >
2915- < BlockTool title = { "← Edit " + pathFormatter . format ( stringValue ( props . input . filePath ) ) } part = { props . part } >
2916- < box paddingLeft = { 1 } >
2917- < diff
2918- diff = { diffContent ( ) }
2919- view = { view ( ) }
2920- filetype = { ft ( ) }
2921- syntaxStyle = { syntax ( ) }
2922- showLineNumbers = { true }
2923- width = "100%"
2924- wrapMode = { ctx . diffWrapMode ( ) }
2925- fg = { theme . text }
2926- addedBg = { theme . diffAddedBg }
2927- removedBg = { theme . diffRemovedBg }
2928- contextBg = { theme . diffContextBg }
2929- addedSignColor = { theme . diffHighlightAdded }
2930- removedSignColor = { theme . diffHighlightRemoved }
2931- lineNumberFg = { theme . diffLineNumber }
2932- lineNumberBg = { theme . diffContextBg }
2933- addedLineNumberBg = { theme . diffAddedLineNumberBg }
2934- removedLineNumberBg = { theme . diffRemovedLineNumberBg }
2935- />
2936- </ box >
2982+ < BlockTool
2983+ title = { "← Edit " + pathFormatter . format ( stringValue ( props . input . filePath ) ) }
2984+ part = { props . part }
2985+ hint = { collapse . hint ( ) }
2986+ onClick = { collapse . onClick ( ) }
2987+ >
2988+ < ClippedRows limit = { collapse . limit ( ) } expanded = { collapse . expanded ( ) } onClip = { collapse . setClipped } >
2989+ < box paddingLeft = { 1 } >
2990+ < diff
2991+ diff = { diffContent ( ) }
2992+ view = { view ( ) }
2993+ filetype = { ft ( ) }
2994+ syntaxStyle = { syntax ( ) }
2995+ showLineNumbers = { true }
2996+ width = "100%"
2997+ wrapMode = { ctx . diffWrapMode ( ) }
2998+ fg = { theme . text }
2999+ addedBg = { theme . diffAddedBg }
3000+ removedBg = { theme . diffRemovedBg }
3001+ contextBg = { theme . diffContextBg }
3002+ addedSignColor = { theme . diffHighlightAdded }
3003+ removedSignColor = { theme . diffHighlightRemoved }
3004+ lineNumberFg = { theme . diffLineNumber }
3005+ lineNumberBg = { theme . diffContextBg }
3006+ addedLineNumberBg = { theme . diffAddedLineNumberBg }
3007+ removedLineNumberBg = { theme . diffRemovedLineNumberBg }
3008+ />
3009+ </ box >
3010+ </ ClippedRows >
29373011 < Diagnostics diagnostics = { props . metadata . diagnostics } filePath = { stringValue ( props . input . filePath ) ?? "" } />
29383012 </ BlockTool >
29393013 </ Match >
@@ -2996,21 +3070,31 @@ function ApplyPatch(props: ToolProps) {
29963070 < Switch >
29973071 < Match when = { files ( ) . length > 0 } >
29983072 < For each = { files ( ) } >
2999- { ( file ) => (
3000- < BlockTool title = { title ( file ) } part = { props . part } >
3001- < Show
3002- when = { file . type !== "delete" }
3003- fallback = {
3004- < text fg = { theme . diffRemoved } >
3005- -{ file . deletions } line{ file . deletions !== 1 ? "s" : "" }
3006- </ text >
3007- }
3008- >
3009- < Diff diff = { file . patch } filePath = { file . filePath } />
3010- < Diagnostics diagnostics = { props . metadata . diagnostics } filePath = { file . movePath ?? file . filePath } />
3011- </ Show >
3012- </ BlockTool >
3013- ) }
3073+ { ( file ) => {
3074+ // A deleted file is one line already, so only the other files collapse.
3075+ const collapse = useBlockCollapse (
3076+ ( ) => props . tool ,
3077+ "apply_patch" ,
3078+ ( ) => file . type !== "delete" && file . patch . length > 0 ,
3079+ )
3080+ return (
3081+ < BlockTool title = { title ( file ) } part = { props . part } hint = { collapse . hint ( ) } onClick = { collapse . onClick ( ) } >
3082+ < Show
3083+ when = { file . type !== "delete" }
3084+ fallback = {
3085+ < text fg = { theme . diffRemoved } >
3086+ -{ file . deletions } line{ file . deletions !== 1 ? "s" : "" }
3087+ </ text >
3088+ }
3089+ >
3090+ < ClippedRows limit = { collapse . limit ( ) } expanded = { collapse . expanded ( ) } onClip = { collapse . setClipped } >
3091+ < Diff diff = { file . patch } filePath = { file . filePath } />
3092+ </ ClippedRows >
3093+ < Diagnostics diagnostics = { props . metadata . diagnostics } filePath = { file . movePath ?? file . filePath } />
3094+ </ Show >
3095+ </ BlockTool >
3096+ )
3097+ } }
30143098 </ For >
30153099 </ Match >
30163100 < Match when = { true } >
0 commit comments