mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add pr, repo, pr_title chat search filters (#25569)
Relates to CODAGT-432 Adds three new search filters to the chat list endpoint (`GET /api/experimental/chats/`): - `pr:<number>` - exact PR number match - `repo:<owner/repo>` - substring match against git remote origin or URL - `pr_title:<text>` - case-insensitive PR title substring match Includes SQL filter clauses (EXISTS against `chat_diff_statuses`), parser with validation, handler wiring, unit tests, swagger annotation update, and a new search syntax documentation page. > 🤖 Generated with [Coder Agents](https://coder.com/agents)
This commit is contained in:
Generated
+1
-1
@@ -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, and diff_url:\u003curl\u003e (quote URLs). 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, 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"
|
||||
},
|
||||
|
||||
Generated
+1
-1
@@ -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, and diff_url:\u003curl\u003e (quote URLs). 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, 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"
|
||||
},
|
||||
|
||||
@@ -778,6 +778,9 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
|
||||
arg.TitleQuery,
|
||||
arg.HasUnread,
|
||||
pq.Array(arg.PullRequestStatuses),
|
||||
arg.PrNumber,
|
||||
arg.RepoQuery,
|
||||
arg.PrTitleQuery,
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
|
||||
@@ -14118,6 +14118,37 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
linkPRFull := func(chatID uuid.UUID, url, state string, draft bool, prNumber int32, gitRemoteOrigin string, prTitle string) {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
// First set the git remote origin via the reference upsert.
|
||||
if gitRemoteOrigin != "" {
|
||||
_, err := store.UpsertChatDiffStatusReference(ctx, database.UpsertChatDiffStatusReferenceParams{
|
||||
ChatID: chatID,
|
||||
Url: sql.NullString{String: url, Valid: url != ""},
|
||||
GitBranch: "main",
|
||||
GitRemoteOrigin: gitRemoteOrigin,
|
||||
StaleAt: now.Add(time.Hour),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// Then set PR metadata via the status upsert.
|
||||
_, err := store.UpsertChatDiffStatus(ctx, database.UpsertChatDiffStatusParams{
|
||||
ChatID: chatID,
|
||||
Url: sql.NullString{String: url, Valid: url != ""},
|
||||
PullRequestState: sql.NullString{String: state, Valid: state != ""},
|
||||
PullRequestTitle: prTitle,
|
||||
PullRequestDraft: draft,
|
||||
PrNumber: sql.NullInt32{Int32: prNumber, Valid: prNumber > 0},
|
||||
Additions: 1,
|
||||
Deletions: 1,
|
||||
ChangedFiles: 1,
|
||||
RefreshedAt: now,
|
||||
StaleAt: now.Add(time.Hour),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
makeUnread := func(chatID uuid.UUID) {
|
||||
t.Helper()
|
||||
_, err := store.InsertChatMessages(ctx, database.InsertChatMessagesParams{
|
||||
@@ -14199,12 +14230,25 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
linkPR(childWithDraftPR.ID, "https://github.com/coder/coder/pull/1005", "open", true)
|
||||
makeUnread(childWithDraftPR.ID)
|
||||
|
||||
// Chats with specific PR numbers and repos for new filter tests.
|
||||
// Use "acme/widget" and "acme/other-repo" origins to avoid overlapping
|
||||
// with the "coder/coder" URLs in the earlier PR fixtures.
|
||||
prNumberChat := createRoot("pr number 42 chat")
|
||||
linkPRFull(prNumberChat.ID, "https://github.com/acme/widget/pull/42", "open", false, 42, "https://github.com/acme/widget.git", "Fix authentication bug")
|
||||
|
||||
repoChat := createRoot("repo filter chat")
|
||||
linkPRFull(repoChat.ID, "https://github.com/acme/other-repo/pull/7", "merged", false, 7, "https://github.com/acme/other-repo.git", "Add feature X")
|
||||
|
||||
prTitleChat := createRoot("pr title filter chat")
|
||||
linkPRFull(prTitleChat.ID, "https://github.com/acme/widget/pull/99", "open", false, 99, "https://github.com/acme/widget.git", "Deploy new dashboard")
|
||||
|
||||
// All root chat IDs (for "returns everything" baseline).
|
||||
allRootIDs := []uuid.UUID{
|
||||
alphaProject.ID, betaProject.ID, gammaUnrelated.ID,
|
||||
percentComplete.ID, thousandOne.ID, underscoreConfig.ID, hyphenConfig.ID,
|
||||
draftPR.ID, openPR.ID, mergedPR.ID, closedPR.ID,
|
||||
unreadNoPR.ID, readChat.ID, childParent.ID,
|
||||
prNumberChat.ID, repoChat.ID, prTitleChat.ID,
|
||||
}
|
||||
|
||||
// --- test cases ---
|
||||
@@ -14228,15 +14272,32 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
|
||||
// PR status filter.
|
||||
{"PRStatus/Draft", database.GetChatsParams{PullRequestStatuses: []string{"draft"}}, []uuid.UUID{draftPR.ID}},
|
||||
{"PRStatus/Open", database.GetChatsParams{PullRequestStatuses: []string{"open"}}, []uuid.UUID{openPR.ID}},
|
||||
{"PRStatus/Merged", database.GetChatsParams{PullRequestStatuses: []string{"merged"}}, []uuid.UUID{mergedPR.ID}},
|
||||
{"PRStatus/Open", database.GetChatsParams{PullRequestStatuses: []string{"open"}}, []uuid.UUID{openPR.ID, prNumberChat.ID, prTitleChat.ID}},
|
||||
{"PRStatus/Merged", database.GetChatsParams{PullRequestStatuses: []string{"merged"}}, []uuid.UUID{mergedPR.ID, repoChat.ID}},
|
||||
{"PRStatus/Closed", database.GetChatsParams{PullRequestStatuses: []string{"closed"}}, []uuid.UUID{closedPR.ID}},
|
||||
{"PRStatus/MultiStatus", database.GetChatsParams{PullRequestStatuses: []string{"draft", "closed"}}, []uuid.UUID{draftPR.ID, closedPR.ID}},
|
||||
|
||||
// Unread filter.
|
||||
{"Unread/MatchesUnread", database.GetChatsParams{HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{draftPR.ID, unreadNoPR.ID}},
|
||||
// HasUnread=false returns chats without unread messages.
|
||||
{"Unread/ExcludesRead", database.GetChatsParams{HasUnread: sql.NullBool{Bool: false, Valid: true}}, []uuid.UUID{alphaProject.ID, betaProject.ID, gammaUnrelated.ID, percentComplete.ID, thousandOne.ID, underscoreConfig.ID, hyphenConfig.ID, openPR.ID, mergedPR.ID, closedPR.ID, readChat.ID, childParent.ID}},
|
||||
{"Unread/ExcludesRead", database.GetChatsParams{HasUnread: sql.NullBool{Bool: false, Valid: true}}, []uuid.UUID{alphaProject.ID, betaProject.ID, gammaUnrelated.ID, percentComplete.ID, thousandOne.ID, underscoreConfig.ID, hyphenConfig.ID, openPR.ID, mergedPR.ID, closedPR.ID, readChat.ID, childParent.ID, prNumberChat.ID, repoChat.ID, prTitleChat.ID}},
|
||||
|
||||
// PR number filter.
|
||||
{"PRNumber/ExactMatch", database.GetChatsParams{PrNumber: 42}, []uuid.UUID{prNumberChat.ID}},
|
||||
{"PRNumber/NoMatch", database.GetChatsParams{PrNumber: 999}, nil},
|
||||
{"PRNumber/ZeroIsNoOp", database.GetChatsParams{PrNumber: 0}, allRootIDs},
|
||||
|
||||
// Repo filter.
|
||||
{"Repo/SubstringMatch", database.GetChatsParams{RepoQuery: "acme/widget"}, []uuid.UUID{prNumberChat.ID, prTitleChat.ID}},
|
||||
{"Repo/DifferentRepo", database.GetChatsParams{RepoQuery: "acme/other-repo"}, []uuid.UUID{repoChat.ID}},
|
||||
{"Repo/NoMatch", database.GetChatsParams{RepoQuery: "nonexistent/repo"}, nil},
|
||||
{"Repo/CaseInsensitive", database.GetChatsParams{RepoQuery: "ACME/WIDGET"}, []uuid.UUID{prNumberChat.ID, prTitleChat.ID}},
|
||||
{"Repo/MatchesViaURL", database.GetChatsParams{RepoQuery: "coder/coder"}, []uuid.UUID{draftPR.ID, openPR.ID, mergedPR.ID, closedPR.ID}},
|
||||
|
||||
// PR title filter.
|
||||
{"PRTitle/SubstringMatch", database.GetChatsParams{PrTitleQuery: "auth"}, []uuid.UUID{prNumberChat.ID}},
|
||||
{"PRTitle/CaseInsensitive", database.GetChatsParams{PrTitleQuery: "DEPLOY"}, []uuid.UUID{prTitleChat.ID}},
|
||||
{"PRTitle/NoMatch", database.GetChatsParams{PrTitleQuery: "nonexistent title"}, nil},
|
||||
|
||||
// Composed filters.
|
||||
{"Composed/TitleAndPRStatus", database.GetChatsParams{TitleQuery: "draft", PullRequestStatuses: []string{"draft"}}, []uuid.UUID{draftPR.ID}},
|
||||
@@ -14244,6 +14305,8 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
{"Composed/PRStatusAndUnread", database.GetChatsParams{PullRequestStatuses: []string{"draft"}, HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{draftPR.ID}},
|
||||
{"Composed/AllFilters", database.GetChatsParams{TitleQuery: "draft", PullRequestStatuses: []string{"draft"}, HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{draftPR.ID}},
|
||||
{"Composed/TitleNarrowsUnread", database.GetChatsParams{TitleQuery: "no pr", HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{unreadNoPR.ID}},
|
||||
{"Composed/PRNumberAndStatus", database.GetChatsParams{PrNumber: 42, PullRequestStatuses: []string{"closed"}}, nil},
|
||||
{"Composed/RepoAndPRTitle", database.GetChatsParams{RepoQuery: "acme/widget", PrTitleQuery: "auth"}, []uuid.UUID{prNumberChat.ID}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -7638,6 +7638,39 @@ WHERE
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by PR number (exact match on chat's diff status).
|
||||
AND CASE
|
||||
WHEN $11::int != 0 THEN EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_diff_statuses cds
|
||||
WHERE cds.chat_id = chats_expanded.id
|
||||
AND cds.pr_number = $11
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by repository (substring match on remote origin or PR URL).
|
||||
AND CASE
|
||||
WHEN $12::text != '' THEN EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_diff_statuses cds
|
||||
WHERE cds.chat_id = chats_expanded.id
|
||||
AND (
|
||||
cds.git_remote_origin ILIKE '%' || $12 || '%'
|
||||
OR cds.url ILIKE '%' || $12 || '%'
|
||||
)
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by pull request title (case-insensitive substring).
|
||||
AND CASE
|
||||
WHEN $13::text != '' THEN EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_diff_statuses cds
|
||||
WHERE cds.chat_id = chats_expanded.id
|
||||
AND cds.pull_request_title ILIKE '%' || $13 || '%'
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Paginate over root chats only. Children are fetched
|
||||
-- separately via GetChildChatsByParentIDs and embedded under
|
||||
-- each parent. Other callers that need the full set should
|
||||
@@ -7654,11 +7687,11 @@ ORDER BY
|
||||
-chats_expanded.pin_order DESC,
|
||||
chats_expanded.updated_at DESC,
|
||||
chats_expanded.id DESC
|
||||
OFFSET $11
|
||||
OFFSET $14
|
||||
LIMIT
|
||||
-- The chat list is unbounded and expected to grow large.
|
||||
-- Default to 50 to prevent accidental excessively large queries.
|
||||
COALESCE(NULLIF($12 :: int, 0), 50)
|
||||
COALESCE(NULLIF($15 :: int, 0), 50)
|
||||
`
|
||||
|
||||
type GetChatsParams struct {
|
||||
@@ -7672,6 +7705,9 @@ type GetChatsParams struct {
|
||||
TitleQuery string `db:"title_query" json:"title_query"`
|
||||
HasUnread sql.NullBool `db:"has_unread" json:"has_unread"`
|
||||
PullRequestStatuses []string `db:"pull_request_statuses" json:"pull_request_statuses"`
|
||||
PrNumber int32 `db:"pr_number" json:"pr_number"`
|
||||
RepoQuery string `db:"repo_query" json:"repo_query"`
|
||||
PrTitleQuery string `db:"pr_title_query" json:"pr_title_query"`
|
||||
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
@@ -7693,6 +7729,9 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha
|
||||
arg.TitleQuery,
|
||||
arg.HasUnread,
|
||||
pq.Array(arg.PullRequestStatuses),
|
||||
arg.PrNumber,
|
||||
arg.RepoQuery,
|
||||
arg.PrTitleQuery,
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
|
||||
@@ -569,6 +569,39 @@ WHERE
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by PR number (exact match on chat's diff status).
|
||||
AND CASE
|
||||
WHEN @pr_number::int != 0 THEN EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_diff_statuses cds
|
||||
WHERE cds.chat_id = chats_expanded.id
|
||||
AND cds.pr_number = @pr_number
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by repository (substring match on remote origin or PR URL).
|
||||
AND CASE
|
||||
WHEN @repo_query::text != '' THEN EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_diff_statuses cds
|
||||
WHERE cds.chat_id = chats_expanded.id
|
||||
AND (
|
||||
cds.git_remote_origin ILIKE '%' || @repo_query || '%'
|
||||
OR cds.url ILIKE '%' || @repo_query || '%'
|
||||
)
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by pull request title (case-insensitive substring).
|
||||
AND CASE
|
||||
WHEN @pr_title_query::text != '' THEN EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_diff_statuses cds
|
||||
WHERE cds.chat_id = chats_expanded.id
|
||||
AND cds.pull_request_title ILIKE '%' || @pr_title_query || '%'
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Paginate over root chats only. Children are fetched
|
||||
-- separately via GetChildChatsByParentIDs and embedded under
|
||||
-- each parent. Other callers that need the full set should
|
||||
|
||||
+4
-1
@@ -311,7 +311,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, and diff_url:<url> (quote URLs). 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, 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]
|
||||
@@ -372,6 +372,9 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
|
||||
TitleQuery: searchParams.TitleQuery,
|
||||
HasUnread: searchParams.HasUnread,
|
||||
PullRequestStatuses: searchParams.PullRequestStatuses,
|
||||
PrNumber: searchParams.PrNumber,
|
||||
RepoQuery: searchParams.RepoQuery,
|
||||
PrTitleQuery: searchParams.PrTitleQuery,
|
||||
// #nosec G115 - Pagination offsets are small and fit in int32
|
||||
OffsetOpt: int32(paginationParams.Offset),
|
||||
// #nosec G115 - Pagination limits are small and fit in int32
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -553,6 +554,9 @@ func Tasks(ctx context.Context, db database.Store, query string, actorID uuid.UU
|
||||
// - diff_url: string (matches chats whose linked diff URL equals the
|
||||
// given value, case-insensitively; URLs typically contain ':' so
|
||||
// they must be quoted, e.g. q=diff_url:"https://github.com/o/r/pull/1")
|
||||
// - 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) {
|
||||
filter := database.GetChatsParams{
|
||||
// Default to hiding archived chats.
|
||||
@@ -598,6 +602,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")
|
||||
|
||||
// pr: requires a positive integer.
|
||||
if prStr := parser.String(values, "", "pr"); prStr != "" {
|
||||
n, err := strconv.ParseInt(prStr, 10, 32)
|
||||
if err != nil || n <= 0 {
|
||||
parser.Errors = append(parser.Errors, codersdk.ValidationError{
|
||||
Field: "pr",
|
||||
Detail: fmt.Sprintf("%q is not a valid positive integer", prStr),
|
||||
})
|
||||
} else {
|
||||
filter.PrNumber = int32(n)
|
||||
}
|
||||
}
|
||||
|
||||
parser.ErrorExcessParams(values)
|
||||
return filter, parser.Errors
|
||||
|
||||
@@ -1459,6 +1459,55 @@ func TestSearchChats(t *testing.T) {
|
||||
DiffURL: sql.NullString{String: "https://github.com/coder/coder/pull/456", Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PrNumber",
|
||||
Query: "pr:42",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PrNumber: 42,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PrNumberInvalid",
|
||||
Query: "pr:abc",
|
||||
ExpectedErrorContains: "pr",
|
||||
},
|
||||
{
|
||||
Name: "PrNumberZero",
|
||||
Query: "pr:0",
|
||||
ExpectedErrorContains: "pr",
|
||||
},
|
||||
{
|
||||
Name: "PrNumberNegative",
|
||||
Query: "pr:-1",
|
||||
ExpectedErrorContains: "pr",
|
||||
},
|
||||
{
|
||||
Name: "RepoQuery",
|
||||
Query: "repo:coder/coder",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
RepoQuery: "coder/coder",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PrTitleQuery",
|
||||
Query: `pr_title:"fix auth bug"`,
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PrTitleQuery: "fix auth bug",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "CombinedPRRepoTitle",
|
||||
Query: "pr:99 repo:coder/coder pr_title:deploy",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PrNumber: 99,
|
||||
RepoQuery: "coder/coder",
|
||||
PrTitleQuery: "deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "BareTermsRejected",
|
||||
Query: "some random words",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Conversation Search Syntax
|
||||
|
||||
The chat list endpoint accepts a `q` query parameter for filtering
|
||||
conversations. All filters use `key:value` syntax. Bare search terms
|
||||
are rejected; use `title:` for title filtering.
|
||||
|
||||
## Filters
|
||||
|
||||
| Key | Values | Description |
|
||||
|--------------|-------------------------------------|----------------------------------------------------------------------------------------------------|
|
||||
| `title` | substring | Case-insensitive substring match. Quote multi-word values. |
|
||||
| `archived` | `true`, `false` | Filter by archived state. Default: `false`. |
|
||||
| `has_unread` | `true`, `false` | Conversations with unread assistant messages. |
|
||||
| `pr_status` | `draft`, `open`, `merged`, `closed` | Linked pull request state. Comma-separated for OR. |
|
||||
| `diff_url` | URL | Match by associated diff URL. Quote values containing colons. |
|
||||
| `pr` | positive integer | Exact PR number match. |
|
||||
| `repo` | substring | Case-insensitive substring match against git remote origin or URL. Quote values containing colons. |
|
||||
| `pr_title` | substring | Case-insensitive PR title substring match. Quote multi-word values. |
|
||||
|
||||
Multiple filters in one query combine with AND logic.
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
# Title substring (case-insensitive)
|
||||
?q=title:deploy
|
||||
|
||||
# Multi-word title (URL-encode the space or use +)
|
||||
?q=title:my+project
|
||||
|
||||
# Unread conversations
|
||||
?q=has_unread:true
|
||||
|
||||
# Conversations with open or draft PRs
|
||||
?q=pr_status:open,draft
|
||||
|
||||
# Filter by diff URL (quote values containing colons)
|
||||
?q=diff_url:"https://github.com/coder/coder/pull/123"
|
||||
|
||||
# Combine filters
|
||||
?q=title:refactor+has_unread:true+pr_status:merged
|
||||
|
||||
# Conversations linked to PR #42
|
||||
?q=pr:42
|
||||
|
||||
# Conversations for a specific repository
|
||||
?q=repo:coder/coder
|
||||
|
||||
# Conversations with a specific PR title
|
||||
?q=pr_title:"fix auth bug"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `title:`, `repo:`, and `pr_title:` use ILIKE matching. `%` and `_` act as wildcards.
|
||||
- `pr_status:draft` means the PR is open **and** marked as a draft.
|
||||
`pr_status:open` means the PR is open and not a draft.
|
||||
- Conversations without a linked diff status are excluded when `pr_status`, `pr`, `repo`, or `pr_title` is set. The `repo:` filter also matches chats tracking a branch with no PR.
|
||||
- Unrecognized keys or bare terms return HTTP 400 with a validation error.
|
||||
@@ -995,6 +995,12 @@
|
||||
"path": "./ai-coder/agents/getting-started.md",
|
||||
"state": ["beta"]
|
||||
},
|
||||
{
|
||||
"title": "Search Syntax",
|
||||
"description": "Filter conversations by title, status, and linked pull requests",
|
||||
"path": "./ai-coder/agents/chat-search-syntax.md",
|
||||
"state": ["beta"]
|
||||
},
|
||||
{
|
||||
"title": "Architecture",
|
||||
"description": "How the agent in the control plane communicates with workspaces",
|
||||
|
||||
Generated
+4
-4
@@ -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:<substring> (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:<draft\|open\|merged\|closed> as repeated or comma-separated values, and diff_url:<url> (quote URLs). Bare terms are not supported; use title:<value> 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:<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. |
|
||||
| `label` | query | string | false | Filter by label as key:value. Repeat for multiple (AND logic). |
|
||||
|
||||
### Example responses
|
||||
|
||||
|
||||
Reference in New Issue
Block a user