Skip to content

Unified tree filter with lazy branch loading, search, and cwd prioritization#251

Merged
wesm merged 27 commits intoroborev-dev:mainfrom
nstrayer:filter-to-branch-affordance
Feb 15, 2026
Merged

Unified tree filter with lazy branch loading, search, and cwd prioritization#251
wesm merged 27 commits intoroborev-dev:mainfrom
nstrayer:filter-to-branch-affordance

Conversation

@nstrayer
Copy link
Contributor

@nstrayer nstrayer commented Feb 13, 2026

This PR replaces the separate repo/branch filter modals with a unified tree-style filter view. Repos are listed as expandable nodes with branches lazy-loaded on demand. The current working directory repo and branch are prioritized in sort order.

image

Summary

  • Unified tree filter replacing separate repo and branch modals
  • Lazy branch loading: branches fetch per-repo on expand (right-arrow) or search
  • Search matches unloaded branches by triggering lazy fetches in the background
  • Cwd repo sorts to top of filter tree; cwd branch sorts to top within its repo
  • b key shortcut opens filter with branches auto-expanded for the most relevant repo
  • Left-arrow collapse works during active search (overrides search auto-expansion)
  • Search-triggered loads do not permanently expand repos; only user-initiated expansion persists
  • Stale in-flight branch responses detected via rootPaths identity check
  • Branch fetch errors surface through connection tracking and reconnect logic
  • Slow tests moved behind //go:build integration tag; shared RSA key generation via sync.Once
  • Dead code removed: old fetchBranches, flatFilterEntry.depth, unused message fields

Details

Unified tree filter (5bb3631): Replaces the two-modal flow (repo list → branch list) with a single tree view. Each repo is a collapsible node; right-arrow expands to show branches, left-arrow collapses. Enter selects a repo or branch filter.

Cwd prioritization (e997569): At TUI launch, the model detects cwdRepoRoot and cwdBranch. The cwd repo sorts to index 0 in the filter tree, and the cwd branch sorts to first position when branches load. Independent of autoFilterRepo config.

Stale message guards (e06e319): tuiRepoBranchesMsg carries rootPaths so the handler can verify the tree node at repoIdx still represents the same repo, preventing races when the filter is closed and reopened during in-flight fetches.

'b' key shortcut (ec18ede, db62b33): Opens the unified filter with branches auto-expanded for the target repo (active filter > cwd > first). Cursor lands on the first branch. filterBranchMode clears on escape, enter, and after branches load.

Filter regressions fixed (49e73f8): h/l keys no longer captured by expand/collapse (arrow-keys only), multi-root-path repos matched correctly via rootPathsMatch(), branch fetch failures clear loading state.

Error handling fixes (5e4d7f1): Branch fetch errors bypass the view/staleness gate so connection tracking and reconnect logic always fire. Errors are no longer silently dropped when the filter view is not active.

Lazy search loading (527f35b): Typing in the filter search triggers fetchUnloadedBranches() for repos that haven't loaded branches yet, so search can match branch names across all repos.

Search expansion fixes (d034bbf, 104a9c9): Search-triggered loads use expandOnLoad: false so repos don't permanently expand. Left-arrow sets userCollapsed to override search auto-expansion. Right-arrow during an in-flight search load preserves user expand intent.

Test speedups (57ace30): TestDaemonRunStartsAndShutdownsCleanly and stream tests moved behind //go:build integration. RSA key generation shared via sync.Once. Analyze test uses direct arg validation instead of cmd.Execute().

Test plan

  • Unified tree filter shows repos with expand/collapse via arrow keys
  • Lazy branch loading on right-arrow and search triggers
  • Search matches branches across all repos (including previously unloaded)
  • Search-triggered loads do not permanently expand repos
  • Left-arrow collapses repos during active search
  • Right-arrow during in-flight search load preserves user intent
  • userCollapsed resets when search is cleared
  • Cwd repo/branch sort to first position
  • Stale branch messages with mismatched rootPaths are dropped
  • b key auto-expands target repo and positions cursor on first branch
  • Branch fetch errors clear loading state and trigger reconnect
  • h/l keys pass through to search; only arrow keys expand/collapse
  • Multi-root-path repos matched correctly in b-mode expansion
  • go test ./... passes; go vet ./... clean

🤖 Generated with Claude Code

…odals

