Skip to content

feat: paginate chat messages endpoint with cursor-based infinite scroll#23083

Merged
kylecarbs merged 6 commits intomainfrom
kylecarbs/paginate-chat-messages
Mar 16, 2026
Merged

feat: paginate chat messages endpoint with cursor-based infinite scroll#23083
kylecarbs merged 6 commits intomainfrom
kylecarbs/paginate-chat-messages

Conversation

@kylecarbs
Copy link
Copy Markdown
Member

Adds cursor-based pagination to the chat messages endpoint.

Backend

  • New GetChatMessagesByChatIDPaginated SQL query: returns messages in id DESC order with a before_id keyset cursor and configurable limit
  • Handler parses ?before_id=N&limit=N query params, uses the LIMIT N+1 trick to set has_more without a separate COUNT query
  • Queued messages only returned on the first page (no cursor) since they're always the most recent
  • SDK client updated with ChatMessagesPaginationOptions
  • Fully backward compatible: omitting params returns the 50 newest messages

Frontend

  • Switches getChatMessages from useQuery to useInfiniteQuery with cursor chaining via getNextPageParam
  • Pages flattened and sorted by id ascending for chronological display
  • MessagesPaginationSentinel component uses IntersectionObserver (200px rootMargin prefetch) inside the existing flex-col-reverse scroll container
  • flex-col-reverse handles scroll anchoring natively when older messages are prepended — no manual scrollTop adjustment needed (same pattern as coder/blink)

Why cursor-based instead of offset/limit

Offset-based pagination breaks when new messages arrive while paginating backward (offsets shift, causing duplicates or missed messages). The before_id cursor is stable regardless of inserts — each page is deterministic.

@kylecarbs kylecarbs force-pushed the kylecarbs/paginate-chat-messages branch from 09bfacd to 0b9192f Compare March 16, 2026 13:38
…roll

Adds cursor-based pagination to the chat messages endpoint using
before_id/limit query params. The API returns messages in DESC order
(newest first) with a has_more boolean for page detection via the
LIMIT N+1 pattern.

Frontend switches from useQuery to useInfiniteQuery with an
IntersectionObserver sentinel at the visual top of the flex-col-reverse
scroll container. When the user scrolls up near the oldest loaded
message, the next page of older messages is fetched and prepended.
The flex-col-reverse CSS handles scroll anchoring natively — no
manual scrollTop adjustment needed.

Key changes:
- New GetChatMessagesByChatIDPaginated SQL query with keyset cursor
- Handler parses before_id/limit params, fetches limit+1 for has_more
- Queued messages only returned on first page (no cursor)
- SDK client accepts ChatMessagesPaginationOptions
- Frontend uses chatMessagesInfinite with useInfiniteQuery
- MessagesPaginationSentinel component with IntersectionObserver
- Pages flattened and sorted by ID for chronological display
- Fix dbauthz panic: implement auth check for
  GetChatMessagesByChatIDPaginated matching the pattern from
  GetChatMessagesByChatID (fetch parent chat to verify access).

- Fix ChatContext setQueryData crash: updateChatQueuedMessages now
  writes to InfiniteData<ChatMessagesResponse> shape instead of
  plain ChatMessagesResponse, updating pages[0].queued_messages.

- Fix replaceMessages dropping WebSocket messages: switch from
  store.replaceMessages() to per-message upsertDurableMessage()
  so REST page loads merge into the store without discarding
  messages delivered by the WebSocket that aren't in REST yet.

- Fix unstable onFetchMoreMessages: pass fetchNextPage directly
  instead of an inline arrow to avoid rebuilding the
  IntersectionObserver every render.

- Remove dead chatMessages export superseded by
  chatMessagesInfinite.
- Add #nosec G115 comment for int->int32 conversion (limit is
  validated to 1-200, so limit+1=201 fits in int32)
- Add dbauthz test for GetChatMessagesByChatIDPaginated matching
  the existing GetChatMessagesByChatID test pattern
