@@ -86,6 +86,7 @@ type App struct {
8686 authError string
8787 pendingTurnResults []TurnResult
8888 lastMenuTitles []string
89+ lastMenuRebuild time.Time
8990 incoming []PR
9091 outgoing []PR
9192 updateInterval time.Duration
@@ -121,10 +122,10 @@ func loadCurrentUser(ctx context.Context, app *App) {
121122 return nil
122123 },
123124 retry .Attempts (maxRetries ),
124- retry .DelayType (retry .BackOffDelay ),
125+ retry .DelayType (retry .CombineDelay ( retry . BackOffDelay , retry . RandomDelay )), // Add jitter for better backoff distribution
125126 retry .MaxDelay (maxRetryDelay ),
126127 retry .OnRetry (func (n uint , err error ) {
127- log .Printf ("GitHub Users.Get retry %d/%d: %v" , n + 1 , maxRetries , err )
128+ log .Printf ("[GITHUB] Users.Get retry %d/%d: %v" , n + 1 , maxRetries , err )
128129 }),
129130 retry .Context (ctx ),
130131 )
@@ -449,12 +450,50 @@ func (app *App) updatePRs(ctx context.Context) {
449450 app .updateMenu (ctx )
450451}
451452
453+ // hasIconAboutToExpire checks if any PR icon is near its expiration time.
454+ // Returns true if any blocked PR has been blocked for approximately 25 minutes.
455+ func (app * App ) hasIconAboutToExpire () bool {
456+ now := time .Now ()
457+ windowStart := blockedPRIconDuration - time .Minute
458+ windowEnd := blockedPRIconDuration + time .Minute
459+
460+ // Check incoming PRs
461+ for i := range app .incoming {
462+ if ! app .incoming [i ].NeedsReview || app .incoming [i ].FirstBlockedAt .IsZero () {
463+ continue
464+ }
465+ age := now .Sub (app .incoming [i ].FirstBlockedAt )
466+ // Icon expires at blockedPRIconDuration; check if we're within a minute of that
467+ if age > windowStart && age < windowEnd {
468+ log .Printf ("[MENU] Incoming PR %s #%d icon expiring soon (blocked %v ago)" ,
469+ app .incoming [i ].Repository , app .incoming [i ].Number , age .Round (time .Second ))
470+ return true
471+ }
472+ }
473+
474+ // Check outgoing PRs
475+ for i := range app .outgoing {
476+ if ! app .outgoing [i ].IsBlocked || app .outgoing [i ].FirstBlockedAt .IsZero () {
477+ continue
478+ }
479+ age := now .Sub (app .outgoing [i ].FirstBlockedAt )
480+ if age > windowStart && age < windowEnd {
481+ log .Printf ("[MENU] Outgoing PR %s #%d icon expiring soon (blocked %v ago)" ,
482+ app .outgoing [i ].Repository , app .outgoing [i ].Number , age .Round (time .Second ))
483+ return true
484+ }
485+ }
486+
487+ return false
488+ }
489+
452490// updateMenu rebuilds the menu only when content actually changes.
453491func (app * App ) updateMenu (ctx context.Context ) {
454492 app .mu .RLock ()
455493 // Skip menu updates while Turn data is still loading
456494 if app .loadingTurnData {
457495 app .mu .RUnlock ()
496+ log .Println ("[MENU] Skipping menu update: Turn data still loading" )
458497 return
459498 }
460499
@@ -468,18 +507,33 @@ func (app *App) updateMenu(ctx context.Context) {
468507 }
469508
470509 lastTitles := app .lastMenuTitles
510+ lastRebuild := app .lastMenuRebuild
511+ hasExpiringIcons := app .hasIconAboutToExpire ()
471512 app .mu .RUnlock ()
472513
473- // Only rebuild if titles changed
474- if reflect .DeepEqual (lastTitles , currentTitles ) {
475- return
476- }
514+ // Rebuild if:
515+ // 1. PR list changed, OR
516+ // 2. An icon is about to expire and we haven't rebuilt recently
517+ titlesChanged := ! reflect .DeepEqual (lastTitles , currentTitles )
518+ timeSinceLastRebuild := time .Since (lastRebuild )
519+ iconUpdateDue := hasExpiringIcons && timeSinceLastRebuild > 30 * time .Second
477520
478- app .mu .Lock ()
479- app .lastMenuTitles = currentTitles
480- app .mu .Unlock ()
521+ if titlesChanged || iconUpdateDue {
522+ app .mu .Lock ()
523+ if titlesChanged {
524+ app .lastMenuTitles = currentTitles
525+ log .Printf ("[MENU] PR list changed, triggering rebuild (was %d items, now %d items)" ,
526+ len (lastTitles ), len (currentTitles ))
527+ }
528+ app .lastMenuRebuild = time .Now ()
529+ app .mu .Unlock ()
481530
482- app .rebuildMenu (ctx )
531+ if iconUpdateDue {
532+ log .Printf ("[MENU] Rebuilding menu: party popper icon expiring (last rebuild: %v ago)" ,
533+ timeSinceLastRebuild .Round (time .Second ))
534+ }
535+ app .rebuildMenu (ctx )
536+ }
483537}
484538
485539// updatePRsWithWait fetches PRs and waits for Turn data before building initial menu.
0 commit comments