@@ -27,6 +27,7 @@ import { PRAttentionPanel, PRSummaryMeta } from "./PRSummaryDisplay";
2727
2828type ProjectConfig = components [ "schemas" ] [ "ProjectConfig" ] ;
2929type ReviewRun = components [ "schemas" ] [ "ReviewRun" ] ;
30+ type PRReviewItem = components [ "schemas" ] [ "PRReviewItem" ] ;
3031type ReviewsResponse = components [ "schemas" ] [ "ListReviewsResponse" ] ;
3132type OpenReviewerTerminal = ( target : { handleId : string ; harness : string } ) => void ;
3233
@@ -387,14 +388,17 @@ function ReviewsView({
387388 enabled : hasPr ,
388389 refetchInterval : ( query ) => {
389390 const data = query . state . data as ReviewsResponse | undefined ;
390- return data ?. reviews . some ( ( review ) => review . status === "running" ) ? 2500 : false ;
391+ const reviewItems = data ?. reviewItems ?? data ?. items ?? [ ] ;
392+ return reviewItems . some ( ( item ) => item . status === "running" ) || data ?. reviews . some ( ( review ) => review . status === "running" )
393+ ? 2500
394+ : false ;
391395 } ,
392396 queryFn : async ( ) => {
393397 const { data, error } = await apiClient . GET ( "/api/v1/sessions/{sessionId}/reviews" , {
394398 params : { path : { sessionId : session . id } } ,
395399 } ) ;
396400 if ( error ) throw new Error ( apiErrorMessage ( error , "Unable to load reviews" ) ) ;
397- return data ?? ( { reviewerHandleId : "" , reviews : [ ] } satisfies ReviewsResponse ) ;
401+ return data ?? ( { reviewerHandleId : "" , reviews : [ ] , reviewItems : [ ] , items : [ ] } satisfies ReviewsResponse ) ;
398402 } ,
399403 } ) ;
400404 const projectConfigQuery = useQuery ( {
@@ -422,16 +426,18 @@ function ReviewsView({
422426 onSuccess : ( { data, reused } ) => {
423427 void queryClient . invalidateQueries ( { queryKey : [ "session-reviews" , session . id ] } ) ;
424428 void queryClient . invalidateQueries ( { queryKey : workspaceQueryKey } ) ;
425- if ( reused ) {
426- setReviewNotice ( "Review is already up to date for this commit ." ) ;
429+ if ( reused || ! data ?. reviews ?. length ) {
430+ setReviewNotice ( "No needed reviews were started ." ) ;
427431 return ;
428432 }
429433 if ( data ?. reviewerHandleId ) {
430- onOpenReviewerTerminal ?.( { handleId : data . reviewerHandleId , harness : data . review . harness || "reviewer" } ) ;
434+ const harness = data . reviews [ 0 ] ?. harness || data . review . harness || "reviewer" ;
435+ onOpenReviewerTerminal ?.( { handleId : data . reviewerHandleId , harness } ) ;
431436 }
432437 } ,
433438 } ) ;
434439 const reviews = reviewsQuery . data ?. reviews ?? [ ] ;
440+ const items = reviewsQuery . data ?. reviewItems ?? reviewsQuery . data ?. items ?? [ ] ;
435441
436442 return (
437443 < div role = "tabpanel" >
@@ -445,6 +451,7 @@ function ReviewsView({
445451 onTrigger = { ( ) => triggerReview . mutate ( ) }
446452 reviewerHandleId = { reviewsQuery . data ?. reviewerHandleId ?? "" }
447453 reviews = { reviews }
454+ items = { items }
448455 notice = { reviewNotice }
449456 session = { session }
450457 />
@@ -462,6 +469,7 @@ function ReviewPanel({
462469 session,
463470 config,
464471 reviews,
472+ items,
465473 reviewerHandleId,
466474 isLoading,
467475 isTriggering,
@@ -473,6 +481,7 @@ function ReviewPanel({
473481 session : WorkspaceSession ;
474482 config ?: ProjectConfig ;
475483 reviews : ReviewRun [ ] ;
484+ items : PRReviewItem [ ] ;
476485 reviewerHandleId : string ;
477486 isLoading : boolean ;
478487 isTriggering : boolean ;
@@ -490,19 +499,50 @@ function ReviewPanel({
490499
491500 const latest = latestReview ( reviews ) ;
492501 const harness = latest ?. harness || config ?. reviewers ?. [ 0 ] ?. harness || session . provider || "reviewer" ;
502+ const terminalEnabled = Boolean ( reviewerHandleId && onOpenTerminal ) ;
493503
494504 return (
495505 < div className = "reviewer-list" >
496506 { error ? < p className = "reviewer-error" > { apiErrorMessage ( error , "Review request failed" ) } </ p > : null }
497507 { notice ? < p className = "reviewer-notice" > { notice } </ p > : null }
498- < ReviewerCard
499- handleId = { reviewerHandleId }
500- harness = { harness }
501- isTriggering = { isTriggering }
502- onOpenTerminal = { onOpenTerminal }
503- onTrigger = { onTrigger }
504- review = { latest }
505- />
508+ < div className = "reviewer-card" >
509+ < div className = "reviewer-card__top" >
510+ < div className = "reviewer-card__name" >
511+ < Shield aria-hidden = "true" />
512+ < span > { harness } </ span >
513+ </ div >
514+ < span className = "reviewer-status reviewer-status--neutral" > { items . length } PRs</ span >
515+ </ div >
516+ < div className = "reviewer-card__actions" >
517+ < button
518+ className = "reviewer-card__action reviewer-card__action--primary"
519+ disabled = { isTriggering }
520+ onClick = { onTrigger }
521+ type = "button"
522+ >
523+ < Play aria-hidden = "true" />
524+ { isTriggering ? "Starting..." : "Run needed reviews" }
525+ </ button >
526+ < button
527+ className = "reviewer-card__action"
528+ disabled = { ! terminalEnabled }
529+ onClick = { ( ) => {
530+ if ( ! terminalEnabled ) return ;
531+ onOpenTerminal ?.( { handleId : reviewerHandleId , harness } ) ;
532+ } }
533+ type = "button"
534+ >
535+ < Terminal aria-hidden = "true" />
536+ Open terminal
537+ </ button >
538+ </ div >
539+ </ div >
540+ < div className = "flex flex-col gap-2" >
541+ { items . length === 0 ? < p className = "inspector-empty" > No review state loaded yet.</ p > : null }
542+ { items . map ( ( item ) => (
543+ < ReviewItemCard key = { `${ item . prUrl } :${ item . targetSha } ` } item = { item } />
544+ ) ) }
545+ </ div >
506546 </ div >
507547 ) ;
508548}
@@ -511,85 +551,48 @@ function latestReview(reviews: ReviewRun[]): ReviewRun | undefined {
511551 return [ ...reviews ] . sort ( ( a , b ) => Date . parse ( b . createdAt ) - Date . parse ( a . createdAt ) ) [ 0 ] ;
512552}
513553
514- function ReviewerCard ( {
515- harness,
516- review,
517- handleId,
518- isTriggering,
519- onTrigger,
520- onOpenTerminal,
521- } : {
522- harness : string ;
523- review ?: ReviewRun ;
524- handleId : string ;
525- isTriggering : boolean ;
526- onTrigger : ( ) => void ;
527- onOpenTerminal ?: OpenReviewerTerminal ;
528- } ) {
529- const status = reviewStatus ( review ) ;
530- const terminalEnabled = Boolean ( handleId && onOpenTerminal ) ;
531- const runLabel = review ? "Re-run review" : "Run review" ;
532-
554+ function ReviewItemCard ( { item } : { item : PRReviewItem } ) {
555+ const status = reviewItemStatus ( item ) ;
533556 return (
534- < div className = { cn ( "reviewer-card" , status . tone && `reviewer-card--${ status . tone } ` ) } >
557+ < div className = { cn ( "reviewer-card" , status . tone && `reviewer-card--${ status . tone } ` , item . status === "ineligible" && "opacity-70" ) } >
535558 < div className = "reviewer-card__top" >
536559 < div className = "reviewer-card__name" >
537- < Shield aria-hidden = "true" />
538- < span > { harness } </ span >
560+ < GitPullRequest aria-hidden = "true" />
561+ < span > PR # { item . prNumber } </ span >
539562 </ div >
540563 < span className = { cn ( "reviewer-status" , `reviewer-status--${ status . tone } ` ) } >
541564 { status . icon }
542565 { status . label }
543566 </ span >
544567 </ div >
545- < div className = "reviewer-card__actions" >
546- < button
547- className = "reviewer-card__action reviewer-card__action--primary"
548- disabled = { isTriggering }
549- onClick = { onTrigger }
550- type = "button"
551- >
552- < Play aria-hidden = "true" />
553- { isTriggering ? "Starting..." : runLabel }
554- </ button >
555- { review ? (
556- < button
557- className = "reviewer-card__action"
558- disabled = { ! terminalEnabled }
559- onClick = { ( ) => {
560- if ( ! terminalEnabled ) return ;
561- onOpenTerminal ?.( { handleId, harness } ) ;
562- } }
563- type = "button"
564- >
565- < Terminal aria-hidden = "true" />
566- Open terminal
567- </ button >
568- ) : null }
568+ < div className = "mt-2 min-w-0 truncate font-mono text-[10.5px] text-passive" >
569+ < a href = { item . prUrl } target = "_blank" rel = "noopener noreferrer" className = "text-accent hover:underline" >
570+ { item . prUrl }
571+ </ a >
572+ { item . targetSha ? < span className = "ml-2" > { item . targetSha . slice ( 0 , 7 ) } </ span > : null }
569573 </ div >
570574 </ div >
571575 ) ;
572576}
573577
574- function reviewStatus ( review ?: ReviewRun ) : {
578+ function reviewItemStatus ( item : PRReviewItem ) : {
575579 label : string ;
576580 tone : "neutral" | "running" | "success" | "danger" ;
577581 icon : ReactNode ;
578582} {
579- if ( ! review ) return { label : "Not run" , tone : "neutral" , icon : null } ;
580- if ( review . status === "running" ) {
581- return { label : "Running" , tone : "running" , icon : < Play aria-hidden = "true" /> } ;
582- }
583- if ( review . status === "failed" ) {
584- return { label : "Failed" , tone : "danger" , icon : < AlertCircle aria-hidden = "true" /> } ;
585- }
586- if ( review . verdict === "approved" ) {
587- return { label : "Approved" , tone : "success" , icon : < CheckCircle2 aria-hidden = "true" /> } ;
588- }
589- if ( review . verdict === "changes_requested" ) {
590- return { label : "Changes requested" , tone : "danger" , icon : < CircleMinus aria-hidden = "true" /> } ;
583+ switch ( item . status ) {
584+ case "needs_review" :
585+ return { label : "Needs review" , tone : "neutral" , icon : null } ;
586+ case "running" :
587+ return { label : "Running" , tone : "running" , icon : < Play aria-hidden = "true" /> } ;
588+ case "up_to_date" :
589+ return { label : "Up to date" , tone : "success" , icon : < CheckCircle2 aria-hidden = "true" /> } ;
590+ case "changes_requested" :
591+ return { label : "Changes requested" , tone : "danger" , icon : < CircleMinus aria-hidden = "true" /> } ;
592+ case "ineligible" :
593+ return { label : "Ineligible" , tone : "neutral" , icon : < AlertCircle aria-hidden = "true" /> } ;
591594 }
592- return { label : "Complete" , tone : "success " , icon : < CheckCircle2 aria-hidden = "true" /> } ;
595+ return { label : item . status , tone : "neutral " , icon : null } ;
593596}
594597
595598function BrowserView ( {
0 commit comments