Skip to content

feat: add agents sidebar filters#25402

Draft
DanielleMaywood wants to merge 1 commit into
mainfrom
feat/agents-sidebar-filters
Draft

feat: add agents sidebar filters#25402
DanielleMaywood wants to merge 1 commit into
mainfrom
feat/agents-sidebar-filters

Conversation

@DanielleMaywood
Copy link
Copy Markdown
Contributor

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_status and chat_status:unread with 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

For agentic workers: use subagent-driven-development for independent
tasks, or executing-plans for 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, and Closed.
  • Chat status means Unread only, based on Chat.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:
    • Date keeps Today, Yesterday, This Week, Older using root updated_at.
    • Chat status uses Unread then Read, based on root has_unread.
  • Keep the existing Pinned section 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_unread and pull_request_statuses filters to GetChats.
    • Do not change GetChildChatsByParentIDs beyond generated callsite updates if sqlc requires formatting.
  • coderd/searchquery/search.go
    • Extend Chats query parsing for chat_status:unread and pr_status:<draft|open|merged|closed>.
  • coderd/searchquery/search_test.go
    • Add parser coverage under TestSearchChats.
  • coderd/exp_chats.go
    • Pass parsed filter params to database.GetChatsParams.
    • Update the swagger @Param q text.
  • coderd/exp_chats_test.go
    • Add API-level TestListChats subtests for PR status and unread filters.
  • coderd/database/querier_test.go
    • Add SQL behavior tests for GetChats root-only filters.

Generated backend files

Run make gen after SQL changes. Expect updates in generated database wrappers such as:

  • coderd/database/queries.sql.go
  • coderd/database/querier.go
  • coderd/database/dbmock/dbmock.go
  • coderd/database/dbmetrics/querymetrics.go
  • coderd/database/dbauthz/dbauthz.go
  • API docs generated from the swagger comment if make gen updates 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.ts with site/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.tsx for archived URL preservation stories and update them if they assert only archived.

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 default date grouping.
  • 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|false
  • diff_url:"https://..."
  • chat_status:unread
  • pr_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
END

draft 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/PRStatusFilter subtests.

Create root chats with dbgen.Chat and db.UpsertChatDiffStatus, following the existing DiffURLFilter setup. Create these root chats:

  • root draft pr, with PullRequestState: sql.NullString{String: "open", Valid: true} and PullRequestDraft: true.
  • root open pr, with PullRequestState: sql.NullString{String: "open", Valid: true} and PullRequestDraft: false.
  • root merged pr, with PullRequestState: sql.NullString{String: "merged", Valid: true}.
  • root closed pr, with PullRequestState: 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:

  • MatchesDraft
  • MatchesOpen
  • MatchesMerged
  • MatchesClosed
  • MultipleStatusesAreUnion
  • ChildMatchDoesNotSurfaceParent
  • ArchivedTrueComposes
  • InvalidPRStatus

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/UnreadFilter subtests.

Create:

  • root unread, with an assistant message inserted after last_read_message_id.
  • root read, with an assistant message and UpdateChatLastReadMessageID set 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:

  • MatchesRootUnread
  • ReadRootExcluded
  • ChildUnreadDoesNotSurfaceParent
  • ArchivedTrueComposes
  • InvalidChatStatus

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:

make gen

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, expects HasUnread: sql.NullBool{Bool: true, Valid: true}.
  • ChatStatusUnreadCaseInsensitive, with query chat_status:UNREAD.
  • ChatStatusInvalid, with query chat_status:read, expects an error containing chat_status.
  • PRStatusDraft, expects PullRequestStatuses: []string{"draft"}.
  • PRStatusOpen, expects []string{"open"}.
  • PRStatusMerged, expects []string{"merged"}.
  • PRStatusClosed, expects []string{"closed"}.
  • PRStatusMultipleRepeated, with pr_status:draft pr_status:merged, expects []string{"draft", "merged"}.
  • PRStatusMultipleCSV, with pr_status:draft,closed, expects []string{"draft", "closed"}.
  • PRStatusValueCaseInsensitive, with pr_status:DRAFT, expects []string{"draft"}.
  • PRStatusInvalid, with pr_status:review, expects an error containing pr_status.
  • PRStatusWithArchived, with archived:true pr_status:open.

Run:

go test ./coderd/searchquery -run TestSearchChats

Expected: 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_status as a single string. Accept only unread, case-insensitive. Set filter.HasUnread = sql.NullBool{Bool: true, Valid: true}.

  • Parse pr_status with httpapi.ParseCustomList, accepting CSV and repeated params. Trim and lowercase each value. Accept only draft, 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 q text to include chat_status:unread and repeated or CSV pr_status values.
  • Add these fields to the database.GetChatsParams literal:
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 with pull_request_state='open' and pull_request_draft=true.
  • PullRequestStatuses: []string{"open"} returns only root chats with pull_request_state='open' and pull_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 TestGetChatsFilterByPRStatus

