99
1010import type { ExtensionAPI , ExtensionContext } from '@earendil-works/pi-coding-agent'
1111import { DynamicBorder , truncateTail } from '@earendil-works/pi-coding-agent'
12- import { Container , Text } from '@earendil-works/pi-tui'
12+ import { Container , Text , visibleWidth } from '@earendil-works/pi-tui'
1313import { errorMessage } from './shared/errors'
1414import {
1515 firstText ,
1616 meta ,
17- primary ,
1817 renderError ,
1918 renderLines ,
2019 renderMuted ,
20+ renderTextLinesPreview ,
2121 renderToolCall ,
2222 title ,
23+ truncateLine ,
2324 toolError ,
2425 toolText
2526} from './shared/render'
27+ import { compactLines , normalizeTerminalOutput } from './shared/format'
2628import { Type } from 'typebox'
2729import { spawn , spawnSync } from 'child_process'
2830import * as crypto from 'crypto'
@@ -223,22 +225,6 @@ function stopProcess(projectDir: string, name: string): void {
223225 }
224226}
225227
226- function stripProgressNoise ( text : string ) : string {
227- // eslint-disable-next-line no-control-regex
228- let clean = text . replace ( / \x1b \[ [ 0 - 9 ; ] * [ a - z A - Z ] / g, '' )
229-
230- clean = clean
231- . split ( '\n' )
232- . map ( ( line ) => {
233- if ( ! line . includes ( '\r' ) ) return line
234- const parts = line . split ( '\r' )
235- return parts [ parts . length - 1 ]
236- } )
237- . join ( '\n' )
238-
239- return clean
240- }
241-
242228function readLogs ( projectDir : string , name : string , lines : number ) : string {
243229 const dir = findProcessDir ( projectDir , name )
244230 if ( ! dir ) {
@@ -254,7 +240,7 @@ function readLogs(projectDir: string, name: string, lines: number): string {
254240 encoding : 'utf8'
255241 } )
256242
257- const raw = stripProgressNoise ( result . stdout || result . stderr || '' )
243+ const raw = normalizeTerminalOutput ( result . stdout || result . stderr || '' )
258244 const truncation = truncateTail ( raw , { maxLines : lines } )
259245
260246 if ( truncation . truncated ) {
@@ -364,15 +350,39 @@ function updateStatus(ctx: ExtensionContext) {
364350 for ( const proc of running ) {
365351 const displayName = getDisplayName ( proc )
366352 try {
367- const logs = readLogs ( ctx . cwd , proc . name , 2 )
368- container . addChild ( new Text ( theme . fg ( 'muted' , ` ${ displayName } ` ) , 0 , 0 ) )
369- if ( logs . trim ( ) ) {
370- for ( const line of logs . trim ( ) . split ( '\n' ) ) {
371- container . addChild ( new Text ( theme . fg ( 'dim' , ` ${ line } ` ) , 0 , 0 ) )
372- }
353+ const logs = readLogs ( ctx . cwd , proc . name , 10 )
354+ const lines = compactLines ( logs )
355+ const latest = lines . at ( - 1 )
356+ const hidden = Math . max ( 0 , lines . length - 1 )
357+ container . addChild ( {
358+ render : ( width ) => [
359+ truncateLine ( theme . fg ( 'muted' , ` ${ displayName } ` ) , width , theme . fg ( 'muted' , '…' ) )
360+ ] ,
361+ invalidate : ( ) => undefined
362+ } )
363+ if ( latest ) {
364+ container . addChild ( {
365+ render : ( width ) => {
366+ const suffix = hidden > 0 ? ` ${ theme . fg ( 'muted' , `… ${ hidden } more lines` ) } ` : ''
367+ const suffixWidth = visibleWidth ( suffix )
368+ if ( suffixWidth >= width )
369+ return [ truncateLine ( suffix , width , theme . fg ( 'muted' , '…' ) ) ]
370+ const available = Math . max ( 1 , width - suffixWidth )
371+ return [
372+ truncateLine ( theme . fg ( 'dim' , ` ${ latest } ` ) , available , theme . fg ( 'dim' , '…' ) ) +
373+ suffix
374+ ]
375+ } ,
376+ invalidate : ( ) => undefined
377+ } )
373378 }
374379 } catch {
375- container . addChild ( new Text ( theme . fg ( 'muted' , ` ${ displayName } ` ) , 0 , 0 ) )
380+ container . addChild ( {
381+ render : ( width ) => [
382+ truncateLine ( theme . fg ( 'muted' , ` ${ displayName } ` ) , width , theme . fg ( 'muted' , '…' ) )
383+ ] ,
384+ invalidate : ( ) => undefined
385+ } )
376386 container . addChild ( new Text ( theme . fg ( 'dim' , ' (no logs)' ) , 0 , 0 ) )
377387 }
378388 }
@@ -635,20 +645,23 @@ export default function (pi: ExtensionAPI) {
635645 const safeArgs = args ?? { }
636646 return renderToolCall ( theme , 'bg logs' , {
637647 segments : [ { text : safeArgs . name } ] ,
638- suffix : safeArgs . lines ? `${ safeArgs . lines } lines ` : undefined
648+ suffix : safeArgs . lines ? `last ${ safeArgs . lines } ` : undefined
639649 } )
640650 } ,
641651
642- renderResult ( result , _options , theme ) {
652+ renderResult ( result , { expanded } , theme ) {
643653 const details = result . details as LogsDetails
644654 if ( details . error ) return renderError ( firstText ( result , 'Error' ) , theme )
645- if ( ! details . logs . trim ( ) ) return renderMuted ( '(empty)' , theme )
646- const preview = details . logs . split ( '\n' ) . slice ( - 3 )
647- return renderLines ( [
648- ...renderProcessRow ( theme , details . name , `logs last ${ preview . length } ` ) ,
649- '' ,
650- ...preview . map ( ( line ) => primary ( line , theme ) )
651- ] )
655+ const lines = compactLines ( details . logs )
656+ if ( lines . length === 0 ) return renderMuted ( '(empty)' , theme )
657+ return renderTextLinesPreview ( lines , theme , {
658+ expanded,
659+ compactLimit : 3 ,
660+ mode : 'tail' ,
661+ hiddenUnit : 'hidden' ,
662+ inlineHidden : true ,
663+ truncationMarker : theme . fg ( 'toolOutput' , '…' )
664+ } )
652665 }
653666 } )
654667}
0 commit comments