feat: add agents sidebar filters by DanielleMaywood · Pull Request #25402 · coder/coder
Adds a staged Agents sidebar filter popover with server-backed PR status and read or unread chat status filters, plus client-side grouping by date or chat status. This branch builds on the merged chat search backend in main from #25391, compiling sidebar filters into pr_status and has_unread:true|false query terms while keeping chat_status=read|unread as the Agents page URL state.
The sidebar keeps pinned roots separate, preserves Active and Archived behavior, preserves filter params when creating or opening agents, and disables pinned drag reorder when PR or chat status result filters are active.
Agents Page Filters Implementation Plan
For agentic workers: use
subagent-driven-developmentfor independent
tasks, orexecuting-plansfor inline execution. Track steps with checkbox
syntax.
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 /agents URL 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 to GetChats, 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, shared Button, Checkbox, RadioGroup, SearchField, Storybook interaction tests, Vitest unit tests, pnpm, make lint.
Confirmed requirements and decisions
- Keep the existing Active and Archived filter behavior.
- PR status filter values are
Draft,Open,Merged, andClosed. - Chat status means
Unreadonly, based onChat.has_unread. - PR status and unread filters must be server-backed so they apply before pagination.
- Root matching is root-only. A root agent matches PR or unread filters only when the root chat itself matches. Child sub-agents do not make the root visible.
- Grouping is client-side and applies to returned root agents:
DatekeepsToday,Yesterday,This Week,Olderusing rootupdated_at.Chat statususesUnreadthenRead, based on roothas_unread.
- Keep the existing
Pinnedsection separate and above grouped unpinned sections. - The Figma search field is a local search for filter options inside the popover. It is not an agent title search and does not need a backend query.
File map
Backend files to modify
coderd/database/queries/chats.sql- Add root-only
has_unreadandpull_request_statusesfilters toGetChats. - Do not change
GetChildChatsByParentIDsbeyond generated callsite updates if sqlc requires formatting.
- Add root-only
coderd/searchquery/search.go- Extend
Chatsquery parsing forchat_status:unreadandpr_status:<draft|open|merged|closed>.
- Extend
coderd/searchquery/search_test.go- Add parser coverage under
TestSearchChats.
- Add parser coverage under
coderd/exp_chats.go- Pass parsed filter params to
database.GetChatsParams. - Update the swagger
@Param qtext.
- Pass parsed filter params to
coderd/exp_chats_test.go- Add API-level
TestListChatssubtests for PR status and unread filters.
- Add API-level
coderd/database/querier_test.go- Add SQL behavior tests for
GetChatsroot-only filters.
- Add SQL behavior tests for
Generated backend files
Run make gen after 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.go- API docs generated from the swagger comment if
make genupdates them.
No migration and no audit table update are needed because the required columns already exist.
Frontend files to create or modify
- Create
site/src/pages/AgentsPage/hooks/useAgentSidebarFilters.ts. - Rename or replace
site/src/pages/AgentsPage/hooks/useArchivedFilterParam.test.tswithsite/src/pages/AgentsPage/hooks/useAgentSidebarFilters.test.ts. - Modify
site/src/pages/AgentsPage/AgentsPage.tsx. - Modify
site/src/pages/AgentsPage/AgentsPageView.tsx. - Modify
site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.tsx. - Modify
site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.stories.tsx. - Modify
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx. - Modify
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx. - Modify
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx. - Modify
site/src/api/queries/chats.ts. - Modify
site/src/api/queries/chats.test.ts. - Check
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 q string:
archived:false pr_status:draft,open chat_status:unread
The backend q grammar should accept:
archived:true|falsediff_url:"https://..."chat_status:unreadpr_status:draft|open|merged|closed- repeated or CSV
pr_status, with OR semantics inside PR status values.
Different keys compose with AND semantics. For example, pr_status:draft chat_status:unread returns unread root chats whose own PR bucket is draft.
PR status buckets
Map chat_diff_statuses rows 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
ENDdraft and open must be disjoint. merged and closed use pull_request_state directly.
Root-only SQL filters
Add these filters to GetChats, before chats_expanded.parent_chat_id IS NULL and before authorization injection.
Unread filter:
AND CASE WHEN sqlc.narg('has_unread')::boolean IS NOT NULL THEN EXISTS ( SELECT 1 FROM chat_messages cm WHERE cm.chat_id = chats_expanded.id AND cm.role = 'assistant' AND cm.deleted = false AND cm.id > COALESCE(chats_expanded.last_read_message_id, 0) ) = sqlc.narg('has_unread')::boolean ELSE true END
PR status filter:
AND CASE WHEN cardinality(@pull_request_statuses::text[]) > 0 THEN EXISTS ( SELECT 1 FROM chat_diff_statuses cds WHERE cds.chat_id = chats_expanded.id AND ( 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 END ) = ANY(@pull_request_statuses::text[]) ) ELSE true END
Expected generated fields on database.GetChatsParams:
HasUnread sql.NullBool PullRequestStatuses []string
Task 1: Add failing backend API tests for server-backed filters
Files:
-
Modify:
coderd/exp_chats_test.go -
Step 1: Add
TestListChats/PRStatusFiltersubtests.
Create root chats with dbgen.Chat and db.UpsertChatDiffStatus, following the existing DiffURLFilter setup. 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.- A child chat with a matching PR under
root without pr, to prove root-only matching does not surface the parent.
Add subtests:
MatchesDraftMatchesOpenMatchesMergedMatchesClosedMultipleStatusesAreUnionChildMatchDoesNotSurfaceParentArchivedTrueComposesInvalidPRStatus
Use client.ListChats(ctx, &codersdk.ListChatsOptions{Query: "pr_status:draft"}) and assert returned root IDs exactly match the root-only expectation.
- Step 2: Add
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 TestChatHasUnread in coderd/database/querier_test.go.
Add subtests:
MatchesRootUnreadReadRootExcludedChildUnreadDoesNotSurfaceParentArchivedTrueComposesInvalidChatStatus
Use client.ListChats(ctx, &codersdk.ListChatsOptions{Query: "chat_status:unread"}).
- Step 3: Run the new API tests and verify they fail for the expected reason.
go test ./coderd -run 'TestListChats/(PRStatusFilter|UnreadFilter)'
Expected: tests fail because pr_status and chat_status are unsupported search terms.
Task 2: Implement backend parsing, SQL filters, and generated code
Files:
-
Modify:
coderd/database/queries/chats.sql -
Modify:
coderd/searchquery/search.go -
Modify:
coderd/searchquery/search_test.go -
Modify:
coderd/exp_chats.go -
Generated: database and API docs files from
make gen -
Step 1: Add SQL filter arguments and run generation.
Edit GetChats in coderd/database/queries/chats.sql with the root-only SQL clauses from the backend contract section.
Run:
Expected: sqlc generates HasUnread and PullRequestStatuses fields on database.GetChatsParams.
- Step 2: Add parser tests under
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.
- Step 3: Implement parser support in
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:
- Update the swagger
@Param qtext to includechat_status:unreadand repeated or CSVpr_statusvalues. - Add these fields to the
database.GetChatsParamsliteral:
HasUnread: searchParams.HasUnread, PullRequestStatuses: searchParams.PullRequestStatuses,
- Step 5: Run backend parser and API tests.
go test ./coderd/searchquery -run TestSearchChats go test ./coderd -run 'TestListChats/(PRStatusFilter|UnreadFilter)'
Expected: parser tests pass and API tests pass.
Task 3: Add direct SQL tests for root-only filters
Files:
-
Modify:
coderd/database/querier_test.go -
Step 1: Add
TestGetChatsFilterByPRStatus.
Use dbtestutil.NewDB, dbgen.Organization, dbgen.User, InsertChatModelConfig, InsertChat, and UpsertChatDiffStatus patterns 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.- A matching child PR under a nonmatching root does not return the root.
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.
- Step 2: Add
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.- Read roots are excluded after
UpdateChatLastReadMessageID. - A child with unread messages under a read root does not return the root.
Run:
go test ./coderd/database -run 'TestGetChatsFilterByUnread|TestChatHasUnread'
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.ts -
Rename:
site/src/pages/AgentsPage/hooks/useArchivedFilterParam.test.tstosite/src/pages/AgentsPage/hooks/useAgentSidebarFilters.test.ts -
Remove:
site/src/pages/AgentsPage/hooks/useArchivedFilterParam.ts -
Step 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 filters
Run:
cd site && pnpm exec vitest run --project=unit src/pages/AgentsPage/hooks/useAgentSidebarFilters.test.ts
Expected: tests fail because the hook does not exist yet.
- Step 2: Implement
useAgentSidebarFilters.
Export these types and constants:
import type { ChatListPRStatusFilter } from "#/api/queries/chats"; export type ArchivedFilter = "active" | "archived"; export type AgentSidebarGroupBy = "date" | "chat_status"; export type AgentPRStatusFilter = ChatListPRStatusFilter; export type AgentSidebarFilters = Readonly<{ archived: ArchivedFilter; groupBy: AgentSidebarGroupBy; prStatuses: readonly AgentPRStatusFilter[]; unreadOnly: boolean; }>;
Use canonical PR order:
export const AGENT_PR_STATUS_ORDER = ["draft", "open", "merged", "closed"] as const;
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.ts after AgentsPage.tsx imports useAgentSidebarFilters. Rename the old archived-filter test file to useAgentSidebarFilters.test.ts rather than keeping two hook test files.
- Step 4: Run the hook tests.
cd site && pnpm exec vitest run --project=unit src/pages/AgentsPage/hooks/useAgentSidebarFilters.test.ts
Expected: tests pass.
Task 5: Make infiniteChats compile server-backed filters and make cache handling filter-aware
Files:
-
Modify:
site/src/api/queries/chats.ts -
Modify:
site/src/api/queries/chats.test.ts -
Modify:
site/src/pages/AgentsPage/AgentsPage.tsx -
Step 1: Add failing
infiniteChatstests.
In site/src/api/queries/chats.test.ts, update test helpers so the infinite query key comes from infiniteChats(opts).queryKey instead 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 chats
Run:
cd site && pnpm exec vitest run --project=unit src/api/queries/chats.test.ts
Expected: tests fail because the helpers and options are not implemented.
- Step 2: Extend
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 from pages/AgentsPage:
export type ChatListPRStatusFilter = "draft" | "open" | "merged" | "closed"; export type InfiniteChatsFilters = Readonly<{ archived?: boolean; prStatuses?: readonly ChatListPRStatusFilter[]; unreadOnly?: boolean; }>;
Normalize options before building the query key:
archivedremains explicittrueorfalsefrom the page.prStatusesare deduped and sorted in canonical order.- Empty PR status arrays become
undefined. unreadOnly: falsebecomesundefined.
Build q tokens:
archived:${normalized.archived} pr_status:${normalized.prStatuses.join(",")} chat_status:unread
Do not include groupBy or the popover option-search string in q.
- Step 3: Add cache helpers for multiple list variants.
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 isChatListQuery behavior for per-chat query exclusion.
- Step 4: Adjust websocket cache behavior in
AgentsPage.tsx.
Use findChatInInfiniteChatsCaches where readInfiniteChatsCache(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.- Child events must not invalidate root-only PR or unread filters solely because the child changed.
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.
cd site && pnpm exec vitest run --project=unit src/api/queries/chats.test.ts
Expected: tests pass.
Task 6: Wire unified filters through the page and sidebar
Files:
-
Modify:
site/src/pages/AgentsPage/AgentsPage.tsx -
Modify:
site/src/pages/AgentsPage/AgentsPageView.tsx -
Modify:
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx -
Modify:
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx -
Step 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 used
Run:
cd site && pnpm exec vitest run --project=unit src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx
Expected: tests fail because the richer filter state is not wired yet.
- Step 2: Update
AgentsPage.tsx.
Replace:
const [archivedFilter, setArchivedFilter] = useArchivedFilterParam();
with the new hook. Pass server-backed filters into React Query:
const [sidebarFilters, setSidebarFilters, clearSidebarFilters] = useAgentSidebarFilters(); const chatsQuery = useInfiniteQuery( infiniteChats({ archived: sidebarFilters.archived === "archived", prStatuses: sidebarFilters.prStatuses, unreadOnly: sidebarFilters.unreadOnly, }), );
Pass sidebarFilters, setSidebarFilters, and clearSidebarFilters to AgentsPageView.
- Step 3: Update
AgentsPageView.tsxprops.
Replace archived-only props with:
sidebarFilters: AgentSidebarFilters; onSidebarFiltersChange: (filters: AgentSidebarFilters) => void; onClearSidebarFilters: () => void;
Forward these to AgentsSidebar.
- Step 4: Update
AgentsSidebar.tsxprops and grouping.
Replace archivedFilter and onArchivedFilterChange with the unified filter props.
For grouping:
- Keep
Pinnedas the first section when pinned roots are visible. - For
sidebarFilters.groupBy === "date", keep the existingTIME_GROUPS.map(...)behavior. - For
sidebarFilters.groupBy === "chat_status", render unpinned groups in this order:Unread, count roots wherechat.has_unreadis true.Read, count roots wherechat.has_unreadis false.
- Preserve server order inside each group.
- Keep children nested under their parent.
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.unreadOnly so reordering a filtered subset cannot corrupt global pin order. Pin and unpin menu actions can remain enabled.
- Step 5: Update empty states.
Always render the filter trigger in the sidebar toolbar when the list has loaded, even if visibleRootIDs.length === 0.
Empty-state copy:
- If PR or unread filters are active and no roots are returned, show
No agents match these filtersand aClear filtersbutton that callsonClearSidebarFilters. - If no PR or unread filters are active and archived is active, keep
No archived agentswithBack to active. - If no PR or unread filters are active and archived is active false, keep
No agents yetwithView archived.
The archive toggle in the empty state must preserve groupBy, prStatuses, and unreadOnly when it changes only archived.
- Step 6: Run sidebar tests.
cd site && pnpm exec vitest run --project=unit src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx
Expected: tests pass.
Task 7: Build the Figma-style filter popover
Files:
-
Modify:
site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.tsx -
Modify:
site/src/pages/AgentsPage/components/Sidebar/FilterDropdown.stories.tsx -
Modify:
site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx -
Step 1: Add failing Storybook interaction coverage for the popover.
Update FilterDropdown.stories.tsx:
- Rename or replace
OpensFilterMenuwithOpensFilterPopover. - Add
AppliesStagedFilters. - Add
ClearAllResetsFilters. - Add
SearchFiltersOptions. - Add
EscapeClosesPopover.
Use role queries:
- Trigger:
button, nameFilter agents. - Popover:
dialog, nameFilter agents. - Group by:
radiogroup, nameGrouporGroup by. - Archive status:
radiogroup, nameArchive status. - Filter option search:
textbox, nameSearch filters. - Checkboxes:
Draft,Open,Merged,Closed,Unread. - Footer buttons:
Clear all,Apply.
Run:
cd site && pnpm test:storybook src/pages/AgentsPage/components/Sidebar/FilterDropdown.stories.tsx
Expected: stories fail until the popover is implemented.
- Step 2: Implement the popover using shared primitives.
In FilterDropdown.tsx:
- Keep the exported component name
FilterDropdownto minimize imports. - Replace
DropdownMenuwithPopover,PopoverTrigger, andPopoverContent. - Use
Buttonfor the trigger and footer actions. - Use
RadioGroupandRadioGroupItemfor group selection and Active or Archived selection. - Use
Checkboxfor PR status and Unread. - Use
SearchFieldfor the local filter-option search. - Use visible labels with
htmlFor, generated withuseIdor deterministic IDs scoped throughuseId. - Use staged local state. Opening the popover copies applied filters into staged filters.
Applycommits staged filters and closes the popover.Clear allresets staged filters to defaults but does not commit untilApplyis clicked. - The local option search filters only the visible option rows under
Filter by. It should not write URL params and should not call the chats API. - Use Tailwind tokens and existing mobile classes:
mobile-full-width-dropdown mobile-full-width-dropdown-top-below-header. - Do not add
useMemo,useCallback, ormemo()in this AgentsPage path.
Suggested visual structure:
Group
( ) Date
( ) Chat status
Filter by
Search filters...
Archive status
( ) Active
( ) Archived
PR status
[ ] Draft
[ ] Open
[ ] Merged
[ ] Closed
Chat status
[ ] Unread
Clear all Apply
- Step 3: Update Agents sidebar stories.
In AgentsSidebar.stories.tsx:
- Update
SidebarFilterMenuto assert popover dialog content instead of menu items. - Add
GroupByChatStatuswith one unread root and one read root. - Add
GroupByChatStatusKeepsPinnedSection. - Add
UnreadFilterEmptyState. - Update
PreservesArchivedFilterOnChatNavigationtoPreservesSidebarFiltersOnChatNavigationand assertarchived=archived,group_by=chat_status,pr_status=draft,open, andchat_status=unreadsurvive navigation. - Update
PreservesArchivedFilterOnSettingsNavigationthe same way.
Run:
cd site && pnpm test:storybook src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx
Expected: stories pass.
Task 8: Final integration and verification
Files:
-
All modified files from previous tasks.
-
Step 1: Run focused backend tests.
go test ./coderd/searchquery -run TestSearchChats go test ./coderd/database -run 'TestGetChatsFilterByPRStatus|TestGetChatsFilterByUnread|TestChatHasUnread' go test ./coderd -run 'TestListChats/(PRStatusFilter|UnreadFilter)'
Expected: all pass.
- Step 2: Run focused frontend tests.
cd site && pnpm exec vitest run --project=unit src/pages/AgentsPage/hooks/useAgentSidebarFilters.test.ts src/api/queries/chats.test.ts src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx
Expected: all pass.
- Step 3: Run Storybook interaction tests for changed stories.
cd site && pnpm test:storybook src/pages/AgentsPage/components/Sidebar/FilterDropdown.stories.tsx src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx
Expected: all pass.
- Step 4: Run formatting and lint checks.
make fmt cd site && pnpm format make lint
Expected: all pass. If make lint is too broad for the iteration, run the narrower failing package command first, then finish with make lint before PR.
- Step 5: Manual browser or Storybook verification.
Start Storybook if visual verification is needed:
cd site && pnpm storybook --no-open
Verify:
- The popover matches the Figma structure and spacing closely enough for implementation review.
- Active and Archived still fetch different server results.
- Applying
Draft,Open,Merged, orClosedchanges the networkqstring before pagination. - Applying
Unreadchanges the networkqstring to includechat_status:unread. - Group by
Chat statusswitches sections without a network refetch. - Clearing filters returns
/agentsto its canonical default URL. - Opening an unread root removes it from an unread-filtered list after the optimistic read update or refetch.
Risks and review notes
- Server-backed root-only filtering means a root with only a matching child sub-agent will not appear. This follows the user's clarification.
- Returned roots still include children filtered only by Active or Archived. Children are not individually filtered by PR status or unread state.
- Grouping is not server-backed. It groups the returned root page locally, which is correct because the filter set is already server-backed.
- Pinned roots stay in a separate
Pinnedsection. Disable drag reorder when PR or unread filters are active to avoid reordering a filtered subset of pinned agents. - WebSocket cache updates need special care because filtered list caches now represent different server result sets. Prefer invalidating membership-sensitive filtered caches over trying to insert a root at an exact sorted position.