fix(desktop): cache external-session catalog per selection so switching sources shows loaded rows instantly - #3905
Conversation
…ng sources shows loaded rows instantly Switching import source (or toggling the archived filter / search) blanked the list and flashed the full-width 'reading external conversations' spinner on every switch, including returning to a source already loaded, because loadCatalog unconditionally reset the catalog and loading flag with no per-selection caching. Cache the last loaded CatalogState per (adapterId, includeArchived, search): a cache hit renders instantly and refreshes in the background instead of blanking; misses keep the spinner. The poll now observes rather than claims the request generation so it can never strand an in-flight load's spinner. Adds source-switching tests (harness now supports multiple adapters). Generated-by: Claude Code
3d9b5c9 to
fbf120f
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for addressing the repeated blank-loading state when switching external-session sources. Keeping the Host as the catalog authority while caching only the renderer projection is the right boundary, and the selection key plus request generation model is appropriately scoped.
I left one inline P2 for a recoverable stale-loading case. It does not risk catalog correctness or persisted data, so I am approving the overall direction, but clearing the obsolete loading state would make the cache behavior complete.
Reviewed with Codex and an independent @Reviewer agent. I verified the exact head, cache ownership, selection and request generations, switching and import-recovery paths, and passing CI.
中文对照
谢谢你处理切换外部 Session 来源时反复出现空白加载状态的问题。Host 仍然是 catalog authority,renderer 只缓存展示 projection,这个边界是正确的;selection key 和 request generation 的范围也比较合适。
我留了一条 P2 行内评论:切回缓存 selection 时,旧的 loading 状态可能没有被清理。它不会影响 catalog 正确性或持久化数据,因此我认可整体方向;不过清理过期 loading 状态可以让缓存行为真正闭环。
本次审查使用了 Codex 和一个独立的 @Reviewer 子代理;我核对了精确 head、缓存职责、selection 与 request generation、切换和导入恢复路径,以及 CI 结果。
… hit Restoring a cached catalog only reset the rows and the recovery banner, so a search or pagination request still in flight from the previous selection kept its loading flag. That older generation can never reach its own `finally` reset once a newer request supersedes it, and this hit's background refresh may not have landed yet, so the full-page spinner or a disabled Load More could strand over otherwise-complete cached rows indefinitely. Clear both `catalogLoading` and `loadingMore` when publishing a non-append cache hit so the instant, no-spinner contract holds regardless of a pending prior request or a slow background refresh. Adds two source-switching tests: a pending search returning to a cached term (reachable via the search box, the only control not disabled during a load) and a pending Load More when switching to a cached source. Both fail without the change (19/21) and pass with it (21/21). Addresses the P2 review comment on apache#3905. Generated-by: Claude Code
…d selection Revisiting a cached selection restored its full CatalogState — which can be several pages deep after Load More — but the background refresh that followed requested only the first page (no cursor) and then replaced the cache and the view with that single page. So a multi-page source, revisited, flashed all its pages and then snapped back to page one once the refresh landed, silently dropping everything the user had paged in. Refresh a cache hit through the existing `readCatalogWindow`, seeded with the cached `sessions.length`, so it re-reads the entire loaded window instead of just page one. The uncached first-page load and the Load More append paths are unchanged. Adds a source-switching test: a two-page source, revisited, keeps both pages after the refresh (re-reading every page). It fails without the change (21/22) and passes with it (22/22). Addresses the P2 pagination-shrink review comment on apache#3905. Generated-by: Claude Code
Summary
Switching the import source — or toggling the archived filter / editing the search, and including returning to a source already loaded — blanked the list and flashed the full-width "Reading external conversations…" spinner on every switch, because
loadCatalogunconditionally reset the catalog and loading flag with no per-selection cache.This caches the last loaded
CatalogStateper(adapterId, includeArchived, search): a cache hit renders the rows immediately and refreshes in the background, while a miss keeps the existing spinner. The background import poll now observes the request generation instead of claiming a new one, so it can never strand an in-flight load'scatalogLoadingreset.Two follow-ups from review: a cache hit now also clears any stale
catalogLoading/loadingMoreleft by a superseded search or pagination request (so the spinner or a disabled Load More can't strand over otherwise-complete cached rows), and it refreshes throughreadCatalogWindowseeded with the cached page count, so a multi-page selection keeps every loaded page instead of snapping back to page one when the refresh lands.Fixes #3904
Verification
tscfortsconfig.preload.json,tsconfig.main.json,tsconfig.renderer.json— all clean.biome checkon both changed files — clean.node --test dist/main/__tests__/import-tasks-settings-page.test.js— 22/22 pass (15 existing + 7 source-switching cases).shows a previously-loaded source instantly with no spinner,does not let a stale background refresh overwrite a newer source selection), the stale-loading reset (clears the reading spinner when a pending search returns to a cached term,clears a pending Load More lock when switching back to a cached source), and the full-window refresh (keeps every loaded page when a revisited multi-page source refreshes); restoring each returns the suite to green (22/22).format:check/knip, fullnpm run build, and the cross-workspace suite.tsconfig.storybook.jsonreports one pre-existing, unrelated error (UsageRequestLog.sessionNameinstories/settings/settings-pages.stories.tsx) that exists on the base commit and this PR does not touch.Review focus (UI evidence)
I could not capture a headless screenshot of the Electron window in my environment, so the before/after is shown via the deterministic test output above instead of an image. To see it live: run the desktop app, open Settings → Activity → Import Tasks with ≥2 sources, and switch back and forth — a revisited source now appears instantly with no spinner flash (first-time loads still show the spinner).
AI use
Select exactly one:
Tool(s) and scope: Claude Code (Claude Opus) — root-cause investigation, the per-selection cache implementation, the two review-driven follow-ups (stale-loading reset and full-window refresh), and the added tests. Reviewed and submitted by the human contributor of record; commit carries a
Generated-by: Claude Codetrailer.Checklist
Does this PR entail a change in behavior?