Expected 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.ts to site/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 /agents
  • parses archived, group_by, pr_status, and chat_status from the URL
  • drops invalid pr_status values and canonicalizes order
  • omits default values when writing filters
  • clearAll resets the URL to the canonical default
  • preserves 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:

  • archived reads archived=archived, otherwise active.

  • groupBy reads group_by=chat_status, otherwise date.

  • prStatuses reads pr_status CSV, drops invalid values, dedupes, and returns canonical order.

  • unreadOnly reads chat_status=unread.

  • setFilters(next) writes canonical params and removes default params.

  • clearFilters() removes archived, group_by, pr_status, and chat_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 infiniteChats tests.

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 unreadOnly
  • uses a stable key for equivalent pr_status orderings
  • does not include groupBy in the query key
  • findChatInInfiniteChatsCaches scans every cached list query
  • unread filtered list queries are invalidated when unread membership can change
  • pr 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 infiniteChats options.

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:

  • archived remains explicit true or false from the page.
  • prStatuses are deduped and sorted in canonical order.
  • Empty PR status arrays become undefined.
  • unreadOnly: false becomes undefined.

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.
  • created root 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.
  • created child chat: keep existing addChildToParentInCache behavior.
  • diff_status_change for root chats: invalidate PR-filtered list queries, then merge into unfiltered lists and per-chat caches.
  • status_change or any event that updates has_unread for 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: false in unfiltered caches.

  • Remove the root from unread-filtered list caches or invalidate unread-filtered list queries so chat_status=unread does 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 clicked
  • does not commit staged changes before Apply is clicked
  • clear all resets staged controls to defaults
  • groups unpinned chats by chat status
  • keeps pinned chats out of the Unread and Read sections
  • keeps the filter button visible when applied filters return no agents
  • preserves 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.tsx props.

Replace archived-only props with:

sidebarFilters: AgentSidebarFilters;
onSidebarFiltersChange: (filters: AgentSidebarFilters) => void;
onClearSidebarFilters: () => void;

Forward these to AgentsSidebar.

  • Step 4: Update AgentsSidebar.tsx props and grouping.

Replace archivedFilter and onArchivedFilterChange with the unified filter props.

For grouping:

  • Keep Pinned as the first section when pinned roots are visible.
  • For sidebarFilters.groupBy === "date", keep the existing TIME_GROUPS.map(...) behavior.
  • For sidebarFilters.groupBy === "chat_status", render unpinned groups in this order:
    • Unread, count roots where chat.has_unread is true.
    • Read, count roots where chat.has_unread is 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 filters and a Clear filters button that calls onClearSidebarFilters.
  • If no PR or unread filters are active and archived is active, keep No archived agents with Back to active.
  • If no PR or unread filters are active and archived is active false, keep No agents yet with View 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 OpensFilterMenu with OpensFilterPopover.
  • Add AppliesStagedFilters.
  • Add ClearAllResetsFilters.
  • Add SearchFiltersOptions.
  • Add EscapeClosesPopover.

Use role queries:

  • Trigger: button, name Filter agents.
  • Popover: dialog, name Filter agents.
  • Group by: radiogroup, name Group or Group by.
  • Archive status: radiogroup, name Archive status.
  • Filter option search: textbox, name Search 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 FilterDropdown to minimize imports.
  • Replace DropdownMenu with Popover, PopoverTrigger, and PopoverContent.
  • Use Button for the trigger and footer actions.
  • Use RadioGroup and RadioGroupItem for group selection and Active or Archived selection.
  • Use Checkbox for PR status and Unread.
  • Use SearchField for the local filter-option search.
  • Use visible labels with htmlFor, generated with useId or deterministic IDs scoped through useId.
  • Use staged local state. Opening the popover copies applied filters into staged filters. Apply commits staged filters and closes the popover. Clear all resets staged filters to defaults but does not commit until Apply is 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, or memo() 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 SidebarFilterMenu to assert popover dialog content instead of menu items.
  • Add GroupByChatStatus with one unread root and one read root.
  • Add GroupByChatStatusKeepsPinnedSection.
  • Add UnreadFilterEmptyState.
  • Update PreservesArchivedFilterOnChatNavigation to PreservesSidebarFiltersOnChatNavigation and assert archived=archived, group_by=chat_status, pr_status=draft,open, and chat_status=unread survive navigation.
  • Update PreservesArchivedFilterOnSettingsNavigation the 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, or Closed changes the network q string before pagination.
  • Applying Unread changes the network q string to include chat_status:unread.
  • Group by Chat status switches sections without a network refetch.
  • Clearing filters returns /agents to 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 Pinned section. 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.

@github-actions
Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/reference/api/chats.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant