Skip to content

Commit bdba1d6

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
improve state tracking for icon changes
1 parent 50368b7 commit bdba1d6

File tree

5 files changed

+332
-53
lines changed

5 files changed

+332
-53
lines changed

cmd/goose/main.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ const (
4646
maxConcurrentTurnAPICalls = 20
4747
defaultMaxBrowserOpensDay = 20
4848
startupGracePeriod = 1 * time.Minute // Don't play sounds or auto-open for first minute
49-
maxAgeForNewlyBlocked = 1 * time.Hour // PRs older than this won't be treated as newly blocked
5049
)
5150

5251
// PR represents a pull request with metadata.
@@ -93,6 +92,7 @@ type App struct {
9392
mu sync.RWMutex
9493
enableAutoBrowser bool
9594
hideStaleIncoming bool
95+
hasPerformedInitialDiscovery bool // Track if we've done the first poll to distinguish from real state changes
9696
noCache bool
9797
enableAudioCues bool
9898
initialLoadComplete bool
@@ -522,10 +522,10 @@ func (app *App) updatePRs(ctx context.Context) {
522522
slog.Info("[UPDATE] PR counts after update",
523523
"incoming_count", len(incoming),
524524
"outgoing_count", len(outgoing))
525-
// Log ALL outgoing PRs for debugging (temporarily)
526-
slog.Info("[UPDATE] Listing ALL outgoing PRs for debugging")
525+
// Log ALL outgoing PRs for debugging
526+
slog.Debug("[UPDATE] Listing ALL outgoing PRs for debugging")
527527
for i, pr := range outgoing {
528-
slog.Info("[UPDATE] Outgoing PR details",
528+
slog.Debug("[UPDATE] Outgoing PR details",
529529
"index", i,
530530
"repo", pr.Repository,
531531
"number", pr.Number,
@@ -550,6 +550,7 @@ func (app *App) updatePRs(ctx context.Context) {
550550

551551
// updateMenu rebuilds the menu only if there are changes to improve UX.
552552
func (app *App) updateMenu(ctx context.Context) {
553+
slog.Debug("[MENU] updateMenu called, generating current titles")
553554
// Generate current menu titles
554555
currentTitles := app.generateMenuTitles()
555556

@@ -566,6 +567,21 @@ func (app *App) updateMenu(ctx context.Context) {
566567

567568
// Titles have changed, rebuild menu
568569
slog.Info("[MENU] Changes detected, rebuilding menu", "oldCount", len(lastTitles), "newCount", len(currentTitles))
570+
571+
// Log what changed for debugging
572+
for i, current := range currentTitles {
573+
if i < len(lastTitles) {
574+
if current != lastTitles[i] {
575+
slog.Debug("[MENU] Title changed", "index", i, "old", lastTitles[i], "new", current)
576+
}
577+
} else {
578+
slog.Debug("[MENU] New title added", "index", i, "title", current)
579+
}
580+
}
581+
for i := len(currentTitles); i < len(lastTitles); i++ {
582+
slog.Debug("[MENU] Title removed", "index", i, "title", lastTitles[i])
583+
}
584+
569585
app.rebuildMenu(ctx)
570586

571587
// Store new titles
@@ -669,6 +685,7 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
669685
if !app.menuInitialized {
670686
// Create initial menu with Turn data
671687
// Initialize menu structure
688+
slog.Info("[FLOW] Creating initial menu (first time)")
672689
app.rebuildMenu(ctx)
673690
app.menuInitialized = true
674691
// Store initial menu titles to prevent unnecessary rebuild on first update
@@ -678,14 +695,17 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
678695
app.lastMenuTitles = menuTitles
679696
app.mu.Unlock()
680697
// Menu initialization complete
698+
slog.Info("[FLOW] Initial menu created successfully")
681699
} else {
700+
slog.Info("[FLOW] Updating existing menu")
682701
app.updateMenu(ctx)
702+
slog.Info("[FLOW] Menu update completed")
683703
}
684704

685705
// Process notifications using the simplified state manager
686-
slog.Debug("[DEBUG] Processing PR state updates and notifications")
706+
slog.Info("[FLOW] About to process PR state updates and notifications")
687707
app.updatePRStatesAndNotify(ctx)
688-
slog.Debug("[DEBUG] Completed PR state updates and notifications")
708+
slog.Info("[FLOW] Completed PR state updates and notifications")
689709
// Mark initial load as complete after first successful update
690710
if !app.initialLoadComplete {
691711
app.mu.Lock()

cmd/goose/notifications.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,17 @@ func (app *App) processNotifications(ctx context.Context) {
2626
copy(outgoing, app.outgoing)
2727
app.mu.RUnlock()
2828

29+
// Determine if this is the initial discovery
30+
isInitialDiscovery := !app.hasPerformedInitialDiscovery
31+
2932
// Let the state manager figure out what needs notifications
30-
toNotify := app.stateManager.UpdatePRs(incoming, outgoing, hiddenOrgs)
33+
toNotify := app.stateManager.UpdatePRs(incoming, outgoing, hiddenOrgs, isInitialDiscovery)
34+
35+
// Mark that we've performed initial discovery
36+
if isInitialDiscovery {
37+
app.hasPerformedInitialDiscovery = true
38+
slog.Info("[STATE] Initial discovery completed", "incoming_count", len(incoming), "outgoing_count", len(outgoing))
39+
}
3140

3241
// Update deprecated fields for test compatibility
3342
app.mu.Lock()
@@ -81,8 +90,9 @@ func (app *App) processNotifications(ctx context.Context) {
8190

8291
// Update menu if we sent notifications
8392
if len(toNotify) > 0 {
84-
slog.Debug("[NOTIFY] Updating menu after notifications")
93+
slog.Info("[FLOW] Updating menu after sending notifications", "notified_count", len(toNotify))
8594
app.updateMenu(ctx)
95+
slog.Info("[FLOW] Menu update after notifications completed")
8696
}
8797
}
8898

cmd/goose/pr_state.go

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99

1010
// PRState tracks the complete state of a PR including blocking history.
1111
type PRState struct {
12-
FirstBlockedAt time.Time
13-
LastSeenBlocked time.Time
14-
PR PR
15-
HasNotified bool
12+
FirstBlockedAt time.Time
13+
LastSeenBlocked time.Time
14+
PR PR
15+
HasNotified bool
16+
IsInitialDiscovery bool // True if this PR was discovered as already blocked during startup
1617
}
1718

1819
// PRStateManager manages all PR states with proper synchronization.
@@ -34,13 +35,18 @@ func NewPRStateManager(startTime time.Time) *PRStateManager {
3435

3536
// UpdatePRs updates the state with new PR data and returns which PRs need notifications.
3637
// This function is thread-safe and handles all state transitions atomically.
37-
func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[string]bool) (toNotify []PR) {
38+
// isInitialDiscovery should be true only on the very first poll to prevent notifications for already-blocked PRs.
39+
func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[string]bool, isInitialDiscovery bool) (toNotify []PR) {
3840
m.mu.Lock()
3941
defer m.mu.Unlock()
4042

4143
now := time.Now()
4244
inGracePeriod := time.Since(m.startTime) < time.Duration(m.gracePeriodSeconds)*time.Second
4345

46+
slog.Debug("[STATE] UpdatePRs called",
47+
"incoming", len(incoming), "outgoing", len(outgoing),
48+
"existing_states", len(m.states), "in_grace_period", inGracePeriod, "is_initial_discovery", isInitialDiscovery)
49+
4450
// Track which PRs are currently blocked
4551
currentlyBlocked := make(map[string]bool)
4652

@@ -62,7 +68,10 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
6268
if !isBlocked {
6369
// PR is not blocked - remove from tracking if it was
6470
if state, exists := m.states[pr.URL]; exists && state != nil {
65-
slog.Info("[STATE] PR no longer blocked", "repo", pr.Repository, "number", pr.Number)
71+
slog.Info("[STATE] State transition: blocked -> unblocked",
72+
"repo", pr.Repository, "number", pr.Number, "url", pr.URL,
73+
"was_blocked_since", state.FirstBlockedAt.Format(time.RFC3339),
74+
"blocked_duration", time.Since(state.FirstBlockedAt).Round(time.Second))
6675
delete(m.states, pr.URL)
6776
}
6877
continue
@@ -73,37 +82,44 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
7382
// Get or create state for this PR
7483
state, exists := m.states[pr.URL]
7584
if !exists {
76-
// Check if PR is too old to be considered "newly blocked"
77-
// If the PR hasn't been updated in over an hour, don't treat it as new
78-
const maxAgeForNewlyBlocked = 1 * time.Hour
79-
prAge := time.Since(pr.UpdatedAt)
80-
81-
if prAge > maxAgeForNewlyBlocked {
82-
// Old PR, track it but don't notify
85+
// This PR was not in our state before
86+
if isInitialDiscovery {
87+
// Initial discovery: PR was already blocked when we started, no state transition
8388
state = &PRState{
84-
PR: pr,
85-
FirstBlockedAt: now,
86-
LastSeenBlocked: now,
87-
HasNotified: true, // Mark as already notified to prevent future notifications
89+
PR: pr,
90+
FirstBlockedAt: now,
91+
LastSeenBlocked: now,
92+
HasNotified: false, // Don't consider this as notified since no actual notification was sent
93+
IsInitialDiscovery: true, // Mark as initial discovery to prevent notifications and party poppers
8894
}
8995
m.states[pr.URL] = state
90-
slog.Debug("[STATE] Blocked PR detected but too old to notify",
91-
"repo", pr.Repository, "number", pr.Number,
92-
"age", prAge.Round(time.Minute), "maxAge", maxAgeForNewlyBlocked)
96+
97+
slog.Info("[STATE] Initial discovery: already blocked PR",
98+
"repo", pr.Repository,
99+
"number", pr.Number,
100+
"url", pr.URL,
101+
"pr_updated_at", pr.UpdatedAt.Format(time.RFC3339),
102+
"firstBlockedAt", state.FirstBlockedAt.Format(time.RFC3339))
93103
} else {
94-
// New blocked PR that was recently updated!
104+
// Actual state transition: unblocked -> blocked
95105
state = &PRState{
96-
PR: pr,
97-
FirstBlockedAt: now,
98-
LastSeenBlocked: now,
99-
HasNotified: false,
106+
PR: pr,
107+
FirstBlockedAt: now,
108+
LastSeenBlocked: now,
109+
HasNotified: false,
110+
IsInitialDiscovery: false, // This is a real state transition
100111
}
101112
m.states[pr.URL] = state
102113

103-
slog.Info("[STATE] New blocked PR detected", "repo", pr.Repository, "number", pr.Number,
104-
"age", prAge.Round(time.Minute))
114+
slog.Info("[STATE] State transition: unblocked -> blocked",
115+
"repo", pr.Repository,
116+
"number", pr.Number,
117+
"url", pr.URL,
118+
"pr_updated_at", pr.UpdatedAt.Format(time.RFC3339),
119+
"firstBlockedAt", state.FirstBlockedAt.Format(time.RFC3339),
120+
"inGracePeriod", inGracePeriod)
105121

106-
// Should we notify?
122+
// Should we notify for actual state transitions?
107123
if !inGracePeriod && !state.HasNotified {
108124
slog.Debug("[STATE] Will notify for newly blocked PR", "repo", pr.Repository, "number", pr.Number)
109125
toNotify = append(toNotify, pr)
@@ -113,12 +129,20 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
113129
}
114130
}
115131
} else {
116-
// PR was already blocked
132+
// PR was already blocked in our state - just update data, preserve FirstBlockedAt
133+
originalFirstBlocked := state.FirstBlockedAt
117134
state.LastSeenBlocked = now
118135
state.PR = pr // Update PR data
119136

137+
slog.Debug("[STATE] State transition: blocked -> blocked (no change)",
138+
"repo", pr.Repository, "number", pr.Number, "url", pr.URL,
139+
"original_first_blocked", originalFirstBlocked.Format(time.RFC3339),
140+
"time_since_first_blocked", time.Since(originalFirstBlocked).Round(time.Second),
141+
"has_notified", state.HasNotified)
142+
120143
// If we haven't notified yet and we're past grace period, notify now
121-
if !state.HasNotified && !inGracePeriod {
144+
// But don't notify for initial discovery PRs
145+
if !state.HasNotified && !inGracePeriod && !state.IsInitialDiscovery {
122146
slog.Info("[STATE] Past grace period, notifying for previously blocked PR",
123147
"repo", pr.Repository, "number", pr.Number)
124148
toNotify = append(toNotify, pr)
@@ -128,13 +152,26 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
128152
}
129153

130154
// Clean up states for PRs that are no longer in our lists
131-
for url := range m.states {
155+
// Add more conservative cleanup with logging
156+
removedCount := 0
157+
for url, state := range m.states {
132158
if !currentlyBlocked[url] {
133-
slog.Debug("[STATE] Removing stale state for PR", "url", url)
159+
timeSinceLastSeen := time.Since(state.LastSeenBlocked)
160+
slog.Info("[STATE] Removing stale PR state (no longer blocked)",
161+
"url", url, "repo", state.PR.Repository, "number", state.PR.Number,
162+
"first_blocked_at", state.FirstBlockedAt.Format(time.RFC3339),
163+
"last_seen_blocked", state.LastSeenBlocked.Format(time.RFC3339),
164+
"time_since_last_seen", timeSinceLastSeen.Round(time.Second),
165+
"was_notified", state.HasNotified)
134166
delete(m.states, url)
167+
removedCount++
135168
}
136169
}
137170

171+
if removedCount > 0 {
172+
slog.Info("[STATE] State cleanup completed", "removed_states", removedCount, "remaining_states", len(m.states))
173+
}
174+
138175
return toNotify
139176
}
140177

0 commit comments

Comments
 (0)