Fix search & navigation bugs: pagination, scroll, stats, and zero-result handling#71
Merged
Fix search & navigation bugs: pagination, scroll, stats, and zero-result handling#71
Conversation
When searching from top-level (no drill-down), the header metrics showed correct message count but zero size and zero attachments. The root cause was replaceSearchResults creating contextStats with only MessageCount, leaving TotalSize and AttachmentCount at their zero values. Fix: call GetTotalStats with the search query during initial search execution to fetch accurate aggregate stats (size, attachment count), and use them in replaceSearchResults when no drill-down stats exist. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace 3-4 separate Parquet scans in loadSearchWithOffset with a single SearchFastWithStats call that materializes matching messages into a DuckDB temp table, then reuses it for count, pagination, and stats. The temp table stores denormalized message data (including sender info), so later phases never re-read msg Parquet — only small page-scoped label/attachment lookups remain. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…races Two fixes: 1. SQLiteEngine.GetTotalStats now respects opts.SearchQuery by parsing it with search.Parse() and applying conditions via buildSearchQueryParts(). Previously it returned global stats regardless of search filters. When search joins are present, a subquery pattern avoids duplicate counting from 1:N joins. 2. DuckDB SearchFastWithStats now uses unique temp table names (via atomic counter) to prevent concurrent goroutines from clobbering each other's temp tables. The old fixed name "_search_matches" could be dropped or replaced by an overlapping search goroutine. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…cans SearchFastWithStats now keeps the materialized temp table alive between calls with the same conditions+args (e.g. page down/scroll). On cache hit, Phase 1 (Parquet scan) is skipped entirely and the page is served from the cached in-memory table. A new search invalidates the old cache automatically. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a search returned no matches, the early return in messageListView() skipped the entire rendering including the search bar, making it impossible to see or edit the query. Now the early return only fires for non-search empty states; search contexts render the full layout with a "No results found" indicator, the search bar, and "(0 results)" in the info line. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes from reviews #4602, #4603, #4604: - Cache key: use JSON encoding instead of delimiter-based concatenation to prevent ambiguous collisions (e.g. args containing commas/pipes) - SQLite SearchFastWithStats: make count best-effort (log + use -1) instead of aborting the entire search on count failure - DuckDB SearchFastWithStats: same best-effort count behavior - source_id: remove COALESCE(source_id, 0) in temp table so NULL source_ids don't inflate COUNT(DISTINCT) account stats - dropSearchCache: always use context.Background() so cleanup succeeds even when the caller's context is canceled, preventing leaked temp tables on the single DuckDB connection - Stats retry: if computeSearchStats fails (transient error), retry on next cache-hit pagination instead of permanently caching nil stats - SQLite attachment stats: only use IN subquery when search joins are present; use direct JOIN when no search is active (faster path) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Covers the three key scenarios from review #4605: - Inline search active with zero results: search bar visible, "No results found" shown - Completed search with zero results: "(0 results)" in info line, query visible - Non-search empty state: "No messages" shown without search UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Addresses review testing gaps from #4606 and #4607: - TestSearchCacheKeyFor: table-driven tests verifying JSON cache keys avoid collisions from delimiters, type differences, and special chars - TestSearchFastWithStats_CacheHitSkipsRescan: paginates the same search twice, asserts no new temp table is created and count/stats are stable - TestSearchFastWithStats_CacheInvalidatedOnNewSearch: changes the search query and asserts cache is invalidated (new temp table created) - Non-search empty state: negative assertions for search bar prefix "[Fast]/", "(0 results)" to fully cover "without search UI" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When the user modified a search query (typing more or pressing delete), only MessageCount updated in the header — TotalSize and AttachmentCount stayed stale from the first search. The bug was in replaceSearchResults() where hasDrillDownStats incorrectly treated stats from a previous search as drill-down stats, preventing fresh stats from being applied. Fix: when msg.stats != nil (fresh stats from SearchFastWithStats), always use them. Only fall back to preserving existing stats when no fresh stats are available (e.g. deep/FTS search which doesn't compute aggregates). Adds 4 regression tests covering: - Subsequent search updates all stats fields - Delete key (broadening search) updates all stats fields - Drill-down stats preserved when search has no fresh stats - Fresh stats override even existing drill-down stats Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Owner
Author
|
Pagination has some issues, so I'm working on those now |
Fix two issues: (1) cursor could scroll onto the info/notification line because ensureCursorVisible used pageSize as the visible window, but views only render pageSize-1 data rows. (2) Fast search pagination never triggered because navigateList's early return exited before maybeLoadMoreSearchResults could fire. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Return a copy of TotalStats from searchPageFromCache to prevent UI mutations from corrupting the cache (review #4610) - Gate maybeLoadMoreSearchResults behind a cursor-changed check so non-navigation handled keys don't trigger unnecessary I/O (#4612) - Add TestFastSearchPaginationTriggersOnNavigation with 4 subtests covering: near-end triggers load, far-from-end skips, all-loaded skips, and deep-mode skips (#4612) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove the cursorMoved gate for maybeLoadMoreSearchResults since it blocked pagination when cursor was already at the last loaded item (pressing down couldn't move cursor, so pagination never triggered). navigateList only returns handled=true for navigation keys, so using handled as the gate is sufficient. Add test for cursor-at-end case. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, message lists were capped at 500 results with no way to load more. This adds pagination mirroring the fast search approach: messages load in 500-item pages, and navigating within 20 rows of the end triggers loading the next page (appended to the existing list). Also addresses review #4613: remove overly-strict cursorMoved gate that blocked pagination when cursor was already at the last item. Changes: - Extract buildMessageFilter from loadMessages for reuse - Add loadMessagesWithOffset with append mode support - Add maybeLoadMoreMessages (mirrors maybeLoadMoreSearchResults) - Add msgListOffset/msgListLoadingMore state fields - Handle append flag in messagesLoadedMsg/handleMessagesLoaded - Reset pagination state on fresh message list entry points - Add TestMessageListPaginationTriggersOnNavigation (6 subtests) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move msgListOffset/msgListLoadingMore into viewState so breadcrumb snapshots preserve pagination position across navigation. Add msgListComplete flag set when an append returns zero messages, preventing infinite empty load requests when total is an exact multiple of page size. - Move pagination fields from Model to viewState (review #4615 medium) - Add msgListComplete end-of-data flag (review #4615 low) - Reset msgListComplete on fresh loads and new list entry points - Add TestMessageListPaginationBreadcrumbRestore (2 subtests) - Add empty-append and fresh-load-reset subtests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a load-more pagination request is in-flight and the user navigates forward (e.g. to detail view), the breadcrumb snapshot captures msgListLoadingMore=true. The in-flight request becomes stale and gets discarded, so goBack must clear the flag to allow fresh pagination. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allows disabling the DuckDB sqlite_scanner extension on Linux/macOS to exercise the direct SQLite fallback code path (normally Windows-only). Added to both tui and mcp commands as a hidden flag. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Comprehensive fixes for search, navigation, and pagination bugs in the TUI. Multiple serious issues were discovered during QA and fixed incrementally.
Fixes #42, #69.
Search engine fixes
GetTotalStatsignoredSearchQuery— fixed to include search filter in stats queriesTotalStatspointer — now returns a defensive copyTUI crash and rendering fixes
replaceSearchResultsto prioritize fresh stats over stale drill-down statsNavigation and scroll fixes
ensureCursorVisibleusedpageSizebut views renderpageSize-1data rows — fixed to use visible row countnavigateListearly return preventedmaybeLoadMoreSearchResultsfrom ever firing — moved pagination check before the early returnCode quality
dropSearchCacheusescontext.Background()for cleanup that must not be skippedTest plan
TestSearchCacheKeyFor— 7 table-driven cases for cache key constructionTestSearchFastWithStats_CacheHitSkipsRescan— verifies no re-materialization on cache hitTestSearchFastWithStats_CacheInvalidatedOnNewSearch— verifies cache invalidationTestZeroSearchResultsRendersSearchBar— 3 subtests for zero-result renderingTestSearchStatsUpdateOnSubsequentSearch— stats refresh on re-searchTestSearchStatsUpdateOnDeleteKey— stats refresh on backspaceTestDrillDownStatsPreservedWhenSearchHasNoStats— drill-down stats preservationTestFreshStatsOverrideDrillDownStats— fresh stats take priorityTestFastSearchPaginationTriggersOnNavigation— 5 subtests including cursor-at-end edge caseTestMessageListPaginationTriggersOnNavigation— 6 subtests for non-search paginationTestDetailNavigationFromThreadView— updated scroll offset expectations🤖 Generated with Claude Code