feat: add labels to chats (#23594)

## Summary

Adds a general-purpose `map[string]string` label system to chats, stored
as jsonb with a GIN index for efficient containment queries.

This is a standalone foundational feature that will be used by the
upcoming Automations feature for session identity (matching webhook
events to existing chats), replacing the need for bespoke session-key
tables.

## Changes

### Database
- **Migration 000451**: Adds `labels jsonb NOT NULL DEFAULT '{}'` column
to `chats` table with a GIN index (`idx_chats_labels`)
- **`InsertChat`**: Accepts labels on creation via `COALESCE(@labels,
'{}')`
- **`UpdateChatByID`**: Supports partial update —
`COALESCE(sqlc.narg('labels'), labels)` preserves existing labels when
NULL is passed
- **`GetChats`**: New `has_labels` filter using PostgreSQL `@>`
containment operator
- **`GetAuthorizedChats`**: Synced with generated `GetChats` (new column
scan + query param)

### API
- **Create chat** (`POST /chats`): Accepts optional `labels` field,
validated before creation
- **Update chat** (`PATCH /chats/{chat}`): Supports `labels` field for
atomic label replacement
- **List chats** (`GET /chats`): Supports `?label=key:value` query
parameters (multiple are AND-ed)

### SDK
- `Chat`, `CreateChatRequest`, `UpdateChatRequest`, `ListChatsOptions`
all gain `Labels` fields
- `UpdateChatRequest.Labels` is a pointer (`*map[string]string`) so
`nil` means "don't change" vs empty map means "clear all"

### Validation (`coderd/httpapi/labels.go`)
- Max 50 labels per chat
- Key: 1–64 chars, must match `[a-zA-Z0-9][a-zA-Z0-9._/-]*` (supports
namespaced keys like `github.repo`, `automation/pr-number`)
- Value: 1–256 chars
- 13 test cases covering all edge cases

### Chat runtime
- `chatd.CreateOptions` gains `Labels` field, threaded through to
`InsertChat`
- Existing `UpdateChatByID` callers (e.g., quickgen title updates) are
unaffected — NULL labels preserve existing values via COALESCE
This commit is contained in:
Kyle Carberry
2026-03-25 17:26:26 +00:00
committed by GitHub
parent 84740f4619
commit d4660d8a69
28 changed files with 796 additions and 56 deletions
+1
View File
@@ -78,6 +78,7 @@ const makeChat = (
owner_id: "owner-1",
last_model_config_id: "model-1",
mcp_server_ids: [],
labels: {},
title: `Chat ${id}`,
status: "running",
created_at: "2025-01-01T00:00:00.000Z",
+4
View File
@@ -1100,6 +1100,7 @@ export interface Chat {
readonly updated_at: string;
readonly archived: boolean;
readonly mcp_server_ids: readonly string[];
readonly labels: Record<string, string>;
}
// From codersdk/chats.go
@@ -2245,6 +2246,7 @@ export interface CreateChatRequest {
readonly workspace_id?: string;
readonly model_config_id?: string;
readonly mcp_server_ids?: readonly string[];
readonly labels?: Record<string, string>;
}
// From codersdk/users.go
@@ -3777,6 +3779,7 @@ export interface LinkConfig {
*/
export interface ListChatsOptions extends Pagination {
readonly Query: string;
readonly Labels: Record<string, string>;
}
// From codersdk/inboxnotification.go
@@ -7008,6 +7011,7 @@ export interface UpdateChatProviderConfigRequest {
export interface UpdateChatRequest {
readonly title?: string;
readonly archived?: boolean;
readonly labels?: Record<string, string>;
}
// From codersdk/chats.go
@@ -112,6 +112,7 @@ const baseChatFields = {
workspace_id: mockWorkspace.id,
last_model_config_id: "model-config-1",
mcp_server_ids: [],
labels: {},
created_at: "2026-02-18T00:00:00.000Z",
updated_at: "2026-02-18T00:00:00.000Z",
archived: false,
@@ -120,6 +120,7 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
status: "completed",
last_model_config_id: defaultModelConfigs[0].id,
mcp_server_ids: [],
labels: {},
created_at: oneWeekAgo,
updated_at: oneWeekAgo,
archived: false,
@@ -201,6 +201,7 @@ const makeChat = (chatID: string): TypesGen.Chat => ({
owner_id: "owner-1",
last_model_config_id: "model-1",
mcp_server_ids: [],
labels: {},
title: "test",
status: "running",
created_at: "2025-01-01T00:00:00.000Z",
@@ -52,6 +52,7 @@ export const WithParentChat: Story = {
owner_id: "owner-id",
last_model_config_id: "model-config-1",
mcp_server_ids: [],
labels: {},
title: "Set up CI/CD pipeline",
status: "completed",
last_error: null,
@@ -39,6 +39,7 @@ const buildChat = (overrides: Partial<TypesGen.Chat> = {}): TypesGen.Chat => ({
status: "completed",
last_model_config_id: "model-config-1",
mcp_server_ids: [],
labels: {},
created_at: oneWeekAgo,
updated_at: oneWeekAgo,
archived: false,
@@ -41,6 +41,7 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
status: "completed",
last_model_config_id: defaultModelConfigs[0].id,
mcp_server_ids: [],
labels: {},
created_at: oneWeekAgo,
updated_at: oneWeekAgo,
archived: false,
@@ -64,6 +64,7 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
archived: false,
last_error: null,
mcp_server_ids: [],
labels: {},
...overrides,
});