Skip to content

perf: lazy-load chat messages on scroll-to-top#1108

Open
chaehyun2 wants to merge 10 commits intoslopus:mainfrom
chaehyun2:perf/lazy-load-messages
Open

perf: lazy-load chat messages on scroll-to-top#1108
chaehyun2 wants to merge 10 commits intoslopus:mainfrom
chaehyun2:perf/lazy-load-messages

Conversation

@chaehyun2
Copy link
Copy Markdown
Contributor

Summary

  • Initial session load fetches only the latest 50 messages instead of the entire history
  • Server adds a before_seq query parameter to fetch older messages
  • ChatList triggers loadOlderMessages via onEndReached (inverted FlatList) when the user scrolls to the oldest visible message
  • Loading spinner is shown while fetching the next batch
  • The latest=true parameter returns messages in descending order, then they're reversed to ASC for the client

Why

For long sessions (thousands of messages), the existing flow fetched the entire history on every page refresh, causing multi-second loading delays. With lazy loading, the first paint is fast and older messages load only when the user actually scrolls back.

Changes

  • Server (v3SessionRoutes.ts): Add before_seq and latest query params; reuse the latest=true reverse-order path when before_seq is provided
  • App (sync.ts): Track sessionOldestSeq and sessionHasMoreOlder per session; add loadOlderMessages(sessionId) that fetches a 50-message page older than the current oldest
  • App (ChatList.tsx): Wire onEndReached to sync.loadOlderMessages; render an ActivityIndicator while loading

Test plan

  • Open a session with > 50 messages — only the latest 50 render initially and the chat is responsive immediately
  • Scroll up to the oldest visible message — older messages load with a spinner, position is preserved
  • Scroll all the way back — loading stops cleanly when there are no more messages
  • Open a short session (< 50 messages) — loadOlderMessages no-ops without an extra request

🤖 Generated with Claude Code
via Happy

@chaehyun2 chaehyun2 force-pushed the perf/lazy-load-messages branch from c1d3479 to bb10bbb Compare May 2, 2026 08:12
Chaehyun Lee and others added 10 commits May 2, 2026 17:21
- Add "Restart Session" action to web UI popover (forks session with --resume, preserving full conversation history)
- Fix markdown table row misalignment by switching from column-first to row-first layout
- Add resumeSession RPC handler in CLI
- Add translations for all 10 languages

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Users can paste images from clipboard in the web chat input.
Images flow through the full pipeline: web UI → sync → CLI → Claude SDK
as base64 content blocks.

- Web UI: onPaste handler, preview thumbnails, remove button
- Sync: images field in message schema and sendMessage
- CLI: images in MessageQueue2, claudeRemote buildContent, launcher passthrough
- Fix base64 encoding stack overflow for large image buffers

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Images are now sent as separate encrypted messages with a TTL marker
(expiresIn) outside the encryption layer. Server stores expiresAt and
a cleanup job deletes expired messages every 5 minutes.

Server: expiresAt field on SessionMessage, TTL cleanup in main.ts,
  v3 POST accepts expiresIn per message
Web: sendMessage splits images into separate ephemeral message with
  groupId (inside encryption) and expiresIn: 300 (outside encryption)
CLI: routeIncomingMessage buffers image messages by groupId, attaches
  to matching text message before passing to UserMessageSchema

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Show image thumbnails (120x120) in user message bubbles.
Click thumbnail to expand full-size with dark overlay.
Images stored in ephemeral messageImageStore keyed by localId.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Reducer assigns new internal IDs, losing the original localId needed
to look up images in messageImageStore. Pass realID as localId in
user messages, and add id-based fallback lookup in MessageView.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
The schema only accepted 'text' type for user messages, causing TypeScript
errors when sending image-only messages.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Node 25's ESM loader no longer synthesizes a default export for signal-exit@4
(named-only ESM), so ink@6.6's transitive restore-cursor@4 crashes at startup
with "does not provide an export named 'default'". restore-cursor@5 uses the
named onExit import and is API-compatible, so we pin cli-cursor@4's
restore-cursor to ^5.1.0 via pnpm overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `latest=true` query parameter to GET /v3/sessions/:id/messages
that fetches messages in reverse order (most recent first). Web app
uses this on first load to avoid fetching entire chat history.
Subsequent real-time messages arrive via WebSocket.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- Initial load fetches latest 50 messages (was 100)
- Server adds `before_seq` query parameter to fetch older messages
- ChatList triggers `loadOlderMessages` via onEndReached (inverted FlatList)
- Loading spinner shown while fetching older batch

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@chaehyun2 chaehyun2 force-pushed the perf/lazy-load-messages branch from bb10bbb to fa99eaf Compare May 2, 2026 08:30
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