feat: add agents sidebar filters#25402
Draft
DanielleMaywood wants to merge 1 commit into
Draft
Conversation
Docs preview📖 View docs preview for |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
🤖 This PR was written by Coder Agent on behalf of Danielle Maywood
Adds a staged Agents sidebar filter popover with server-backed PR status and unread filters, plus client-side grouping by date or chat status. The backend chat search query now supports
pr_statusandchat_status:unreadwith root-only matching so filtered pagination stays correct.The sidebar keeps pinned roots separate, preserves Active and Archived behavior, and disables pinned drag reorder when PR or unread filters are active.
Implementation plan
Agents Page Filters Implementation Plan
Goal: Add a Figma-style Agents sidebar filter popover that keeps Active and Archived, filters server-side by PR status and unread chat status, and groups loaded root agents by date or unread state.
Architecture: Keep
/agentsURL params as the UI source of truth, then compile applied PR and unread filters into the existing/api/experimental/chats?q=...search query so pagination is correct. Add root-only SQL filters toGetChats, keep child chats embedded under returned roots, and keep grouping client-side because grouping only changes presentation.Tech Stack: Go, PostgreSQL, sqlc,
make gen, React, TypeScript, React Query, Radix Popover, sharedButton,Checkbox,RadioGroup,SearchField, Storybook interaction tests, Vitest unit tests,pnpm,make lint.Confirmed requirements and decisions
Draft,Open,Merged, andClosed.Unreadonly, based onChat.has_unread.DatekeepsToday,Yesterday,This Week,Olderusing rootupdated_at.Chat statususesUnreadthenRead, based on roothas_unread.Pinnedsection separate and above grouped unpinned sections.File map
Backend files to modify
coderd/database/queries/chats.sqlhas_unreadandpull_request_statusesfilters toGetChats.GetChildChatsByParentIDsbeyond generated callsite updates if sqlc requires formatting.coderd/searchquery/search.goChatsquery parsing forchat_status:unreadandpr_status:<draft|open|merged|closed>.coderd/searchquery/search_test.goTestSearchChats.coderd/exp_chats.godatabase.GetChatsParams.@Param qtext.coderd/exp_chats_test.goTestListChatssubtests for PR status and unread filters.coderd/database/querier_test.goGetChatsroot-only filters.Generated backend files
Run
make genafter SQL changes. Expect updates in generated database wrappers such as:coderd/database/queries.sql.gocoderd/database/querier.gocoderd/database/dbmock/dbmock.gocoderd/database/dbmetrics/querymetrics.gocoderd/database/dbauthz/dbauthz.gomake genupdates them.No migration and no audit table update are needed because the required columns already exist.
Frontend files to create or modify
site/src/pages/AgentsPage/hooks/useAgentSidebarFilters.ts.site/src/pages/AgentsPage/hooks/useArchivedFilterParam.test.tswithsite/src/pages/AgentsPage/hooks/useAgentSidebarFilters.test.ts.site/src/pages/AgentsPage/AgentsPage.tsx.site/src/pages/AgentsPage/AgentsPageView.tsx.site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.tsx.site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.stories.tsx.site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx.site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx.site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx.site/src/api/queries/chats.ts.site/src/api/queries/chats.test.ts.site/src/pages/AgentsPage/components/ChatTopBar.stories.tsxfor archived URL preservation stories and update them if they assert onlyarchived.Backend contract
URL and API query shape
The page URL should use semantic params:
archived=archived, omitted for the default active view.group_by=chat_status, omitted for defaultdategrouping.pr_status=draft,open,merged,closed, CSV in canonical order, omitted when empty.chat_status=unread, omitted when not selected.The frontend should compile those params into the existing chats
qstring:The backend
qgrammar should accept:archived:true|falsediff_url:"https://..."chat_status:unreadpr_status:draft|open|merged|closedpr_status, with OR semantics inside PR status values.Different keys compose with AND semantics. For example,
pr_status:draft chat_status:unreadreturns unread root chats whose own PR bucket is draft.PR status buckets
Map
chat_diff_statusesrows into buckets like this:CASE WHEN cds.pull_request_state = 'open' AND cds.pull_request_draft THEN 'draft' WHEN cds.pull_request_state = 'open' THEN 'open' ELSE cds.pull_request_state ENDdraftandopenmust be disjoint.mergedandclosedusepull_request_statedirectly.Root-only SQL filters
Add these filters to
GetChats, beforechats_expanded.parent_chat_id IS NULLand before authorization injection.Unread filter:
PR status filter:
Expected generated fields on
database.GetChatsParams:Task 1: Add failing backend API tests for server-backed filters
Files:
Modify:
coderd/exp_chats_test.goStep 1: Add
TestListChats/PRStatusFiltersubtests.Create root chats with
dbgen.Chatanddb.UpsertChatDiffStatus, following the existingDiffURLFiltersetup. Create these root chats:root draft pr, withPullRequestState: sql.NullString{String: "open", Valid: true}andPullRequestDraft: true.root open pr, withPullRequestState: sql.NullString{String: "open", Valid: true}andPullRequestDraft: false.root merged pr, withPullRequestState: sql.NullString{String: "merged", Valid: true}.root closed pr, withPullRequestState: sql.NullString{String: "closed", Valid: true}.root without pr, with no diff status.root without pr, to prove root-only matching does not surface the parent.Add subtests:
MatchesDraftMatchesOpenMatchesMergedMatchesClosedMultipleStatusesAreUnionChildMatchDoesNotSurfaceParentArchivedTrueComposesInvalidPRStatusUse
client.ListChats(ctx, &codersdk.ListChatsOptions{Query: "pr_status:draft"})and assert returned root IDs exactly match the root-only expectation.TestListChats/UnreadFiltersubtests.Create:
root unread, with an assistant message inserted afterlast_read_message_id.root read, with an assistant message andUpdateChatLastReadMessageIDset to the last assistant message ID.root child unread only, where only a child has unread assistant messages.Use the message insertion pattern from
TestChatHasUnreadincoderd/database/querier_test.go.Add subtests:
MatchesRootUnreadReadRootExcludedChildUnreadDoesNotSurfaceParentArchivedTrueComposesInvalidChatStatusUse
client.ListChats(ctx, &codersdk.ListChatsOptions{Query: "chat_status:unread"}).Expected: tests fail because
pr_statusandchat_statusare unsupported search terms.Task 2: Implement backend parsing, SQL filters, and generated code
Files:
Modify:
coderd/database/queries/chats.sqlModify:
coderd/searchquery/search.goModify:
coderd/searchquery/search_test.goModify:
coderd/exp_chats.goGenerated: database and API docs files from
make genStep 1: Add SQL filter arguments and run generation.
Edit
GetChatsincoderd/database/queries/chats.sqlwith the root-only SQL clauses from the backend contract section.Run:
Expected: sqlc generates
HasUnreadandPullRequestStatusesfields ondatabase.GetChatsParams.TestSearchChats.Add table cases in
coderd/searchquery/search_test.go:ChatStatusUnread, expectsHasUnread: sql.NullBool{Bool: true, Valid: true}.ChatStatusUnreadCaseInsensitive, with querychat_status:UNREAD.ChatStatusInvalid, with querychat_status:read, expects an error containingchat_status.PRStatusDraft, expectsPullRequestStatuses: []string{"draft"}.PRStatusOpen, expects[]string{"open"}.PRStatusMerged, expects[]string{"merged"}.PRStatusClosed, expects[]string{"closed"}.PRStatusMultipleRepeated, withpr_status:draft pr_status:merged, expects[]string{"draft", "merged"}.PRStatusMultipleCSV, withpr_status:draft,closed, expects[]string{"draft", "closed"}.PRStatusValueCaseInsensitive, withpr_status:DRAFT, expects[]string{"draft"}.PRStatusInvalid, withpr_status:review, expects an error containingpr_status.PRStatusWithArchived, witharchived:true pr_status:open.Run:
go test ./coderd/searchquery -run TestSearchChatsExpected: tests fail until parser support is added.
searchquery.Chats.In
coderd/searchquery/search.go:Update the supported query parameter comment.
Parse
chat_statusas a single string. Accept onlyunread, case-insensitive. Setfilter.HasUnread = sql.NullBool{Bool: true, Valid: true}.Parse
pr_statuswithhttpapi.ParseCustomList, accepting CSV and repeated params. Trim and lowercase each value. Accept onlydraft,open,merged,closed.Keep value case preservation for
diff_url.Keep
parser.ErrorExcessParams(values)after all supported fields are parsed.Step 4: Wire the parsed fields into
listChats.In
coderd/exp_chats.go:@Param qtext to includechat_status:unreadand repeated or CSVpr_statusvalues.database.GetChatsParamsliteral:Expected: parser tests pass and API tests pass.
Task 3: Add direct SQL tests for root-only filters
Files:
Modify:
coderd/database/querier_test.goStep 1: Add
TestGetChatsFilterByPRStatus.Use
dbtestutil.NewDB,dbgen.Organization,dbgen.User,InsertChatModelConfig,InsertChat, andUpsertChatDiffStatuspatterns already in the file.Subtests or assertions must cover:
PullRequestStatuses: []string{"draft"}returns only root chats withpull_request_state='open'andpull_request_draft=true.PullRequestStatuses: []string{"open"}returns only root chats withpull_request_state='open'andpull_request_draft=false.PullRequestStatuses: []string{"draft", "closed"}returns the union.Run:
go test ./coderd/database -run TestGetChatsFilterByPRStatusExpected before SQL support is complete: the test fails or does not compile. Expected after Task 2: it passes.
TestGetChatsFilterByUnread.Use the message insertion helper shape from
TestChatHasUnread.Assertions must cover:
HasUnread: sql.NullBool{Bool: true, Valid: true}returns root chats with unread assistant messages.UpdateChatLastReadMessageID.Run:
Expected: both tests pass.
Task 4: Replace archived-only URL state with unified Agents sidebar filter state
Files:
Create:
site/src/pages/AgentsPage/hooks/useAgentSidebarFilters.tsRename:
site/src/pages/AgentsPage/hooks/useArchivedFilterParam.test.tstosite/src/pages/AgentsPage/hooks/useAgentSidebarFilters.test.tsRemove:
site/src/pages/AgentsPage/hooks/useArchivedFilterParam.tsStep 1: Write failing hook tests.
Create these tests in
useAgentSidebarFilters.test.ts:returns defaults for /agentsparses archived, group_by, pr_status, and chat_status from the URLdrops invalid pr_status values and canonicalizes orderomits default values when writing filtersclearAll resets the URL to the canonical defaultpreserves unrelated search params when writing filtersRun:
Expected: tests fail because the hook does not exist yet.
useAgentSidebarFilters.Export these types and constants:
Use canonical PR order:
Hook behavior:
archivedreadsarchived=archived, otherwiseactive.groupByreadsgroup_by=chat_status, otherwisedate.prStatusesreadspr_statusCSV, drops invalid values, dedupes, and returns canonical order.unreadOnlyreadschat_status=unread.setFilters(next)writes canonical params and removes default params.clearFilters()removesarchived,group_by,pr_status, andchat_status.Use
setSearchParams((prev) => { const next = new URLSearchParams(prev); ...; return next; }, { replace: true })so unrelated params survive.Step 3: Update old archived hook usage.
Remove
useArchivedFilterParam.tsafterAgentsPage.tsximportsuseAgentSidebarFilters. Rename the old archived-filter test file touseAgentSidebarFilters.test.tsrather than keeping two hook test files.Expected: tests pass.
Task 5: Make
infiniteChatscompile server-backed filters and make cache handling filter-awareFiles:
Modify:
site/src/api/queries/chats.tsModify:
site/src/api/queries/chats.test.tsModify:
site/src/pages/AgentsPage/AgentsPage.tsxStep 1: Add failing
infiniteChatstests.In
site/src/api/queries/chats.test.ts, update test helpers so the infinite query key comes frominfiniteChats(opts).queryKeyinstead of hardcoding[...chatsKey, undefined].Add tests:
builds q from archived, prStatuses, and unreadOnlyuses a stable key for equivalent pr_status orderingsdoes not include groupBy in the query keyfindChatInInfiniteChatsCaches scans every cached list queryunread filtered list queries are invalidated when unread membership can changepr filtered list queries are invalidated on diff_status_change for root chatsRun:
Expected: tests fail because the helpers and options are not implemented.
infiniteChatsoptions.In
site/src/api/queries/chats.ts, export the PR status type from this API query module so page hooks can reuse it without making the API layer import frompages/AgentsPage:Normalize options before building the query key:
archivedremains explicittrueorfalsefrom the page.prStatusesare deduped and sorted in canonical order.undefined.unreadOnly: falsebecomesundefined.Build
qtokens:Do not include
groupByor the popover option-search string inq.In
site/src/api/queries/chats.ts, add helpers with tests:getInfiniteChatsFiltersFromQueryKey(queryKey)returns normalized filter metadata for["chats", opts]keys.findChatInInfiniteChatsCaches(queryClient, chatId)scans all list query pages and returns the first matching root chat.invalidateChatListQueriesWhere(queryClient, predicate)invalidates only list queries whose extracted filters match a predicate.Keep
isChatListQuerybehavior for per-chat query exclusion.AgentsPage.tsx.Use
findChatInInfiniteChatsCacheswherereadInfiniteChatsCache(queryClient)?.find(...)is currently used.For watch events:
deleted: removal from every cached list remains safe.createdroot chat: prepend only to cached lists whose filters are definitely matched by the new root. Invalidate PR-filtered or unread-filtered lists when membership cannot be trusted.createdchild chat: keep existingaddChildToParentInCachebehavior.diff_status_changefor root chats: invalidate PR-filtered list queries, then merge into unfiltered lists and per-chat caches.status_changeor any event that updateshas_unreadfor root chats: invalidate unread-filtered list queries, then merge into unfiltered lists and per-chat caches.For the active-chat unread clearing effect:
Continue setting
has_unread: falsein unfiltered caches.Remove the root from unread-filtered list caches or invalidate unread-filtered list queries so
chat_status=unreaddoes not show a stale read chat after it is opened.Step 5: Run query/cache tests.
Expected: tests pass.
Task 6: Wire unified filters through the page and sidebar
Files:
Modify:
site/src/pages/AgentsPage/AgentsPage.tsxModify:
site/src/pages/AgentsPage/AgentsPageView.tsxModify:
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsxModify:
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsxStep 1: Add failing sidebar tests for grouping and applied filters.
In
AgentsSidebar.test.tsx, add or update tests:calls the filter change callback after Apply is clickeddoes not commit staged changes before Apply is clickedclear all resets staged controls to defaultsgroups unpinned chats by chat statuskeeps pinned chats out of the Unread and Read sectionskeeps the filter button visible when applied filters return no agentspreserves other applied filters when the empty-state archive toggle is usedRun:
Expected: tests fail because the richer filter state is not wired yet.
AgentsPage.tsx.Replace:
with the new hook. Pass server-backed filters into React Query:
Pass
sidebarFilters,setSidebarFilters, andclearSidebarFilterstoAgentsPageView.AgentsPageView.tsxprops.Replace archived-only props with:
Forward these to
AgentsSidebar.AgentsSidebar.tsxprops and grouping.Replace
archivedFilterandonArchivedFilterChangewith the unified filter props.For grouping:
Pinnedas the first section when pinned roots are visible.sidebarFilters.groupBy === "date", keep the existingTIME_GROUPS.map(...)behavior.sidebarFilters.groupBy === "chat_status", render unpinned groups in this order:Unread, count roots wherechat.has_unreadis true.Read, count roots wherechat.has_unreadis false.Remove or neutralize client-side search filtering in
collectVisibleChatIDs; the new popover search is only for filter options.Disable pinned drag reordering when
sidebarFilters.prStatuses.length > 0 || sidebarFilters.unreadOnlyso reordering a filtered subset cannot corrupt global pin order. Pin and unpin menu actions can remain enabled.Always render the filter trigger in the sidebar toolbar when the list has loaded, even if
visibleRootIDs.length === 0.Empty-state copy:
No agents match these filtersand aClear filtersbutton that callsonClearSidebarFilters.No archived agentswithBack to active.No agents yetwithView archived.The archive toggle in the empty state must preserve
groupBy,prStatuses, andunreadOnlywhen it changes onlyarchived.Expected: tests pass.
Task 7: Build the Figma-style filter popover
Files:
Modify:
site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.tsxModify:
site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.stories.tsxModify:
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsxStep 1: Add failing Storybook interaction coverage for the popover.
Update
FilterDropdown.stories.tsx:OpensFilterMenuwithOpensFilterPopover.AppliesStagedFilters.ClearAllResetsFilters.SearchFiltersOptions.EscapeClosesPopover.Use role queries:
button, nameFilter agents.dialog, nameFilter agents.radiogroup, nameGrouporGroup by.radiogroup, nameArchive status.textbox, nameSearch filters.Draft,Open,Merged,Closed,Unread.Clear all,Apply.Run:
Expected: stories fail until the popover is implemented.
In
FilterDropdown.tsx:FilterDropdownto minimize imports.DropdownMenuwithPopover,PopoverTrigger, andPopoverContent.Buttonfor the trigger and footer actions.RadioGroupandRadioGroupItemfor group selection and Active or Archived selection.Checkboxfor PR status and Unread.SearchFieldfor the local filter-option search.htmlFor, generated withuseIdor deterministic IDs scoped throughuseId.Applycommits staged filters and closes the popover.Clear allresets staged filters to defaults but does not commit untilApplyis clicked.Filter by. It should not write URL params and should not call the chats API.mobile-full-width-dropdown mobile-full-width-dropdown-top-below-header.useMemo,useCallback, ormemo()in this AgentsPage path.Suggested visual structure:
In
AgentsSidebar.stories.tsx:SidebarFilterMenuto assert popover dialog content instead of menu items.GroupByChatStatuswith one unread root and one read root.GroupByChatStatusKeepsPinnedSection.UnreadFilterEmptyState.PreservesArchivedFilterOnChatNavigationtoPreservesSidebarFiltersOnChatNavigationand assertarchived=archived,group_by=chat_status,pr_status=draft,open, andchat_status=unreadsurvive navigation.PreservesArchivedFilterOnSettingsNavigationthe same way.Run:
Expected: stories pass.
Task 8: Final integration and verification
Files:
All modified files from previous tasks.
Step 1: Run focused backend tests.
Expected: all pass.
Expected: all pass.
Expected: all pass.
Expected: all pass. If
make lintis too broad for the iteration, run the narrower failing package command first, then finish withmake lintbefore PR.Start Storybook if visual verification is needed:
Verify:
Draft,Open,Merged, orClosedchanges the networkqstring before pagination.Unreadchanges the networkqstring to includechat_status:unread.Chat statusswitches sections without a network refetch./agentsto its canonical default URL.Risks and review notes
Pinnedsection. Disable drag reorder when PR or unread filters are active to avoid reordering a filtered subset of pinned agents.