@@ -11,6 +11,7 @@ import (
1111 "log/slog"
1212 "os"
1313 "path/filepath"
14+ "runtime"
1415 "slices"
1516 "strings"
1617 "sync"
@@ -192,7 +193,7 @@ func main() {
192193 Level : slog .LevelInfo ,
193194 }
194195 slog .SetDefault (slog .New (slog .NewTextHandler (os .Stderr , opts )))
195- slog .Info ("Starting GitHub PR Monitor " , "version" , version , "commit" , commit , "date" , date )
196+ slog .Info ("Starting Goose " , "version" , version , "commit" , commit , "date" , date )
196197 slog .Info ("Configuration" , "update_interval" , updateInterval , "max_retries" , maxRetries , "max_delay" , maxRetryDelay )
197198 slog .Info ("Browser auto-open configuration" , "startup_delay" , browserOpenDelay , "max_per_minute" , maxBrowserOpensMinute , "max_per_day" , maxBrowserOpensDay )
198199
@@ -283,23 +284,87 @@ func main() {
283284func (app * App ) onReady (ctx context.Context ) {
284285 slog .Info ("System tray ready" )
285286
287+ // On Linux, immediately build a minimal menu to ensure it's visible
288+ if runtime .GOOS == "linux" {
289+ slog .Info ("[LINUX] Building initial minimal menu" )
290+ app .systrayInterface .ResetMenu ()
291+ placeholderItem := app .systrayInterface .AddMenuItem ("Loading..." , "Goose is starting up" )
292+ if placeholderItem != nil {
293+ placeholderItem .Disable ()
294+ }
295+ app .systrayInterface .AddSeparator ()
296+ quitItem := app .systrayInterface .AddMenuItem ("Quit" , "Quit Goose" )
297+ if quitItem != nil {
298+ quitItem .Click (func () {
299+ slog .Info ("Quit clicked" )
300+ systray .Quit ()
301+ })
302+ }
303+ }
304+
286305 // Set up click handlers first (needed for both success and error states)
287306 systray .SetOnClick (func (menu systray.IMenu ) {
288307 slog .Debug ("Icon clicked" )
289308
290- // Check if we can perform a forced refresh (rate limited to every 10 seconds)
309+ // Check if we're in auth error state and should retry
291310 app .mu .RLock ()
292- timeSinceLastSearch := time . Since ( app .lastSearchAttempt )
311+ hasAuthError := app .authError != ""
293312 app .mu .RUnlock ()
294313
295- if timeSinceLastSearch >= minUpdateInterval {
296- slog .Info ("[CLICK] Forcing search refresh" , "lastSearchAgo" , timeSinceLastSearch )
314+ if hasAuthError {
315+ slog .Info ("[CLICK] Auth error detected, attempting to re-authenticate" )
297316 go func () {
298- app .updatePRs (ctx )
317+ // Try to reinitialize clients which will attempt to get token via gh auth token
318+ if err := app .initClients (ctx ); err != nil {
319+ slog .Warn ("[CLICK] Re-authentication failed" , "error" , err )
320+ app .mu .Lock ()
321+ app .authError = err .Error ()
322+ app .mu .Unlock ()
323+ } else {
324+ // Success! Clear auth error and reload user
325+ slog .Info ("[CLICK] Re-authentication successful" )
326+ app .mu .Lock ()
327+ app .authError = ""
328+ app .mu .Unlock ()
329+
330+ // Load current user
331+ loadCurrentUser (ctx , app )
332+
333+ // Update tooltip
334+ tooltip := "Goose - Loading PRs..."
335+ if app .targetUser != "" {
336+ tooltip = fmt .Sprintf ("Goose - Loading PRs... (@%s)" , app .targetUser )
337+ }
338+ systray .SetTooltip (tooltip )
339+
340+ // Rebuild menu to remove error state
341+ app .rebuildMenu (ctx )
342+
343+ // Start update loop if not already running
344+ if ! app .menuInitialized {
345+ app .menuInitialized = true
346+ go app .updateLoop (ctx )
347+ } else {
348+ // Just do a single update to refresh data
349+ go app .updatePRs (ctx )
350+ }
351+ }
299352 }()
300353 } else {
301- remainingTime := minUpdateInterval - timeSinceLastSearch
302- slog .Debug ("[CLICK] Rate limited" , "lastSearchAgo" , timeSinceLastSearch , "remaining" , remainingTime )
354+ // Normal operation - check if we can perform a forced refresh
355+ app .mu .RLock ()
356+ timeSinceLastSearch := time .Since (app .lastSearchAttempt )
357+ app .mu .RUnlock ()
358+
359+ if timeSinceLastSearch >= minUpdateInterval {
360+ slog .Info ("[CLICK] Forcing search refresh" , "lastSearchAgo" , timeSinceLastSearch )
361+ go func () {
362+ app .updatePRs (ctx )
363+ }()
364+ } else {
365+ remainingTime := minUpdateInterval - timeSinceLastSearch
366+ slog .Debug ("[CLICK] Rate limited" , "lastSearchAgo" , timeSinceLastSearch , "remaining" , remainingTime )
367+ }
303368 }
304369
305370 if menu != nil {
@@ -320,21 +385,23 @@ func (app *App) onReady(ctx context.Context) {
320385
321386 // Check if we have an auth error
322387 if app .authError != "" {
323- systray .SetTitle ("⚠️" )
324- systray .SetTooltip ("GitHub PR Monitor - Authentication Error" )
388+ systray .SetTitle ("" )
389+ app .setTrayIcon (IconLock )
390+ systray .SetTooltip ("Goose - Authentication Error" )
325391 // Create initial error menu
326392 app .rebuildMenu (ctx )
327393 // Clean old cache on startup
328394 app .cleanupOldCache ()
329395 return
330396 }
331397
332- systray .SetTitle ("Loading PRs..." )
398+ systray .SetTitle ("" )
399+ app .setTrayIcon (IconSmiling ) // Start with smiling icon while loading
333400
334401 // Set tooltip based on whether we're using a custom user
335- tooltip := "GitHub PR Monitor "
402+ tooltip := "Goose - Loading PRs... "
336403 if app .targetUser != "" {
337- tooltip = fmt .Sprintf ("GitHub PR Monitor - @%s" , app .targetUser )
404+ tooltip = fmt .Sprintf ("Goose - Loading PRs... ( @%s) " , app .targetUser )
338405 }
339406 systray .SetTooltip (tooltip )
340407
@@ -352,8 +419,9 @@ func (app *App) updateLoop(ctx context.Context) {
352419 slog .Error ("PANIC in update loop" , "panic" , r )
353420
354421 // Set error state in UI
355- systray .SetTitle ("💥" )
356- systray .SetTooltip ("GitHub PR Monitor - Critical error" )
422+ systray .SetTitle ("" )
423+ app .setTrayIcon (IconWarning )
424+ systray .SetTooltip ("Goose - Critical error" )
357425
358426 // Update failure count
359427 app .mu .Lock ()
@@ -406,23 +474,19 @@ func (app *App) updatePRs(ctx context.Context) {
406474 app .mu .Unlock ()
407475
408476 // Progressive degradation based on failure count
409- var title , tooltip string
477+ var tooltip string
478+ var iconType IconType
410479 switch {
411- case failureCount == 1 :
412- title = "⚠️"
413- tooltip = "GitHub PR Monitor - Temporary error, retrying..."
414480 case failureCount <= minorFailureThreshold :
415- title = "⚠️"
416- tooltip = fmt .Sprintf ("GitHub PR Monitor - %d consecutive failures" , failureCount )
417- case failureCount <= majorFailureThreshold :
418- title = "❌"
419- tooltip = "GitHub PR Monitor - Multiple failures, check connection"
481+ iconType = IconWarning
482+ tooltip = fmt .Sprintf ("Goose - %d consecutive failures" , failureCount )
420483 default :
421- title = "💀"
422- tooltip = "GitHub PR Monitor - Service degraded , check authentication "
484+ iconType = IconWarning
485+ tooltip = "Goose - Connection failures , check network/auth "
423486 }
424487
425- systray .SetTitle (title )
488+ systray .SetTitle ("" )
489+ app .setTrayIcon (iconType )
426490
427491 // Include time since last success and user info
428492 timeSinceSuccess := "never"
@@ -553,23 +617,19 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
553617 app .mu .Unlock ()
554618
555619 // Progressive degradation based on failure count
556- var title , tooltip string
620+ var tooltip string
621+ var iconType IconType
557622 switch {
558- case failureCount == 1 :
559- title = "⚠️"
560- tooltip = "GitHub PR Monitor - Temporary error, retrying..."
561623 case failureCount <= minorFailureThreshold :
562- title = "⚠️"
563- tooltip = fmt .Sprintf ("GitHub PR Monitor - %d consecutive failures" , failureCount )
564- case failureCount <= majorFailureThreshold :
565- title = "❌"
566- tooltip = "GitHub PR Monitor - Multiple failures, check connection"
624+ iconType = IconWarning
625+ tooltip = fmt .Sprintf ("Goose - %d consecutive failures" , failureCount )
567626 default :
568- title = "💀"
569- tooltip = "GitHub PR Monitor - Service degraded , check authentication "
627+ iconType = IconWarning
628+ tooltip = "Goose - Connection failures , check network/auth "
570629 }
571630
572- systray .SetTitle (title )
631+ systray .SetTitle ("" )
632+ app .setTrayIcon (iconType )
573633 systray .SetTooltip (tooltip )
574634
575635 // Create or update menu to show error state
@@ -676,7 +736,7 @@ func (app *App) tryAutoOpenPR(ctx context.Context, pr PR, autoBrowserEnabled boo
676736 slog .Warn ("Auto-open strict validation failed" , "url" , sanitizeForLog (pr .URL ), "error" , err )
677737 return
678738 }
679- if err := openURL (ctx , pr .URL , pr . ActionKind ); err != nil {
739+ if err := openURL (ctx , pr .URL ); err != nil {
680740 slog .Error ("[BROWSER] Failed to auto-open PR" , "url" , pr .URL , "error" , err )
681741 } else {
682742 app .browserRateLimiter .RecordOpen (pr .URL )
0 commit comments