@@ -42,6 +42,7 @@ type DetailedStats = {
4242 sortSettings ?: {
4343 editor ?: { key ?: string ; dir ?: string } ;
4444 model ?: { key ?: string ; dir ?: string } ;
45+ modelOtherExpanded ?: boolean ;
4546 } ;
4647} ;
4748
@@ -72,6 +73,7 @@ let editorSortKey: TableSortKey = (_initSort?.editor?.key as TableSortKey) ?? 'n
7273let editorSortDir : SortDir = ( _initSort ?. editor ?. dir as SortDir ) ?? 'asc' ;
7374let modelSortKey : TableSortKey = ( _initSort ?. model ?. key as TableSortKey ) ?? 'name' ;
7475let modelSortDir : SortDir = ( _initSort ?. model ?. dir as SortDir ) ?? 'asc' ;
76+ let modelOtherExpanded : boolean = ( _initSort ?. modelOtherExpanded ) ?? false ;
7577
7678function calculateProjection ( last30DaysValue : number ) : number {
7779 // Project annual value based on last 30 days average
@@ -265,7 +267,8 @@ function saveSortSettings(): void {
265267 command : 'saveSortSettings' ,
266268 settings : {
267269 editor : { key : editorSortKey , dir : editorSortDir } ,
268- model : { key : modelSortKey , dir : modelSortDir }
270+ model : { key : modelSortKey , dir : modelSortDir } ,
271+ modelOtherExpanded
269272 }
270273 } ) ;
271274}
@@ -419,7 +422,9 @@ function buildEditorUsageSection(stats: DetailedStats): HTMLElement | null {
419422 return section ;
420423}
421424
422- function buildModelTbody ( stats : DetailedStats , allModels : string [ ] ) : HTMLTableSectionElement {
425+ const TOP_N_MODELS = 5 ;
426+
427+ function buildModelTbody ( stats : DetailedStats , topModels : string [ ] , otherModels : string [ ] , onToggleOther : ( ) => void ) : HTMLTableSectionElement {
423428 type ModelItem = {
424429 model : string ;
425430 todayTotal : number ;
@@ -435,7 +440,7 @@ function buildModelTbody(stats: DetailedStats, allModels: string[]): HTMLTableSe
435440 charsPerToken : number ;
436441 } ;
437442
438- const items : ModelItem [ ] = allModels . map ( model => {
443+ function toModelItem ( model : string ) : ModelItem {
439444 const todayUsage = stats . today . modelUsage [ model ] || { inputTokens : 0 , outputTokens : 0 } ;
440445 const last30DaysUsage = stats . last30Days . modelUsage [ model ] || { inputTokens : 0 , outputTokens : 0 } ;
441446 const lastMonthUsage = stats . lastMonth . modelUsage [ model ] || { inputTokens : 0 , outputTokens : 0 } ;
@@ -456,29 +461,33 @@ function buildModelTbody(stats: DetailedStats, allModels: string[]): HTMLTableSe
456461 projected : Math . round ( calculateProjection ( last30DaysTotal ) ) ,
457462 charsPerToken : getCharsPerToken ( model )
458463 } ;
459- } ) ;
460-
461- items . sort ( ( a , b ) => {
462- let cmp : number ;
463- switch ( modelSortKey ) {
464- case 'name' : cmp = a . model . localeCompare ( b . model ) ; break ;
465- case 'today' : cmp = a . todayTotal - b . todayTotal ; break ;
466- case 'last30Days' : cmp = a . last30DaysTotal - b . last30DaysTotal ; break ;
467- case 'lastMonth' : cmp = a . lastMonthTotal - b . lastMonthTotal ; break ;
468- case 'projected' : cmp = a . projected - b . projected ; break ;
469- default : cmp = 0 ;
470- }
471- return modelSortDir === 'asc' ? cmp : - cmp ;
472- } ) ;
464+ }
473465
474- const tbody = document . createElement ( 'tbody' ) ;
466+ function sortItems ( items : ModelItem [ ] ) : void {
467+ items . sort ( ( a , b ) => {
468+ let cmp : number ;
469+ switch ( modelSortKey ) {
470+ case 'name' : cmp = a . model . localeCompare ( b . model ) ; break ;
471+ case 'today' : cmp = a . todayTotal - b . todayTotal ; break ;
472+ case 'last30Days' : cmp = a . last30DaysTotal - b . last30DaysTotal ; break ;
473+ case 'lastMonth' : cmp = a . lastMonthTotal - b . lastMonthTotal ; break ;
474+ case 'projected' : cmp = a . projected - b . projected ; break ;
475+ default : cmp = 0 ;
476+ }
477+ return modelSortDir === 'asc' ? cmp : - cmp ;
478+ } ) ;
479+ }
475480
476- items . forEach ( item => {
481+ function buildModelRow ( item : ModelItem , isOtherChild : boolean ) : HTMLTableRowElement {
477482 const tr = document . createElement ( 'tr' ) ;
483+ if ( isOtherChild ) {
484+ tr . style . opacity = '0.85' ;
485+ }
478486 const labelTd = document . createElement ( 'td' ) ;
479487 const labelWrapper = document . createElement ( 'span' ) ;
480488 labelWrapper . className = 'metric-label' ;
481- labelWrapper . innerHTML = `${ getModelDisplayName ( item . model ) } <span style="color:#9aa0a6;font-size:11px; font-weight:500;">(~${ item . charsPerToken . toFixed ( 1 ) } chars/tk)</span>` ;
489+ const indent = isOtherChild ? '<span style="display:inline-block;width:12px"></span>' : '' ;
490+ labelWrapper . innerHTML = `${ indent } ${ getModelDisplayName ( item . model ) } <span style="color:#9aa0a6;font-size:11px; font-weight:500;">(~${ item . charsPerToken . toFixed ( 1 ) } chars/tk)</span>` ;
482491 labelTd . append ( labelWrapper ) ;
483492
484493 const todayTd = document . createElement ( 'td' ) ;
@@ -504,8 +513,89 @@ function buildModelTbody(stats: DetailedStats, allModels: string[]): HTMLTableSe
504513 projTd . textContent = formatCompact ( item . projected ) ;
505514
506515 tr . append ( labelTd , todayTd , last30DaysTd , lastMonthTd , projTd ) ;
507- tbody . append ( tr ) ;
508- } ) ;
516+ return tr ;
517+ }
518+
519+ const topItems = topModels . map ( toModelItem ) ;
520+ sortItems ( topItems ) ;
521+
522+ const tbody = document . createElement ( 'tbody' ) ;
523+ topItems . forEach ( item => tbody . append ( buildModelRow ( item , false ) ) ) ;
524+
525+ // "Other" group — only rendered when there are more than TOP_N_MODELS models
526+ if ( otherModels . length > 0 ) {
527+ // Aggregate summed stats across all periods for the "Other" group
528+ const sumUsage = ( period : 'today' | 'last30Days' | 'lastMonth' ) =>
529+ otherModels . reduce (
530+ ( acc , m ) => {
531+ const u = stats [ period ] . modelUsage [ m ] || { inputTokens : 0 , outputTokens : 0 } ;
532+ return { inputTokens : acc . inputTokens + u . inputTokens , outputTokens : acc . outputTokens + u . outputTokens } ;
533+ } ,
534+ { inputTokens : 0 , outputTokens : 0 }
535+ ) ;
536+ const otherToday = sumUsage ( 'today' ) ;
537+ const otherLast30 = sumUsage ( 'last30Days' ) ;
538+ const otherLastMonth = sumUsage ( 'lastMonth' ) ;
539+ const otherTodayTotal = otherToday . inputTokens + otherToday . outputTokens ;
540+ const otherLast30Total = otherLast30 . inputTokens + otherLast30 . outputTokens ;
541+ const otherLastMonthTotal = otherLastMonth . inputTokens + otherLastMonth . outputTokens ;
542+ const otherProjected = Math . round ( calculateProjection ( otherLast30Total ) ) ;
543+
544+ const pct = ( part : number , total : number ) => ( total > 0 ? ( part / total ) * 100 : 0 ) ;
545+
546+ // "Other" summary row
547+ const otherTr = document . createElement ( 'tr' ) ;
548+ otherTr . style . cursor = 'pointer' ;
549+ otherTr . style . background = 'var(--list-hover-bg)' ;
550+ otherTr . title = modelOtherExpanded ? 'Collapse other models' : 'Expand other models' ;
551+
552+ const otherLabelTd = document . createElement ( 'td' ) ;
553+ const otherLabelWrapper = document . createElement ( 'span' ) ;
554+ otherLabelWrapper . className = 'metric-label' ;
555+ const toggleIcon = modelOtherExpanded ? '▲' : '▼' ;
556+ otherLabelWrapper . innerHTML = `<span style="color:var(--text-secondary);font-weight:600;">📦 Other (${ otherModels . length } model${ otherModels . length !== 1 ? 's' : '' } )</span> <span style="font-size:10px;color:var(--text-muted)">${ toggleIcon } </span>` ;
557+ otherLabelTd . append ( otherLabelWrapper ) ;
558+
559+ const otherTodayTd = document . createElement ( 'td' ) ;
560+ otherTodayTd . className = 'value-right align-right' ;
561+ otherTodayTd . textContent = formatCompact ( otherTodayTotal ) ;
562+ if ( otherTodayTotal > 0 ) {
563+ otherTodayTd . append ( el ( 'div' , 'muted' , `↑${ formatPercent ( pct ( otherToday . inputTokens , otherTodayTotal ) ) } ↓${ formatPercent ( pct ( otherToday . outputTokens , otherTodayTotal ) ) } ` ) ) ;
564+ }
565+
566+ const otherLast30Td = document . createElement ( 'td' ) ;
567+ otherLast30Td . className = 'value-right align-right' ;
568+ otherLast30Td . textContent = formatCompact ( otherLast30Total ) ;
569+ if ( otherLast30Total > 0 ) {
570+ otherLast30Td . append ( el ( 'div' , 'muted' , `↑${ formatPercent ( pct ( otherLast30 . inputTokens , otherLast30Total ) ) } ↓${ formatPercent ( pct ( otherLast30 . outputTokens , otherLast30Total ) ) } ` ) ) ;
571+ }
572+
573+ const otherLastMonthTd = document . createElement ( 'td' ) ;
574+ otherLastMonthTd . className = 'value-right align-right' ;
575+ otherLastMonthTd . textContent = formatCompact ( otherLastMonthTotal ) ;
576+ if ( otherLastMonthTotal > 0 ) {
577+ otherLastMonthTd . append ( el ( 'div' , 'muted' , `↑${ formatPercent ( pct ( otherLastMonth . inputTokens , otherLastMonthTotal ) ) } ↓${ formatPercent ( pct ( otherLastMonth . outputTokens , otherLastMonthTotal ) ) } ` ) ) ;
578+ }
579+
580+ const otherProjTd = document . createElement ( 'td' ) ;
581+ otherProjTd . className = 'value-right align-right' ;
582+ otherProjTd . textContent = formatCompact ( otherProjected ) ;
583+
584+ otherTr . append ( otherLabelTd , otherTodayTd , otherLast30Td , otherLastMonthTd , otherProjTd ) ;
585+ otherTr . addEventListener ( 'click' , ( ) => {
586+ modelOtherExpanded = ! modelOtherExpanded ;
587+ saveSortSettings ( ) ;
588+ onToggleOther ( ) ;
589+ } ) ;
590+ tbody . append ( otherTr ) ;
591+
592+ // When expanded, show individual "other" model rows beneath the summary row
593+ if ( modelOtherExpanded ) {
594+ const otherItems = otherModels . map ( toModelItem ) ;
595+ sortItems ( otherItems ) ;
596+ otherItems . forEach ( item => tbody . append ( buildModelRow ( item , true ) ) ) ;
597+ }
598+ }
509599
510600 return tbody ;
511601}
@@ -521,6 +611,15 @@ function buildModelUsageSection(stats: DetailedStats): HTMLElement | null {
521611 return null ;
522612 }
523613
614+ // Determine top N models by last30Days usage; the rest go into the "Other" group
615+ const sortedByLast30Days = Array . from ( allModels ) . sort ( ( a , b ) => {
616+ const aUsage = stats . last30Days . modelUsage [ a ] || { inputTokens : 0 , outputTokens : 0 } ;
617+ const bUsage = stats . last30Days . modelUsage [ b ] || { inputTokens : 0 , outputTokens : 0 } ;
618+ return ( bUsage . inputTokens + bUsage . outputTokens ) - ( aUsage . inputTokens + aUsage . outputTokens ) ;
619+ } ) ;
620+ const topModels = sortedByLast30Days . slice ( 0 , TOP_N_MODELS ) ;
621+ const otherModels = sortedByLast30Days . slice ( TOP_N_MODELS ) ;
622+
524623 const section = el ( 'div' , 'section' ) ;
525624 const heading = el ( 'h3' ) ;
526625 heading . textContent = '🎯 Model Usage (Tokens)' ;
@@ -539,6 +638,13 @@ function buildModelUsageSection(stats: DetailedStats): HTMLElement | null {
539638 { icon : '🌍' , text : 'Projected Year' , key : 'projected' }
540639 ] ;
541640 const modelHeaderWraps : HTMLElement [ ] = [ ] ;
641+
642+ function rebuildTbody ( ) : void {
643+ const newTbody = buildModelTbody ( stats , topModels , otherModels , rebuildTbody ) ;
644+ const oldTbody = table . querySelector ( 'tbody' ) ;
645+ if ( oldTbody ) { table . replaceChild ( newTbody , oldTbody ) ; } else { table . append ( newTbody ) ; }
646+ }
647+
542648 modelColHeaders . forEach ( ( h , idx ) => {
543649 const th = document . createElement ( 'th' ) ;
544650 th . className = idx === 0 ? '' : 'align-right' ;
@@ -559,16 +665,14 @@ function buildModelUsageSection(stats: DetailedStats): HTMLElement | null {
559665 modelHeaderWraps . forEach ( ( w , i ) => {
560666 w . textContent = `${ modelColHeaders [ i ] . icon } ${ modelColHeaders [ i ] . text } ${ getSortIndicator ( modelColHeaders [ i ] . key , modelSortKey , modelSortDir ) } ` ;
561667 } ) ;
562- const newTbody = buildModelTbody ( stats , Array . from ( allModels ) ) ;
563- const oldTbody = table . querySelector ( 'tbody' ) ;
564- if ( oldTbody ) { table . replaceChild ( newTbody , oldTbody ) ; } else { table . append ( newTbody ) ; }
668+ rebuildTbody ( ) ;
565669 saveSortSettings ( ) ;
566670 } ) ;
567671 headerRow . append ( th ) ;
568672 } ) ;
569673 thead . append ( headerRow ) ;
570674 table . append ( thead ) ;
571- table . append ( buildModelTbody ( stats , Array . from ( allModels ) ) ) ;
675+ rebuildTbody ( ) ;
572676 section . append ( table ) ;
573677 return section ;
574678}
0 commit comments