feat: add agents sidebar filters#25402
Conversation
Docs preview📖 View docs preview for |
0b72796 to
13d1734
Compare
f3650b6 to
a552474
Compare
|
/coder-agents-review |
a552474 to
f4317c8
Compare
There was a problem hiding this comment.
Good work wiring the existing backend filters into a coherent sidebar popover. The filter-aware cache invalidation strategy, selective prepend to matching caches, and stable query-key normalization are well-designed. The test coverage ratio (~1:1.3 production:test) is solid, and the hook and cache-helper tests are thorough.
Severity count: 2 P2, 13 P3, 2 Nit.
The P2 accessibility finding (DEREM-4) is the primary blocker: Chat Status and Archive Status use Checkbox components for mutually exclusive selection. Screen readers announce "checkbox" but the behavior is radio. The same popover already uses RadioGroup for the Group section, so the fix is a straightforward replacement.
The second P2 (DEREM-5) flags untested branches in the exported chatMatchesInfiniteChatsFilters function. The PR status bucket mapping and child-chat rejection branches have no test coverage.
The P3 findings cluster around three themes: (1) duplication between the API query layer and the hook layer (DEFAULT_FILTERS, PR status canonical order arrays, normalization functions), (2) dead or misleading code in the WebSocket event handler ("created" and "action_required" in the event kinds set), and (3) missing documentation on new exported cache helpers that have non-obvious selective behavior.
Melody's pairing walk confirmed that all filter chains agree across layers: URL params, hook state, query key normalization, API query string generation, and the SQL filter. The cache invalidation dispatch matrix is correct.
"A reader seeing
FilterDropdowninChatsPanel.tsxexpects a simple selection dropdown and finds a multi-section filter dialog." (Gon)
🤖 This review was automatically generated with Coder Agents.
f4317c8 to
02a286c
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Strong fix round: 16 of 17 R1 findings addressed in a single commit with consistent quality. The shared normalization utility, cache lookup priority fix, and doc comment additions are all structurally correct.
DEREM-4 (Checkbox vs RadioGroup) is closed. The panel unanimously (6/6) verified that the implementation genuinely supports multi-select. Checkbox is the correct ARIA primitive for inclusive filters with a min-1 constraint.
Severity count (new this round): 1 P2, 2 P3, 1 Nit.
The P2 (DEREM-18) is a consequence of the multi-select redesign: the UI now lets users check both Active and Archived, but the backend defaults to active-only when no archived: term is present. The user sees both checkboxes checked but gets only active chats. This needs a decision: prevent both-selected in the UI, or extend the backend to support an explicit "show all" state.
"The abstraction layers disagree:
useAgentSidebarFiltersmodels archive as a multi-select array;InfiniteChatsFiltersmodels it asboolean | undefined; the backend models it assql.NullBool. The 'both' state falls through each translation boundary until it lands on the backend default." (Pariston)
🤖 This review was automatically generated with Coder Agents.
| }; | ||
| }, []); | ||
|
|
||
| const archivedFilter = |
There was a problem hiding this comment.
P2 [DEREM-18] Selecting both Active and Archived in the popover silently shows only active chats.
When both archive checkboxes are checked, archiveStatuses.length is 2, so archivedFilter = undefined. normalizeInfiniteChatsFilters drops undefined, emitting no archived: query term. The backend parser defaults to Archived: sql.NullBool{Bool: false, Valid: true}, returning only active chats. The user explicitly asked to see both but gets only active, with no indication anything is missing.
The chat status filter avoids this because its backend default is {Valid: false} (unset = show all). The archive filter defaults to {Bool: false, Valid: true} (show active only). The asymmetric backend defaults break the frontend's symmetric both-selected-means-all assumption.
Options: (a) prevent both-selected in the UI until the backend supports it, (b) add archived:all or similar to the backend parser, (c) send no archived: term but change the backend default to {Valid: false} when the term is absent. (Pariston P2, Nami P2)
🤖
There was a problem hiding this comment.
Note
🤖 This PR/comment was written by Coder Agent on behalf of Danielle Maywood
Fixed in 7c911be8b. Archive status is now a singular radio selection, so the UI can only request Active or Archived and no longer represents an unsupported both-selected state.
| .filter((chat): chat is Chat => chat !== undefined && chat.pin_order === 0); | ||
| const hasAppliedResultFilters = | ||
| sidebarFilters.prStatuses.length > 0 || | ||
| sidebarFilters.chatStatuses.length !== 2; |
There was a problem hiding this comment.
P3 [DEREM-19] hasAppliedResultFilters hardcodes chat status count as the literal 2. AGENT_CHAT_STATUS_ORDER is already imported in this file (line 41) and used at line 358. Replace !== 2 with !== AGENT_CHAT_STATUS_ORDER.length so a third status value doesn't silently make this always-true. (Mafu-san P3, Razor P3)
🤖
There was a problem hiding this comment.
Note
🤖 This PR/comment was written by Coder Agent on behalf of Danielle Maywood
Fixed in 7c911be8b. hasAppliedResultFilters now compares against AGENT_CHAT_STATUS_ORDER.length instead of a literal count.
| ).toBe(false); | ||
| }); | ||
|
|
||
| it("matches draft PR status filters", () => { |
There was a problem hiding this comment.
P3 [DEREM-20] chatMatchesInfiniteChatsFilters PR status tests cover only the "draft" path. The fix for DEREM-5 added child, archived, and chat status coverage, but the open, merged, closed, and no-PR branches of getChatPRStatusFilter remain untested. If the condition at chats.ts:761 were deleted, every existing test would still pass. Add sibling test cases for each untested status bucket. (Bisky P3)
🤖
There was a problem hiding this comment.
Note
🤖 This PR/comment was written by Coder Agent on behalf of Danielle Maywood
Fixed in 7c911be8b. chatMatchesInfiniteChatsFilters now has PR-status coverage for draft, open, merged, closed, and no-PR cases.
| activeChatId, | ||
| isSettingsPanel, | ||
| isChatsActive, | ||
| location, |
There was a problem hiding this comment.
Nit [DEREM-21] const normalizedSearch = "" is dead code. The empty-state logic was rewritten to use hasAppliedResultFilters and isShowingOnlyArchived. This variable has no remaining consumer. (Razor)
🤖
There was a problem hiding this comment.
Note
🤖 This PR/comment was written by Coder Agent on behalf of Danielle Maywood
Fixed in 7c911be8b. Removed the dead local normalizedSearch binding and inlined the empty search value at its call sites.
|
/coder-agents-review |
There was a problem hiding this comment.
All prior P2 findings verified fixed. DEREM-18 (Active+Archived both-selected) resolved by switching archive status back to RadioGroup. DEREM-19 (magic number), DEREM-20 (incomplete PR test coverage), and DEREM-21 (dead code) all addressed cleanly in 7c911be.
The filter chain is consistent end-to-end: URL params, hook state, React Query key normalization, API query string, and backend SQL all agree. Cache coherence is structurally sound: the three-tier lookup prefers unfiltered data, selective invalidation scopes refetches to affected filter variants, and the prepend logic correctly refuses to guess server-owned filter membership.
4 P3 findings remain, all test coverage gaps or dead code, none blocking.
"I tried to build a case that the author's understanding of the problem is incomplete, that the solution addresses the wrong cause, or that the fix is at the wrong level in the causal chain. I could not." (Pariston)
🤖 This review was automatically generated with Coder Agents.
8f6392a to
7b203bc
Compare
4ca0eb3 to
ade1e3c
Compare
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 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
mainfrom #25391, compiling sidebar filters intopr_statusandhas_unread:true|falsequery terms while keepingchat_status=read|unreadas 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.
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.