Skip to content

feat(home): infinite scroll + pull-to-refresh for the home sessions list - #331

Open
DatScreamer wants to merge 15 commits into
0xSero:mainfrom
DatScreamer:feat/home-sessions-infinite-scroll
Open

feat(home): infinite scroll + pull-to-refresh for the home sessions list#331
DatScreamer wants to merge 15 commits into
0xSero:mainfrom
DatScreamer:feat/home-sessions-infinite-scroll

Conversation

@DatScreamer

@DatScreamer DatScreamer commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Supersedes #308 (feat(ios): paginate older turns with infinite scroll). That PR's conversation older-turns work is included on this branch; keeping both open would race on the same AppModel.swift paging code and Rust cursor state. #308 should be closed in favor of this PR.

Summary

Adds infinite scroll and pull-to-refresh to the home sessions list on both iOS and Android, replacing the fixed top-N list. The shared Rust layer owns cursor-paged session loading (AppStore::load_threads_page), retained per-runtime cursors, and a sessionListHasMore projection; the platforms just grow a recentLimit window as the user scrolls near the bottom. Also includes the conversation older-turns infinite scroll (from #308).

Key changes (14 commits, on top of #319 which is already in main)

  • Conversation: paginate older turns with infinite scroll.
  • Home sessions: infinite scroll + pull-to-refresh, session list on connect, plus page-size, warmup, reconnect, and cursor-state fixes.
  • iOS: HomeDashboardModel grows a recentLimit window fed by AppModel.loadThreadsPage, drag-down refresh, a near-bottom load-more trigger in the UIKit scroll view, plus a local-only path when server cursors are exhausted.
  • Android: LazyColumn scroll-proximity load-more, Material3 PullToRefreshBox, and the same recentLimit window / local-only path.
  • Subagent exclusion: subagent/agent sessions are excluded from the home list (the opencode bridge drops them at the source via parentID; the client adds a !isSubagent filter as defense-in-depth).

Verification

  • cargo check -p codex-mobile-client passes.
  • iOS: home list loads pages on scroll, pull-to-refresh drains and reconciles; session count grows past the initial page.
  • Android: same behavior via LazyColumn + PullToRefreshBox.
  • Subagent sessions no longer appear in the dashboard list (verified on iOS with the opencode-bridge fix at alleycat d503bfc).

DatScreamer and others added 14 commits August 24, 2026 16:48
Replace the explicit Load-earlier-messages button with scroll-triggered
pagination. Track visible turn IDs and prefetch the next older page as the
user approaches the top, debounced per cursor so concurrent prefetches
collapse. Bump page size to 20, make loadOlderTurns report whether a page
was actually loaded, and re-route the Latest button through a semantic
ScrollPosition with a UIKit scroll-view resolver so the jump executes even
while momentum is decelerating. Anchor merged exploration rows to their
newest constituent turn so prepending older pages keeps scroll identity
stable, and auto-collapse large conversations past a 200-item threshold.
Add cursor-paged session loading in the shared Rust store
(AppStore::load_threads_page) with retained per-runtime cursors and a
sessionListHasMore projection, so the home sessions list loads pages on
scroll instead of a fixed top-10.

iOS: HomeDashboardModel grows a recentLimit window (10 + 10 per load-more)
fed by AppModel.loadThreadsPage, a drag-down UIRefreshControl full reload,
and a near-bottom load-more trigger in the UIKit scroll view; topInset bumped
48 -> 56 for extra spacing above the list.

Android: HomeDashboardScreen grows the same recentLimit window via
AppModel.loadSessionsPage, adds Material3 PullToRefreshBox + snapshotFlow
near-bottom load-more, and +8dp top content padding. Connect-time loads use
the paged first page instead of a full drain.

Android Conversation parity with the iOS infinite-scroll PR: replace the
manual 'Load earlier messages' button with scroll-proximity auto-prefetch,
stable LazyColumn turn keys for prepend scroll preservation, a 200-item
auto-collapse policy, and turn page sizes bumped 5 -> 20.
Sessions were only ever loaded from the (unreachable) sessions screen or
search, so a freshly connected server never populated its session list.
Fetch the thread list in the post-connect warmup via a shared
refresh_thread_list fanout, and pin the alleycat bridges to a fork whose
opencode bridge enumerates sessions across all project worktrees (a bare
GET /session only returns the default project). Relax update-alleycat-main
to treat any rev-pinned git dep as pinned so the fork pin survives.
Restore auth state when needsLocalAuthRestore is true, without the
redundant loadSessionsPage call (the Rust post-connect warmup handles
thread population).
clear_thread_page_state after the warmup's full drain wiped session_pages
→ session_list_has_more = false → infinite scroll never triggered. After a
full drain the cursor is naturally None and has_more reflects the server's
actual state, so clearing is unnecessary and harmful.
The warmup drained all pages (limit: None) which set session_list_has_more
= false immediately, preventing loadMoreSessions from ever firing. Now
loads just the first page (limit: 20) so the server's has_more flag is
accurate and the home list's infinite-scroll gate works.

Also persists cursor state in refresh_thread_list so session_pages is
non-empty after the warmup.
The warmup (spawn_post_connect_warmup) is a fire-and-forget Tokio task.
refreshSnapshot() in reconnectSavedServers() runs before the warmup
finishes, so the Kotlin snapshot always has stale session data and
session_list_has_more=false — making infinite scroll impossible.

Replace refreshSnapshot() with loadSessionsPage() for connected servers,
which synchronously loads the first page via Rust's load_threads_page,
sets accurate cursor/has_more state, and then snapshots.
- iOS: local-only load-more path when server cursors are exhausted,
  firstPageLoaded/hadVisibleServers guards so the first page loads once
  runtime kinds are ready, and targetServerIds scoping for servers that
  still have remote pages.
- iOS: fix loadMoreFired reset so a new page can trigger after the
  in-flight flag flips.
- Android: add isLoadingMoreSessions in-flight guard, local-only
  recentLimit bump path, and LazyColumn listState wiring so the load-more
  footer reflects the right state.
- Bump home session page size 10 -> 20 and fix preview wiring for the
  new sessionListHasMore field.
Keep subagent/agent threads off the home session list. The opencode
bridge now excludes them at the source (parentID filter + drop), so the
list is defense-in-depth on top of that. Re-pins alleycat to d503bfc
which contains the opencode-bridge subagent exclusion (thread/list drop
+ session.created guard).
@DatScreamer
DatScreamer force-pushed the feat/home-sessions-infinite-scroll branch from 88af0b8 to d393140 Compare August 24, 2026 22:48
The infinite-scroll change added the required session_list_has_more field
to AppServerSnapshot; five test files still built it with the old
signature and failed to compile under xcodebuild test. Add
sessionListHasMore: false to each.
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.

1 participant