From 3452ab3166bd9e1441adc22e647798a9034a9e42 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 16 Apr 2026 23:57:05 +1000 Subject: [PATCH] chore: add client_type field to chats and telemetry (#24342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `chat_client_type` enum (`ui` | `api`) and `client_type` column to the `chats` table. The column defaults to `api` for new rows so API callers don't need to set it explicitly. Existing rows are backfilled to `ui`. The field flows through `CreateChatRequest`, `chatd.CreateOptions`, `InsertChat`, and is returned in the `Chat` response via `db2sdk`.
Implementation notes (Coder Agents generated) ### Changes **Database migration (000469)** - New enum `chat_client_type` with values `ui`, `api`. - New `client_type` column, `NOT NULL DEFAULT 'api'`. - Backfill: `UPDATE chats SET client_type = 'ui'`. **SQL query** — `InsertChat` now includes `client_type`. **SDK** — `ChatClientType` type added; `ClientType` field added to both `CreateChatRequest` (optional, defaults server-side to `api`) and `Chat` response. **Handler** — `postChats` maps the request field (defaulting to `api`) and passes it through `chatd.CreateOptions`. **Sub-agent** — Child chats inherit their parent's `client_type`. **db2sdk** — Maps the database value to the SDK type. ### Decision log - Default is `api` (not `ui`) so existing API integrations get the correct value without code changes. - Backfill sets existing rows to `ui` per requirement. - Child chats inherit `client_type` from parent rather than defaulting.
--- coderd/database/db2sdk/db2sdk.go | 1 + coderd/database/db2sdk/db2sdk_test.go | 1 + coderd/database/dbauthz/dbauthz_test.go | 3 +- coderd/database/dbpurge/dbpurge_test.go | 2 + coderd/database/dump.sql | 8 +- .../000470_chat_client_type.down.sql | 3 + .../migrations/000470_chat_client_type.up.sql | 10 ++ coderd/database/modelqueries.go | 1 + coderd/database/models.go | 59 +++++++++ coderd/database/querier_test.go | 21 ++++ coderd/database/queries.sql.go | 98 +++++++++------ coderd/database/queries/chats.sql | 8 +- coderd/exp_chats.go | 13 ++ coderd/exp_chats_test.go | 113 ++++++++++++++++++ coderd/httpmw/chatparam_test.go | 1 + coderd/telemetry/telemetry.go | 2 + coderd/telemetry/telemetry_test.go | 6 +- ...rkspaceagents_active_chat_internal_test.go | 1 + coderd/workspaceagents_chat_context_test.go | 2 + coderd/x/chatd/chatd.go | 8 +- coderd/x/chatd/chatd_test.go | 8 ++ coderd/x/chatd/chatprompt/chatprompt_test.go | 2 + .../x/chatd/chattool/startworkspace_test.go | 10 ++ coderd/x/chatd/recording_internal_test.go | 2 + coderd/x/chatd/subagent.go | 1 + coderd/x/gitsync/worker_test.go | 1 + codersdk/chats.go | 15 ++- docs/ai-coder/agents/chats-api.md | 4 +- enterprise/coderd/x/chatd/chatd_test.go | 1 + enterprise/coderd/x/chatd/usagelimit_test.go | 1 + site/src/api/queries/chats.test.ts | 1 + site/src/api/typesGenerated.ts | 7 ++ .../AgentsPage/AgentChatPage.stories.tsx | 1 + .../AgentsPage/AgentChatPageView.stories.tsx | 1 + site/src/pages/AgentsPage/AgentCreatePage.tsx | 1 + .../AgentsPage/AgentsPageView.stories.tsx | 1 + .../ChatConversation/chatStore.test.tsx | 1 + .../components/ChatTopBar.stories.tsx | 1 + .../Sidebar/AgentsSidebar.stories.tsx | 1 + .../components/Sidebar/AgentsSidebar.test.tsx | 1 + 40 files changed, 375 insertions(+), 47 deletions(-) create mode 100644 coderd/database/migrations/000470_chat_client_type.down.sql create mode 100644 coderd/database/migrations/000470_chat_client_type.up.sql diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 4715c6330a..e4771b68c3 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -1597,6 +1597,7 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database UpdatedAt: c.UpdatedAt, MCPServerIDs: mcpServerIDs, Labels: labels, + ClientType: codersdk.ChatClientType(c.ClientType), } if c.LastError.Valid { chat.LastError = &c.LastError.String diff --git a/coderd/database/db2sdk/db2sdk_test.go b/coderd/database/db2sdk/db2sdk_test.go index 2bb75afb34..fb423c82f7 100644 --- a/coderd/database/db2sdk/db2sdk_test.go +++ b/coderd/database/db2sdk/db2sdk_test.go @@ -812,6 +812,7 @@ func TestChat_AllFieldsPopulated(t *testing.T) { LastModelConfigID: uuid.New(), Title: "all-fields-test", Status: database.ChatStatusRunning, + ClientType: database.ChatClientTypeUi, LastError: sql.NullString{String: "boom", Valid: true}, CreatedAt: now, UpdatedAt: now, diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0777373c79..1ea0ff5b94 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -866,7 +866,8 @@ func (s *MethodTestSuite) TestChats() { })) s.Run("InsertChat", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { arg := testutil.Fake(s.T(), faker, database.InsertChatParams{ - Status: database.ChatStatusWaiting, + Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, }) chat := testutil.Fake(s.T(), faker, database.Chat{OwnerID: arg.OwnerID}) dbm.EXPECT().InsertChat(gomock.Any(), arg).Return(chat, nil).AnyTimes() diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index 0659fc68d5..d1d5676ec8 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -1678,6 +1678,7 @@ func TestDeleteOldChatFiles(t *testing.T) { LastModelConfigID: modelConfigID, Title: "test-chat", Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, }) require.NoError(t, err) if archived { @@ -2017,6 +2018,7 @@ func TestDeleteOldChatFiles(t *testing.T) { LastModelConfigID: deps.modelConfig.ID, Title: "child-chat", Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, }) require.NoError(t, err) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index aa3485e42d..2b697de1e0 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -270,6 +270,11 @@ CREATE TYPE build_reason AS ENUM ( 'task_resume' ); +CREATE TYPE chat_client_type AS ENUM ( + 'ui', + 'api' +); + CREATE TYPE chat_message_role AS ENUM ( 'system', 'user', @@ -1475,7 +1480,8 @@ CREATE TABLE chats ( last_injected_context jsonb, dynamic_tools jsonb, organization_id uuid NOT NULL, - plan_mode chat_plan_mode + plan_mode chat_plan_mode, + client_type chat_client_type DEFAULT 'api'::chat_client_type NOT NULL ); CREATE TABLE connection_logs ( diff --git a/coderd/database/migrations/000470_chat_client_type.down.sql b/coderd/database/migrations/000470_chat_client_type.down.sql new file mode 100644 index 0000000000..13ebaabee4 --- /dev/null +++ b/coderd/database/migrations/000470_chat_client_type.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE chats DROP COLUMN IF EXISTS client_type; + +DROP TYPE IF EXISTS chat_client_type; diff --git a/coderd/database/migrations/000470_chat_client_type.up.sql b/coderd/database/migrations/000470_chat_client_type.up.sql new file mode 100644 index 0000000000..f287be8351 --- /dev/null +++ b/coderd/database/migrations/000470_chat_client_type.up.sql @@ -0,0 +1,10 @@ +CREATE TYPE chat_client_type AS ENUM ( + 'ui', + 'api' +); + +ALTER TABLE chats ADD COLUMN client_type chat_client_type NOT NULL DEFAULT 'api'::chat_client_type; + +-- Backfill all existing rows to 'ui' since they were created +-- from the web interface before this column existed. +UPDATE chats SET client_type = 'ui'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 6e392e0df7..cf8f969692 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -801,6 +801,7 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams, &i.Chat.DynamicTools, &i.Chat.OrganizationID, &i.Chat.PlanMode, + &i.Chat.ClientType, &i.HasUnread); err != nil { return nil, err } diff --git a/coderd/database/models.go b/coderd/database/models.go index fe2664042e..48123f5a8a 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1107,6 +1107,64 @@ func AllBuildReasonValues() []BuildReason { } } +type ChatClientType string + +const ( + ChatClientTypeUi ChatClientType = "ui" + ChatClientTypeApi ChatClientType = "api" +) + +func (e *ChatClientType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ChatClientType(s) + case string: + *e = ChatClientType(s) + default: + return fmt.Errorf("unsupported scan type for ChatClientType: %T", src) + } + return nil +} + +type NullChatClientType struct { + ChatClientType ChatClientType `json:"chat_client_type"` + Valid bool `json:"valid"` // Valid is true if ChatClientType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullChatClientType) Scan(value interface{}) error { + if value == nil { + ns.ChatClientType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ChatClientType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullChatClientType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ChatClientType), nil +} + +func (e ChatClientType) Valid() bool { + switch e { + case ChatClientTypeUi, + ChatClientTypeApi: + return true + } + return false +} + +func AllChatClientTypeValues() []ChatClientType { + return []ChatClientType{ + ChatClientTypeUi, + ChatClientTypeApi, + } +} + type ChatMessageRole string const ( @@ -4303,6 +4361,7 @@ type Chat struct { DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"` + ClientType ChatClientType `db:"client_type" json:"client_type"` } type ChatDebugRun struct { diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 881e800f0b..0c35b18348 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -1293,6 +1293,7 @@ func TestGetAuthorizedChats(t *testing.T) { _, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: owner.ID, LastModelConfigID: modelCfg.ID, Title: fmt.Sprintf("owner chat %d", i+1), @@ -1305,6 +1306,7 @@ func TestGetAuthorizedChats(t *testing.T) { _, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: member.ID, LastModelConfigID: modelCfg.ID, Title: fmt.Sprintf("member chat %d", i+1), @@ -1444,6 +1446,7 @@ func TestGetAuthorizedChats(t *testing.T) { _, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: paginationUser.ID, LastModelConfigID: modelCfg.ID, Title: fmt.Sprintf("pagination chat %d", i+1), @@ -9898,6 +9901,7 @@ func TestInsertChatMessages(t *testing.T) { chat, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelConfigA.ID, Title: "test-chat-" + uuid.NewString(), @@ -10072,6 +10076,7 @@ func TestGetChatMessagesForPromptByChatID(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "test-chat-" + uuid.NewString(), @@ -10457,6 +10462,7 @@ func TestGetPRInsights(t *testing.T) { chat, err := p.Store.InsertChat(context.Background(), database.InsertChatParams{ OrganizationID: p.OrgID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: p.UserID, LastModelConfigID: p.ModelConfigID, Title: title, @@ -10594,6 +10600,7 @@ func TestGetPRInsights(t *testing.T) { chat, err := p.Store.InsertChat(context.Background(), database.InsertChatParams{ OrganizationID: p.OrgID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: p.UserID, LastModelConfigID: p.ModelConfigID, Title: title, @@ -11014,6 +11021,7 @@ func TestChatPinOrderQueries(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: orgID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: ownerID, LastModelConfigID: modelCfgID, Title: title, @@ -11199,6 +11207,7 @@ func TestChatLabels(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: owner.ID, LastModelConfigID: modelCfg.ID, Title: "labeled-chat", @@ -11223,6 +11232,7 @@ func TestChatLabels(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: owner.ID, LastModelConfigID: modelCfg.ID, Title: "no-labels-chat", @@ -11240,6 +11250,7 @@ func TestChatLabels(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: owner.ID, LastModelConfigID: modelCfg.ID, Title: "update-labels-chat", @@ -11282,6 +11293,7 @@ func TestChatLabels(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: owner.ID, LastModelConfigID: modelCfg.ID, Title: "original-title", @@ -11320,6 +11332,7 @@ func TestChatLabels(t *testing.T) { _, err = db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: owner.ID, LastModelConfigID: modelCfg.ID, Title: tc.title, Labels: pqtype.NullRawMessage{ @@ -11410,6 +11423,7 @@ func TestDeleteChatDebugDataAfterMessageIDIncludesTriggeredRuns(t *testing.T) { chat, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "chat-debug-rollback-" + uuid.NewString(), @@ -11596,6 +11610,7 @@ func TestFinalizeStaleChatDebugRows(t *testing.T) { chat, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "chat-finalize-" + uuid.NewString(), @@ -11952,6 +11967,7 @@ func TestChatDebugSQLGuards(t *testing.T) { chatA, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "chat-guard-A-" + uuid.NewString(), @@ -11961,6 +11977,7 @@ func TestChatDebugSQLGuards(t *testing.T) { chatB, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "chat-guard-B-" + uuid.NewString(), @@ -12082,6 +12099,7 @@ func TestChatDebugRunCOALESCEPreservation(t *testing.T) { chat, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "chat-debug-coalesce-" + uuid.NewString(), @@ -12195,6 +12213,7 @@ func TestChatDebugStepCOALESCEPreservation(t *testing.T) { chat, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "chat-step-coalesce-" + uuid.NewString(), @@ -12318,6 +12337,7 @@ func TestDeleteChatDebugDataAfterMessageIDNullMessagesSurvive(t *testing.T) { chat, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "chat-debug-null-msg-" + uuid.NewString(), @@ -12406,6 +12426,7 @@ func TestChatHasUnread(t *testing.T) { chat, err := store.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "test-chat-" + uuid.NewString(), diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f6cfc65021..89c180c957 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4816,7 +4816,7 @@ WHERE $3::int ) RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type AcquireChatsParams struct { @@ -4863,6 +4863,7 @@ func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) ( &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ); err != nil { return nil, err } @@ -5001,9 +5002,9 @@ WITH chats AS ( UPDATE chats SET archived = true, pin_order = 0, updated_at = NOW() WHERE id = $1::uuid OR root_chat_id = $1::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type FROM chats ORDER BY (id = $1::uuid) DESC, created_at ASC, id ASC ` @@ -5044,6 +5045,7 @@ func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ); err != nil { return nil, err } @@ -5193,7 +5195,7 @@ func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParam } const getActiveChatsByAgentID = `-- name: GetActiveChatsByAgentID :many -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type FROM chats WHERE agent_id = $1::uuid AND archived = false @@ -5240,6 +5242,7 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ); err != nil { return nil, err } @@ -5256,7 +5259,7 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U const getChatByID = `-- name: GetChatByID :one SELECT - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type FROM chats WHERE @@ -5293,12 +5296,13 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode FROM chats WHERE id = $1::uuid FOR UPDATE +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type FROM chats WHERE id = $1::uuid FOR UPDATE ` func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error) { @@ -5331,6 +5335,7 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -6385,7 +6390,7 @@ func (q *sqlQuerier) GetChatUsageLimitUserOverride(ctx context.Context, userID u const getChats = `-- name: GetChats :many SELECT - chats.id, chats.owner_id, chats.workspace_id, chats.title, chats.status, chats.worker_id, chats.started_at, chats.heartbeat_at, chats.created_at, chats.updated_at, chats.parent_chat_id, chats.root_chat_id, chats.last_model_config_id, chats.archived, chats.last_error, chats.mode, chats.mcp_server_ids, chats.labels, chats.build_id, chats.agent_id, chats.pin_order, chats.last_read_message_id, chats.last_injected_context, chats.dynamic_tools, chats.organization_id, chats.plan_mode, + chats.id, chats.owner_id, chats.workspace_id, chats.title, chats.status, chats.worker_id, chats.started_at, chats.heartbeat_at, chats.created_at, chats.updated_at, chats.parent_chat_id, chats.root_chat_id, chats.last_model_config_id, chats.archived, chats.last_error, chats.mode, chats.mcp_server_ids, chats.labels, chats.build_id, chats.agent_id, chats.pin_order, chats.last_read_message_id, chats.last_injected_context, chats.dynamic_tools, chats.organization_id, chats.plan_mode, chats.client_type, EXISTS ( SELECT 1 FROM chat_messages cm WHERE cm.chat_id = chats.id @@ -6500,6 +6505,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha &i.Chat.DynamicTools, &i.Chat.OrganizationID, &i.Chat.PlanMode, + &i.Chat.ClientType, &i.HasUnread, ); err != nil { return nil, err @@ -6516,7 +6522,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha } const getChatsByWorkspaceIDs = `-- name: GetChatsByWorkspaceIDs :many -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type FROM chats WHERE archived = false AND workspace_id = ANY($1::uuid[]) @@ -6559,6 +6565,7 @@ func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ); err != nil { return nil, err } @@ -6578,23 +6585,24 @@ SELECT id, owner_id, created_at, updated_at, status, (parent_chat_id IS NOT NULL)::bool AS has_parent, root_chat_id, workspace_id, - mode, archived, last_model_config_id + mode, archived, last_model_config_id, client_type FROM chats WHERE updated_at > $1 ` type GetChatsUpdatedAfterRow struct { - ID uuid.UUID `db:"id" json:"id"` - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Status ChatStatus `db:"status" json:"status"` - HasParent bool `db:"has_parent" json:"has_parent"` - RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"` - WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` - Mode NullChatMode `db:"mode" json:"mode"` - Archived bool `db:"archived" json:"archived"` - LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"` + ID uuid.UUID `db:"id" json:"id"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Status ChatStatus `db:"status" json:"status"` + HasParent bool `db:"has_parent" json:"has_parent"` + RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"` + WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` + Mode NullChatMode `db:"mode" json:"mode"` + Archived bool `db:"archived" json:"archived"` + LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"` + ClientType ChatClientType `db:"client_type" json:"client_type"` } // Retrieves chats updated after the given timestamp for telemetry @@ -6621,6 +6629,7 @@ func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time &i.Mode, &i.Archived, &i.LastModelConfigID, + &i.ClientType, ); err != nil { return nil, err } @@ -6686,7 +6695,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh const getStaleChats = `-- name: GetStaleChats :many SELECT - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type FROM chats WHERE @@ -6736,6 +6745,7 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ); err != nil { return nil, err } @@ -6828,7 +6838,8 @@ INSERT INTO chats ( status, mcp_server_ids, labels, - dynamic_tools + dynamic_tools, + client_type ) VALUES ( $1::uuid, $2::uuid, @@ -6844,10 +6855,11 @@ INSERT INTO chats ( $12::chat_status, COALESCE($13::uuid[], '{}'::uuid[]), COALESCE($14::jsonb, '{}'::jsonb), - $15::jsonb + $15::jsonb, + $16::chat_client_type ) RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type InsertChatParams struct { @@ -6866,6 +6878,7 @@ type InsertChatParams struct { MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"` Labels pqtype.NullRawMessage `db:"labels" json:"labels"` DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"` + ClientType ChatClientType `db:"client_type" json:"client_type"` } func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) { @@ -6885,6 +6898,7 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat pq.Array(arg.MCPServerIDs), arg.Labels, arg.DynamicTools, + arg.ClientType, ) var i Chat err := row.Scan( @@ -6914,6 +6928,7 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -7442,9 +7457,9 @@ WITH chats AS ( archived = false, updated_at = NOW() WHERE id = $1::uuid OR root_chat_id = $1::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type FROM chats ORDER BY (id = $1::uuid) DESC, created_at ASC, id ASC ` @@ -7489,6 +7504,7 @@ func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Cha &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ); err != nil { return nil, err } @@ -7569,7 +7585,7 @@ UPDATE chats SET updated_at = NOW() WHERE id = $3::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatBuildAgentBindingParams struct { @@ -7608,6 +7624,7 @@ func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg Update &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -7621,7 +7638,7 @@ SET WHERE id = $2::uuid RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatByIDParams struct { @@ -7659,6 +7676,7 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -7717,7 +7735,7 @@ SET WHERE id = $2::uuid RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatLabelsByIDParams struct { @@ -7755,6 +7773,7 @@ func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLab &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -7764,7 +7783,7 @@ UPDATE chats SET last_injected_context = $1::jsonb WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatLastInjectedContextParams struct { @@ -7806,6 +7825,7 @@ func (q *sqlQuerier) UpdateChatLastInjectedContext(ctx context.Context, arg Upda &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -7819,7 +7839,7 @@ SET WHERE id = $2::uuid RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatLastModelConfigByIDParams struct { @@ -7857,6 +7877,7 @@ func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg Upda &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -7888,7 +7909,7 @@ SET WHERE id = $2::uuid RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatMCPServerIDsParams struct { @@ -7926,6 +7947,7 @@ func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatM &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -8057,7 +8079,7 @@ SET WHERE id = $2::uuid RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatPlanModeByIDParams struct { @@ -8095,6 +8117,7 @@ func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatP &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -8112,7 +8135,7 @@ SET WHERE id = $6::uuid RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatStatusParams struct { @@ -8161,6 +8184,7 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -8178,7 +8202,7 @@ SET WHERE id = $7::uuid RETURNING - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatStatusPreserveUpdatedAtParams struct { @@ -8229,6 +8253,7 @@ func (q *sqlQuerier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } @@ -8240,7 +8265,7 @@ UPDATE chats SET agent_id = $3::uuid, updated_at = NOW() WHERE id = $4::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type ` type UpdateChatWorkspaceBindingParams struct { @@ -8285,6 +8310,7 @@ func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateC &i.DynamicTools, &i.OrganizationID, &i.PlanMode, + &i.ClientType, ) return i, err } diff --git a/coderd/database/queries/chats.sql b/coderd/database/queries/chats.sql index 018c058704..73d2806c06 100644 --- a/coderd/database/queries/chats.sql +++ b/coderd/database/queries/chats.sql @@ -406,7 +406,8 @@ INSERT INTO chats ( status, mcp_server_ids, labels, - dynamic_tools + dynamic_tools, + client_type ) VALUES ( @organization_id::uuid, @owner_id::uuid, @@ -422,7 +423,8 @@ INSERT INTO chats ( @status::chat_status, COALESCE(@mcp_server_ids::uuid[], '{}'::uuid[]), COALESCE(sqlc.narg('labels')::jsonb, '{}'::jsonb), - sqlc.narg('dynamic_tools')::jsonb + sqlc.narg('dynamic_tools')::jsonb, + @client_type::chat_client_type ) RETURNING *; @@ -1299,7 +1301,7 @@ SELECT id, owner_id, created_at, updated_at, status, (parent_chat_id IS NOT NULL)::bool AS has_parent, root_chat_id, workspace_id, - mode, archived, last_model_config_id + mode, archived, last_model_config_id, client_type FROM chats WHERE updated_at > @updated_after; diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 5e9b7c567f..bfcfd1ae4b 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -568,6 +568,18 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) { } } + clientType := database.ChatClientTypeApi + if req.ClientType != "" { + clientType = database.ChatClientType(req.ClientType) + if !clientType.Valid() { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid client_type.", + Detail: fmt.Sprintf("got %q, want one of %v", req.ClientType, database.AllChatClientTypeValues()), + }) + return + } + } + chat, err := api.chatDaemon.CreateChat(ctx, chatd.CreateOptions{ OrganizationID: req.OrganizationID, OwnerID: apiKey.UserID, @@ -575,6 +587,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) { Title: title, ModelConfigID: modelConfigID, PlanMode: planModeToNullChatPlanMode(req.PlanMode), + ClientType: clientType, SystemPrompt: req.SystemPrompt, InitialUserContent: contentBlocks, MCPServerIDs: mcpServerIDs, diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index 5188914117..9b5c194cdd 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -633,6 +633,7 @@ func TestPostChats(t *testing.T) { existingChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "existing-limit-chat", @@ -734,6 +735,85 @@ func TestPostChats(t *testing.T) { }) } +func TestPostChats_ClientType(t *testing.T) { + t.Parallel() + + client := newChatClient(t) + firstUser := coderdtest.CreateFirstUser(t, client.Client) + _ = createChatModelConfig(t, client) + + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) + memberClient := codersdk.NewExperimentalClient(memberClientRaw) + + newChat := func(t *testing.T, clientType codersdk.ChatClientType) codersdk.Chat { + t.Helper() + ctx := testutil.Context(t, testutil.WaitLong) + chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ + OrganizationID: firstUser.OrganizationID, + Content: []codersdk.ChatInputPart{{ + Type: codersdk.ChatInputPartTypeText, + Text: "client type test", + }}, + ClientType: clientType, + }) + require.NoError(t, err) + return chat + } + + t.Run("DefaultIsAPI", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + // Omit ClientType entirely — should default to "api". + chat := newChat(t, "") + require.Equal(t, codersdk.ChatClientTypeAPI, chat.ClientType) + + got, err := memberClient.GetChat(ctx, chat.ID) + require.NoError(t, err) + require.Equal(t, codersdk.ChatClientTypeAPI, got.ClientType) + }) + + t.Run("ExplicitAPI", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + chat := newChat(t, codersdk.ChatClientTypeAPI) + require.Equal(t, codersdk.ChatClientTypeAPI, chat.ClientType) + + got, err := memberClient.GetChat(ctx, chat.ID) + require.NoError(t, err) + require.Equal(t, codersdk.ChatClientTypeAPI, got.ClientType) + }) + + t.Run("ExplicitUI", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + chat := newChat(t, codersdk.ChatClientTypeUI) + require.Equal(t, codersdk.ChatClientTypeUI, chat.ClientType) + + got, err := memberClient.GetChat(ctx, chat.ID) + require.NoError(t, err) + require.Equal(t, codersdk.ChatClientTypeUI, got.ClientType) + }) + + t.Run("InvalidClientType", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ + OrganizationID: firstUser.OrganizationID, + Content: []codersdk.ChatInputPart{{ + Type: codersdk.ChatInputPartTypeText, + Text: "bad client type", + }}, + ClientType: "bogus", + }) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Contains(t, sdkErr.Message, "Invalid client_type") + }) +} + func TestListChats(t *testing.T) { t.Parallel() @@ -772,6 +852,7 @@ func TestListChats(t *testing.T) { memberDBChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: member.ID, LastModelConfigID: modelConfig.ID, Title: "member chat only", @@ -857,6 +938,7 @@ func TestListChats(t *testing.T) { _, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: member.ID, LastModelConfigID: modelConfig.ID, Title: "member chat", @@ -1500,6 +1582,7 @@ func TestWatchChats(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "diff status watch test", @@ -1632,6 +1715,7 @@ func TestWatchChats(t *testing.T) { childOne, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "watch child 1", ParentChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true}, @@ -1642,6 +1726,7 @@ func TestWatchChats(t *testing.T) { childTwo, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "watch child 2", ParentChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true}, @@ -3650,6 +3735,7 @@ func TestPatchChat(t *testing.T) { dbChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: orgID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: ownerID, LastModelConfigID: modelConfigID, Title: title, @@ -3953,6 +4039,7 @@ func TestArchiveChat(t *testing.T) { child1, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "child 1", @@ -3964,6 +4051,7 @@ func TestArchiveChat(t *testing.T) { child2, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "child 2", @@ -4074,6 +4162,7 @@ func TestUnarchiveChat(t *testing.T) { child1, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "child 1", ParentChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true}, @@ -4084,6 +4173,7 @@ func TestUnarchiveChat(t *testing.T) { child2, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "child 2", ParentChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true}, @@ -4414,6 +4504,7 @@ func TestPostChatMessages(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: member.ID, LastModelConfigID: modelConfig.ID, Title: "member chat", @@ -5766,6 +5857,7 @@ func TestInterruptChat(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "interrupt route test", @@ -5847,6 +5939,7 @@ func TestRegenerateChatTitle(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "chat with update denied", @@ -5960,6 +6053,7 @@ func TestRegenerateChatTitle(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "chat with lock held", @@ -6002,6 +6096,7 @@ func TestRegenerateChatTitle(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "pending chat without worker", @@ -6120,6 +6215,7 @@ func TestGetChatDiffStatus(t *testing.T) { noCachedStatusChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "get diff status route no cache", @@ -6134,6 +6230,7 @@ func TestGetChatDiffStatus(t *testing.T) { cachedStatusChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "get diff status route cached", @@ -6243,6 +6340,7 @@ func TestGetChatDiffContents(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "diff contents with cached repository reference", @@ -6343,6 +6441,7 @@ func TestDeleteChatQueuedMessage(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "delete queued message route test", @@ -6396,6 +6495,7 @@ func TestDeleteChatQueuedMessage(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "delete queued invalid id", @@ -6432,6 +6532,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "promote queued message route test", @@ -6504,6 +6605,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "promote queued usage limit", @@ -6580,6 +6682,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "promote queued invalid id", @@ -6617,6 +6720,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: member.ID, LastModelConfigID: modelConfig.ID, Title: "promote queued no agents access", @@ -7210,6 +7314,7 @@ func seedChatCostFixture(t *testing.T) chatCostTestFixture { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: firstUser.UserID, LastModelConfigID: modelConfig.ID, Title: "test chat", @@ -7332,6 +7437,7 @@ func TestChatCostSummary_AdminDrilldown(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(seedCtx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: member.ID, LastModelConfigID: modelConfig.ID, Title: "member chat", @@ -7402,6 +7508,7 @@ func TestChatCostUsers(t *testing.T) { adminChat, err := db.InsertChat(dbauthz.AsSystemRestricted(seedCtx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: firstUser.UserID, LastModelConfigID: modelConfig.ID, Title: "admin chat", @@ -7431,6 +7538,7 @@ func TestChatCostUsers(t *testing.T) { memberChat, err := db.InsertChat(dbauthz.AsSystemRestricted(seedCtx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: member.ID, LastModelConfigID: modelConfig.ID, Title: "member chat", @@ -7516,6 +7624,7 @@ func TestChatCostSummary_DateRange(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(seedCtx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: firstUser.UserID, LastModelConfigID: modelConfig.ID, Title: "date range test", @@ -7583,6 +7692,7 @@ func TestChatCostSummary_UnpricedMessages(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: firstUser.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: firstUser.UserID, LastModelConfigID: modelConfig.ID, Title: "unpriced test", @@ -8786,6 +8896,7 @@ func TestGetChatsByWorkspace(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: title, @@ -8930,6 +9041,7 @@ func TestSubmitToolResults(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: organizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: ownerID, LastModelConfigID: modelConfigID, Title: "tool-results-test", DynamicTools: pqtype.NullRawMessage{RawMessage: dtJSON, Valid: true}, @@ -9037,6 +9149,7 @@ func TestSubmitToolResults(t *testing.T) { chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OrganizationID: user.OrganizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.UserID, LastModelConfigID: modelConfig.ID, Title: "wrong-status-test", diff --git a/coderd/httpmw/chatparam_test.go b/coderd/httpmw/chatparam_test.go index 0c758f15bd..4e40ff4c27 100644 --- a/coderd/httpmw/chatparam_test.go +++ b/coderd/httpmw/chatparam_test.go @@ -65,6 +65,7 @@ func TestChatParam(t *testing.T) { chat, err := db.InsertChat(context.Background(), database.InsertChatParams{ OrganizationID: organizationID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: ownerID, WorkspaceID: uuid.NullUUID{}, ParentChatID: uuid.NullUUID{}, diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index f59343b6e5..f2bb525160 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -2172,6 +2172,7 @@ func ConvertChat(dbChat database.GetChatsUpdatedAfterRow) Chat { mode := string(dbChat.Mode.ChatMode) c.Mode = &mode } + c.ClientType = string(dbChat.ClientType) return c } @@ -2345,6 +2346,7 @@ type Chat struct { Mode *string `json:"mode"` Archived bool `json:"archived"` LastModelConfigID uuid.UUID `json:"last_model_config_id"` + ClientType string `json:"client_type"` } // ChatMessageSummary contains per-chat aggregated message metrics diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index a07f1c74cf..16fffbf657 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -1651,6 +1651,7 @@ func TestChatsTelemetry(t *testing.T) { LastModelConfigID: modelCfg.ID, Title: "Root Chat", Status: database.ChatStatusRunning, + ClientType: database.ChatClientTypeUi, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, Mode: database.NullChatMode{ChatMode: database.ChatModeComputerUse, Valid: true}, }) @@ -1663,6 +1664,7 @@ func TestChatsTelemetry(t *testing.T) { LastModelConfigID: modelCfg2.ID, Title: "Child Chat", Status: database.ChatStatusCompleted, + ClientType: database.ChatClientTypeUi, ParentChatID: uuid.NullUUID{UUID: rootChat.ID, Valid: true}, RootChatID: uuid.NullUUID{UUID: rootChat.ID, Valid: true}, }) @@ -1770,7 +1772,7 @@ func TestChatsTelemetry(t *testing.T) { require.NotNil(t, foundRoot.Mode) assert.Equal(t, "computer_use", *foundRoot.Mode) assert.False(t, foundRoot.Archived) - + assert.Equal(t, "ui", foundRoot.ClientType) // Child chat assertions. assert.Equal(t, childChat.ID, foundChild.ID) assert.Equal(t, user.ID, foundChild.OwnerID) @@ -1782,7 +1784,7 @@ func TestChatsTelemetry(t *testing.T) { assert.Equal(t, modelCfg2.ID, foundChild.LastModelConfigID) assert.Nil(t, foundChild.Mode) assert.False(t, foundChild.Archived) - + assert.Equal(t, "ui", foundChild.ClientType) // --- Assert ChatMessageSummaries --- require.Len(t, snapshot.ChatMessageSummaries, 2) diff --git a/coderd/workspaceagents_active_chat_internal_test.go b/coderd/workspaceagents_active_chat_internal_test.go index 4af46f6605..4834bbedf2 100644 --- a/coderd/workspaceagents_active_chat_internal_test.go +++ b/coderd/workspaceagents_active_chat_internal_test.go @@ -37,6 +37,7 @@ func TestActiveAgentChatDefinitionsAgree(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: status, + ClientType: database.ChatClientTypeUi, OwnerID: owner.ID, LastModelConfigID: modelConfig.ID, Title: fmt.Sprintf("%s-archived-%t", status, archived), diff --git a/coderd/workspaceagents_chat_context_test.go b/coderd/workspaceagents_chat_context_test.go index aa7d474f00..c6893e2ffd 100644 --- a/coderd/workspaceagents_chat_context_test.go +++ b/coderd/workspaceagents_chat_context_test.go @@ -1033,6 +1033,7 @@ func createAgentChatContextChat( chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OrganizationID: orgID, OwnerID: ownerID, LastModelConfigID: modelConfigID, @@ -1059,6 +1060,7 @@ func createAgentChatContextChildChat( chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OrganizationID: orgID, OwnerID: ownerID, LastModelConfigID: modelConfigID, diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index 04927110d8..0ef24e2250 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -2,6 +2,7 @@ package chatd import ( "bytes" + "cmp" "context" "database/sql" "encoding/json" @@ -789,6 +790,7 @@ type CreateOptions struct { ModelConfigID uuid.UUID ChatMode database.NullChatMode PlanMode database.NullChatPlanMode + ClientType database.ChatClientType SystemPrompt string InitialUserContent []codersdk.ChatMessagePart MCPServerIDs []uuid.UUID @@ -885,7 +887,10 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C deploymentPrompt := p.resolveDeploymentSystemPrompt(ctx) effectivePlanMode := opts.PlanMode - + opts.ClientType = cmp.Or(opts.ClientType, database.ChatClientTypeApi) + if !opts.ClientType.Valid() { + return database.Chat{}, xerrors.Errorf("invalid client_type: %q", opts.ClientType) + } var chat database.Chat txErr := p.db.InTx(func(tx database.Store) error { if limitErr := p.checkUsageLimit(ctx, tx, opts.OwnerID, uuid.NullUUID{UUID: opts.OrganizationID, Valid: true}); limitErr != nil { @@ -909,6 +914,7 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C Title: opts.Title, Mode: opts.ChatMode, PlanMode: effectivePlanMode, + ClientType: opts.ClientType, // Chats created with an initial user message start pending. // Waiting is reserved for idle chats with no pending work. Status: database.ChatStatusPending, diff --git a/coderd/x/chatd/chatd_test.go b/coderd/x/chatd/chatd_test.go index ee5841a3be..17efc962a8 100644 --- a/coderd/x/chatd/chatd_test.go +++ b/coderd/x/chatd/chatd_test.go @@ -1235,6 +1235,7 @@ func TestCreateChatRejectsWhenUsageLimitReached(t *testing.T) { existingChat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, Title: "existing-limit-chat", LastModelConfigID: model.ID, @@ -1530,6 +1531,7 @@ func TestInterruptAutoPromotionIgnoresLaterUsageLimitIncrease(t *testing.T) { spendChat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{}, ParentChatID: uuid.NullUUID{}, @@ -1785,6 +1787,7 @@ func TestRecoverStaleChatsPeriodically(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, Title: "stale-recovery-periodic", LastModelConfigID: model.ID, @@ -1832,6 +1835,7 @@ func TestRecoverStaleChatsPeriodically(t *testing.T) { chat2, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, Title: "stale-recovery-periodic-2", LastModelConfigID: model.ID, @@ -1876,6 +1880,7 @@ func TestRecoverStaleRequiresActionChat(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, Title: "stale-requires-action", LastModelConfigID: model.ID, @@ -1937,6 +1942,7 @@ func TestNewReplicaRecoversStaleChatFromDeadReplica(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, Title: "orphaned-chat", LastModelConfigID: model.ID, @@ -1981,6 +1987,7 @@ func TestWaitingChatsAreNotRecoveredAsStale(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, Title: "waiting-chat", LastModelConfigID: model.ID, @@ -2025,6 +2032,7 @@ func TestUpdateChatStatusPersistsLastError(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, Title: "error-persisted", LastModelConfigID: model.ID, diff --git a/coderd/x/chatd/chatprompt/chatprompt_test.go b/coderd/x/chatd/chatprompt/chatprompt_test.go index 73815fcb33..fc1d214d53 100644 --- a/coderd/x/chatd/chatprompt/chatprompt_test.go +++ b/coderd/x/chatd/chatprompt/chatprompt_test.go @@ -1491,6 +1491,7 @@ func TestNulEscapeRoundTrip(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: model.ID, Title: "nul-roundtrip-test", @@ -1995,6 +1996,7 @@ func TestMediaToolResultRoundTrip(t *testing.T) { chat, chatErr := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: model.ID, Title: "media-roundtrip-" + callID, diff --git a/coderd/x/chatd/chattool/startworkspace_test.go b/coderd/x/chatd/chattool/startworkspace_test.go index 33cd8089f6..294ea029b8 100644 --- a/coderd/x/chatd/chattool/startworkspace_test.go +++ b/coderd/x/chatd/chattool/startworkspace_test.go @@ -44,6 +44,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "test-no-workspace", @@ -88,6 +89,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -170,6 +172,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -231,6 +234,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -295,6 +299,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -353,6 +358,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -425,6 +431,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -521,6 +528,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -603,6 +611,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, @@ -701,6 +710,7 @@ func TestStartWorkspace(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, LastModelConfigID: modelCfg.ID, diff --git a/coderd/x/chatd/recording_internal_test.go b/coderd/x/chatd/recording_internal_test.go index 7f0acc5edd..5c42e7def6 100644 --- a/coderd/x/chatd/recording_internal_test.go +++ b/coderd/x/chatd/recording_internal_test.go @@ -88,6 +88,7 @@ func createComputerUseParentChild( LastModelConfigID: model.ID, Title: parentTitle, Status: database.ChatStatusPending, + ClientType: database.ChatClientTypeUi, }) require.NoError(t, err) @@ -105,6 +106,7 @@ func createComputerUseParentChild( Title: childTitle, Mode: database.NullChatMode{ChatMode: database.ChatModeComputerUse, Valid: true}, Status: database.ChatStatusPending, + ClientType: database.ChatClientTypeUi, }) require.NoError(t, err) diff --git a/coderd/x/chatd/subagent.go b/coderd/x/chatd/subagent.go index 692b4dbda5..8aa541b5a7 100644 --- a/coderd/x/chatd/subagent.go +++ b/coderd/x/chatd/subagent.go @@ -486,6 +486,7 @@ func (p *Server) createChildSubagentChatWithOptions( Title: title, Mode: opts.chatMode, PlanMode: parent.PlanMode, + ClientType: parent.ClientType, Status: database.ChatStatusPending, MCPServerIDs: mcpServerIDs, Labels: pqtype.NullRawMessage{ diff --git a/coderd/x/gitsync/worker_test.go b/coderd/x/gitsync/worker_test.go index a1b664d879..ab8e843f15 100644 --- a/coderd/x/gitsync/worker_test.go +++ b/coderd/x/gitsync/worker_test.go @@ -981,6 +981,7 @@ func TestWorker(t *testing.T) { chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: org.ID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: modelCfg.ID, Title: "integration-test", diff --git a/codersdk/chats.go b/codersdk/chats.go index 8baccef57b..d738cbe24b 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -52,6 +52,15 @@ const ( ChatStatusRequiresAction ChatStatus = "requires_action" ) +// ChatClientType indicates whether a chat was created from the +// web UI or programmatically via the API. +type ChatClientType string + +const ( + ChatClientTypeUI ChatClientType = "ui" + ChatClientTypeAPI ChatClientType = "api" +) + // Chat represents a chat session with an AI agent. type Chat struct { ID uuid.UUID `json:"id" format:"uuid"` @@ -85,6 +94,7 @@ type Chat struct { // attach or agent change. LastInjectedContext []ChatMessagePart `json:"last_injected_context,omitempty"` Warnings []string `json:"warnings,omitempty"` + ClientType ChatClientType `json:"client_type"` } // ChatFileMetadata contains lightweight metadata about a file @@ -393,8 +403,9 @@ type CreateChatRequest struct { // UnsafeDynamicTools declares client-executed tools that the // LLM can invoke. This API is highly experimental and highly // subject to change. - UnsafeDynamicTools []DynamicTool `json:"unsafe_dynamic_tools,omitempty"` - PlanMode ChatPlanMode `json:"plan_mode,omitempty"` + UnsafeDynamicTools []DynamicTool `json:"unsafe_dynamic_tools,omitempty"` + PlanMode ChatPlanMode `json:"plan_mode,omitempty"` + ClientType ChatClientType `json:"client_type,omitempty"` } // UpdateChatRequest is the request to update a chat. diff --git a/docs/ai-coder/agents/chats-api.md b/docs/ai-coder/agents/chats-api.md index 666b1a60ea..64be4812d7 100644 --- a/docs/ai-coder/agents/chats-api.md +++ b/docs/ai-coder/agents/chats-api.md @@ -56,7 +56,8 @@ The response is the newly created `Chat` object: "pin_order": 0, "mcp_server_ids": [], "labels": {}, - "has_unread": false + "has_unread": false, + "client_type": "api" } ``` @@ -89,6 +90,7 @@ A typical integration follows three steps: | `model_config_id` | `uuid` | no | Override the default model configuration. | | `mcp_server_ids` | `uuid[]` | no | Attach MCP servers to this chat. | | `labels` | `map[string]string` | no | Key-value labels for the chat (max 50). | +| `client_type` | `string` | no | `"ui"` or `"api"`. Defaults to `"api"`. | Each `ChatInputPart` has a `type` field. The simplest form is a text part: diff --git a/enterprise/coderd/x/chatd/chatd_test.go b/enterprise/coderd/x/chatd/chatd_test.go index f192995e91..d8b9595830 100644 --- a/enterprise/coderd/x/chatd/chatd_test.go +++ b/enterprise/coderd/x/chatd/chatd_test.go @@ -149,6 +149,7 @@ func seedWaitingChat( chat, err := db.InsertChat(ctx, database.InsertChatParams{ OrganizationID: orgID, Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, OwnerID: user.ID, LastModelConfigID: model.ID, Title: title, diff --git a/enterprise/coderd/x/chatd/usagelimit_test.go b/enterprise/coderd/x/chatd/usagelimit_test.go index 7dbfa0cbb0..aeea060725 100644 --- a/enterprise/coderd/x/chatd/usagelimit_test.go +++ b/enterprise/coderd/x/chatd/usagelimit_test.go @@ -122,6 +122,7 @@ func TestResolveUsageLimitStatus_OrgScoped(t *testing.T) { LastModelConfigID: modelCfgID, Title: "test chat", Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, MCPServerIDs: []uuid.UUID{}, }) require.NoError(t, err) diff --git a/site/src/api/queries/chats.test.ts b/site/src/api/queries/chats.test.ts index c6299d5a14..34a3121285 100644 --- a/site/src/api/queries/chats.test.ts +++ b/site/src/api/queries/chats.test.ts @@ -93,6 +93,7 @@ const makeChat = ( archived: false, pin_order: 0, has_unread: false, + client_type: "ui", last_error: null, ...overrides, }); diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c9371353e0..cb6b600f76 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1260,6 +1260,7 @@ export interface Chat { */ readonly last_injected_context?: readonly ChatMessagePart[]; readonly warnings?: readonly string[]; + readonly client_type: ChatClientType; } // From codersdk/chats.go @@ -1267,6 +1268,11 @@ export type ChatBusyBehavior = "interrupt" | "queue"; export const ChatBusyBehaviors: ChatBusyBehavior[] = ["interrupt", "queue"]; +// From codersdk/chats.go +export type ChatClientType = "api" | "ui"; + +export const ChatClientTypes: ChatClientType[] = ["api", "ui"]; + // From codersdk/chats.go /** * ChatCompactionThresholdKeyPrefix scopes per-model chat compaction @@ -2685,6 +2691,7 @@ export interface CreateChatRequest { */ readonly unsafe_dynamic_tools?: readonly DynamicTool[]; readonly plan_mode?: ChatPlanMode; + readonly client_type?: ChatClientType; } // From codersdk/users.go diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx index 81df5f6a44..9542a230df 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx @@ -132,6 +132,7 @@ const baseChatFields = { archived: false, pin_order: 0, has_unread: false, + client_type: "ui", last_error: null, } as const; diff --git a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx index 7b589869e8..a6475da996 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx @@ -56,6 +56,7 @@ const buildChat = (overrides: Partial = {}): TypesGen.Chat => ({ archived: false, pin_order: 0, has_unread: false, + client_type: "ui", last_error: null, ...overrides, }); diff --git a/site/src/pages/AgentsPage/AgentCreatePage.tsx b/site/src/pages/AgentsPage/AgentCreatePage.tsx index 2e6486294a..7dbe1ec3a3 100644 --- a/site/src/pages/AgentsPage/AgentCreatePage.tsx +++ b/site/src/pages/AgentsPage/AgentCreatePage.tsx @@ -66,6 +66,7 @@ const AgentCreatePage: FC = () => { mcp_server_ids: mcpServerIds && mcpServerIds.length > 0 ? mcpServerIds : undefined, plan_mode: planMode === "plan" ? "plan" : undefined, + client_type: "ui", }); if (modelConfigID !== nilUUID) { diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 921d64216f..83ef06c6f7 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -140,6 +140,7 @@ const buildChat = (overrides: Partial = {}): Chat => ({ archived: false, pin_order: 0, has_unread: false, + client_type: "ui", last_error: null, ...overrides, }); diff --git a/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx b/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx index eb264d5d9a..fd5d67518a 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx @@ -217,6 +217,7 @@ const makeChat = (chatID: string): TypesGen.Chat => ({ archived: false, pin_order: 0, has_unread: false, + client_type: "ui", last_error: null, }); diff --git a/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx b/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx index a950cf8104..65b27b3a40 100644 --- a/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx @@ -62,6 +62,7 @@ export const WithParentChat: Story = { archived: false, pin_order: 0, has_unread: false, + client_type: "ui", }, }, }; diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx index cb5455ca23..3e3ae7f383 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx @@ -52,6 +52,7 @@ const buildChat = (overrides: Partial = {}): Chat => ({ archived: false, pin_order: 0, has_unread: false, + client_type: "ui", last_error: null, ...overrides, }); diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx index 2e7040b71d..9a5953bd3e 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx @@ -63,6 +63,7 @@ const buildChat = (overrides: Partial = {}): Chat => ({ archived: false, pin_order: 0, has_unread: false, + client_type: "ui", last_error: null, mcp_server_ids: [], labels: {},