Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions coderd/exp_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:<substring> (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:<draft\|open\|merged\|closed> as repeated or comma-separated values, diff_url:<url> (quote values containing colons), pr:<number> (exact PR number match), repo:<owner/repo> (case-insensitive substring match against git remote origin or URL), pr_title:<text> (case-insensitive PR title substring). Bare terms are not supported; use title:<value> for title filtering."
// @Param q query string false "Search query. Supports title:<substring> (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:<draft\|open\|merged\|closed> as repeated or comma-separated values, source:<created_by_me\|shared_with_me\|all>, diff_url:<url> (quote values containing colons), pr:<number> (exact PR number match), repo:<owner/repo> (case-insensitive substring match against git remote origin or URL), pr_title:<text> (case-insensitive PR title substring). Bare terms are not supported; use title:<value> 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]
Expand All @@ -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.",
Expand Down Expand Up @@ -390,8 +390,13 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
}
}

if sourceFilter == searchquery.ChatSourceFilterDefault {
searchParams.OwnedOnly = true
}

params := database.GetChatsParams{
OwnedOnly: true,
OwnedOnly: searchParams.OwnedOnly,
SharedOnly: searchParams.SharedOnly,
ViewerID: apiKey.UserID,
Archived: searchParams.Archived,
AfterID: paginationParams.AfterID,
Expand Down
50 changes: 46 additions & 4 deletions coderd/exp_chats_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ func TestSharedReaderStreamChat(t *testing.T) {
require.False(t, persisted.LastReadMessageID.Valid)
}

func TestListChatsExcludesSharedChats(t *testing.T) {
//nolint:tparallel,paralleltest // Subtests share a single coderdtest instance.
func TestListChatsSharedScope(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitLong)
Expand All @@ -389,6 +390,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{
Expand All @@ -397,9 +404,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.
Expand Down
34 changes: 30 additions & 4 deletions coderd/searchquery/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,14 +559,25 @@ 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)
func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) {
// - source: created_by_me, shared_with_me, or all
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
Expand All @@ -577,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()
Expand Down Expand Up @@ -606,6 +617,21 @@ 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 != "" {
sourceFilter = ChatSourceFilter(source)
switch sourceFilter {
case ChatSourceFilterCreatedByMe:
filter.OwnedOnly = true
case ChatSourceFilterSharedWithMe:
filter.SharedOnly = true
case ChatSourceFilterAll:
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 != "" {
Expand All @@ -621,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)
Expand Down
2 changes: 1 addition & 1 deletion coderd/searchquery/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions codersdk/chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()
})
}
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/api/chats.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions site/src/api/queries/chats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1542,20 +1542,21 @@ 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 });

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",
});
});

Expand Down
12 changes: 7 additions & 5 deletions site/src/api/queries/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
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",
Expand Down Expand Up @@ -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;
};

Expand Down
10 changes: 10 additions & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions site/src/pages/AgentsPage/AgentCreatePage.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions site/src/pages/AgentsPage/AgentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions site/src/pages/AgentsPage/AgentsPageView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const defaultSidebarFilters: AgentSidebarFilters = {
groupBy: "date",
prStatuses: [],
chatStatuses: ["unread", "read"],
sources: ["created_by_me"],
};

const defaultModelOptions: ModelSelectorOption[] = [
Expand Down
Loading
Loading