feat(coderd): add pagination to chat list endpoint (#22887)

Adds offset and cursor-based pagination to the `GET
/api/experimental/chats` endpoint, following the exact same patterns
used by `GetUsers` and `GetTemplateVersionsByTemplateID`.

## Changes

### Database
- Add `after_id`, `offset_opt`, `limit_opt` params to
`GetChatsByOwnerID` SQL query
- Use composite `(updated_at, id) DESC` cursor for stable, deterministic
pagination
- Add migration with composite index on `chats (owner_id, updated_at
DESC, id DESC)`

### Backend
- Use `ParsePagination()` in `listChats` handler (matches `users.go`
pattern)
- Add `Pagination` field to `ListChatsOptions` SDK struct

### Frontend
- Add `infiniteChats()` query factory using `useInfiniteQuery` with
offset-based page params (same pattern as `infiniteWorkspaceBuilds`)
- Update `AgentsPage` to use `useInfiniteQuery`
- Add "Show more" button at the bottom of the agents sidebar (matches
`HistorySidebar` pattern)
- Keep existing `chats()` query for non-paginated uses (e.g., parent
chat lookup in `AgentDetail`)

### Tests
- Add `TestListChats/Pagination` covering `limit`, `after_id` cursor,
`offset`, and no-limit behavior
This commit is contained in:
Kyle Carberry
2026-03-10 13:55:33 +00:00
committed by GitHub
parent beed379b1d
commit e18ce505ec
14 changed files with 244 additions and 19 deletions
+12 -5
View File
@@ -7,7 +7,6 @@ import (
"io"
"mime"
"net/http"
"net/url"
"strings"
"time"
@@ -540,15 +539,23 @@ type chatStreamEnvelope struct {
// ListChatsOptions are optional parameters for ListChats.
type ListChatsOptions struct {
Archived *bool
Pagination
}
// ListChats returns all chats for the authenticated user.
func (c *Client) ListChats(ctx context.Context, opts *ListChatsOptions) ([]Chat, error) {
qp := url.Values{}
if opts != nil && opts.Archived != nil {
qp.Set("archived", fmt.Sprintf("%t", *opts.Archived))
var reqOpts []RequestOption
if opts != nil {
reqOpts = append(reqOpts, opts.Pagination.asRequestOption())
if opts.Archived != nil {
reqOpts = append(reqOpts, func(r *http.Request) {
q := r.URL.Query()
q.Set("archived", fmt.Sprintf("%t", *opts.Archived))
r.URL.RawQuery = q.Encode()
})
}
}
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats?%s", qp.Encode()), nil)
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats", nil, reqOpts...)
if err != nil {
return nil, err
}