Skip to content

Commit eb63183

Browse files
wesmclaude
andauthored
Fix branch filter showing no results due to limited fetch (#147)
## Summary Fixes a bug where branch filter in the TUI would show "no matching jobs" even when matching jobs existed in the database. ## Root Cause When branch filter was active, `fetchJobs` only fetched a limited number of recent jobs (e.g., 50). Since branch filtering is done client-side, if none of those 50 jobs matched the filtered branch, the TUI showed 0 results. ## Fix - Fetch all jobs (`limit=0`) when branch filter is active, matching existing behavior for repo filter and hideAddressed modes - Disable pagination when branch filter is active since all data is already loaded ## Test plan - [x] Added `TestTUINavigateDownNoLoadMoreWhenBranchFiltered` test - [x] All existing tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b6591c9 commit eb63183

File tree

2 files changed

+102
-7
lines changed

2 files changed

+102
-7
lines changed

cmd/roborev/tui.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ func (m tuiModel) fetchJobs() tea.Cmd {
489489

490490
return func() tea.Msg {
491491
// Determine limit:
492-
// - No limit (limit=0) when filtering to show full repo/addressed history
492+
// - No limit (limit=0) when filtering to show full repo/branch/addressed history
493493
// - If we've paginated beyond visible area, maintain current view size
494494
// - Otherwise fetch enough to fill visible area
495495
var url string
@@ -499,6 +499,9 @@ func (m tuiModel) fetchJobs() tea.Cmd {
499499
} else if len(m.activeRepoFilter) > 1 {
500500
// Multiple repos (shared display name) - fetch all, filter client-side
501501
url = fmt.Sprintf("%s/api/jobs?limit=0", m.serverAddr)
502+
} else if m.activeBranchFilter != "" {
503+
// Fetch all jobs when filtering by branch - client-side filtering needs full dataset
504+
url = fmt.Sprintf("%s/api/jobs?limit=0", m.serverAddr)
502505
} else if m.hideAddressed {
503506
// Fetch all jobs when hiding addressed - client-side filtering needs full dataset
504507
url = fmt.Sprintf("%s/api/jobs?limit=0", m.serverAddr)
@@ -533,7 +536,7 @@ func (m tuiModel) fetchJobs() tea.Cmd {
533536
func (m tuiModel) fetchMoreJobs() tea.Cmd {
534537
return func() tea.Msg {
535538
// Only fetch more when not filtering (filtered view loads all)
536-
if len(m.activeRepoFilter) > 0 {
539+
if len(m.activeRepoFilter) > 0 || m.activeBranchFilter != "" {
537540
return nil
538541
}
539542
offset := len(m.jobs)
@@ -1759,8 +1762,18 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
17591762
}
17601763
m.currentView = tuiViewQueue
17611764
m.branchFilterSearch = ""
1762-
// Selection stays valid - client-side filtering
1763-
return m, nil
1765+
// Branch filter changes fetch behavior (limited vs unlimited),
1766+
// so we need to refetch
1767+
m.jobs = nil
1768+
m.hasMore = false
1769+
m.selectedIdx = -1
1770+
m.selectedJobID = 0
1771+
if m.loadingJobs || m.loadingMore {
1772+
m.pendingRefetch = true
1773+
return m, nil
1774+
}
1775+
m.loadingJobs = true
1776+
return m, m.fetchJobs()
17641777
}
17651778
return m, nil
17661779
case "backspace":
@@ -2404,8 +2417,8 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
24042417
if m.currentView == tuiViewQueue && len(m.filterStack) > 0 {
24052418
// Pop the most recent filter from the stack
24062419
popped := m.popFilter()
2407-
if popped == "repo" {
2408-
// Repo filter is server-side, need to refetch
2420+
if popped == "repo" || popped == "branch" {
2421+
// Repo/branch filter changes fetch behavior, need to refetch
24092422
m.jobs = nil
24102423
m.hasMore = false
24112424
m.selectedIdx = -1
@@ -2417,7 +2430,6 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
24172430
m.loadingJobs = true
24182431
return m, m.fetchJobs()
24192432
}
2420-
// Branch filter is client-side, no refetch needed
24212433
return m, nil
24222434
} else if m.currentView == tuiViewQueue && m.hideAddressed {
24232435
// Clear hide-addressed filter (no project/branch filter active)

cmd/roborev/tui_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7386,6 +7386,89 @@ func TestTUIRemoveFilterFromStack(t *testing.T) {
73867386
}
73877387
}
73887388

7389+
func TestTUINavigateDownNoLoadMoreWhenBranchFiltered(t *testing.T) {
7390+
// Test that pagination is disabled when branch filter is active
7391+
// (since branch filtering fetches all jobs upfront)
7392+
m := newTuiModel("http://localhost")
7393+
7394+
// Set up at last job with branch filter active
7395+
m.jobs = []storage.ReviewJob{{ID: 1, Branch: "feature"}}
7396+
m.selectedIdx = 0
7397+
m.selectedJobID = 1
7398+
m.hasMore = true
7399+
m.loadingMore = false
7400+
m.activeBranchFilter = "feature" // Branch filter active
7401+
m.currentView = tuiViewQueue
7402+
7403+
// Press down at bottom - should NOT trigger load more (filtered view loads all)
7404+
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyDown})
7405+
m2 := updated.(tuiModel)
7406+
7407+
if m2.loadingMore {
7408+
t.Error("loadingMore should not be set when branch filter is active")
7409+
}
7410+
if cmd != nil {
7411+
t.Error("Should not return command when branch filter is active")
7412+
}
7413+
}
7414+
7415+
func TestTUIBranchFilterTriggersRefetch(t *testing.T) {
7416+
// Test that applying a branch filter triggers a refetch
7417+
// (needed because branch filter changes fetch from limited to unlimited)
7418+
m := newTuiModel("http://localhost")
7419+
7420+
m.currentView = tuiViewBranchFilter
7421+
m.filterBranches = []branchFilterItem{
7422+
{name: "main", count: 5},
7423+
{name: "feature", count: 3},
7424+
}
7425+
m.branchFilterSelectedIdx = 1 // Select "feature"
7426+
m.jobs = []storage.ReviewJob{
7427+
{ID: 1, Branch: "main"},
7428+
{ID: 2, Branch: "feature"},
7429+
}
7430+
m.loadingJobs = false
7431+
7432+
// Press Enter to select
7433+
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
7434+
m2 := updated.(tuiModel)
7435+
7436+
if !m2.loadingJobs {
7437+
t.Error("loadingJobs should be true after applying branch filter")
7438+
}
7439+
if m2.jobs != nil {
7440+
t.Error("jobs should be cleared when applying branch filter")
7441+
}
7442+
if cmd == nil {
7443+
t.Error("Should return fetchJobs command when applying branch filter")
7444+
}
7445+
}
7446+
7447+
func TestTUIBranchFilterClearTriggersRefetch(t *testing.T) {
7448+
// Test that clearing a branch filter triggers a refetch
7449+
m := newTuiModel("http://localhost")
7450+
7451+
m.currentView = tuiViewQueue
7452+
m.activeBranchFilter = "feature"
7453+
m.filterStack = []string{"branch"}
7454+
m.jobs = []storage.ReviewJob{{ID: 1, Branch: "feature"}}
7455+
m.loadingJobs = false
7456+
7457+
// Press Escape to clear filter
7458+
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEscape})
7459+
m2 := updated.(tuiModel)
7460+
7461+
if m2.activeBranchFilter != "" {
7462+
t.Errorf("Expected activeBranchFilter to be cleared, got '%s'", m2.activeBranchFilter)
7463+
}
7464+
if !m2.loadingJobs {
7465+
t.Error("loadingJobs should be true after clearing branch filter")
7466+
}
7467+
if cmd == nil {
7468+
t.Error("Should return fetchJobs command when clearing branch filter")
7469+
}
7470+
}
7471+
73897472
// Backfill gating tests
73907473

73917474
func TestTUIBranchBackfillDoneSetWhenNoNullsRemain(t *testing.T) {

0 commit comments

Comments
 (0)