- Fix ChatContext.test.tsx: seed query cache with InfiniteData
  shape and read back from pages[0] to match the new
  useInfiniteQuery cache structure
The chatMessages query is now useInfiniteQuery, so the cache
must be seeded with { pages: [...], pageParams: [...] } instead
of a plain ChatMessagesResponse.
@kylecarbs kylecarbs force-pushed the kylecarbs/paginate-chat-messages branch from ee7a5c3 to f899eb2 Compare March 16, 2026 15:12
@kylecarbs kylecarbs requested a review from mafredri March 16, 2026 15:13
ORDER BY
id DESC
LIMIT
COALESCE(NULLIF(@limit_val::int, 0), 50);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider updating or replacing the previous GetChatMessagesByChatID, or do we have a use-case for keeping it around?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old GetChatMessagesByChatID is still used in three internal server-side callers (chatd.go x2, subagent.go) that use the AfterID parameter to fetch messages after a known ID for streaming replay and subagent context. Different use case from the paginated endpoint — kept it.

WHERE
chat_id = @chat_id::uuid
AND CASE
WHEN @before_id::bigint > 0 THEN id < @before_id::bigint
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering this is non-standard reverse pagination, I wonder if we should make this obvious in the name?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Open to suggestions — maybe GetChatMessagesByChatIDDescPaginated or GetChatMessagesByChatIDReversePaginated? The current name at least differentiates it from the non-paginated version.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Desc in the name makes sense, either as proposed or suffix 👍🏻

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to GetChatMessagesByChatIDDescPaginated.

@kylecarbs kylecarbs changed the title feat(chat): paginate /messages endpoint with cursor-based infinite scroll feat(coderd): paginate chat messages endpoint with cursor-based infinite scroll Mar 16, 2026
- Use httpapi.NewQueryParamParser() for before_id/limit parsing
  instead of manual strconv, matching the ParsePagination pattern
- Rename chatMessagesInfinite -> chatMessagesForInfiniteScroll
- Use flatMap instead of for-loop for page flattening
- Use .at(-1) instead of bracket indexing for last page
@coder-tasks
Copy link
Copy Markdown
Contributor

coder-tasks bot commented Mar 16, 2026

Documentation Check

Updates Needed

  • docs/ai-coder/agents/chats-api.md — The "Get chat messages" section documents GET /api/experimental/chats/{chat}/messages but does not mention the new cursor-based pagination query parameters (before_id, limit) or the new has_more field in the response. Add parameter descriptions and note the default/max limit (50 default, 200 max).

    ⚠️ Still unaddressed after multiple commits — no documentation changes found in this PR


Automated review via Coder Tasks

@kylecarbs kylecarbs requested a review from mafredri March 16, 2026 15:50
Copy link
Copy Markdown
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BE looks good. FE looks good to me, but I'm not @DanielleMaywood.

@DanielleMaywood
Copy link
Copy Markdown
Contributor

With regards to frontend changes, I have nothing blocking. I think our infinite query cache logic is getting quite gnarly though so I might take a look in the future into how we can simplify this.

@kylecarbs kylecarbs changed the title feat(coderd): paginate chat messages endpoint with cursor-based infinite scroll feat: paginate chat messages endpoint with cursor-based infinite scroll Mar 16, 2026
Makes the reverse sort order obvious in the query name per review
feedback.
@kylecarbs kylecarbs force-pushed the kylecarbs/paginate-chat-messages branch from 63d53cc to 5ecb36f Compare March 16, 2026 16:11
@kylecarbs kylecarbs enabled auto-merge (squash) March 16, 2026 16:14
@kylecarbs kylecarbs merged commit 741af05 into main Mar 16, 2026
47 of 49 checks passed
@kylecarbs kylecarbs deleted the kylecarbs/paginate-chat-messages branch March 16, 2026 16:41
@github-actions github-actions bot locked and limited conversation to collaborators Mar 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants