@@ -44,6 +44,7 @@ const (
4444 panicFailureIncrement = 10
4545 turnAPITimeout = 10 * time .Second
4646 maxConcurrentTurnAPICalls = 10
47+ defaultMaxBrowserOpensDay = 20
4748)
4849
4950// PR represents a pull request with metadata.
@@ -90,8 +91,10 @@ type App struct {
9091 loadingTurnData bool
9192 menuInitialized bool
9293 initialLoadComplete bool
93- enableReminders bool
9494 enableAudioCues bool
95+ enableAutoBrowser bool
96+ browserRateLimiter * BrowserRateLimiter
97+ startTime time.Time
9598}
9699
97100func loadCurrentUser (ctx context.Context , app * App ) {
@@ -145,9 +148,15 @@ func main() {
145148 var targetUser string
146149 var noCache bool
147150 var updateInterval time.Duration
151+ var browserOpenDelay time.Duration
152+ var maxBrowserOpensMinute int
153+ var maxBrowserOpensDay int
148154 flag .StringVar (& targetUser , "user" , "" , "GitHub user to query PRs for (defaults to authenticated user)" )
149155 flag .BoolVar (& noCache , "no-cache" , false , "Bypass cache for debugging" )
150156 flag .DurationVar (& updateInterval , "interval" , defaultUpdateInterval , "Update interval (e.g. 30s, 1m, 5m)" )
157+ flag .DurationVar (& browserOpenDelay , "browser-delay" , 1 * time .Minute , "Minimum delay before opening PRs in browser after startup" )
158+ flag .IntVar (& maxBrowserOpensMinute , "browser-max-per-minute" , 2 , "Maximum browser windows to open per minute" )
159+ flag .IntVar (& maxBrowserOpensDay , "browser-max-per-day" , defaultMaxBrowserOpensDay , "Maximum browser windows to open per day" )
151160 flag .Parse ()
152161
153162 // Validate target user if provided
@@ -163,9 +172,25 @@ func main() {
163172 updateInterval = minUpdateInterval
164173 }
165174
175+ // Validate browser rate limit parameters
176+ if maxBrowserOpensMinute < 0 {
177+ log .Printf ("Invalid browser-max-per-minute %d, using default of 2" , maxBrowserOpensMinute )
178+ maxBrowserOpensMinute = 2
179+ }
180+ if maxBrowserOpensDay < 0 {
181+ log .Printf ("Invalid browser-max-per-day %d, using default of %d" , maxBrowserOpensDay , defaultMaxBrowserOpensDay )
182+ maxBrowserOpensDay = defaultMaxBrowserOpensDay
183+ }
184+ if browserOpenDelay < 0 {
185+ log .Printf ("Invalid browser-delay %v, using default of 1 minute" , browserOpenDelay )
186+ browserOpenDelay = 1 * time .Minute
187+ }
188+
166189 log .SetFlags (log .LstdFlags | log .Lshortfile )
167190 log .Printf ("Starting GitHub PR Monitor (version=%s, commit=%s, date=%s)" , version , commit , date )
168191 log .Printf ("Configuration: update_interval=%v, max_retries=%d, max_delay=%v" , updateInterval , maxRetries , maxRetryDelay )
192+ log .Printf ("Browser auto-open: startup_delay=%v, max_per_minute=%d, max_per_day=%d" ,
193+ browserOpenDelay , maxBrowserOpensMinute , maxBrowserOpensDay )
169194
170195 ctx := context .Background ()
171196
@@ -187,8 +212,10 @@ func main() {
187212 noCache : noCache ,
188213 updateInterval : updateInterval ,
189214 pendingTurnResults : make ([]TurnResult , 0 ),
190- enableReminders : true ,
191215 enableAudioCues : true ,
216+ enableAutoBrowser : false , // Default to false for safety
217+ browserRateLimiter : NewBrowserRateLimiter (browserOpenDelay , maxBrowserOpensMinute , maxBrowserOpensDay ),
218+ startTime : time .Now (),
192219 }
193220
194221 // Load saved settings
@@ -492,6 +519,26 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
492519 app .checkForNewlyBlockedPRs (ctx )
493520}
494521
522+ // tryAutoOpenPR attempts to open a PR in the browser if enabled and rate limits allow.
523+ func (app * App ) tryAutoOpenPR (ctx context.Context , pr PR , autoBrowserEnabled bool , startTime time.Time ) {
524+ if ! autoBrowserEnabled {
525+ return
526+ }
527+
528+ if app .browserRateLimiter .CanOpen (startTime , pr .URL ) {
529+ log .Printf ("[BROWSER] Auto-opening newly blocked PR: %s #%d - %s" ,
530+ pr .Repository , pr .Number , pr .URL )
531+ // Use strict validation for auto-opened URLs
532+ if err := openURLAutoStrict (ctx , pr .URL ); err != nil {
533+ log .Printf ("[BROWSER] Failed to auto-open PR %s: %v" , pr .URL , err )
534+ } else {
535+ app .browserRateLimiter .RecordOpen (pr .URL )
536+ log .Printf ("[BROWSER] Successfully opened PR %s #%d in browser" ,
537+ pr .Repository , pr .Number )
538+ }
539+ }
540+ }
541+
495542// notifyWithSound sends a notification and plays sound only once per cycle.
496543func (app * App ) notifyWithSound (ctx context.Context , pr PR , isIncoming bool , playedSound * bool ) {
497544 var title , soundType string
@@ -521,6 +568,8 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
521568 incoming := app .incoming
522569 outgoing := app .outgoing
523570 previousBlocked := app .previousBlockedPRs
571+ autoBrowserEnabled := app .enableAutoBrowser
572+ startTime := app .startTime
524573 app .mu .RUnlock ()
525574
526575 currentBlocked := make (map [string ]bool )
@@ -534,6 +583,7 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
534583 // Notify if newly blocked
535584 if ! previousBlocked [incoming [i ].URL ] {
536585 app .notifyWithSound (ctx , incoming [i ], true , & playedHonk )
586+ app .tryAutoOpenPR (ctx , incoming [i ], autoBrowserEnabled , startTime )
537587 }
538588 }
539589 }
@@ -549,6 +599,7 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
549599 time .Sleep (2 * time .Second )
550600 }
551601 app .notifyWithSound (ctx , outgoing [i ], false , & playedJet )
602+ app .tryAutoOpenPR (ctx , outgoing [i ], autoBrowserEnabled , startTime )
552603 }
553604 }
554605 }
0 commit comments