@@ -8,10 +8,52 @@ import { ScoreGauge } from '../components/ScoreGauge'
88import { SeverityBadge } from '../components/SeverityBadge'
99import { CommentCard } from '../components/CommentCard'
1010import { parseDiff } from '../lib/parseDiff'
11- import type { CommentLifecycleStatus , MergeReadiness , Severity , ReviewEvent } from '../api/types'
11+ import type { Comment , CommentLifecycleStatus , MergeReadiness , Severity , ReviewEvent } from '../api/types'
1212
1313type ViewMode = 'diff' | 'list'
1414
15+ const BLOCKING_SEVERITIES = new Set < Severity > ( [ 'Error' , 'Warning' ] )
16+
17+ type ReviewCommentFilters = {
18+ severityFilter : Set < Severity >
19+ selectedFile ?: string | null
20+ categoryFilter : string | null
21+ lifecycleFilter : CommentLifecycleStatus | 'All'
22+ blockerOnly ?: boolean
23+ }
24+
25+ function normalizeCommentFilePath ( filePath : string ) : string {
26+ return filePath . replace ( / ^ \. \/ / , '' )
27+ }
28+
29+ function isBlockingComment ( comment : Pick < Comment , 'severity' | 'status' > ) : boolean {
30+ return ( comment . status ?? 'Open' ) === 'Open' && BLOCKING_SEVERITIES . has ( comment . severity )
31+ }
32+
33+ function filterReviewComments (
34+ comments : Comment [ ] ,
35+ {
36+ severityFilter,
37+ selectedFile = null ,
38+ categoryFilter,
39+ lifecycleFilter,
40+ blockerOnly = false ,
41+ } : ReviewCommentFilters ,
42+ ) : Comment [ ] {
43+ return comments . filter ( ( comment ) => {
44+ if ( selectedFile && normalizeCommentFilePath ( comment . file_path ) !== selectedFile ) return false
45+ if ( categoryFilter && comment . category !== categoryFilter ) return false
46+
47+ if ( blockerOnly ) {
48+ return isBlockingComment ( comment )
49+ }
50+
51+ if ( ! severityFilter . has ( comment . severity ) ) return false
52+ if ( lifecycleFilter !== 'All' && ( comment . status ?? 'Open' ) !== lifecycleFilter ) return false
53+ return true
54+ } )
55+ }
56+
1557export function ReviewView ( ) {
1658 const { id } = useParams < { id : string } > ( )
1759 const { data : review , isLoading } = useReview ( id )
@@ -22,14 +64,45 @@ export function ReviewView() {
2264 const [ severityFilter , setSeverityFilter ] = useState < Set < Severity > > ( new Set ( [ 'Error' , 'Warning' , 'Info' , 'Suggestion' ] ) )
2365 const [ categoryFilter , setCategoryFilter ] = useState < string | null > ( null )
2466 const [ lifecycleFilter , setLifecycleFilter ] = useState < CommentLifecycleStatus | 'All' > ( 'All' )
67+ const [ showOnlyBlockers , setShowOnlyBlockers ] = useState ( false )
2568 const [ showEvent , setShowEvent ] = useState ( false )
2669 const diffContent = review ?. diff_content
70+ const comments = useMemo ( ( ) => review ?. comments ?? [ ] , [ review ?. comments ] )
2771
2872 const diffFiles = useMemo ( ( ) => {
2973 if ( ! diffContent ) return [ ]
3074 return parseDiff ( diffContent )
3175 } , [ diffContent ] )
3276
77+ const blockerCount = useMemo ( ( ) => (
78+ review ?. summary ?. open_blocking_comments ?? comments . filter ( isBlockingComment ) . length
79+ ) , [ comments , review ?. summary ?. open_blocking_comments ] )
80+
81+ const blockerFilteredComments = useMemo ( ( ) => filterReviewComments ( comments , {
82+ severityFilter,
83+ categoryFilter,
84+ lifecycleFilter,
85+ blockerOnly : showOnlyBlockers ,
86+ } ) , [ comments , severityFilter , categoryFilter , lifecycleFilter , showOnlyBlockers ] )
87+
88+ const filteredDiffFiles = useMemo ( ( ) => {
89+ if ( ! showOnlyBlockers ) return diffFiles
90+ const blockerFiles = new Set ( blockerFilteredComments . map ( ( comment ) => normalizeCommentFilePath ( comment . file_path ) ) )
91+ return diffFiles . filter ( ( file ) => blockerFiles . has ( file . path ) )
92+ } , [ diffFiles , blockerFilteredComments , showOnlyBlockers ] )
93+
94+ const activeSelectedFile = selectedFile && filteredDiffFiles . some ( ( file ) => file . path === selectedFile )
95+ ? selectedFile
96+ : null
97+
98+ const filteredComments = useMemo ( ( ) => filterReviewComments ( comments , {
99+ severityFilter,
100+ selectedFile : activeSelectedFile ,
101+ categoryFilter,
102+ lifecycleFilter,
103+ blockerOnly : showOnlyBlockers ,
104+ } ) , [ comments , severityFilter , activeSelectedFile , categoryFilter , lifecycleFilter , showOnlyBlockers ] )
105+
33106 // All hooks MUST be above this line — no hooks after early returns
34107
35108 if ( isLoading || ! review ) {
@@ -103,15 +176,7 @@ export function ReviewView() {
103176 setSeverityFilter ( next )
104177 }
105178
106- const filteredComments = review . comments . filter ( ( c ) => {
107- if ( ! severityFilter . has ( c . severity ) ) return false
108- if ( selectedFile && c . file_path . replace ( / ^ \. \/ / , '' ) !== selectedFile ) return false
109- if ( categoryFilter && c . category !== categoryFilter ) return false
110- if ( lifecycleFilter !== 'All' && ( c . status ?? 'Open' ) !== lifecycleFilter ) return false
111- return true
112- } )
113-
114- const categories = [ ...new Set ( review . comments . map ( c => c . category ) ) ]
179+ const categories = [ ...new Set ( comments . map ( c => c . category ) ) ]
115180
116181 const handleFeedback = ( commentId : string , action : 'accept' | 'reject' ) => {
117182 feedback . mutate ( { commentId, action } )
@@ -129,9 +194,9 @@ export function ReviewView() {
129194 groupedByFile . get ( key ) ! . push ( c )
130195 }
131196
132- const visibleDiffFiles = selectedFile
133- ? diffFiles . filter ( f => f . path === selectedFile )
134- : diffFiles
197+ const visibleDiffFiles = activeSelectedFile
198+ ? filteredDiffFiles . filter ( f => f . path === activeSelectedFile )
199+ : filteredDiffFiles
135200
136201 const readinessStyles : Record < MergeReadiness , string > = {
137202 Ready : 'bg-sev-suggestion/10 text-sev-suggestion border border-sev-suggestion/20' ,
@@ -151,11 +216,11 @@ export function ReviewView() {
151216 return (
152217 < div className = "flex h-full" >
153218 { /* File sidebar */ }
154- { diffFiles . length > 0 && (
219+ { filteredDiffFiles . length > 0 && (
155220 < FileSidebar
156- files = { diffFiles }
157- comments = { review . comments }
158- selectedFile = { selectedFile }
221+ files = { filteredDiffFiles }
222+ comments = { showOnlyBlockers ? blockerFilteredComments : review . comments }
223+ selectedFile = { activeSelectedFile }
159224 onSelectFile = { setSelectedFile }
160225 />
161226 ) }
@@ -316,8 +381,11 @@ export function ReviewView() {
316381 < button
317382 key = { sev }
318383 onClick = { ( ) => toggleSeverity ( sev ) }
384+ disabled = { showOnlyBlockers }
319385 className = { `text-[11px] px-2 py-0.5 rounded transition-colors ${
320- severityFilter . has ( sev )
386+ showOnlyBlockers
387+ ? 'text-text-muted/40 cursor-not-allowed'
388+ : severityFilter . has ( sev )
321389 ? 'bg-surface-3 text-text-primary'
322390 : 'text-text-muted/40 hover:text-text-muted'
323391 } `}
@@ -347,7 +415,8 @@ export function ReviewView() {
347415 < select
348416 value = { lifecycleFilter }
349417 onChange = { e => setLifecycleFilter ( e . target . value as CommentLifecycleStatus | 'All' ) }
350- className = "text-[11px] bg-surface-2 border border-border rounded px-2 py-1 text-text-secondary appearance-none pr-6 cursor-pointer"
418+ disabled = { showOnlyBlockers }
419+ className = { `text-[11px] bg-surface-2 border border-border rounded px-2 py-1 text-text-secondary appearance-none pr-6 ${ showOnlyBlockers ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' } ` }
351420 >
352421 < option value = "All" > All statuses</ option >
353422 < option value = "Open" > Open</ option >
@@ -357,14 +426,31 @@ export function ReviewView() {
357426 < ChevronDown size = { 10 } className = "absolute right-1.5 top-1/2 -translate-y-1/2 text-text-muted pointer-events-none" />
358427 </ div >
359428
429+ < button
430+ onClick = { ( ) => setShowOnlyBlockers ( value => ! value ) }
431+ className = { `text-[11px] px-2 py-1 rounded border transition-colors ${
432+ showOnlyBlockers
433+ ? 'bg-sev-warning/10 text-sev-warning border-sev-warning/20'
434+ : 'bg-surface-2 text-text-muted border-border hover:text-text-primary'
435+ } `}
436+ title = "Show only open Error and Warning findings"
437+ >
438+ Blockers only
439+ < span className = "ml-1 font-code" > { blockerCount } </ span >
440+ </ button >
441+
442+ { showOnlyBlockers && (
443+ < span className = "text-[10px] text-sev-warning font-code" > Open Error + Warning</ span >
444+ ) }
445+
360446 < span className = "ml-auto text-[11px] text-text-muted" >
361447 { filteredComments . length } /{ review . comments . length }
362448 </ span >
363449 </ div >
364450
365451 { /* Main content */ }
366452 < div className = "flex-1 overflow-auto p-4" >
367- { viewMode === 'diff' && diffFiles . length > 0 ? (
453+ { viewMode === 'diff' && visibleDiffFiles . length > 0 ? (
368454 < DiffViewer
369455 files = { visibleDiffFiles }
370456 comments = { filteredComments }
@@ -398,9 +484,13 @@ export function ReviewView() {
398484
399485 { filteredComments . length === 0 && (
400486 < div className = "text-center py-16 text-text-muted" >
401- { review . comments . length === 0
402- ? 'No findings. Code looks good!'
403- : 'No findings match the current filters.' }
487+ { showOnlyBlockers
488+ ? blockerCount === 0
489+ ? 'No open blockers remain in this review.'
490+ : 'No blockers match the current filters.'
491+ : review . comments . length === 0
492+ ? 'No findings. Code looks good!'
493+ : 'No findings match the current filters.' }
404494 </ div >
405495 ) }
406496 </ div >
0 commit comments