@@ -70,6 +70,7 @@ const (
7070 maxConcurrentTurnAPICalls = 20
7171 defaultMaxBrowserOpensDay = 20
7272 startupGracePeriod = 1 * time .Minute // Don't play sounds or auto-open for first minute
73+ authRetryInterval = 2 * time .Minute // Retry authentication periodically when in error state
7374)
7475
7576// PR represents a pull request with metadata.
@@ -420,6 +421,46 @@ func (app *App) handleReauthentication(ctx context.Context) {
420421 }
421422}
422423
424+ // authRetryLoop periodically attempts to re-authenticate when in auth error state.
425+ // This ensures the app can recover from transient auth failures without user intervention.
426+ func (app * App ) authRetryLoop (ctx context.Context ) {
427+ ticker := time .NewTicker (authRetryInterval )
428+ defer ticker .Stop ()
429+
430+ slog .Info ("[AUTH] Starting auth retry loop" , "interval" , authRetryInterval )
431+
432+ for {
433+ select {
434+ case <- ctx .Done ():
435+ slog .Info ("[AUTH] Auth retry loop stopped (context cancelled)" )
436+ return
437+ case <- ticker .C :
438+ app .mu .RLock ()
439+ hasAuthError := app .authError != ""
440+ app .mu .RUnlock ()
441+
442+ if ! hasAuthError {
443+ slog .Info ("[AUTH] Auth error cleared, stopping retry loop" )
444+ return
445+ }
446+
447+ slog .Info ("[AUTH] Attempting automatic re-authentication" )
448+ app .handleReauthentication (ctx )
449+
450+ // Check if we recovered
451+ app .mu .RLock ()
452+ stillHasError := app .authError != ""
453+ app .mu .RUnlock ()
454+
455+ if ! stillHasError {
456+ slog .Info ("[AUTH] Automatic re-authentication successful, stopping retry loop" )
457+ return
458+ }
459+ slog .Info ("[AUTH] Re-authentication failed, will retry" , "nextRetry" , authRetryInterval )
460+ }
461+ }
462+ }
463+
423464func (app * App ) onReady (ctx context.Context ) {
424465 slog .Info ("System tray ready" )
425466
@@ -501,6 +542,8 @@ func (app *App) onReady(ctx context.Context) {
501542 app .rebuildMenu (ctx )
502543 // Clean old cache on startup
503544 app .cleanupOldCache ()
545+ // Start background auth retry loop
546+ go app .authRetryLoop (ctx )
504547 return
505548 }
506549
@@ -721,7 +764,7 @@ func (app *App) updatePRs(ctx context.Context) {
721764
722765 // Process notifications using the simplified state manager
723766 slog .Debug ("[DEBUG] Processing PR state updates and notifications" )
724- app .updatePRStatesAndNotify (ctx )
767+ app .processNotifications (ctx )
725768 slog .Debug ("[DEBUG] Completed PR state updates and notifications" )
726769}
727770
@@ -884,7 +927,7 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
884927
885928 // Process notifications using the simplified state manager
886929 slog .Info ("[FLOW] About to process PR state updates and notifications" )
887- app .updatePRStatesAndNotify (ctx )
930+ app .processNotifications (ctx )
888931 slog .Info ("[FLOW] Completed PR state updates and notifications" )
889932 // Mark initial load as complete after first successful update
890933 if ! app .initialLoadComplete {
@@ -955,9 +998,7 @@ func (app *App) tryAutoOpenPR(ctx context.Context, pr *PR, autoBrowserEnabled bo
955998 }
956999}
9571000
958- // checkForNewlyBlockedPRs provides backward compatibility for tests
959- // while using the new state manager internally.
1001+ // checkForNewlyBlockedPRs provides backward compatibility for tests.
9601002func (app * App ) checkForNewlyBlockedPRs (ctx context.Context ) {
961- // Simply delegate to the new implementation
962- app .updatePRStatesAndNotify (ctx )
1003+ app .processNotifications (ctx )
9631004}
0 commit comments