@@ -471,6 +471,134 @@ export function formatAggregate(analyses: SessionAnalysis[], label: string): str
471471 return lines . join ( '\n' ) ;
472472}
473473
474+ // ── Aggregate live mode ──
475+
476+ export function formatAggregateLive ( analyses : SessionAnalysis [ ] , label : string ) : string {
477+ const lines : string [ ] = [ ] ;
478+
479+ lines . push ( '' ) ;
480+ lines . push ( ` ${ chalk . bold ( 'cctime' ) } \u00b7 ${ chalk . bgRed . white . bold ( ' LIVE ' ) } ${ chalk . gray ( ' \u00b7 ' + label ) } ` ) ;
481+ lines . push ( ` ${ analyses . length } sessions` ) ;
482+
483+ // Aggregate enhanced stats
484+ const totalEnhanced : EnhancedStats = { humanWait : 0 , humanAway : 0 , claudeThink : 0 , toolExec : 0 , subagent : 0 , planning : 0 } ;
485+ let totalDuration = 0 ;
486+ let totalTokensIn = 0 , totalTokensOut = 0 ;
487+ let totalCost = 0 ;
488+ const allModels : Record < string , number > = { } ;
489+ const allTools : Record < string , number > = { } ;
490+ const allToolLatencies = new Map < string , { totalMs : number ; count : number } > ( ) ;
491+
492+ for ( const a of analyses ) {
493+ totalDuration += a . durationMs ;
494+ for ( const phase of activePhaseOrder ) {
495+ totalEnhanced [ phase ] += a . enhancedStats [ phase ] ;
496+ }
497+ totalEnhanced . humanAway += a . enhancedStats . humanAway ;
498+ totalTokensIn += a . tokens . input + a . tokens . cacheRead + a . tokens . cacheCreation ;
499+ totalTokensOut += a . tokens . output ;
500+ totalCost += a . estimatedCostUsd ;
501+ for ( const [ m , c ] of Object . entries ( a . models ) ) allModels [ m ] = ( allModels [ m ] || 0 ) + c ;
502+ for ( const [ t , c ] of Object . entries ( a . tools ) ) allTools [ t ] = ( allTools [ t ] || 0 ) + c ;
503+ for ( const tl of a . toolLatencies ) {
504+ const existing = allToolLatencies . get ( tl . name ) || { totalMs : 0 , count : 0 } ;
505+ existing . totalMs += tl . totalMs ;
506+ existing . count += tl . count ;
507+ allToolLatencies . set ( tl . name , existing ) ;
508+ }
509+ }
510+
511+ const totalActive = Math . max ( 0 , totalDuration - totalEnhanced . humanAway ) ;
512+
513+ // Time breakdown
514+ lines . push ( '' ) ;
515+ lines . push ( hr ( 'Time Breakdown' ) ) ;
516+ const awayAgg = totalEnhanced . humanAway > 0 ? chalk . gray ( ` (${ formatDuration ( totalEnhanced . humanAway ) } away)` ) : '' ;
517+ lines . push ( ` ${ chalk . bold ( formatDuration ( totalActive ) ) } active${ chalk . gray ( ' of ' + formatDuration ( totalDuration ) ) } ${ awayAgg } ` ) ;
518+ lines . push ( '' ) ;
519+
520+ for ( const phase of activePhaseOrder ) {
521+ const ms = totalEnhanced [ phase ] ;
522+ if ( ms === 0 ) continue ;
523+ const fraction = totalActive > 0 ? ms / totalActive : 0 ;
524+ const pct = Math . round ( fraction * 100 ) ;
525+ const bar = renderBar ( fraction , eColors [ phase ] ) ;
526+ const pLabel = eLabels [ phase ] . padEnd ( 16 ) ;
527+ lines . push ( ` ${ pLabel } ${ bar } ${ formatDuration ( ms ) . padStart ( 5 ) } (${ String ( pct ) . padStart ( 2 ) } %)` ) ;
528+ }
529+
530+ // Tokens & Cost
531+ lines . push ( '' ) ;
532+ lines . push ( hr ( 'Tokens & Cost' ) ) ;
533+
534+ const aggTotalTok = totalTokensIn + totalTokensOut ;
535+ if ( aggTotalTok > 0 ) {
536+ const inFrac = totalTokensIn / aggTotalTok ;
537+ const inBar = Math . round ( inFrac * BAR_WIDTH ) ;
538+ const outBar = BAR_WIDTH - inBar ;
539+ lines . push ( ` Tokens ${ chalk . cyan ( '\u2588' . repeat ( inBar ) ) } ${ chalk . green ( '\u2588' . repeat ( outBar ) ) } ${ chalk . cyan ( formatTokens ( totalTokensIn ) + ' in' ) } ${ chalk . green ( formatTokens ( totalTokensOut ) + ' out' ) } ` ) ;
540+ }
541+ const avgCost = analyses . length > 0 ? totalCost / analyses . length : 0 ;
542+ lines . push ( ` Cost ~${ formatCost ( totalCost ) } (${ analyses . length } sessions, avg ${ formatCost ( avgCost ) } /session)` ) ;
543+
544+ // Models
545+ const modelEntries = Object . entries ( allModels ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) ;
546+ if ( modelEntries . length > 0 ) {
547+ lines . push ( '' ) ;
548+ lines . push ( hr ( 'Models' ) ) ;
549+ const totalModelCalls = modelEntries . reduce ( ( s , [ , n ] ) => s + n , 0 ) ;
550+ for ( const [ name , count ] of modelEntries ) {
551+ const frac = count / totalModelCalls ;
552+ const pct = Math . round ( frac * 100 ) ;
553+ const bar = renderBar ( frac , name === 'Opus' ? chalk . magenta : name === 'Haiku' ? chalk . green : chalk . blue ) ;
554+ lines . push ( ` ${ name . padEnd ( 8 ) } ${ bar } ${ String ( pct ) . padStart ( 3 ) } % (${ count } calls)` ) ;
555+ }
556+ }
557+
558+ // Tools
559+ const toolEntries = Object . entries ( allTools ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) . slice ( 0 , 8 ) ;
560+ if ( toolEntries . length > 0 ) {
561+ lines . push ( '' ) ;
562+ lines . push ( hr ( 'Tools' ) ) ;
563+ const maxCalls = toolEntries [ 0 ] [ 1 ] ;
564+ for ( const [ name , count ] of toolEntries ) {
565+ const frac = count / maxCalls ;
566+ const bar = renderBar ( frac , chalk . yellow ) ;
567+ const latData = allToolLatencies . get ( name ) ;
568+ const latStr = latData && latData . count > 0
569+ ? chalk . gray ( ` avg ${ formatLatency ( latData . totalMs / latData . count ) } ` )
570+ : '' ;
571+ lines . push ( ` ${ truncName ( name ) } ${ bar } ${ String ( count ) . padStart ( 4 ) } calls${ latStr } ` ) ;
572+ }
573+ }
574+
575+ // Activity heatmap
576+ lines . push ( '' ) ;
577+ lines . push ( hr ( 'Activity' ) ) ;
578+
579+ const hourlyMs = new Array ( 24 ) . fill ( 0 ) ;
580+ for ( const a of analyses ) {
581+ const startHour = new Date ( a . startTime ) . getHours ( ) ;
582+ const aMs = Math . max ( 0 , a . durationMs - a . enhancedStats . humanAway ) ;
583+ hourlyMs [ startHour ] += aMs ;
584+ }
585+ const maxHourMs = Math . max ( ...hourlyMs , 1 ) ;
586+ const sparkChars = '\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588' ;
587+ const heatmap = hourlyMs . map ( ms => {
588+ if ( ms === 0 ) return chalk . gray ( '\u2581' ) ;
589+ const idx = Math . round ( ( ms / maxHourMs ) * ( sparkChars . length - 1 ) ) ;
590+ return chalk . cyan ( sparkChars [ idx ] ) ;
591+ } ) . join ( '' ) ;
592+ lines . push ( ` ${ heatmap } ` ) ;
593+ lines . push ( chalk . gray ( ' 12a 3a 6a 9a 12p 3p 6p 9p' ) ) ;
594+
595+ // Footer
596+ lines . push ( '' ) ;
597+ lines . push ( chalk . gray ( ' Ctrl+C to exit' ) ) ;
598+ lines . push ( '' ) ;
599+ return lines . join ( '\n' ) ;
600+ }
601+
474602// ── Compact view (one line per session) ──
475603
476604export function formatCompact ( analyses : SessionAnalysis [ ] ) : string {
0 commit comments