@@ -64,6 +64,7 @@ type FilterMode =
6464 | "review-requested"
6565 | "reviewed"
6666 | "authored"
67+ | "authored-by"
6768 | "involves"
6869 | "all" ;
6970
@@ -74,6 +75,7 @@ const ALL_REPOS_KEY = "__all_repos__";
7475interface RepoFilter {
7576 name : string ;
7677 mode : FilterMode ;
78+ authoredBy ?: string ; // Username for "authored-by" filter mode
7779}
7880
7981// Filter configuration stored in localStorage
@@ -136,14 +138,16 @@ function saveFilterConfig(config: FilterConfig): void {
136138// Query Builder
137139// ============================================================================
138140
139- function getModeFilter ( mode : FilterMode ) : string {
141+ function getModeFilter ( mode : FilterMode , authoredBy ?: string ) : string {
140142 switch ( mode ) {
141143 case "review-requested" :
142144 return "review-requested:@me" ;
143145 case "reviewed" :
144146 return "reviewed-by:@me" ;
145147 case "authored" :
146148 return "author:@me" ;
149+ case "authored-by" :
150+ return authoredBy ? `author:${ authoredBy } ` : "" ;
147151 case "involves" :
148152 return "involves:@me" ;
149153 default :
@@ -175,29 +179,38 @@ function buildSearchQueries(config: FilterConfig): string[] {
175179 for ( const filter of allReposFilters ) {
176180 const parts = [ "is:pr" , "archived:false" ] ;
177181 if ( stateFilter ) parts . push ( stateFilter ) ;
178- const modeFilter = getModeFilter ( filter . mode ) ;
182+ const modeFilter = getModeFilter ( filter . mode , filter . authoredBy ) ;
179183 if ( modeFilter ) parts . push ( modeFilter ) ;
180184 // Note: "all" mode on All Repos would be too broad, so we skip it
181- if ( filter . mode !== "all" ) {
185+ // Also skip "authored-by" without a username
186+ if ( filter . mode !== "all" && ! ( filter . mode === "authored-by" && ! filter . authoredBy ) ) {
182187 queries . push ( parts . join ( " " ) ) ;
183188 }
184189 }
185190
186- // Group specific repos by mode
191+ // Group specific repos by mode+authoredBy (for authored-by, different authors need separate queries)
187192 if ( specificRepos . length > 0 ) {
188- const byMode = new Map < FilterMode , string [ ] > ( ) ;
193+ // Use a composite key: mode + authoredBy for authored-by mode
194+ const byModeKey = new Map < string , { mode : FilterMode ; authoredBy ?: string ; repos : string [ ] } > ( ) ;
189195 for ( const repo of specificRepos ) {
190- const existing = byMode . get ( repo . mode ) || [ ] ;
191- existing . push ( repo . name ) ;
192- byMode . set ( repo . mode , existing ) ;
196+ const key = repo . mode === "authored-by" ? `${ repo . mode } :${ repo . authoredBy || "" } ` : repo . mode ;
197+ const existing = byModeKey . get ( key ) ;
198+ if ( existing ) {
199+ existing . repos . push ( repo . name ) ;
200+ } else {
201+ byModeKey . set ( key , { mode : repo . mode , authoredBy : repo . authoredBy , repos : [ repo . name ] } ) ;
202+ }
193203 }
194204
195- for ( const [ mode , repos ] of byMode ) {
205+ for ( const [ , { mode, authoredBy, repos } ] of byModeKey ) {
206+ // Skip authored-by without a username
207+ if ( mode === "authored-by" && ! authoredBy ) continue ;
208+
196209 const parts = [ "is:pr" , "archived:false" ] ;
197210 if ( stateFilter ) parts . push ( stateFilter ) ;
198211 // Multiple repo: qualifiers act as OR
199212 parts . push ( ...repos . map ( ( r ) => `repo:${ r } ` ) ) ;
200- const modeFilter = getModeFilter ( mode ) ;
213+ const modeFilter = getModeFilter ( mode , authoredBy ) ;
201214 if ( modeFilter ) parts . push ( modeFilter ) ;
202215 queries . push ( parts . join ( " " ) ) ;
203216 }
@@ -258,6 +271,13 @@ const MODE_OPTIONS = [
258271 icon : User ,
259272 description : "PRs you authored" ,
260273 } ,
274+ {
275+ value : "authored-by" ,
276+ label : "Created by User" ,
277+ icon : User ,
278+ description : "PRs created by a specific user" ,
279+ hasInput : true ,
280+ } ,
261281 {
262282 value : "involves" ,
263283 label : "Involves Me" ,
@@ -388,11 +408,11 @@ export function Home() {
388408 } , [ ] ) ;
389409
390410 const handleRepoModeChange = useCallback (
391- ( repoName : string , mode : FilterMode ) => {
411+ ( repoName : string , mode : FilterMode , authoredBy ?: string ) => {
392412 setConfig ( ( prev ) => ( {
393413 ...prev ,
394414 repos : prev . repos . map ( ( r ) =>
395- r . name === repoName ? { ...r , mode } : r
415+ r . name === repoName ? { ...r , mode, authoredBy } : r
396416 ) ,
397417 } ) ) ;
398418 } ,
@@ -418,6 +438,9 @@ export function Home() {
418438 top : 0 ,
419439 left : 0 ,
420440 } ) ;
441+ // Track author input for "authored-by" mode
442+ const [ authoredByInput , setAuthoredByInput ] = useState < string > ( "" ) ;
443+ const [ showAuthoredByInput , setShowAuthoredByInput ] = useState < string | null > ( null ) ;
421444 const [ showAddRepo , setShowAddRepo ] = useState ( false ) ;
422445 const [ addRepoButtonRef , setAddRepoButtonRef ] =
423446 useState < HTMLButtonElement | null > ( null ) ;
@@ -534,6 +557,11 @@ export function Home() {
534557 < span className = { isAllRepos ? "font-medium" : "font-mono" } >
535558 { isAllRepos ? "All Repos" : repo . name }
536559 </ span >
560+ { repo . mode === "authored-by" && repo . authoredBy && (
561+ < span className = "text-muted-foreground" >
562+ @{ repo . authoredBy }
563+ </ span >
564+ ) }
537565 < ChevronDown className = "w-3 h-3 text-muted-foreground" />
538566 < button
539567 onClick = { ( e ) => {
@@ -551,7 +579,11 @@ export function Home() {
551579 { /* Backdrop to close dropdown when clicking outside */ }
552580 < div
553581 className = "fixed inset-0 z-40"
554- onClick = { ( ) => setOpenRepoDropdown ( null ) }
582+ onClick = { ( ) => {
583+ setOpenRepoDropdown ( null ) ;
584+ setShowAuthoredByInput ( null ) ;
585+ setAuthoredByInput ( "" ) ;
586+ } }
555587 />
556588 < div
557589 className = "fixed w-56 bg-card border border-border rounded-lg shadow-xl z-50 max-w-[calc(100vw-1rem)] sm:max-w-none"
@@ -560,32 +592,89 @@ export function Home() {
560592 left : repoDropdownPosition . left ,
561593 } }
562594 >
563- { availableModes . map ( ( option ) => (
564- < button
565- key = { option . value }
566- onClick = { ( ) => {
567- handleRepoModeChange ( repo . name , option . value ) ;
568- setOpenRepoDropdown ( null ) ;
569- } }
570- className = { cn (
571- "w-full flex items-start gap-2.5 px-3 py-2 hover:bg-muted/50 transition-colors text-left" ,
572- repo . mode === option . value && "bg-muted/50"
573- ) }
574- >
575- < option . icon className = "w-3.5 h-3.5 mt-0.5 shrink-0" />
576- < div className = "flex-1 min-w-0" >
577- < div className = "font-medium text-xs" >
578- { option . label }
595+ { showAuthoredByInput === repo . name ? (
596+ < div className = "p-3" >
597+ < div className = "text-xs font-medium mb-2" > Enter GitHub username</ div >
598+ < form
599+ onSubmit = { ( e ) => {
600+ e . preventDefault ( ) ;
601+ if ( authoredByInput . trim ( ) ) {
602+ handleRepoModeChange ( repo . name , "authored-by" , authoredByInput . trim ( ) ) ;
603+ setOpenRepoDropdown ( null ) ;
604+ setShowAuthoredByInput ( null ) ;
605+ setAuthoredByInput ( "" ) ;
606+ }
607+ } }
608+ >
609+ < div className = "flex gap-2" >
610+ < div className = "relative flex-1" >
611+ < span className = "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground text-xs" > @</ span >
612+ < input
613+ type = "text"
614+ value = { authoredByInput }
615+ onChange = { ( e ) => setAuthoredByInput ( e . target . value ) }
616+ placeholder = "username"
617+ className = "w-full h-7 pl-6 pr-2 rounded-md border border-border bg-muted/50 text-xs placeholder:text-muted-foreground/60 focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
618+ autoFocus
619+ />
620+ </ div >
621+ < button
622+ type = "submit"
623+ disabled = { ! authoredByInput . trim ( ) }
624+ className = "px-2 py-1 rounded-md bg-primary text-primary-foreground text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed"
625+ >
626+ Apply
627+ </ button >
579628 </ div >
580- < div className = "text-[10px] text-muted-foreground" >
581- { option . description }
629+ </ form >
630+ < button
631+ onClick = { ( ) => {
632+ setShowAuthoredByInput ( null ) ;
633+ setAuthoredByInput ( "" ) ;
634+ } }
635+ className = "mt-2 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
636+ >
637+ ← Back to modes
638+ </ button >
639+ </ div >
640+ ) : (
641+ availableModes . map ( ( option ) => (
642+ < button
643+ key = { option . value }
644+ onClick = { ( ) => {
645+ if ( option . value === "authored-by" ) {
646+ setShowAuthoredByInput ( repo . name ) ;
647+ setAuthoredByInput ( repo . authoredBy || "" ) ;
648+ } else {
649+ handleRepoModeChange ( repo . name , option . value ) ;
650+ setOpenRepoDropdown ( null ) ;
651+ }
652+ } }
653+ className = { cn (
654+ "w-full flex items-start gap-2.5 px-3 py-2 hover:bg-muted/50 transition-colors text-left" ,
655+ repo . mode === option . value && "bg-muted/50"
656+ ) }
657+ >
658+ < option . icon className = "w-3.5 h-3.5 mt-0.5 shrink-0" />
659+ < div className = "flex-1 min-w-0" >
660+ < div className = "font-medium text-xs" >
661+ { option . label }
662+ { option . value === "authored-by" && repo . mode === "authored-by" && repo . authoredBy && (
663+ < span className = "text-muted-foreground font-normal ml-1" >
664+ @{ repo . authoredBy }
665+ </ span >
666+ ) }
667+ </ div >
668+ < div className = "text-[10px] text-muted-foreground" >
669+ { option . description }
670+ </ div >
582671 </ div >
583- </ div >
584- { repo . mode === option . value && (
585- < Check className = "w-3.5 h-3.5 text-primary mt-0.5" />
586- ) }
587- </ button >
588- ) ) }
672+ { repo . mode === option . value && (
673+ < Check className = "w-3.5 h-3.5 text-primary mt-0.5" />
674+ ) }
675+ </ button >
676+ ) )
677+ ) }
589678 </ div >
590679 </ >
591680 ) }
0 commit comments