Replace the two separate filter modals (repo filter 'f' key, branch filter
'b' key) with a single tree-style filter where repos are expandable nodes
with branches as children. Users can select a repo to filter by repo only,
or expand a repo and select a branch to filter by both repo and branch.

- Right arrow expands a collapsed repo (lazy-loads branches from the API)
- Left arrow collapses an expanded repo (or collapses parent from a branch)
- Enter on "All" clears all filters, on repo sets repo filter, on branch
  sets both repo and branch filter
- Search filters both repo names and branch names, auto-expanding repos
  with matching branches
- Remove 'b' key binding for separate branch filter modal
Detect the cwd repo root and branch at TUI launch and use them to
sort the filter tree so the current repo appears first and, when
expanded, the current branch appears first. This makes the most
common filtering action a single keypress away.

The detection is independent of the autoFilterRepo config option --
autoFilterRepo controls whether the filter is applied on launch,
while this controls sort order in the filter modal.
Add rootPaths to tuiRepoBranchesMsg and verify repo identity in the
handler to prevent stale in-flight responses from updating the wrong
tree node after a filter tree rebuild. Remove unused totalCount
variable in rebuildFilterFlatList (dead code from earlier approach).
Press 'b' from the queue view to open the unified filter tree with the
target repo automatically expanded to its branches. Target repo is
chosen by priority: single active repo filter > cwd repo > first repo.
The cursor lands on the first branch entry once branches load.
…talCount)

Strip fetchBranches down to backfill-only (renamed backfillBranches),
extract generic moveToFront helper replacing duplicate reorder blocks
and goto labels, and remove unused struct fields (flatFilterEntry.depth,
tuiReposMsg.totalCount, tuiBranchesMsg branch fields).
Guard 'b' key handler to reject presses outside queue view before
setting filterBranchMode, and clear the flag when exiting filter via
Enter (matching the existing Esc behavior). Add regression tests for
both scenarios.
@roborev-ci
Copy link

roborev-ci bot commented Feb 13, 2026

roborev: Combined Review

Verdict: 1 Medium issue found (no Critical/High findings; security reviews reported no vulnerabilities).

Medium

  1. Branch node can get stuck in loading state after a fetch error
    • File refs: cmd/roborev/tui_handlers.go:84, cmd/roborev/tui.go:1820, cmd/roborev/tui.go (case tuiErrMsg)
    • Impact: If fetchBranchesForRepo fails once, the repo can remain loading=true, and subsequent expand/retry is blocked (!node.loading gate).
    • Suggested fix: Emit a repo-scoped error (include repoIdx/rootPaths) and clear that node’s loading flag in Update on failure; also clear filterBranchMode when auto-expand fails.

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

wesm and others added 5 commits February 14, 2026 15:13
…ading stuck

Three fixes from review #5317:

1. Remove h/l as tree expand/collapse keys in filter mode — they
   prevented typing repo/branch names with those letters. Arrow keys
   are now the only way to expand/collapse nodes.

2. Fix b-key auto-expansion to match multi-path active repo filters
   using full rootPaths equality instead of only matching single-path
   filters.

3. Return fetch errors via tuiRepoBranchesMsg instead of tuiErrMsg so
   the loading state is cleared on failure, preventing permanently
   stuck "..." indicators.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move error handling for tuiRepoBranchesMsg ahead of the view/staleness
gate so errors are always surfaced — even after the user leaves filter
view or the tree is rebuilt. Call handleConnectionError on the error
path so consecutiveErrors is tracked and reconnect logic fires.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four changes to speed up the default test suite:

1. Add //go:build integration to stream_integration_test.go (was named
   as integration but missing the tag).

2. Move TestDaemonRunStartsAndShutdownsCleanly to a new
   daemon_integration_test.go behind the integration tag — it spawns a
   real daemon with a 10s shutdown timeout.

3. Share a single 2048-bit RSA key across githubapp tests via
   sync.Once instead of generating one per test (~0.5-1s each, 7
   calls). Only TestParsePrivateKey_PKCS8 still generates its own key
   since it needs PKCS8 encoding specifically.

4. Fix TestAnalyzeBranchFlagValidation to call ParseFlags + ValidateArgs
   directly instead of cmd.Execute(), which was wasting ~3s on a
   daemon connection timeout.

Results: cmd/roborev 43s -> 14s, internal/daemon 20s -> 16s.
All tests still run in CI which uses -tags integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ic fix

