Paginate the Duo Agentic Chat history list

What does this MR do and why?

Depends on @gitlab/duo-ui 16.2.0, released from duo-ui!691 (merged) and bumped in this MR.

The Duo Agentic Chat history tab (/agentic-chat/history, ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat_history_view.vue) loaded the full list of chat sessions in one request, using getUserWorkflows with first: 99999 and fetch policy network-only. This caused two problems:

  • GraphQL clamps first to 100, because duoWorkflowWorkflows does not override max_page_size. Users with more than 100 threads got a silently truncated list.
  • The query also selected stalled, which runs a batched checkpoint-presence check against the partitioned checkpoint tables on every list request. In production p95 db_duration for the query is 2-4 seconds.

This MR paginates the history list instead:

  • The query asks for first: 20 and returns pageInfo { endCursor hasNextPage }.
  • DuoChatThreads renders a load-more sentinel inside its own scroll area through the has-next-page and loading-more props and load-more event added in duo-ui 16.2.0. When it fires, the view calls fetchMore with the current end cursor (infinite scroll).
  • New pages are merged into the existing list and deduplicated by thread id.
  • Loading a page shows a small inline spinner. The skeleton loader only shows on first load, not on load-more.
  • stalled is removed from the list query. Stalled threads no longer get dimmed styling in the list, but opening one still shows the inactive state, because the chat state manager checks stalled through its own single-workflow query.
  • archived stays in the query. On the backend this is a plain created_at comparison, with no database cost.
  • The query file now has the # @feature_category: duo_agent_platform header, matching its siblings.
  • Fetch policy stays network-only, but now sets nextFetchPolicy: cache-first. In Apollo Client 3.5, fetchMore ends with a reobserve. Under plain network-only that reobserve would refetch page one and drop the merged pages.
  • Deleting a thread now removes its edge from the Apollo cache with cache.updateQuery, instead of filtering a local array. fetchMore merges pages from the cache, so a local-only filter would let the deleted thread come back on the next page.
  • The sentinel has to live inside DuoChatThreads: the component owns the scroll container, so a trigger placed below it sits under the pinned banner, is always visible, and would load every page immediately.
  • ee/spec/frontend/fixtures/ai_duo_panel_integration.rb now generates fixtures with first: 20, so the MSW fixtures match the real query.
  • @gitlab/duo-ui moves from ^16.1.0 to ^16.2.0 in a separate commit. The only change between those versions that this code base uses is the new DuoChatThreads API.

Offset cursors

The resolver uses offset pagination, not keyset. Two known effects:

  • A thread created between two page loads shifts offsets by one, so a page boundary can show the same node twice. The merge step deduplicates by id, so this is harmless.
  • A thread deleted between two page loads can cause one thread to be skipped, until the tab is reopened.

Accepted for now.

Out of scope / follow-ups

  • Server-side search using the existing search: argument. The duo-ui MR above adds a search event with the debounced query; a follow-up can pass it to the query and add a duo-ui prop to skip the client-side filter. Until then the search box only covers loaded pages, and the load-more sentinel hides while a search is active.
  • Proposal item 4 from the issue (cache-and-network plus updatedAfter delta refresh). With paginated pages, cache-and-network would snap a long cached list back to page one after each refetch, and delete handling needs more cache work. Better reviewed separately.
  • Proposal item 1 (hydrate the active thread with a single-workflow query). The file named in the issue, duo_agentic_chat_state_manager_light.vue, is only imported by its own spec. The production state manager, duo_agentic_chat_state_manager.vue, already hydrates from the single-workflow query and does not run getUserWorkflows.
  • Database index work continues in !253146 (merged).

References

Screenshots or screen recordings

How to set up and validate locally

  1. Have a user with more than 20 Duo Agentic Chat threads. Use the rake task gitlab:duo_workflow:populate to seed workflows, or create threads by chatting.
  2. Open the Duo panel and go to the History tab.
  3. Confirm 20 threads render. Scroll to the bottom and confirm a small spinner appears and the next 20 threads load, with no skeleton flash.
  4. In the browser network tab, confirm one getUserWorkflows request per page, each with first: 20, and, for later pages, an after cursor.
  5. Delete a thread from the list and confirm it disappears without a full list refetch.
  6. Open a thread that has no checkpoints (stalled) and confirm it still shows the inactive state.

MR acceptance checklist

Check this MR against the Acceptance Checklist before merging.

Edited by Rafa Frederico

Merge request reports

Loading
Loading