From 23426bf18ba7aacd9bba87eb8131f3ea5d728c7a Mon Sep 17 00:00:00 2001 From: Thomas Stromberg Date: Tue, 5 Aug 2025 14:44:30 -0400 Subject: [PATCH] Add block reason to tooltips --- cache.go | 61 +++++++++++++++++++++++++++++++------------------------ github.go | 30 ++++++++++++++++++++++----- main.go | 19 ++++++++++------- ui.go | 14 +++++++++++++ 4 files changed, 86 insertions(+), 38 deletions(-) diff --git a/cache.go b/cache.go index 51834e2..32727d5 100644 --- a/cache.go +++ b/cache.go @@ -29,23 +29,30 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) ( hash := sha256.Sum256([]byte(key)) cacheFile := filepath.Join(app.cacheDir, hex.EncodeToString(hash[:])[:16]+".json") - // Try to read from cache (gracefully handle all cache errors) - if data, readErr := os.ReadFile(cacheFile); readErr == nil { - var entry cacheEntry - if unmarshalErr := json.Unmarshal(data, &entry); unmarshalErr != nil { - log.Printf("Failed to unmarshal cache data for %s: %v", url, unmarshalErr) - // Remove corrupted cache file - if removeErr := os.Remove(cacheFile); removeErr != nil { - log.Printf("Failed to remove corrupted cache file: %v", removeErr) + // Skip cache if --no-cache flag is set + if !app.noCache { + // Try to read from cache (gracefully handle all cache errors) + if data, readErr := os.ReadFile(cacheFile); readErr == nil { + var entry cacheEntry + if unmarshalErr := json.Unmarshal(data, &entry); unmarshalErr != nil { + log.Printf("Failed to unmarshal cache data for %s: %v", url, unmarshalErr) + // Remove corrupted cache file + if removeErr := os.Remove(cacheFile); removeErr != nil { + log.Printf("Failed to remove corrupted cache file: %v", removeErr) + } + } else if time.Since(entry.CachedAt) < cacheTTL && entry.UpdatedAt.Equal(updatedAt) { + // Check if cache is still valid (2 hour TTL) + return entry.Data, nil } - } else if time.Since(entry.CachedAt) < cacheTTL && entry.UpdatedAt.Equal(updatedAt) { - // Check if cache is still valid (2 hour TTL) - return entry.Data, nil } } // Cache miss, fetch from API - log.Printf("Cache miss for %s, fetching from Turn API", url) + if app.noCache { + log.Printf("Cache bypassed for %s (--no-cache), fetching from Turn API", url) + } else { + log.Printf("Cache miss for %s, fetching from Turn API", url) + } // Just try once with timeout - if Turn API fails, it's not critical turnCtx, cancel := context.WithTimeout(ctx, 10*time.Second) @@ -57,20 +64,22 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) ( return nil, err } - // Save to cache (don't fail if caching fails) - entry := cacheEntry{ - Data: data, - CachedAt: time.Now(), - UpdatedAt: updatedAt, - } - if cacheData, marshalErr := json.Marshal(entry); marshalErr != nil { - log.Printf("Failed to marshal cache data for %s: %v", url, marshalErr) - } else { - // Ensure cache directory exists - if dirErr := os.MkdirAll(filepath.Dir(cacheFile), 0o700); dirErr != nil { - log.Printf("Failed to create cache directory: %v", dirErr) - } else if writeErr := os.WriteFile(cacheFile, cacheData, 0o600); writeErr != nil { - log.Printf("Failed to write cache file for %s: %v", url, writeErr) + // Save to cache (don't fail if caching fails) - skip if --no-cache is set + if !app.noCache { + entry := cacheEntry{ + Data: data, + CachedAt: time.Now(), + UpdatedAt: updatedAt, + } + if cacheData, marshalErr := json.Marshal(entry); marshalErr != nil { + log.Printf("Failed to marshal cache data for %s: %v", url, marshalErr) + } else { + // Ensure cache directory exists + if dirErr := os.MkdirAll(filepath.Dir(cacheFile), 0o700); dirErr != nil { + log.Printf("Failed to create cache directory: %v", dirErr) + } else if writeErr := os.WriteFile(cacheFile, cacheData, 0o600); writeErr != nil { + log.Printf("Failed to write cache file for %s: %v", url, writeErr) + } } } diff --git a/github.go b/github.go index a27a1ec..725e5bf 100644 --- a/github.go +++ b/github.go @@ -226,7 +226,7 @@ func (app *App) fetchPRs(ctx context.Context) (incoming []PR, outgoing []PR, err } // updatePRData updates PR data with Turn API results. -func (app *App) updatePRData(url string, needsReview bool, isOwner bool) *PR { +func (app *App) updatePRData(url string, needsReview bool, isOwner bool, actionReason string) *PR { app.mu.Lock() defer app.mu.Unlock() @@ -236,6 +236,7 @@ func (app *App) updatePRData(url string, needsReview bool, isOwner bool) *PR { if app.outgoing[i].URL == url { app.outgoing[i].NeedsReview = needsReview app.outgoing[i].IsBlocked = needsReview + app.outgoing[i].ActionReason = actionReason return &app.outgoing[i] } } @@ -244,6 +245,7 @@ func (app *App) updatePRData(url string, needsReview bool, isOwner bool) *PR { for i := range app.incoming { if app.incoming[i].URL == url { app.incoming[i].NeedsReview = needsReview + app.incoming[i].ActionReason = actionReason return &app.incoming[i] } } @@ -338,22 +340,40 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue, const minUpdateInterval = 500 * time.Millisecond for result := range results { + // Debug logging for PR #1203 - check all responses + if strings.Contains(result.url, "1203") { + log.Printf("[TURN] DEBUG PR #1203: result.err=%v, turnData=%v", result.err, result.turnData != nil) + if result.turnData != nil { + log.Printf("[TURN] DEBUG PR #1203: PRState.UnblockAction=%v", result.turnData.PRState.UnblockAction != nil) + } + } + if result.err == nil && result.turnData != nil && result.turnData.PRState.UnblockAction != nil { turnSuccesses++ - // Check if user needs to review + // Debug logging for PR #1203 + if strings.Contains(result.url, "1203") { + log.Printf("[TURN] DEBUG PR #1203: UnblockAction keys: %+v", result.turnData.PRState.UnblockAction) + } + + // Check if user needs to review and get action reason needsReview := false - if _, exists := result.turnData.PRState.UnblockAction[user]; exists { + actionReason := "" + if action, exists := result.turnData.PRState.UnblockAction[user]; exists { needsReview = true + actionReason = action.Reason + log.Printf("[TURN] UnblockAction for %s: Reason=%q, Kind=%q", result.url, action.Reason, action.Kind) + } else { + log.Printf("[TURN] No UnblockAction found for user %s on %s", user, result.url) } // Update the PR in our lists - pr := app.updatePRData(result.url, needsReview, result.isOwner) + pr := app.updatePRData(result.url, needsReview, result.isOwner, actionReason) if pr != nil { updatesApplied++ updateBatch++ - log.Printf("[TURN] Turn data received for %s (needsReview=%v)", result.url, needsReview) + log.Printf("[TURN] Turn data received for %s (needsReview=%v, actionReason=%q)", result.url, needsReview, actionReason) // Update the specific menu item immediately app.updatePRMenuItem(*pr) diff --git a/main.go b/main.go index c4c0763..5e812b5 100644 --- a/main.go +++ b/main.go @@ -43,13 +43,14 @@ const ( // PR represents a pull request with metadata. type PR struct { - UpdatedAt time.Time - Title string - URL string - Repository string - Number int - IsBlocked bool - NeedsReview bool + UpdatedAt time.Time + Title string + URL string + Repository string + ActionReason string // Action reason from Turn API when blocked + Number int + IsBlocked bool + NeedsReview bool } // App holds the application state. @@ -74,12 +75,15 @@ type App struct { turnDataLoading bool turnDataLoaded bool menuInitialized bool + noCache bool } func main() { // Parse command line flags var targetUser string + var noCache bool flag.StringVar(&targetUser, "user", "", "GitHub user to query PRs for (defaults to authenticated user)") + flag.BoolVar(&noCache, "no-cache", false, "Bypass cache for debugging") flag.Parse() log.SetFlags(log.LstdFlags | log.Lshortfile) @@ -104,6 +108,7 @@ func main() { targetUser: targetUser, prMenuItems: make(map[string]*systray.MenuItem), sectionHeaders: make(map[string]*systray.MenuItem), + noCache: noCache, } log.Println("Initializing GitHub clients...") diff --git a/ui.go b/ui.go index 754d557..844abca 100644 --- a/ui.go +++ b/ui.go @@ -197,8 +197,17 @@ func (app *App) updatePRMenuItem(pr PR) { if pr.NeedsReview { title = fmt.Sprintf("• %s", title) } + + // Update tooltip with action reason + tooltip := fmt.Sprintf("%s (%s)", pr.Title, formatAge(pr.UpdatedAt)) + if (pr.NeedsReview || pr.IsBlocked) && pr.ActionReason != "" { + tooltip = fmt.Sprintf("%s - %s", tooltip, pr.ActionReason) + log.Printf("[MENU] DEBUG: Updating tooltip for %s with actionReason: %q -> %q", pr.URL, pr.ActionReason, tooltip) + } + log.Printf("[MENU] Updating PR menu item for %s: '%s' -> '%s'", pr.URL, oldTitle, title) item.SetTitle(title) + item.SetTooltip(tooltip) } else { log.Printf("[MENU] WARNING: Tried to update non-existent PR menu item for %s", pr.URL) } @@ -213,6 +222,11 @@ func (app *App) addPRMenuItem(ctx context.Context, pr PR, _ bool) { title = fmt.Sprintf("• %s", title) } tooltip := fmt.Sprintf("%s (%s)", pr.Title, formatAge(pr.UpdatedAt)) + // Add action reason for blocked PRs + if (pr.NeedsReview || pr.IsBlocked) && pr.ActionReason != "" { + tooltip = fmt.Sprintf("%s - %s", tooltip, pr.ActionReason) + log.Printf("[MENU] DEBUG: Setting tooltip for %s with actionReason: %q -> %q", pr.URL, pr.ActionReason, tooltip) + } // Check if menu item already exists if existingItem, exists := app.prMenuItems[pr.URL]; exists {