- Add test for branch fetch connection errors triggering reconnect
  after 3 consecutive failures (review #5320 finding 1)
- Add filterBranchMode=false assertion to out-of-view failure test
  (review #5320 finding 2)
- Replace panic in testKey sync.Once with stored error checked via
  t.Fatalf on each call (review #5324 finding 1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Branch search only matched repos that had already been expanded,
since branches are lazy-loaded. When the user types search text,
trigger fetchBranchesForRepo for any repos with unloaded branches
(children == nil). As branches arrive, rebuildFilterFlatList
re-evaluates the search and shows matching results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: Mostly clean change set, with one Medium UI behavior regression to address before merge.

Medium

  • Search lazy-load permanently expands repos (UI regression)
    Files: cmd/roborev/tui.go:1887, cmd/roborev/tui_handlers.go:185
    Issue: Search-triggered lazy branch fetches mark repos as expanded (expanded = true) after fetch success. When search is cleared, many repos remain expanded even if the user never explicitly expanded them.
    Why it matters: This changes persistent UI state unexpectedly and makes the filter view noisy after normal search usage.
    Suggested fix: Track fetch intent (for example, explicit expand vs search preload) in the branch-fetch message and only set expanded = true for explicit user expansion flows (for example, right-arrow / branch-open behavior), not search preloads.

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

wesm and others added 2 commits February 15, 2026 08:09
…uring search

Search-triggered lazy branch loads (fetchUnloadedBranches) no longer set
expanded=true on the tree node. This prevents repos from staying expanded
after the search is cleared. Added expandOnLoad flag to tuiRepoBranchesMsg
so only user-initiated expansion (right-arrow, b-key) sets expanded=true.

Left-arrow collapse now works during active search by tracking a
userCollapsed flag on tree nodes. When set, search auto-expansion is
suppressed for that repo. The flag resets when the search is cleared.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a search-triggered branch fetch is in-flight (loading=true) and
the user presses right-arrow, set expanded=true immediately. The
arriving response (expandOnLoad=false) won't clear it, so children
display as the user intended.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: 2 Medium-severity issues should be addressed before merge.

Medium

  1. Terminal escape injection in TUI rendering

    • Location: cmd/roborev/tui.go:2831, cmd/roborev/tui.go:2836 (renderFilterView)
    • Finding: Repository/branch names are rendered directly (node.name, branch.name) without output sanitization, allowing ANSI/control-sequence injection into terminal UI.
    • Impact: UI spoofing/corruption in terminal output.
    • Suggested fix: Sanitize names before rendering (for example, apply existing display sanitization like sanitizeForDisplay(...)).
  2. Search-triggered branch loading can cause thundering-herd resource spikes

    • Location: cmd/roborev/tui_handlers.go:203 (fetchUnloadedBranches)
    • Finding: Search input can trigger concurrent fetches for many/all unloaded repos at once.
    • Impact: High local resource usage and degraded responsiveness for large repo sets.
    • Suggested fix: Add debounce/rate limiting and cap concurrent fetches (or otherwise batch/defer loading).

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

@wesm wesm changed the title Improve TUI filter: cwd prioritization, stale guards, and 'b' key branch shortcut Unified tree filter with lazy branch loading, search, and cwd prioritization Feb 15, 2026
Avoid false-positive findings for injection/sanitization of data that
originates from the user's own filesystem, and for thundering-herd
concerns on bounded localhost HTTP requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: Looks good overall, with one Medium regression risk to address before merge.

Medium

  1. Retry can get stuck after branch-load failure
    • Files: cmd/roborev/tui_handlers.go (handleFilterKey, "right" case), cmd/roborev/tui.go (case tuiRepoBranchesMsg, error path)
    • Issue: If a repo is set expanded=true while branch loading is in-flight and that load fails, loading is cleared but expanded remains true with children == nil. Subsequent right-arrow presses are blocked by the if !node.expanded guard, so retry is effectively blocked unless the user first collapses or changes search text.
    • Suggested fix: On fetch error, reset expanded=false when children is nil, or allow right-arrow to fetch when children==nil && !loading even if expanded is already true.

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

When a search-triggered load fails after the user pressed right-arrow
(setting expanded=true), the error handler clears loading but leaves
expanded=true with children=nil. The right-arrow guard previously
blocked retry in this state. Now right-arrow enters the fetch path
whenever children is nil and not loading, regardless of expanded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: One Medium correctness issue was found; no High/Critical or security issues were identified.

Medium

  • Order-sensitive repo identity comparison can mis-handle multi-path groups
    File: cmd/roborev/tui.go:1494
    Also used at: cmd/roborev/tui.go:1850, cmd/roborev/tui.go:1888, cmd/roborev/tui.go:1899
    rootPathsMatch compares paths by index, so equivalent path sets in different orders may be treated as different repos. This can cause incorrect b auto-expand targeting, stale-message misclassification, and loading-state edge-case inconsistencies.
    Suggested fix: compare root paths as sets (or sort copies before comparing).

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

wesm and others added 2 commits February 15, 2026 09:10
rootPathsMatch compared paths by index, so equivalent path sets in
different orders were treated as different repos. This could cause
stale-message misclassification when the tree is rebuilt with a
different path ordering while a branch fetch is in-flight. Now sorts
copies before comparing. Single-path fast path avoids allocation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Sanitize repo/branch names with sanitizeForDisplay before rendering
  in the filter view, consistent with how commit messages are handled
- Cap fetchUnloadedBranches to 5 concurrent fetches per keystroke to
  bound resource usage when many repos are tracked; subsequent
  keystrokes pick up remaining repos
- Add end-to-end test for branch response with reordered root paths
  (accepted) and mismatched paths (rejected as stale)
- Add test verifying fetch cap at maxSearchBranchFetches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: One Medium-severity issue should be addressed before merge.

Medium

  1. Integration test coverage can silently drop from default CI
    • File: cmd/roborev/daemon_integration_test.go:1, internal/daemon/stream_integration_test.go:1
    • Finding: Adding //go:build integration removes daemon lifecycle/stream tests from default go test ./...; if CI does not explicitly run -tags=integration, regressions in startup/shutdown and stream behavior may go undetected.
    • Suggested fix: Add/require a CI job that runs integration-tagged tests (for example: go test -tags=integration ./cmd/roborev ./internal/daemon).

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

fetchUnloadedBranches now counts already in-flight requests toward the
maxSearchBranchFetches cap, so rapid typing cannot exceed 5 concurrent
fetches. On each tuiRepoBranchesMsg (success or error), a top-up fetch
is triggered for remaining unloaded repos while search is active, so
all repos eventually load regardless of keystroke count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: Mostly clean change set, but there is 1 Medium regression risk that should be addressed before merge.

Medium

  1. Incomplete branch search results when many repos are tracked
    • Files: cmd/roborev/tui_handlers.go:173, cmd/roborev/tui_handlers.go:218, cmd/roborev/tui.go:1900
    • Issue: Search-triggered branch loading is capped per keystroke, but completion of a fetch does not schedule subsequent batches. If typing pauses, repos beyond early batches may never load, so valid branch matches can be missing from results.
    • Suggested fix: When handling tuiRepoBranchesMsg and filterSearch != "", keep scheduling fetchUnloadedBranches() until all unloaded repos are processed, with explicit in-flight tracking to enforce a global concurrency cap.

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

- Use rootPathsMatch for active repo preselection instead of inline
  positional comparison, so multi-path repos with different ordering
  are correctly preselected
- Add fetchFailed flag to treeFilterNode to prevent infinite retry
  loops when search-triggered branch fetches fail with persistent
  errors; failed repos are skipped by fetchUnloadedBranches but can
  be retried manually via right-arrow
- Tighten progressive loading test to only complete in-flight repos
  and verify specific top-up targets
- Add tests for multi-path preselection and error-path retry behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: Mostly solid refactor, but there is 1 Medium-severity correctness issue that should be fixed before merge.

Medium

  1. consecutiveErrors is not reset after successful branch fetches
    • File: cmd/roborev/tui.go (handler for tuiRepoBranchesMsg, around @@ -1714,43 +1812,139)
    • Issue: On successful per-repo branch fetches, m.consecutiveErrors is not reset.
    • Impact: Reconnect/failure-threshold logic can trigger on effectively non-consecutive failures (e.g., fail -> success -> fail -> fail still counts as consecutive).
    • Suggested fix: Reset m.consecutiveErrors on successful tuiRepoBranchesMsg handling (optionally clear m.err as done in other successful fetch handlers).

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

- Propagate expandOnLoad to error messages so fetchFailed is only set
  for search-triggered failures (!msg.expandOnLoad). Manual expand
  failures no longer block later search auto-loading.
- Clear fetchFailed alongside userCollapsed when search is empty so
  each search session starts with a clean slate.
- Reset consecutiveErrors on successful branch fetch, consistent with
  all other success handlers.
- Add tests for manual-failure-then-search and fetchFailed session
  reset scenarios.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A search-triggered fetch failing after the user cleared search would
set fetchFailed, blocking the repo in the next search session. Now
fetchFailed is only set when filterSearch is non-empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: Mostly clean, with one Medium functional regression to address before merge.

Medium

  1. Branch search can stay empty if user types before repos load
    • Location: cmd/roborev/tui.go:1837
    • Issue: In case tuiReposMsg, the tree is rebuilt but branch prefetch is not triggered when filterSearch was entered before repositories finished loading.
    • Impact: Branch-only searches may appear empty until another keypress triggers fetchUnloadedBranches().
    • Suggested fix: After m.rebuildFilterFlatList() in tuiReposMsg, if m.filterSearch != "", invoke m.fetchUnloadedBranches() (or equivalent batched branch-fetch logic).

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

If the user typed search text before repos finished loading, the
tuiReposMsg handler now calls fetchUnloadedBranches so branch matches
appear immediately without requiring another keystroke.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: Mostly clean, with one Medium regression risk to address before merge.

Critical

None.

High

None.

Medium

  1. fetchFailed can persist after reconnect and block branch-search recovery
    • cmd/roborev/tui.go:1902
    • cmd/roborev/tui_handlers.go:224
    • cmd/roborev/tui_handlers.go:1281
    • When search-triggered branch fetches fail, fetchFailed=true is set and later skipped by fetchUnloadedBranches(). After a successful reconnect, this state is not cleared, so branch search may stay incomplete until search is cleared or repos are manually retried.
    • Recommended fix: clear fetchFailed on successful reconnect (or only set it for non-connection errors), then trigger a top-up fetch when filterSearch != "".

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

wesm and others added 2 commits February 15, 2026 11:57
After reconnecting to a new daemon address, clear fetchFailed on all
tree nodes and trigger a top-up branch fetch if search is active.
Previously, repos that failed during the old connection would stay
blocked until the user manually cleared and re-entered search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thread filterSearchSeq through fetch requests so error responses from
a previous search session don't mark repos as fetchFailed in the
current session. Also adds test coverage for the search-typed-before-
repos-load path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: One medium-severity regression risk identified; no critical/high issues and no new security vulnerabilities reported.

Medium

  1. fetchFailed may persist across non-empty search edits, blocking future auto-loads
    • Files: cmd/roborev/tui.go (in rebuildFilterFlatList), cmd/roborev/tui_handlers.go (in fetchUnloadedBranches)
    • Issue: fetchFailed is reset only when search becomes exactly empty. If fetch fails for search "a" and user changes to "ab" (still non-empty), that repo can remain skipped for auto-fetch, hiding branch matches until manual retry/reconnect/full clear.
    • Suggested fix: Reset failure state on any filterSearchSeq change (not only empty search), or scope failures to a search generation key (for example fetchFailedSeq). Add a test for non-empty → non-empty search transitions after failure.

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

wesm and others added 2 commits February 15, 2026 12:44
fetchFailed persisted across non-empty search edits (e.g. "a" → "ab"),
blocking auto-fetch for repos that failed in the previous search state.
Now clearFetchFailed() runs on every search keystroke so changed search
text retries previously failed repos. Also adds reconnect test coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wesm
Copy link
Collaborator

wesm commented Feb 15, 2026

Gosh, so many darn edge cases! I'm going to merge this once the build is green

@wesm wesm merged commit 48ac7b2 into roborev-dev:main Feb 15, 2026
7 checks passed
@roborev-ci
Copy link

roborev-ci bot commented Feb 15, 2026

roborev: Combined Review

Verdict: One Medium finding remains; security reviews reported no vulnerabilities.

Medium

  1. Integration tests may no longer run in default CI (testing gap)
    • Files: cmd/roborev/daemon_integration_test.go:1, internal/daemon/stream_integration_test.go:1
    • Issue: These tests are now behind //go:build integration, so they are skipped by default go test ./.... If CI does not explicitly run integration-tagged tests, regressions may be missed.
    • Suggested fix: Add/verify a CI step that runs go test -tags=integration ./....

Synthesized from 4 reviews (agents: codex, gemini | types: security, default)

hughdbrown pushed a commit to hughdbrown/roborev that referenced this pull request Feb 16, 2026
…ization (roborev-dev#251)

This PR replaces the separate repo/branch filter modals with a unified
tree-style filter view. Repos are listed as expandable nodes with
branches lazy-loaded on demand. The current working directory repo and
branch are prioritized in sort order.

<img width="862" height="472" alt="image"
src="https://github.com/user-attachments/assets/e67b5a77-0d4f-4edb-aaab-15bba90972d8"
/>

## Summary

- Unified tree filter replacing separate repo and branch modals
- Lazy branch loading: branches fetch per-repo on expand (right-arrow)
or search
- Search matches unloaded branches by triggering lazy fetches in the
background
- Cwd repo sorts to top of filter tree; cwd branch sorts to top within
its repo
- `b` key shortcut opens filter with branches auto-expanded for the most
relevant repo
- Left-arrow collapse works during active search (overrides search
auto-expansion)
- Search-triggered loads do not permanently expand repos; only
user-initiated expansion persists
- Stale in-flight branch responses detected via `rootPaths` identity
check
- Branch fetch errors surface through connection tracking and reconnect
logic
- Slow tests moved behind `//go:build integration` tag; shared RSA key
generation via `sync.Once`
- Dead code removed: old `fetchBranches`, `flatFilterEntry.depth`,
unused message fields

## Details

**Unified tree filter** (`5bb3631`): Replaces the two-modal flow (repo
list → branch list) with a single tree view. Each repo is a collapsible
node; right-arrow expands to show branches, left-arrow collapses. Enter
selects a repo or branch filter.

**Cwd prioritization** (`e997569`): At TUI launch, the model detects
`cwdRepoRoot` and `cwdBranch`. The cwd repo sorts to index 0 in the
filter tree, and the cwd branch sorts to first position when branches
load. Independent of `autoFilterRepo` config.

**Stale message guards** (`e06e319`): `tuiRepoBranchesMsg` carries
`rootPaths` so the handler can verify the tree node at `repoIdx` still
represents the same repo, preventing races when the filter is closed and
reopened during in-flight fetches.

**'b' key shortcut** (`ec18ede`, `db62b33`): Opens the unified filter
with branches auto-expanded for the target repo (active filter > cwd >
first). Cursor lands on the first branch. `filterBranchMode` clears on
escape, enter, and after branches load.

**Filter regressions fixed** (`49e73f8`): `h`/`l` keys no longer
captured by expand/collapse (arrow-keys only), multi-root-path repos
matched correctly via `rootPathsMatch()`, branch fetch failures clear
loading state.

**Error handling fixes** (`5e4d7f1`): Branch fetch errors bypass the
view/staleness gate so connection tracking and reconnect logic always
fire. Errors are no longer silently dropped when the filter view is not
active.

**Lazy search loading** (`527f35b`): Typing in the filter search
triggers `fetchUnloadedBranches()` for repos that haven't loaded
branches yet, so search can match branch names across all repos.

**Search expansion fixes** (`d034bbf`, `104a9c9`): Search-triggered
loads use `expandOnLoad: false` so repos don't permanently expand.
Left-arrow sets `userCollapsed` to override search auto-expansion.
Right-arrow during an in-flight search load preserves user expand
intent.

**Test speedups** (`57ace30`): `TestDaemonRunStartsAndShutdownsCleanly`
and stream tests moved behind `//go:build integration`. RSA key
generation shared via `sync.Once`. Analyze test uses direct arg
validation instead of `cmd.Execute()`.

## Test plan

- [x] Unified tree filter shows repos with expand/collapse via arrow
keys
- [x] Lazy branch loading on right-arrow and search triggers
- [x] Search matches branches across all repos (including previously
unloaded)
- [x] Search-triggered loads do not permanently expand repos
- [x] Left-arrow collapses repos during active search
- [x] Right-arrow during in-flight search load preserves user intent
- [x] `userCollapsed` resets when search is cleared
- [x] Cwd repo/branch sort to first position
- [x] Stale branch messages with mismatched rootPaths are dropped
- [x] `b` key auto-expands target repo and positions cursor on first
branch
- [x] Branch fetch errors clear loading state and trigger reconnect
- [x] `h`/`l` keys pass through to search; only arrow keys
expand/collapse
- [x] Multi-root-path repos matched correctly in b-mode expansion
- [x] `go test ./...` passes; `go vet ./...` clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Wes McKinney <wesmckinn+git@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants