@@ -10,6 +10,15 @@ interface Props {
1010 onSelectFile : ( file : string | null ) => void
1111}
1212
13+ type FileStats = {
14+ total : number
15+ worst : number
16+ openBlockers : number
17+ openInformational : number
18+ resolved : number
19+ dismissed : number
20+ }
21+
1322const statusIcon = {
1423 added : FilePlus ,
1524 deleted : FileX ,
@@ -26,14 +35,71 @@ const statusDot = {
2635
2736const sevRank : Record < Severity , number > = { Error : 0 , Warning : 1 , Info : 2 , Suggestion : 3 }
2837
38+ function buildReadinessLabel ( stats : FileStats ) : { label : string ; tone : string } {
39+ if ( stats . openBlockers > 0 ) {
40+ return {
41+ label : `${ stats . openBlockers } blocker${ stats . openBlockers === 1 ? '' : 's' } ` ,
42+ tone : 'text-sev-warning' ,
43+ }
44+ }
45+
46+ if ( stats . openInformational > 0 ) {
47+ return {
48+ label : 'Info only' ,
49+ tone : 'text-accent' ,
50+ }
51+ }
52+
53+ return {
54+ label : 'Clear' ,
55+ tone : 'text-sev-suggestion' ,
56+ }
57+ }
58+
59+ function buildReadinessDetails ( stats : FileStats ) : string | null {
60+ const details : string [ ] = [ ]
61+
62+ if ( stats . openBlockers > 0 && stats . openInformational > 0 ) {
63+ details . push ( `${ stats . openInformational } info` )
64+ }
65+ if ( stats . resolved > 0 ) {
66+ details . push ( `${ stats . resolved } resolved` )
67+ }
68+ if ( stats . dismissed > 0 ) {
69+ details . push ( `${ stats . dismissed } dismissed` )
70+ }
71+
72+ return details . length > 0 ? details . join ( ' • ' ) : null
73+ }
74+
2975export function FileSidebar ( { files, comments, selectedFile, onSelectFile } : Props ) {
3076 const fileStats = useMemo ( ( ) => {
31- const stats = new Map < string , { count : number ; worst : number } > ( )
77+ const stats = new Map < string , FileStats > ( )
3278 for ( const c of comments ) {
3379 const path = c . file_path . replace ( / ^ \. \/ / , '' )
34- const existing = stats . get ( path ) || { count : 0 , worst : 4 }
35- existing . count ++
80+ const existing = stats . get ( path ) || {
81+ total : 0 ,
82+ worst : 4 ,
83+ openBlockers : 0 ,
84+ openInformational : 0 ,
85+ resolved : 0 ,
86+ dismissed : 0 ,
87+ }
88+ const lifecycle = c . status ?? 'Open'
89+
90+ existing . total ++
3691 existing . worst = Math . min ( existing . worst , sevRank [ c . severity ] )
92+
93+ if ( lifecycle === 'Resolved' ) {
94+ existing . resolved ++
95+ } else if ( lifecycle === 'Dismissed' ) {
96+ existing . dismissed ++
97+ } else if ( c . severity === 'Error' || c . severity === 'Warning' ) {
98+ existing . openBlockers ++
99+ } else {
100+ existing . openInformational ++
101+ }
102+
37103 stats . set ( path , existing )
38104 }
39105 return stats
@@ -95,23 +161,35 @@ export function FileSidebar({ files, comments, selectedFile, onSelectFile }: Pro
95161 const Icon = statusIcon [ file . status ]
96162 const stats = fileStats . get ( file . path )
97163 const isSelected = selectedFile === file . path
164+ const readiness = stats ? buildReadinessLabel ( stats ) : null
165+ const readinessDetails = stats ? buildReadinessDetails ( stats ) : null
98166
99167 return (
100168 < button
101169 key = { file . path }
102170 onClick = { ( ) => onSelectFile ( file . path ) }
103- className = { `w-full text-left px-3 py-1 flex items-center gap-1.5 text-[12px] transition-colors ${
171+ className = { `w-full text-left px-3 py-1.5 flex items-start gap-1.5 text-[12px] transition-colors ${
104172 isSelected
105173 ? 'bg-accent/10 text-accent'
106174 : 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'
107175 } `}
108176 >
109177 < span className = { `w-1.5 h-1.5 rounded-full shrink-0 ${ statusDot [ file . status ] } ` } />
110178 < Icon size = { 12 } className = "shrink-0 text-text-muted" />
111- < span className = "truncate font-code" > { getFileName ( file . path ) } </ span >
179+ < div className = "min-w-0 flex-1" >
180+ < div className = "truncate font-code" > { getFileName ( file . path ) } </ div >
181+ { readiness && (
182+ < div className = "mt-0.5 flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[10px]" >
183+ < span className = { `font-medium ${ readiness . tone } ` } > { readiness . label } </ span >
184+ { readinessDetails && (
185+ < span className = "text-text-muted" > { readinessDetails } </ span >
186+ ) }
187+ </ div >
188+ ) }
189+ </ div >
112190 { stats && (
113191 < span className = { `ml-auto shrink-0 text-[10px] text-white px-1 py-0 rounded ${ countColor ( stats . worst ) } ` } >
114- { stats . count }
192+ { stats . total }
115193 </ span >
116194 ) }
117195 </ button >
0 commit comments