@@ -15,6 +15,10 @@ import {
1515 Users ,
1616 RefreshCw ,
1717 Github ,
18+ Circle ,
19+ CheckCircle2 ,
20+ XCircle ,
21+ MessageSquare ,
1822} from "lucide-react" ;
1923import { cn } from "../cn" ;
2024import { Skeleton } from "../ui/skeleton" ;
@@ -55,7 +59,12 @@ interface SearchResult {
5559}
5660
5761// Filter mode type
58- type FilterMode = "review-requested" | "authored" | "involves" | "all" ;
62+ type FilterMode =
63+ | "review-requested"
64+ | "reviewed"
65+ | "authored"
66+ | "involves"
67+ | "all" ;
5968
6069// Special constant for "All Repos" global filter
6170const ALL_REPOS_KEY = "__all_repos__" ;
@@ -130,6 +139,8 @@ function getModeFilter(mode: FilterMode): string {
130139 switch ( mode ) {
131140 case "review-requested" :
132141 return "review-requested:@me" ;
142+ case "reviewed" :
143+ return "reviewed-by:@me" ;
133144 case "authored" :
134145 return "author:@me" ;
135146 case "involves" :
@@ -234,6 +245,12 @@ const MODE_OPTIONS = [
234245 icon : Eye ,
235246 description : "PRs where you're requested as reviewer" ,
236247 } ,
248+ {
249+ value : "reviewed" ,
250+ label : "Reviewed" ,
251+ icon : MessageSquare ,
252+ description : "PRs you've already reviewed" ,
253+ } ,
237254 {
238255 value : "authored" ,
239256 label : "My PRs" ,
@@ -301,7 +318,15 @@ export function Home() {
301318 if ( githubReady && ! ( isAnonymous && queriesRequireAuth ) ) {
302319 fetchPRList ( searchQueries , page , perPage ) ;
303320 }
304- } , [ fetchPRList , searchQueries , page , perPage , githubReady , isAnonymous , queriesRequireAuth ] ) ;
321+ } , [
322+ fetchPRList ,
323+ searchQueries ,
324+ page ,
325+ perPage ,
326+ githubReady ,
327+ isAnonymous ,
328+ queriesRequireAuth ,
329+ ] ) ;
305330
306331 // Reset page when config changes
307332 useEffect ( ( ) => {
@@ -388,10 +413,17 @@ export function Home() {
388413
389414 // Track which repo dropdown is open
390415 const [ openRepoDropdown , setOpenRepoDropdown ] = useState < string | null > ( null ) ;
391- const [ repoDropdownPosition , setRepoDropdownPosition ] = useState ( { top : 0 , left : 0 } ) ;
416+ const [ repoDropdownPosition , setRepoDropdownPosition ] = useState ( {
417+ top : 0 ,
418+ left : 0 ,
419+ } ) ;
392420 const [ showAddRepo , setShowAddRepo ] = useState ( false ) ;
393- const [ addRepoButtonRef , setAddRepoButtonRef ] = useState < HTMLButtonElement | null > ( null ) ;
394- const [ addRepoDropdownPosition , setAddRepoDropdownPosition ] = useState ( { top : 0 , right : 0 } ) ;
421+ const [ addRepoButtonRef , setAddRepoButtonRef ] =
422+ useState < HTMLButtonElement | null > ( null ) ;
423+ const [ addRepoDropdownPosition , setAddRepoDropdownPosition ] = useState ( {
424+ top : 0 ,
425+ right : 0 ,
426+ } ) ;
395427
396428 // Show loading/error state while GitHub client initializes
397429 if ( ! githubReady ) {
@@ -561,20 +593,8 @@ export function Home() {
561593 } ) }
562594 </ div >
563595
564- { /* Query Preview & Add Repo - pushed to right */ }
596+ { /* Add Repo - pushed to right */ }
565597 < div className = "flex items-center gap-2 shrink-0 ml-auto" >
566- { /* Query Preview */ }
567- { searchQueries . length > 0 && (
568- < div
569- className = "text-xs text-muted-foreground font-mono truncate max-w-xs hidden lg:block shrink-0"
570- title = { searchQueries . join ( "\n" ) }
571- >
572- { searchQueries . length === 1
573- ? searchQueries [ 0 ]
574- : `${ searchQueries . length } queries` }
575- </ div >
576- ) }
577-
578598 { /* Add Repo Button */ }
579599 < div className = "relative shrink-0" >
580600 < button
@@ -637,64 +657,66 @@ export function Home() {
637657 </ div >
638658
639659 < div className = "add-repo-dropdown max-h-64 overflow-auto" >
640- { /* All Repos option - always shown at top when not already added */ }
641- { ! config . repos . some ( isAllReposFilter ) && ! searchQuery && (
642- < button
643- onMouseDown = { ( ) => {
644- handleAddRepo ( ALL_REPOS_KEY ) ;
645- setShowAddRepo ( false ) ;
646- } }
647- className = "w-full flex items-center gap-2 px-3 py-2.5 hover:bg-primary/10 transition-colors text-left border-b border-border bg-primary/5"
648- >
649- < div className = "w-4 h-4 rounded bg-primary/20 flex items-center justify-center shrink-0" >
650- < Users className = "w-3 h-3 text-primary" />
651- </ div >
652- < div className = "flex-1 min-w-0" >
653- < span className = "font-medium text-xs" > All Repos</ span >
654- < span className = "text-[10px] text-muted-foreground ml-1.5" >
655- PRs across all repositories
656- </ span >
657- </ div >
658- </ button >
659- ) }
660- { searchResults . length > 0 ? (
661- searchResults . map ( ( repo ) => (
660+ { /* All Repos option - always shown at top when not already added */ }
661+ { ! config . repos . some ( isAllReposFilter ) && ! searchQuery && (
662662 < button
663- key = { repo . id }
664663 onMouseDown = { ( ) => {
665- handleAddRepo ( repo . full_name ) ;
664+ handleAddRepo ( ALL_REPOS_KEY ) ;
666665 setShowAddRepo ( false ) ;
667- setSearchQuery ( "" ) ;
668666 } }
669- className = "w-full flex items-center gap-2 px-3 py-2 hover:bg-muted/50 transition-colors text-left border-b border-border/50 last:border-b-0 "
667+ className = "w-full flex items-center gap-2 px-3 py-2.5 hover:bg-primary/10 transition-colors text-left border-b border-border bg-primary/5 "
670668 >
671- { repo . owner && (
672- < img
673- src = { repo . owner . avatar_url }
674- alt = { repo . owner . login }
675- className = "w-4 h-4 rounded shrink-0"
676- />
677- ) }
678- < span className = "font-medium text-xs truncate flex-1" >
679- { repo . full_name }
680- </ span >
681- < span className = "flex items-center gap-1 text-[10px] text-muted-foreground" >
682- < Star className = "w-3 h-3" />
683- { ( repo . stargazers_count ?? 0 ) . toLocaleString ( ) }
684- </ span >
669+ < div className = "w-4 h-4 rounded bg-primary/20 flex items-center justify-center shrink-0" >
670+ < Users className = "w-3 h-3 text-primary" />
671+ </ div >
672+ < div className = "flex-1 min-w-0" >
673+ < span className = "font-medium text-xs" >
674+ All Repos
675+ </ span >
676+ < span className = "text-[10px] text-muted-foreground ml-1.5" >
677+ PRs across all repositories
678+ </ span >
679+ </ div >
685680 </ button >
686- ) )
687- ) : searchQuery ? (
688- < div className = "px-3 py-4 text-xs text-muted-foreground text-center" >
689- { searching ? "Searching..." : "No repositories found" }
690- </ div >
691- ) : (
692- < div className = "px-3 py-4 text-xs text-muted-foreground text-center" >
693- Type to search for repositories
694- </ div >
695- ) }
681+ ) }
682+ { searchResults . length > 0 ? (
683+ searchResults . map ( ( repo ) => (
684+ < button
685+ key = { repo . id }
686+ onMouseDown = { ( ) => {
687+ handleAddRepo ( repo . full_name ) ;
688+ setShowAddRepo ( false ) ;
689+ setSearchQuery ( "" ) ;
690+ } }
691+ className = "w-full flex items-center gap-2 px-3 py-2 hover:bg-muted/50 transition-colors text-left border-b border-border/50 last:border-b-0"
692+ >
693+ { repo . owner && (
694+ < img
695+ src = { repo . owner . avatar_url }
696+ alt = { repo . owner . login }
697+ className = "w-4 h-4 rounded shrink-0"
698+ />
699+ ) }
700+ < span className = "font-medium text-xs truncate flex-1" >
701+ { repo . full_name }
702+ </ span >
703+ < span className = "flex items-center gap-1 text-[10px] text-muted-foreground" >
704+ < Star className = "w-3 h-3" />
705+ { ( repo . stargazers_count ?? 0 ) . toLocaleString ( ) }
706+ </ span >
707+ </ button >
708+ ) )
709+ ) : searchQuery ? (
710+ < div className = "px-3 py-4 text-xs text-muted-foreground text-center" >
711+ { searching ? "Searching..." : "No repositories found" }
712+ </ div >
713+ ) : (
714+ < div className = "px-3 py-4 text-xs text-muted-foreground text-center" >
715+ Type to search for repositories
716+ </ div >
717+ ) }
718+ </ div >
696719 </ div >
697- </ div >
698720 </ >
699721 ) }
700722 </ div >
@@ -906,6 +928,81 @@ function PRListItem({ pr, onSelect }: PRListItemProps) {
906928 }
907929 } ;
908930
931+ // CI status indicator with details
932+ const CIStatusBadge = ( ) => {
933+ if ( ! pr . ciStatus || pr . ciStatus === "none" ) return null ;
934+
935+ const summary =
936+ pr . ciSummary ||
937+ ( pr . ciStatus === "success"
938+ ? "Passed"
939+ : pr . ciStatus === "failure"
940+ ? "Failed"
941+ : "Running" ) ;
942+
943+ switch ( pr . ciStatus ) {
944+ case "success" :
945+ return (
946+ < span
947+ title = {
948+ pr . ciChecks
949+ ?. map (
950+ ( c ) =>
951+ `${ c . state === "success" ? "✓" : c . state === "failure" ? "✗" : "○" } ${ c . name } `
952+ )
953+ . join ( "\n" ) || "CI passed"
954+ }
955+ className = "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-green-500/15 text-green-500 border border-green-500/30"
956+ >
957+ < CheckCircle2 className = "w-3 h-3" />
958+ < span className = "hidden sm:inline max-w-[100px] truncate" >
959+ { summary }
960+ </ span >
961+ </ span >
962+ ) ;
963+ case "failure" :
964+ return (
965+ < span
966+ title = {
967+ pr . ciChecks
968+ ?. map (
969+ ( c ) =>
970+ `${ c . state === "success" ? "✓" : c . state === "failure" ? "✗" : "○" } ${ c . name } `
971+ )
972+ . join ( "\n" ) || "CI failed"
973+ }
974+ className = "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-red-500/15 text-red-500 border border-red-500/30"
975+ >
976+ < XCircle className = "w-3 h-3" />
977+ < span className = "hidden sm:inline max-w-[100px] truncate" >
978+ { summary }
979+ </ span >
980+ </ span >
981+ ) ;
982+ case "pending" :
983+ return (
984+ < span
985+ title = {
986+ pr . ciChecks
987+ ?. map (
988+ ( c ) =>
989+ `${ c . state === "success" ? "✓" : c . state === "failure" ? "✗" : "○" } ${ c . name } `
990+ )
991+ . join ( "\n" ) || "CI running"
992+ }
993+ className = "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-yellow-500/15 text-yellow-500 border border-yellow-500/30"
994+ >
995+ < Circle className = "w-3 h-3 animate-pulse" />
996+ < span className = "hidden sm:inline max-w-[100px] truncate" >
997+ { summary }
998+ </ span >
999+ </ span >
1000+ ) ;
1001+ default :
1002+ return null ;
1003+ }
1004+ } ;
1005+
9091006 return (
9101007 < button
9111008 onClick = { handleClick }
@@ -931,6 +1028,7 @@ function PRListItem({ pr, onSelect }: PRListItemProps) {
9311028 < span className = "font-medium hover:text-blue-400 break-words" >
9321029 { pr . title }
9331030 </ span >
1031+ < CIStatusBadge />
9341032 { pr . hasNewChanges && (
9351033 < span className = "px-1.5 py-0.5 text-[10px] font-semibold rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0" >
9361034 NEW
0 commit comments