Skip to content

Commit 3f19ba7

Browse files
authored
Merge pull request #32 from codeGROOVE-dev/sound-less
Highlight Goose'd menu items, don't honk for stale PRs
2 parents 1c7e162 + b550517 commit 3f19ba7

File tree

6 files changed

+79
-28
lines changed

6 files changed

+79
-28
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
# Ready-to-Review Goose 🪿
1+
# Review Goose 🪿
22

3-
![Experimental](https://img.shields.io/badge/status-beta-orange)
3+
![Beta](https://img.shields.io/badge/status-beta-orange)
44
![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20BSD%20%7C%20Windows-blue)
55
![Goose Noises](https://img.shields.io/badge/goose%20noises-100%25%20more-green)
66
[![GitHub](https://img.shields.io/github/stars/ready-to-review/goose?style=social)](https://github.com/ready-to-review/goose)
77

8+
![Review Goose Logo](media/logo-small.png)
9+
810
The only PR tracker that honks at you when you're the bottleneck. Now shipping with 100% more goose noises!
911

1012
Lives in your menubar like a tiny waterfowl of productivity shame, watching your GitHub PRs and making aggressive bird sounds when you're blocking someone's code from seeing the light of production.
1113

12-
13-
![PR Menubar Screenshot](media/screenshot.png)
14+
![Review Goose Screenshot](media/screenshot.png)
1415

1516
## What It Does
1617

@@ -67,8 +68,8 @@ We don't yet try to persist fine-grained tokens to disk - PR's welcome!
6768
## Pricing
6869

6970
The Goose is part of the [codeGROOVE](https://codegroove.dev) developer acceleration platform:
70-
- **FREE forever** for open-source repositories
71-
- Low-cost fee TBD for access to private repos (the goose needs to eat!)
71+
- **FREE forever** for open-source or public repositories
72+
- GitHub Sponsors gain access to private repos ($2.56/mo recommended)
7273

7374
## Privacy
7475

cmd/goose/main.go

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const (
5151
type PR struct {
5252
UpdatedAt time.Time
5353
TurnDataAppliedAt time.Time
54+
FirstBlockedAt time.Time // When this PR was first detected as blocked
5455
Title string
5556
URL string
5657
Repository string
@@ -72,29 +73,30 @@ type TurnResult struct {
7273
// App holds the application state.
7374
type App struct {
7475
lastSuccessfulFetch time.Time
76+
startTime time.Time
7577
client *github.Client
7678
turnClient *turn.Client
7779
currentUser *github.User
7880
previousBlockedPRs map[string]bool
81+
blockedPRTimes map[string]time.Time
82+
browserRateLimiter *BrowserRateLimiter
7983
targetUser string
8084
cacheDir string
8185
authError string
86+
pendingTurnResults []TurnResult
87+
lastMenuTitles []string
8288
incoming []PR
8389
outgoing []PR
84-
lastMenuTitles []string
85-
pendingTurnResults []TurnResult
8690
updateInterval time.Duration
8791
consecutiveFailures int
8892
mu sync.RWMutex
89-
noCache bool
90-
hideStaleIncoming bool
9193
loadingTurnData bool
9294
menuInitialized bool
9395
initialLoadComplete bool
9496
enableAudioCues bool
9597
enableAutoBrowser bool
96-
browserRateLimiter *BrowserRateLimiter
97-
startTime time.Time
98+
hideStaleIncoming bool
99+
noCache bool
98100
}
99101

100102
func loadCurrentUser(ctx context.Context, app *App) {
@@ -208,6 +210,7 @@ func main() {
208210
cacheDir: cacheDir,
209211
hideStaleIncoming: true,
210212
previousBlockedPRs: make(map[string]bool),
213+
blockedPRTimes: make(map[string]time.Time),
211214
targetUser: targetUser,
212215
noCache: noCache,
213216
updateInterval: updateInterval,
@@ -557,6 +560,7 @@ func (app *App) notifyWithSound(ctx context.Context, pr PR, isIncoming bool, pla
557560

558561
// Play sound only once per refresh cycle
559562
if !*playedSound {
563+
log.Printf("[SOUND] Playing %s sound for PR: %s #%d - %s", soundType, pr.Repository, pr.Number, pr.Title)
560564
app.playSound(ctx, soundType)
561565
*playedSound = true
562566
}
@@ -568,22 +572,43 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
568572
incoming := app.incoming
569573
outgoing := app.outgoing
570574
previousBlocked := app.previousBlockedPRs
575+
blockedTimes := app.blockedPRTimes
571576
autoBrowserEnabled := app.enableAutoBrowser
572577
startTime := app.startTime
578+
hideStaleIncoming := app.hideStaleIncoming
573579
app.mu.RUnlock()
574580

575581
currentBlocked := make(map[string]bool)
582+
newBlockedTimes := make(map[string]time.Time)
576583
playedHonk := false
577584
playedJet := false
585+
now := time.Now()
586+
staleThreshold := now.Add(-stalePRThreshold)
578587

579588
// Check incoming PRs
580589
for i := range incoming {
581590
if incoming[i].NeedsReview {
582591
currentBlocked[incoming[i].URL] = true
583-
// Notify if newly blocked
584-
if !previousBlocked[incoming[i].URL] {
585-
app.notifyWithSound(ctx, incoming[i], true, &playedHonk)
586-
app.tryAutoOpenPR(ctx, incoming[i], autoBrowserEnabled, startTime)
592+
// Track when first blocked
593+
if blockedTime, exists := blockedTimes[incoming[i].URL]; exists {
594+
newBlockedTimes[incoming[i].URL] = blockedTime
595+
incoming[i].FirstBlockedAt = blockedTime
596+
} else if !previousBlocked[incoming[i].URL] {
597+
// Newly blocked PR
598+
newBlockedTimes[incoming[i].URL] = now
599+
incoming[i].FirstBlockedAt = now
600+
601+
// Skip sound and auto-open for stale PRs when hideStaleIncoming is enabled
602+
isStale := incoming[i].UpdatedAt.Before(staleThreshold)
603+
if hideStaleIncoming && isStale {
604+
log.Printf("[BLOCKED] New incoming PR blocked (stale, skipping): %s #%d - %s",
605+
incoming[i].Repository, incoming[i].Number, incoming[i].Title)
606+
} else {
607+
log.Printf("[BLOCKED] New incoming PR blocked: %s #%d - %s",
608+
incoming[i].Repository, incoming[i].Number, incoming[i].Title)
609+
app.notifyWithSound(ctx, incoming[i], true, &playedHonk)
610+
app.tryAutoOpenPR(ctx, incoming[i], autoBrowserEnabled, startTime)
611+
}
587612
}
588613
}
589614
}
@@ -592,19 +617,39 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
592617
for i := range outgoing {
593618
if outgoing[i].IsBlocked {
594619
currentBlocked[outgoing[i].URL] = true
595-
// Notify if newly blocked
596-
if !previousBlocked[outgoing[i].URL] {
597-
// Add delay if we already played honk sound
598-
if playedHonk && !playedJet {
599-
time.Sleep(2 * time.Second)
620+
// Track when first blocked
621+
if blockedTime, exists := blockedTimes[outgoing[i].URL]; exists {
622+
newBlockedTimes[outgoing[i].URL] = blockedTime
623+
outgoing[i].FirstBlockedAt = blockedTime
624+
} else if !previousBlocked[outgoing[i].URL] {
625+
// Newly blocked PR
626+
newBlockedTimes[outgoing[i].URL] = now
627+
outgoing[i].FirstBlockedAt = now
628+
629+
// Skip sound and auto-open for stale PRs when hideStaleIncoming is enabled
630+
isStale := outgoing[i].UpdatedAt.Before(staleThreshold)
631+
if hideStaleIncoming && isStale {
632+
log.Printf("[BLOCKED] New outgoing PR blocked (stale, skipping): %s #%d - %s",
633+
outgoing[i].Repository, outgoing[i].Number, outgoing[i].Title)
634+
} else {
635+
// Add delay if we already played honk sound
636+
if playedHonk && !playedJet {
637+
time.Sleep(2 * time.Second)
638+
}
639+
log.Printf("[BLOCKED] New outgoing PR blocked: %s #%d - %s",
640+
outgoing[i].Repository, outgoing[i].Number, outgoing[i].Title)
641+
app.notifyWithSound(ctx, outgoing[i], false, &playedJet)
642+
app.tryAutoOpenPR(ctx, outgoing[i], autoBrowserEnabled, startTime)
600643
}
601-
app.notifyWithSound(ctx, outgoing[i], false, &playedJet)
602-
app.tryAutoOpenPR(ctx, outgoing[i], autoBrowserEnabled, startTime)
603644
}
604645
}
605646
}
606647

607648
app.mu.Lock()
608649
app.previousBlockedPRs = currentBlocked
650+
app.blockedPRTimes = newBlockedTimes
651+
// Update the PR lists with FirstBlockedAt times
652+
app.incoming = incoming
653+
app.outgoing = outgoing
609654
app.mu.Unlock()
610655
}

cmd/goose/security.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func validateGitHubPRURL(rawURL string) error {
148148
return errors.New("URL contains @ character")
149149
}
150150

151-
// Reject URLs with URL encoding (could hide malicious content)
151+
// Reject URLs with URL encoding (could hide malicious content)
152152
// Exception: %3D which is = in URL encoding, only as part of ?goose=1
153153
if strings.Contains(rawURL, "%") && !strings.HasSuffix(rawURL, "?goose%3D1") {
154154
return errors.New("URL contains encoded characters")

cmd/goose/security_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,4 @@ func TestValidateGitHubPRURL(t *testing.T) {
105105
}
106106
})
107107
}
108-
}
108+
}

cmd/goose/ui.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,14 @@ func (app *App) addPRSection(ctx context.Context, prs []PR, sectionTitle string,
201201
}
202202

203203
title := fmt.Sprintf("%s #%d", sortedPRs[i].Repository, sortedPRs[i].Number)
204-
// Add bullet point for PRs where user is blocking
205-
if sortedPRs[i].NeedsReview {
206-
title = fmt.Sprintf("• %s", title)
204+
// Add bullet point or goose for blocked PRs
205+
if sortedPRs[i].NeedsReview || sortedPRs[i].IsBlocked {
206+
// Show goose emoji for PRs blocked within the last hour
207+
if !sortedPRs[i].FirstBlockedAt.IsZero() && time.Since(sortedPRs[i].FirstBlockedAt) < time.Hour {
208+
title = fmt.Sprintf("🪿 %s", title)
209+
} else {
210+
title = fmt.Sprintf("• %s", title)
211+
}
207212
}
208213
// Format age inline for tooltip
209214
duration := time.Since(sortedPRs[i].UpdatedAt)

media/logo.png

-1.24 MB
Loading

0 commit comments

Comments
 (0)