From 2febbde177a65d146ba1bf2aff40b8babf3cf5dc Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 4 Jun 2026 11:23:55 +0000 Subject: [PATCH 1/6] feat: show shared chats in agents sidebar --- coderd/exp_chats.go | 11 ++- coderd/exp_chats_acl_test.go | 49 +++++++++++- coderd/searchquery/search.go | 15 ++++ codersdk/chats.go | 20 ++++- site/src/api/queries/chats.test.ts | 5 +- site/src/api/queries/chats.ts | 12 +-- site/src/pages/AgentsPage/AgentsPage.tsx | 3 + .../ChatsSidebar/ChatsSidebar.test.tsx | 31 ++++++- .../ChatsSidebar/filters/FilterPopover.tsx | 80 ++++++++++++++++++- .../ChatsSidebar/tree/ChatTreeNode.tsx | 11 ++- .../utils/agentSidebarFilters.test.ts | 9 ++- .../AgentsPage/utils/agentSidebarFilters.ts | 33 ++++++++ 12 files changed, 256 insertions(+), 23 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index c8ed16c740fe7..0af260530736b 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -339,7 +339,7 @@ func (api *API) chatsByWorkspace(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Tags Chats // @Produce json -// @Param q query string false "Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status: as repeated or comma-separated values, diff_url: (quote values containing colons), pr: (exact PR number match), repo: (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering." +// @Param q query string false "Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status: as repeated or comma-separated values, source:, diff_url: (quote values containing colons), pr: (exact PR number match), repo: (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering." // @Param label query string false "Filter by label as key:value. Repeat for multiple (AND logic)." // @Success 200 {array} codersdk.Chat // @Router /api/experimental/chats [get] @@ -390,8 +390,15 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) { } } + ownedOnly := searchParams.OwnedOnly + sharedOnly := searchParams.SharedOnly + if !ownedOnly && !sharedOnly && !strings.Contains(queryStr, "source:") { + ownedOnly = true + } + params := database.GetChatsParams{ - OwnedOnly: true, + OwnedOnly: ownedOnly, + SharedOnly: sharedOnly, ViewerID: apiKey.UserID, Archived: searchParams.Archived, AfterID: paginationParams.AfterID, diff --git a/coderd/exp_chats_acl_test.go b/coderd/exp_chats_acl_test.go index a41b592e9f4b9..104435131188d 100644 --- a/coderd/exp_chats_acl_test.go +++ b/coderd/exp_chats_acl_test.go @@ -368,7 +368,7 @@ func TestSharedReaderStreamChat(t *testing.T) { require.False(t, persisted.LastReadMessageID.Valid) } -func TestListChatsExcludesSharedChats(t *testing.T) { +func TestListChatsSharedScope(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -389,6 +389,12 @@ func TestListChatsExcludesSharedChats(t *testing.T) { LastModelConfigID: modelConfig.ID, Title: "viewer owned", }) + unsharedChat := dbgen.Chat(t, db, database.Chat{ + OrganizationID: firstUser.OrganizationID, + OwnerID: firstUser.UserID, + LastModelConfigID: modelConfig.ID, + Title: "not shared with viewer", + }) err := client.UpdateChatACL(ctx, sharedChat.ID, codersdk.UpdateChatACL{ UserRoles: map[string]codersdk.ChatRole{ @@ -397,9 +403,44 @@ func TestListChatsExcludesSharedChats(t *testing.T) { }) require.NoError(t, err) - ownedOnly, err := viewerClientExp.ListChats(ctx, nil) - require.NoError(t, err) - require.Equal(t, map[uuid.UUID]struct{}{viewerChat.ID: {}}, chatIDSet(ownedOnly)) + for _, tc := range []struct { + name string + opts *codersdk.ListChatsOptions + expected map[uuid.UUID]struct{} + }{ + { + name: "default owned only", + expected: map[uuid.UUID]struct{}{viewerChat.ID: {}}, + }, + { + name: "created by me only", + opts: &codersdk.ListChatsOptions{ + Scope: codersdk.ChatListScopeCreatedByMe, + }, + expected: map[uuid.UUID]struct{}{viewerChat.ID: {}}, + }, + { + name: "shared with me only", + opts: &codersdk.ListChatsOptions{ + Scope: codersdk.ChatListScopeSharedWithMe, + }, + expected: map[uuid.UUID]struct{}{sharedChat.ID: {}}, + }, + { + name: "all", + opts: &codersdk.ListChatsOptions{ + Scope: codersdk.ChatListScopeAll, + }, + expected: map[uuid.UUID]struct{}{viewerChat.ID: {}, sharedChat.ID: {}}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + chats, err := viewerClientExp.ListChats(ctx, tc.opts) + require.NoError(t, err) + require.Equal(t, tc.expected, chatIDSet(chats)) + require.NotContains(t, chatIDSet(chats), unsharedChat.ID) + }) + } } //nolint:paralleltest // This test verifies a process-wide RBAC kill switch. diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 4b808f7df99b5..9c8dd7cb34723 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -559,6 +559,7 @@ func Tasks(ctx context.Context, db database.Store, query string, actorID uuid.UU // - pr: positive integer (exact PR number match) // - repo: string (case-insensitive substring match against git remote origin or URL) // - pr_title: string (case-insensitive PR title substring match) +// - source: created_by_me, shared_with_me, or all func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) { filter := database.GetChatsParams{ // Default to hiding archived chats. @@ -606,6 +607,20 @@ func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) { filter.TitleQuery = parser.String(values, "", "title") filter.PrTitleQuery = parser.String(values, "", "pr_title") filter.RepoQuery = parser.String(values, "", "repo") + if source := parser.String(values, "", "source"); source != "" { + switch codersdk.ChatListScope(source) { + case codersdk.ChatListScopeCreatedByMe: + filter.OwnedOnly = true + case codersdk.ChatListScopeSharedWithMe: + filter.SharedOnly = true + case codersdk.ChatListScopeAll: + default: + parser.Errors = append(parser.Errors, codersdk.ValidationError{ + Field: "source", + Detail: fmt.Sprintf("%q is not a valid value", source), + }) + } + } // pr: requires a positive integer. if prStr := parser.String(values, "", "pr"); prStr != "" { diff --git a/codersdk/chats.go b/codersdk/chats.go index 8770368a3db89..68a1f4bd9a650 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -2036,9 +2036,18 @@ type UpdateChatACL struct { GroupRoles map[string]ChatRole `json:"group_roles,omitempty"` } +type ChatListScope string + +const ( + ChatListScopeCreatedByMe ChatListScope = "created_by_me" + ChatListScopeSharedWithMe ChatListScope = "shared_with_me" + ChatListScopeAll ChatListScope = "all" +) + // ListChatsOptions are optional parameters for ListChats. type ListChatsOptions struct { Query string + Scope ChatListScope Labels map[string]string Pagination } @@ -2048,10 +2057,17 @@ func (c *ExperimentalClient) ListChats(ctx context.Context, opts *ListChatsOptio var reqOpts []RequestOption if opts != nil { reqOpts = append(reqOpts, opts.Pagination.asRequestOption()) - if opts.Query != "" { + query := opts.Query + if opts.Scope != "" { + if query != "" { + query += " " + } + query += "source:" + string(opts.Scope) + } + if query != "" { reqOpts = append(reqOpts, func(r *http.Request) { q := r.URL.Query() - q.Set("q", opts.Query) + q.Set("q", query) r.URL.RawQuery = q.Encode() }) } diff --git a/site/src/api/queries/chats.test.ts b/site/src/api/queries/chats.test.ts index 8b17d17e06393..bcf85a668386c 100644 --- a/site/src/api/queries/chats.test.ts +++ b/site/src/api/queries/chats.test.ts @@ -1542,12 +1542,13 @@ describe("infiniteChats", () => { }); }); - it("builds q from archived, prStatuses, and chatStatus", async () => { + it("builds q from archived, prStatuses, chatStatus, and source", async () => { vi.mocked(API.experimental.getChats).mockResolvedValue([]); const { queryFn } = infiniteChats({ archived: true, prStatuses: ["draft", "open", "merged"], chatStatus: "unread", + source: "all", }); await queryFn({ pageParam: 0 }); @@ -1555,7 +1556,7 @@ describe("infiniteChats", () => { expect(API.experimental.getChats).toHaveBeenCalledWith({ limit: PAGE_LIMIT, offset: 0, - q: "archived:true pr_status:draft,open,merged has_unread:true", + q: "archived:true pr_status:draft,open,merged has_unread:true source:all", }); }); diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index 0da5ec219761f..6adea3c1ad165 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -29,18 +29,17 @@ export const chatACLKey = (chatId: string) => ["chats", chatId, "acl"] as const; export type ChatListPRStatusFilter = "draft" | "open" | "merged" | "closed"; export type ChatListStatusFilter = "read" | "unread"; +export type ChatListSourceFilter = "created_by_me" | "shared_with_me" | "all"; type InfiniteChatsFilters = Readonly<{ archived?: boolean; prStatuses?: readonly ChatListPRStatusFilter[]; chatStatus?: ChatListStatusFilter; + source?: ChatListSourceFilter; }>; -export const infiniteChatsKey = (filters?: { - archived?: boolean; - prStatuses?: readonly ChatListPRStatusFilter[]; - chatStatus?: ChatListStatusFilter; -}) => [...chatsKey, filters] as const; +export const infiniteChatsKey = (filters?: InfiniteChatsFilters) => + [...chatsKey, filters] as const; export const CHAT_LIST_PR_STATUS_ORDER = [ "draft", @@ -561,6 +560,9 @@ const getInfiniteChatsQueryString = ( if (filters?.chatStatus) { qParts.push(`has_unread:${filters.chatStatus === "unread"}`); } + if (filters?.source) { + qParts.push(`source:${filters.source}`); + } return qParts.length > 0 ? qParts.join(" ") : undefined; }; diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index 6faaa1507e940..e18396b920621 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -149,11 +149,14 @@ const AgentsPage: FC = () => { sidebarFilters.chatStatuses.length === 1 ? sidebarFilters.chatStatuses[0] : undefined; + const sourceFilter = + sidebarFilters.sources.length === 2 ? "all" : sidebarFilters.sources[0]; const chatsQuery = useInfiniteQuery( infiniteChats({ archived: archivedFilter, prStatuses: sidebarFilters.prStatuses, chatStatus: chatStatusFilter, + source: sourceFilter, }), ); // Model queries are kept here for the sidebar, which displays diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx index 3f6aaa0c6ed66..02f5b1ea4151c 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx @@ -57,7 +57,9 @@ const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); const buildChat = (overrides: Partial = {}): Chat => ({ id: "chat-default", organization_id: "test-org-id", - owner_id: "owner-1", + owner_id: MockUserOwner.id, + owner_username: MockUserOwner.username, + owner_name: MockUserOwner.name, title: "Agent", status: "completed", last_model_config_id: "model-1", @@ -110,6 +112,7 @@ const defaultSidebarFilters: AgentSidebarFilters = { groupBy: "date", prStatuses: [], chatStatuses: ["unread", "read"], + sources: ["created_by_me"], }; const defaultProps: React.ComponentProps = { @@ -668,6 +671,32 @@ describe("ChatsSidebar subtitles", () => { ).not.toBeInTheDocument(); }); + it("shows who shared chats owned by another user", () => { + render( + + , + ); + + expect(screen.getByText("Shared by Sharing User")).toBeInTheDocument(); + expect( + screen.queryByText("This summary is hidden for shared chats"), + ).not.toBeInTheDocument(); + }); + it("falls back to the model name when no last turn summary exists", () => { render( diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx index 9c017a0e74a37..480185ecc6e4b 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx @@ -21,11 +21,13 @@ import { AGENT_ARCHIVE_STATUS_ORDER, AGENT_CHAT_STATUS_ORDER, AGENT_PR_STATUS_ORDER, + AGENT_SOURCE_ORDER, type AgentArchiveStatusFilter, type AgentChatStatusFilter, type AgentPRStatusFilter, type AgentSidebarFilters, type AgentSidebarGroupBy, + type AgentSourceFilter, DEFAULT_AGENT_SIDEBAR_FILTERS, } from "../../../utils/agentSidebarFilters"; @@ -54,6 +56,11 @@ const ARCHIVE_STATUS_LABELS: Record = { archived: "Archived", }; +const SOURCE_LABELS: Record = { + created_by_me: "Created by me", + shared_with_me: "Shared with me", +}; + const CHAT_STATUS_OPTIONS: readonly Readonly<{ value: AgentChatStatusFilter; label: string; @@ -70,6 +77,14 @@ const ARCHIVE_OPTIONS: readonly Readonly<{ label: ARCHIVE_STATUS_LABELS[status], })); +const SOURCE_OPTIONS: readonly Readonly<{ + value: AgentSourceFilter; + label: string; +}>[] = AGENT_SOURCE_ORDER.map((source) => ({ + value: source, + label: SOURCE_LABELS[source], +})); + const SectionHeading: FC> = ({ className, ...props }) => (

{ !haveSameSelections( filters.chatStatuses, DEFAULT_AGENT_SIDEBAR_FILTERS.chatStatuses, - ) + ) || + !haveSameSelections(filters.sources, DEFAULT_AGENT_SIDEBAR_FILTERS.sources) ); }; @@ -128,14 +144,19 @@ export const FilterPopover: FC = ({ onFiltersChange, }) => { const id = useId(); + const filtersWithDefaults: AgentSidebarFilters = { + ...DEFAULT_AGENT_SIDEBAR_FILTERS, + ...filters, + sources: filters.sources ?? DEFAULT_AGENT_SIDEBAR_FILTERS.sources, + }; const [open, setOpen] = useState(false); const [stagedFilters, setStagedFilters] = - useState(filters); + useState(filtersWithDefaults); const [optionSearch, setOptionSearch] = useState(""); const handleOpenChange = (nextOpen: boolean) => { if (nextOpen) { - setStagedFilters(filters); + setStagedFilters(filtersWithDefaults); setOptionSearch(""); } setOpen(nextOpen); @@ -154,12 +175,16 @@ export const FilterPopover: FC = ({ const visibleChatStatusOptions = CHAT_STATUS_OPTIONS.filter((option) => matchesOption("Chat status", option.label), ); + const visibleSourceOptions = SOURCE_OPTIONS.filter((option) => + matchesOption("Source", option.label), + ); const visibleArchiveOptions = ARCHIVE_OPTIONS.filter((option) => matchesOption("Archive status", option.label), ); const showFilterOptions = visiblePRStatuses.length > 0 || visibleChatStatusOptions.length > 0 || + visibleSourceOptions.length > 0 || visibleArchiveOptions.length > 0; const setGroupBy = (value: string) => { @@ -208,6 +233,22 @@ export const FilterPopover: FC = ({ setStagedFilters({ ...stagedFilters, archiveStatus: value }); }; + const setSource = (source: AgentSourceFilter, checked: boolean) => { + const selected = new Set(stagedFilters.sources); + if (checked) { + selected.add(source); + } else { + selected.delete(source); + } + if (selected.size === 0) { + return; + } + setStagedFilters({ + ...stagedFilters, + sources: AGENT_SOURCE_ORDER.filter((value) => selected.has(value)), + }); + }; + const applyFilters = () => { onFiltersChange(stagedFilters); setOpen(false); @@ -227,7 +268,7 @@ export const FilterPopover: FC = ({ aria-label="Filter agents" className={cn( "h-7 w-7 min-w-0 -mr-0.5 justify-end px-0 text-content-secondary hover:text-content-primary", - hasActiveFilters(filters) && "text-content-primary", + hasActiveFilters(filtersWithDefaults) && "text-content-primary", )} > @@ -352,6 +393,37 @@ export const FilterPopover: FC = ({ )} + {visibleSourceOptions.length > 0 && ( +
+ Source +
+ {visibleSourceOptions.map((option) => { + const optionId = `${id}-source-${option.value}`; + return ( + + + setSource(option.value, nextChecked === true) + } + className="m-0 my-[3px]" + /> + + + ); + })} +
+
+ )} + {visibleArchiveOptions.length > 0 && (
diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx index aa0a693b400c1..eef574cb98ce5 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx @@ -13,6 +13,7 @@ import { type FC, useEffect, useState } from "react"; import { NavLink, useLocation } from "react-router"; import type { Chat } from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; +import { useAuthenticated } from "#/hooks/useAuthenticated"; import { ContextMenu, ContextMenuContent, @@ -45,6 +46,7 @@ interface ChatTreeNodeProps { export const ChatTreeNode: FC = ({ chat, isChildNode }) => { const location = useLocation(); const locationSearch = normalizeLocationSearch(location.search); + const { user } = useAuthenticated(); const { chatTree, chatById, @@ -123,8 +125,15 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { return () => clearTimeout(timeoutId); }, [isStaleTurnSummary]); const displayedTurnSummary = isStaleTurnSummary ? undefined : lastTurnSummary; + const ownerName = chat.owner_name || chat.owner_username; + const sharedSubtitle = + chat.owner_id !== user.id ? `Shared by ${ownerName}` : undefined; const subtitle = - errorReason || streamingSubtitle || displayedTurnSummary || modelName; + errorReason || + streamingSubtitle || + sharedSubtitle || + displayedTurnSummary || + modelName; const { icon: StatusIcon, className: statusClassName, diff --git a/site/src/pages/AgentsPage/utils/agentSidebarFilters.test.ts b/site/src/pages/AgentsPage/utils/agentSidebarFilters.test.ts index fb53ecad301b6..847c7cef7ed0c 100644 --- a/site/src/pages/AgentsPage/utils/agentSidebarFilters.test.ts +++ b/site/src/pages/AgentsPage/utils/agentSidebarFilters.test.ts @@ -11,6 +11,7 @@ const defaultFilters: AgentSidebarFilters = { groupBy: "date", prStatuses: [], chatStatuses: ["unread", "read"], + sources: ["created_by_me"], }; const archivedFilters: AgentSidebarFilters = { @@ -18,6 +19,7 @@ const archivedFilters: AgentSidebarFilters = { groupBy: "chat_status", prStatuses: ["draft", "merged"], chatStatuses: ["unread"], + sources: ["created_by_me", "shared_with_me"], }; const renderFilters = (route = "/agents") => { @@ -44,14 +46,15 @@ describe(getAgentSidebarFilters.name, () => { expected: defaultFilters, }, { - name: "parses archived, group_by, pr_status, and chat_status", + name: "parses archived, group_by, pr_status, chat_status, and source", route: - "/agents?archived=archived&group_by=chat_status&pr_status=open,draft,closed&chat_status=unread", + "/agents?archived=archived&group_by=chat_status&pr_status=open,draft,closed&chat_status=unread&source=shared_with_me", expected: { archiveStatus: "archived", groupBy: "chat_status", prStatuses: ["draft", "open", "closed"], chatStatuses: ["unread"], + sources: ["shared_with_me"], }, }, { @@ -82,6 +85,7 @@ describe(getAgentSidebarFilters.name, () => { expect(search.get("group_by")).toEqual(null); expect(search.get("pr_status")).toEqual(null); expect(search.get("chat_status")).toEqual(null); + expect(search.get("source")).toEqual(null); }); it("writes archived status filter", async () => { @@ -118,5 +122,6 @@ describe(getAgentSidebarFilters.name, () => { expect(search.get("group_by")).toBe("chat_status"); expect(search.get("pr_status")).toBe("draft,merged"); expect(search.get("chat_status")).toBe("unread"); + expect(search.get("source")).toBe("created_by_me,shared_with_me"); }); }); diff --git a/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts b/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts index d898dcac1f187..b8e7a0c34806a 100644 --- a/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts +++ b/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts @@ -12,18 +12,21 @@ export const AGENT_CHAT_STATUS_ORDER = [ "read", ] as const satisfies readonly ChatListStatusFilter[]; export const AGENT_PR_STATUS_ORDER = CHAT_LIST_PR_STATUS_ORDER; +export const AGENT_SOURCE_ORDER = ["created_by_me", "shared_with_me"] as const; export type AgentArchiveStatusFilter = (typeof AGENT_ARCHIVE_STATUS_ORDER)[number]; export type AgentChatStatusFilter = ChatListStatusFilter; export type AgentPRStatusFilter = ChatListPRStatusFilter; export type AgentSidebarGroupBy = "date" | "chat_status"; +export type AgentSourceFilter = (typeof AGENT_SOURCE_ORDER)[number]; export type AgentSidebarFilters = Readonly<{ archiveStatus: AgentArchiveStatusFilter; groupBy: AgentSidebarGroupBy; prStatuses: readonly AgentPRStatusFilter[]; chatStatuses: readonly AgentChatStatusFilter[]; + sources: readonly AgentSourceFilter[]; }>; type AgentSidebarFiltersResult = readonly [ @@ -36,11 +39,13 @@ export const DEFAULT_AGENT_SIDEBAR_FILTERS: AgentSidebarFilters = { groupBy: "date", prStatuses: [], chatStatuses: AGENT_CHAT_STATUS_ORDER, + sources: ["created_by_me"], }; const agentChatStatusSet = new Set( AGENT_CHAT_STATUS_ORDER, ); +const agentSourceSet = new Set(AGENT_SOURCE_ORDER); const canonicalizeChatStatuses = ( values: Iterable, @@ -54,11 +59,24 @@ const canonicalizeChatStatuses = ( return AGENT_CHAT_STATUS_ORDER.filter((status) => selected.has(status)); }; +const canonicalizeSources = ( + values: Iterable, +): readonly AgentSourceFilter[] => { + const selected = new Set(); + for (const value of values) { + if (agentSourceSet.has(value as AgentSourceFilter)) { + selected.add(value as AgentSourceFilter); + } + } + return AGENT_SOURCE_ORDER.filter((source) => selected.has(source)); +}; + const clearSidebarFilterParams = (searchParams: URLSearchParams) => { searchParams.delete("archived"); searchParams.delete("group_by"); searchParams.delete("pr_status"); searchParams.delete("chat_status"); + searchParams.delete("source"); }; const writeSidebarFilters = ( @@ -84,6 +102,16 @@ const writeSidebarFilters = ( if (chatStatuses.length === 1) { searchParams.set("chat_status", chatStatuses[0]); } + + const sources = canonicalizeSources(filters.sources); + if ( + sources.length !== DEFAULT_AGENT_SIDEBAR_FILTERS.sources.length || + sources.some( + (source) => !DEFAULT_AGENT_SIDEBAR_FILTERS.sources.includes(source), + ) + ) { + searchParams.set("source", sources.join(",")); + } }; export const getAgentSidebarFilters = ( @@ -96,6 +124,9 @@ export const getAgentSidebarFilters = ( const chatStatuses = canonicalizeChatStatuses( (searchParams.get("chat_status") ?? "").split(",").filter(Boolean), ); + const sources = canonicalizeSources( + (searchParams.get("source") ?? "").split(",").filter(Boolean), + ); const filters: AgentSidebarFilters = { archiveStatus: @@ -109,6 +140,8 @@ export const getAgentSidebarFilters = ( chatStatuses.length > 0 ? chatStatuses : DEFAULT_AGENT_SIDEBAR_FILTERS.chatStatuses, + sources: + sources.length > 0 ? sources : DEFAULT_AGENT_SIDEBAR_FILTERS.sources, }; const setFilters = (next: AgentSidebarFilters) => { From 5d01e741663b37259b12d4447b8dbb5efd7d497d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 4 Jun 2026 11:34:15 +0000 Subject: [PATCH 2/6] refactor(site): simplify agent sidebar filter parsing --- .../AgentsPage/utils/agentSidebarFilters.ts | 60 ++++++------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts b/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts index b8e7a0c34806a..86bbd5de4c2eb 100644 --- a/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts +++ b/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts @@ -42,35 +42,6 @@ export const DEFAULT_AGENT_SIDEBAR_FILTERS: AgentSidebarFilters = { sources: ["created_by_me"], }; -const agentChatStatusSet = new Set( - AGENT_CHAT_STATUS_ORDER, -); -const agentSourceSet = new Set(AGENT_SOURCE_ORDER); - -const canonicalizeChatStatuses = ( - values: Iterable, -): readonly AgentChatStatusFilter[] => { - const selected = new Set(); - for (const value of values) { - if (agentChatStatusSet.has(value as AgentChatStatusFilter)) { - selected.add(value as AgentChatStatusFilter); - } - } - return AGENT_CHAT_STATUS_ORDER.filter((status) => selected.has(status)); -}; - -const canonicalizeSources = ( - values: Iterable, -): readonly AgentSourceFilter[] => { - const selected = new Set(); - for (const value of values) { - if (agentSourceSet.has(value as AgentSourceFilter)) { - selected.add(value as AgentSourceFilter); - } - } - return AGENT_SOURCE_ORDER.filter((source) => selected.has(source)); -}; - const clearSidebarFilterParams = (searchParams: URLSearchParams) => { searchParams.delete("archived"); searchParams.delete("group_by"); @@ -93,24 +64,21 @@ const writeSidebarFilters = ( searchParams.set("group_by", "chat_status"); } - const prStatuses = canonicalizeChatListPRStatuses(filters.prStatuses); - if (prStatuses.length > 0) { - searchParams.set("pr_status", prStatuses.join(",")); + if (filters.prStatuses.length > 0) { + searchParams.set("pr_status", filters.prStatuses.join(",")); } - const chatStatuses = canonicalizeChatStatuses(filters.chatStatuses); - if (chatStatuses.length === 1) { - searchParams.set("chat_status", chatStatuses[0]); + if (filters.chatStatuses.length === 1) { + searchParams.set("chat_status", filters.chatStatuses[0]); } - const sources = canonicalizeSources(filters.sources); if ( - sources.length !== DEFAULT_AGENT_SIDEBAR_FILTERS.sources.length || - sources.some( + filters.sources.length !== DEFAULT_AGENT_SIDEBAR_FILTERS.sources.length || + filters.sources.some( (source) => !DEFAULT_AGENT_SIDEBAR_FILTERS.sources.includes(source), ) ) { - searchParams.set("source", sources.join(",")); + searchParams.set("source", filters.sources.join(",")); } }; @@ -121,11 +89,17 @@ export const getAgentSidebarFilters = ( const prStatuses = canonicalizeChatListPRStatuses( (searchParams.get("pr_status") ?? "").split(",").filter(Boolean), ); - const chatStatuses = canonicalizeChatStatuses( - (searchParams.get("chat_status") ?? "").split(",").filter(Boolean), + const rawChatStatuses = (searchParams.get("chat_status") ?? "") + .split(",") + .filter(Boolean); + const chatStatuses = AGENT_CHAT_STATUS_ORDER.filter((status) => + rawChatStatuses.includes(status), ); - const sources = canonicalizeSources( - (searchParams.get("source") ?? "").split(",").filter(Boolean), + const rawSources = (searchParams.get("source") ?? "") + .split(",") + .filter(Boolean); + const sources = AGENT_SOURCE_ORDER.filter((source) => + rawSources.includes(source), ); const filters: AgentSidebarFilters = { From d9e25055bcd9b00bef20134b8a961380027bd2e8 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 4 Jun 2026 11:42:01 +0000 Subject: [PATCH 3/6] test(site): move shared chat sidebar coverage to storybook --- .../AgentsPage/AgentsPageView.stories.tsx | 1 + .../ChatsSidebar/ChatsSidebar.stories.tsx | 29 ++++++++++++++++++- .../ChatsSidebar/ChatsSidebar.test.tsx | 26 ----------------- .../filters/FilterPopover.stories.tsx | 3 ++ 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index fcb3de9286881..79313dccb6e40 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -56,6 +56,7 @@ const defaultSidebarFilters: AgentSidebarFilters = { groupBy: "date", prStatuses: [], chatStatuses: ["unread", "read"], + sources: ["created_by_me"], }; const defaultModelOptions: ModelSelectorOption[] = [ diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx index 240cf1bf5bf36..970390b88e282 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx @@ -61,7 +61,9 @@ const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); const buildChat = (overrides: Partial = {}): Chat => ({ id: "chat-default", organization_id: "test-org-id", - owner_id: "owner-1", + owner_id: MockUserOwner.id, + owner_username: MockUserOwner.username, + owner_name: MockUserOwner.name, title: "Agent", status: "completed", last_model_config_id: defaultModelConfigs[0].id, @@ -181,6 +183,31 @@ export const ChatWithTurnSummary: Story = { * holds the previous turn's text. The sidebar replaces it with a live * "{model} streaming…" label so the status does not look stuck. */ +export const SharedChat: Story = { + args: { + chats: [ + buildChat({ + id: "shared-chat", + title: "Shared chat", + owner_id: "sharing-user", + owner_name: "Sharing User", + owner_username: "sharing-user", + last_turn_summary: "This summary is hidden for shared chats", + }), + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await expect( + canvas.getByText("Shared by Sharing User"), + ).toBeInTheDocument(); + expect( + canvas.queryByText("This summary is hidden for shared chats"), + ).not.toBeInTheDocument(); + }, +}; + export const ChatStreamingOverridesTurnSummary: Story = { args: { chats: [ diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx index 02f5b1ea4151c..cd506efa83ac1 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx @@ -671,32 +671,6 @@ describe("ChatsSidebar subtitles", () => { ).not.toBeInTheDocument(); }); - it("shows who shared chats owned by another user", () => { - render( - - , - ); - - expect(screen.getByText("Shared by Sharing User")).toBeInTheDocument(); - expect( - screen.queryByText("This summary is hidden for shared chats"), - ).not.toBeInTheDocument(); - }); - it("falls back to the model name when no last turn summary exists", () => { render( diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.stories.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.stories.tsx index 43826d05a000b..406ddddbc99a7 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.stories.tsx @@ -62,6 +62,7 @@ export const AppliesStagedFilters: Story = { groupBy: "chat_status", prStatuses: ["draft"], chatStatuses: ["unread"], + sources: ["created_by_me"], }); }, }; @@ -73,6 +74,7 @@ export const KeepsOneChatStatusSelected: Story = { groupBy: "date", prStatuses: [], chatStatuses: ["unread"], + sources: ["created_by_me"], } satisfies AgentSidebarFilters, onFiltersChange: fn(), }, @@ -91,6 +93,7 @@ export const KeepsOneChatStatusSelected: Story = { groupBy: "date", prStatuses: [], chatStatuses: ["unread"], + sources: ["created_by_me"], }); }, }; From e1236e915db2e9917bcb0926033e45c7aafe0697 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 4 Jun 2026 11:57:33 +0000 Subject: [PATCH 4/6] fix(site): clarify shared chat sidebar filters --- coderd/exp_chats.go | 12 +++---- coderd/searchquery/search.go | 27 +++++++++++----- coderd/searchquery/search_test.go | 2 +- .../ChatsSidebar/ChatsSidebar.stories.tsx | 1 + .../ChatsSidebar/filters/FilterPopover.tsx | 31 +++++++------------ .../ChatsSidebar/tree/ChatTreeNode.tsx | 13 ++++++-- 6 files changed, 48 insertions(+), 38 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 0af260530736b..f25f0b22003ee 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -354,7 +354,7 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) { } queryStr := r.URL.Query().Get("q") - searchParams, errs := searchquery.Chats(queryStr) + searchParams, sourceFilter, errs := searchquery.Chats(queryStr) if len(errs) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid chat search query.", @@ -390,15 +390,13 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) { } } - ownedOnly := searchParams.OwnedOnly - sharedOnly := searchParams.SharedOnly - if !ownedOnly && !sharedOnly && !strings.Contains(queryStr, "source:") { - ownedOnly = true + if sourceFilter == searchquery.ChatSourceFilterDefault { + searchParams.OwnedOnly = true } params := database.GetChatsParams{ - OwnedOnly: ownedOnly, - SharedOnly: sharedOnly, + OwnedOnly: searchParams.OwnedOnly, + SharedOnly: searchParams.SharedOnly, ViewerID: apiKey.UserID, Archived: searchParams.Archived, AfterID: paginationParams.AfterID, diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 9c8dd7cb34723..283d24be96f3d 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -560,14 +560,24 @@ func Tasks(ctx context.Context, db database.Store, query string, actorID uuid.UU // - repo: string (case-insensitive substring match against git remote origin or URL) // - pr_title: string (case-insensitive PR title substring match) // - source: created_by_me, shared_with_me, or all -func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) { +type ChatSourceFilter string + +const ( + ChatSourceFilterDefault ChatSourceFilter = "" + ChatSourceFilterCreatedByMe ChatSourceFilter = "created_by_me" + ChatSourceFilterSharedWithMe ChatSourceFilter = "shared_with_me" + ChatSourceFilterAll ChatSourceFilter = "all" +) + +func Chats(query string) (database.GetChatsParams, ChatSourceFilter, []codersdk.ValidationError) { filter := database.GetChatsParams{ // Default to hiding archived chats. Archived: sql.NullBool{Bool: false, Valid: true}, } + var sourceFilter ChatSourceFilter if query == "" { - return filter, nil + return filter, sourceFilter, nil } // Lowercase the keys so they match regardless of how the caller @@ -578,7 +588,7 @@ func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) { return xerrors.Errorf("unsupported search term: %q", term) }) if len(errors) > 0 { - return filter, errors + return filter, sourceFilter, errors } parser := httpapi.NewQueryParamParser() @@ -608,12 +618,13 @@ func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) { filter.PrTitleQuery = parser.String(values, "", "pr_title") filter.RepoQuery = parser.String(values, "", "repo") if source := parser.String(values, "", "source"); source != "" { - switch codersdk.ChatListScope(source) { - case codersdk.ChatListScopeCreatedByMe: + sourceFilter = ChatSourceFilter(source) + switch sourceFilter { + case ChatSourceFilterCreatedByMe: filter.OwnedOnly = true - case codersdk.ChatListScopeSharedWithMe: + case ChatSourceFilterSharedWithMe: filter.SharedOnly = true - case codersdk.ChatListScopeAll: + case ChatSourceFilterAll: default: parser.Errors = append(parser.Errors, codersdk.ValidationError{ Field: "source", @@ -636,7 +647,7 @@ func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) { } parser.ErrorExcessParams(values) - return filter, parser.Errors + return filter, sourceFilter, parser.Errors } // validateDiffURL checks that the value is a syntactically valid HTTP(S) diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 5081eb8cd2d57..5aee8d58ec83b 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -1519,7 +1519,7 @@ func TestSearchChats(t *testing.T) { t.Run(c.Name, func(t *testing.T) { t.Parallel() - values, errs := searchquery.Chats(c.Query) + values, _, errs := searchquery.Chats(c.Query) if c.ExpectedErrorContains != "" { require.True(t, len(errs) > 0, "expect some errors") var s strings.Builder diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx index 970390b88e282..941a61a46c88b 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx @@ -199,6 +199,7 @@ export const SharedChat: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); + await expect(canvas.getByText("Shared")).toBeInTheDocument(); await expect( canvas.getByText("Shared by Sharing User"), ).toBeInTheDocument(); diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx index 480185ecc6e4b..753b25e6ec70f 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/filters/FilterPopover.tsx @@ -144,19 +144,14 @@ export const FilterPopover: FC = ({ onFiltersChange, }) => { const id = useId(); - const filtersWithDefaults: AgentSidebarFilters = { - ...DEFAULT_AGENT_SIDEBAR_FILTERS, - ...filters, - sources: filters.sources ?? DEFAULT_AGENT_SIDEBAR_FILTERS.sources, - }; const [open, setOpen] = useState(false); const [stagedFilters, setStagedFilters] = - useState(filtersWithDefaults); + useState(filters); const [optionSearch, setOptionSearch] = useState(""); const handleOpenChange = (nextOpen: boolean) => { if (nextOpen) { - setStagedFilters(filtersWithDefaults); + setStagedFilters(filters); setOptionSearch(""); } setOpen(nextOpen); @@ -234,19 +229,17 @@ export const FilterPopover: FC = ({ }; const setSource = (source: AgentSourceFilter, checked: boolean) => { - const selected = new Set(stagedFilters.sources); - if (checked) { - selected.add(source); - } else { - selected.delete(source); - } - if (selected.size === 0) { + const nextSources = checked + ? AGENT_SOURCE_ORDER.filter( + (value) => value === source || stagedFilters.sources.includes(value), + ) + : stagedFilters.sources.filter((value) => value !== source); + + if (nextSources.length === 0) { return; } - setStagedFilters({ - ...stagedFilters, - sources: AGENT_SOURCE_ORDER.filter((value) => selected.has(value)), - }); + + setStagedFilters({ ...stagedFilters, sources: nextSources }); }; const applyFilters = () => { @@ -268,7 +261,7 @@ export const FilterPopover: FC = ({ aria-label="Filter agents" className={cn( "h-7 w-7 min-w-0 -mr-0.5 justify-end px-0 text-content-secondary hover:text-content-primary", - hasActiveFilters(filtersWithDefaults) && "text-content-primary", + hasActiveFilters(filters) && "text-content-primary", )} > diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx index eef574cb98ce5..c89020021cabf 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx @@ -8,6 +8,7 @@ import { PinOffIcon, SquarePenIcon, Trash2Icon, + UsersIcon, } from "lucide-react"; import { type FC, useEffect, useState } from "react"; import { NavLink, useLocation } from "react-router"; @@ -125,9 +126,9 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { return () => clearTimeout(timeoutId); }, [isStaleTurnSummary]); const displayedTurnSummary = isStaleTurnSummary ? undefined : lastTurnSummary; - const ownerName = chat.owner_name || chat.owner_username; - const sharedSubtitle = - chat.owner_id !== user.id ? `Shared by ${ownerName}` : undefined; + const isSharedChat = chat.owner_id !== user.id; + const ownerName = chat.owner_name || chat.owner_username || "another user"; + const sharedSubtitle = isSharedChat ? `Shared by ${ownerName}` : undefined; const subtitle = errorReason || streamingSubtitle || @@ -289,6 +290,12 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { > {chat.title} + {isSharedChat && ( + + + Shared + + )} {chat.has_unread && !isActiveChat && ( (unread) )} From 69201d9c6abc56294a1e487e9a6c9c54f71f489d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 4 Jun 2026 13:33:14 +0000 Subject: [PATCH 5/6] fix(site): preserve agent filters after chat creation --- site/src/api/typesGenerated.ts | 10 ++++++++++ site/src/pages/AgentsPage/AgentCreatePage.tsx | 8 ++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2f3f46e6794d1..331c7686ca3b2 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2146,6 +2146,15 @@ export const ChatInputPartTypes: ChatInputPartType[] = [ "text", ]; +// From codersdk/chats.go +export type ChatListScope = "all" | "created_by_me" | "shared_with_me"; + +export const ChatListScopes: ChatListScope[] = [ + "all", + "created_by_me", + "shared_with_me", +]; + // From codersdk/chats.go /** * ChatMessage represents a single message in a chat. @@ -5109,6 +5118,7 @@ export interface LinkConfig { */ export interface ListChatsOptions extends Pagination { readonly Query: string; + readonly Scope: ChatListScope; readonly Labels: Record; } diff --git a/site/src/pages/AgentsPage/AgentCreatePage.tsx b/site/src/pages/AgentsPage/AgentCreatePage.tsx index 18415a820b4bf..4e9076a7ca7ca 100644 --- a/site/src/pages/AgentsPage/AgentCreatePage.tsx +++ b/site/src/pages/AgentsPage/AgentCreatePage.tsx @@ -1,6 +1,6 @@ import { type FC, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { useNavigate } from "react-router"; +import { useLocation, useNavigate } from "react-router"; import { toast } from "sonner"; import { getErrorMessage } from "#/api/errors"; import { @@ -36,6 +36,7 @@ const lastModelConfigIDStorageKey = "agents.last-model-config-id"; const AgentCreatePage: FC = () => { const queryClient = useQueryClient(); + const location = useLocation(); const navigate = useNavigate(); const { permissions } = useAuthenticated(); @@ -129,7 +130,10 @@ const AgentCreatePage: FC = () => { if (model) { localStorage.setItem(lastModelConfigIDStorageKey, model); } - navigate(buildAgentChatPath({ chatId: createdChat.id })); + navigate({ + pathname: buildAgentChatPath({ chatId: createdChat.id }), + search: location.search, + }); }; const rootPersonalModelOverride = personalModelOverridesQuery.data?.enabled From 34663b2bdd4d94a495f16e226fec1adc30bcafa2 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 4 Jun 2026 15:12:12 +0000 Subject: [PATCH 6/6] fix: address shared chat sidebar ci failures --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/exp_chats_acl_test.go | 1 + docs/reference/api/chats.md | 8 ++++---- site/src/api/queries/chats.ts | 2 +- .../components/ChatsSidebar/tree/ChatTreeNode.tsx | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 56aaa2a95db40..f88c081c4e466 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -78,7 +78,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "Search query. Supports title:\u003csubstring\u003e (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e as repeated or comma-separated values, diff_url:\u003curl\u003e (quote values containing colons), pr:\u003cnumber\u003e (exact PR number match), repo:\u003cowner/repo\u003e (case-insensitive substring match against git remote origin or URL), pr_title:\u003ctext\u003e (case-insensitive PR title substring). Bare terms are not supported; use title:\u003cvalue\u003e for title filtering.", + "description": "Search query. Supports title:\u003csubstring\u003e (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e as repeated or comma-separated values, source:\u003ccreated_by_me\\|shared_with_me\\|all\u003e, diff_url:\u003curl\u003e (quote values containing colons), pr:\u003cnumber\u003e (exact PR number match), repo:\u003cowner/repo\u003e (case-insensitive substring match against git remote origin or URL), pr_title:\u003ctext\u003e (case-insensitive PR title substring). Bare terms are not supported; use title:\u003cvalue\u003e for title filtering.", "name": "q", "in": "query" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 606f7ae4ea4c6..abe98d56e0b60 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -59,7 +59,7 @@ "parameters": [ { "type": "string", - "description": "Search query. Supports title:\u003csubstring\u003e (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e as repeated or comma-separated values, diff_url:\u003curl\u003e (quote values containing colons), pr:\u003cnumber\u003e (exact PR number match), repo:\u003cowner/repo\u003e (case-insensitive substring match against git remote origin or URL), pr_title:\u003ctext\u003e (case-insensitive PR title substring). Bare terms are not supported; use title:\u003cvalue\u003e for title filtering.", + "description": "Search query. Supports title:\u003csubstring\u003e (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e as repeated or comma-separated values, source:\u003ccreated_by_me\\|shared_with_me\\|all\u003e, diff_url:\u003curl\u003e (quote values containing colons), pr:\u003cnumber\u003e (exact PR number match), repo:\u003cowner/repo\u003e (case-insensitive substring match against git remote origin or URL), pr_title:\u003ctext\u003e (case-insensitive PR title substring). Bare terms are not supported; use title:\u003cvalue\u003e for title filtering.", "name": "q", "in": "query" }, diff --git a/coderd/exp_chats_acl_test.go b/coderd/exp_chats_acl_test.go index 104435131188d..13eef8cf7c969 100644 --- a/coderd/exp_chats_acl_test.go +++ b/coderd/exp_chats_acl_test.go @@ -368,6 +368,7 @@ func TestSharedReaderStreamChat(t *testing.T) { require.False(t, persisted.LastReadMessageID.Valid) } +//nolint:tparallel,paralleltest // Subtests share a single coderdtest instance. func TestListChatsSharedScope(t *testing.T) { t.Parallel() diff --git a/docs/reference/api/chats.md b/docs/reference/api/chats.md index e11363788fc1d..2d46d97b919a6 100644 --- a/docs/reference/api/chats.md +++ b/docs/reference/api/chats.md @@ -17,10 +17,10 @@ Experimental: this endpoint is subject to change. ### Parameters -| Name | In | Type | Required | Description | -|---------|-------|--------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `q` | query | string | false | Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status: as repeated or comma-separated values, diff_url: (quote values containing colons), pr: (exact PR number match), repo: (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering. | -| `label` | query | string | false | Filter by label as key:value. Repeat for multiple (AND logic). | +| Name | In | Type | Required | Description | +|---------|-------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `q` | query | string | false | Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status: as repeated or comma-separated values, source:, diff_url: (quote values containing colons), pr: (exact PR number match), repo: (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering. | +| `label` | query | string | false | Filter by label as key:value. Repeat for multiple (AND logic). | ### Example responses diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index 6adea3c1ad165..8d4cdba22a3c6 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -29,7 +29,7 @@ export const chatACLKey = (chatId: string) => ["chats", chatId, "acl"] as const; export type ChatListPRStatusFilter = "draft" | "open" | "merged" | "closed"; export type ChatListStatusFilter = "read" | "unread"; -export type ChatListSourceFilter = "created_by_me" | "shared_with_me" | "all"; +type ChatListSourceFilter = "created_by_me" | "shared_with_me" | "all"; type InfiniteChatsFilters = Readonly<{ archived?: boolean; diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx index c89020021cabf..5aa1a5edfdc53 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx @@ -14,7 +14,6 @@ import { type FC, useEffect, useState } from "react"; import { NavLink, useLocation } from "react-router"; import type { Chat } from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; -import { useAuthenticated } from "#/hooks/useAuthenticated"; import { ContextMenu, ContextMenuContent, @@ -30,6 +29,7 @@ import { DropdownMenuTrigger, } from "#/components/DropdownMenu/DropdownMenu"; import { Spinner } from "#/components/Spinner/Spinner"; +import { useAuthenticated } from "#/hooks/useAuthenticated"; import { cn } from "#/utils/cn"; import { shortRelativeTime } from "#/utils/time"; import { asNonEmptyString } from "../../ChatConversation/blockUtils";