From 2cf47ec3847fd871c99bd07a67da6029b22c69a5 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Wed, 18 Mar 2026 09:35:13 +0100 Subject: [PATCH] feat: virtual desktop settings toggle backend (#23171) Adds a new `site_config` entry that controls whether the virtual desktop feature for Coder Agents is enabled. It can be set via a new `/api/experimental/chats/config/desktop-enabled` endpoint, which will be used by the frontend. --- coderd/chatd/chatd_test.go | 3 + coderd/chatd/subagent.go | 13 ++- coderd/chatd/subagent_internal_test.go | 40 ++++++++- coderd/chats.go | 43 +++++++++ coderd/chats_test.go | 102 ++++++++++++++++++++++ coderd/coderd.go | 2 + coderd/database/dbauthz/dbauthz.go | 18 ++++ coderd/database/dbauthz/dbauthz_test.go | 8 ++ coderd/database/dbmetrics/querymetrics.go | 16 ++++ coderd/database/dbmock/dbmock.go | 29 ++++++ coderd/database/querier.go | 2 + coderd/database/queries.sql.go | 34 ++++++++ coderd/database/queries/siteconfig.sql | 20 +++++ codersdk/chats.go | 37 ++++++++ site/src/api/typesGenerated.ts | 16 ++++ 15 files changed, 377 insertions(+), 6 deletions(-) diff --git a/coderd/chatd/chatd_test.go b/coderd/chatd/chatd_test.go index 31d5c60534..efafc8b6fb 100644 --- a/coderd/chatd/chatd_test.go +++ b/coderd/chatd/chatd_test.go @@ -2517,6 +2517,9 @@ func TestComputerUseSubagentToolsAndModel(t *testing.T) { }) require.NoError(t, err) + err = db.UpsertChatDesktopEnabled(ctx, true) + require.NoError(t, err) + // Build workspace + agent records so getWorkspaceConn can // resolve the agent for the computer use child. org := dbgen.Organization(t, db, database.Organization{}) diff --git a/coderd/chatd/subagent.go b/coderd/chatd/subagent.go index 96603d9190..f44be88fcc 100644 --- a/coderd/chatd/subagent.go +++ b/coderd/chatd/subagent.go @@ -84,6 +84,14 @@ func (p *Server) isAnthropicConfigured(ctx context.Context) bool { return false } +func (p *Server) isDesktopEnabled(ctx context.Context) bool { + enabled, err := p.db.GetChatDesktopEnabled(ctx) + if err != nil { + return false + } + return enabled +} + func (p *Server) subagentTools(ctx context.Context, currentChat func() database.Chat) []fantasy.AgentTool { tools := []fantasy.AgentTool{ fantasy.NewAgentTool( @@ -253,9 +261,8 @@ func (p *Server) subagentTools(ctx context.Context, currentChat func() database. } // Only include the computer use tool when an Anthropic - // provider is configured, since it requires an Anthropic - // model. - if p.isAnthropicConfigured(ctx) { + // provider is configured and desktop is enabled. + if p.isAnthropicConfigured(ctx) && p.isDesktopEnabled(ctx) { tools = append(tools, fantasy.NewAgentTool( "spawn_computer_use_agent", "Spawn a dedicated computer use agent that can see the desktop "+ diff --git a/coderd/chatd/subagent_internal_test.go b/coderd/chatd/subagent_internal_test.go index e626eacf4f..15327e1e42 100644 --- a/coderd/chatd/subagent_internal_test.go +++ b/coderd/chatd/subagent_internal_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/v2/coderd/chatd/chatprovider" "github.com/coder/coder/v2/coderd/chatd/chattool" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/pubsub" @@ -144,14 +145,20 @@ func findToolByName(tools []fantasy.AgentTool, name string) fantasy.AgentTool { return nil } +func chatdTestContext(t *testing.T) context.Context { + t.Helper() + return dbauthz.AsChatd(testutil.Context(t, testutil.WaitLong)) +} + func TestSpawnComputerUseAgent_NoAnthropicProvider(t *testing.T) { t.Parallel() db, ps := dbtestutil.NewDB(t) + require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true)) // No Anthropic key in ProviderAPIKeys. server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{}) - ctx := testutil.Context(t, testutil.WaitLong) + ctx := chatdTestContext(t) user, model := seedInternalChatDeps(ctx, t, db) // Create a root parent chat. @@ -176,12 +183,13 @@ func TestSpawnComputerUseAgent_NotAvailableForChildChats(t *testing.T) { t.Parallel() db, ps := dbtestutil.NewDB(t) + require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true)) // Provide an Anthropic key so the provider check passes. server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{ Anthropic: "test-anthropic-key", }) - ctx := testutil.Context(t, testutil.WaitLong) + ctx := chatdTestContext(t) user, model := seedInternalChatDeps(ctx, t, db) // Create a root parent chat. @@ -232,16 +240,42 @@ func TestSpawnComputerUseAgent_NotAvailableForChildChats(t *testing.T) { assert.Contains(t, resp.Content, "delegated chats cannot create child subagents") } +func TestSpawnComputerUseAgent_DesktopDisabled(t *testing.T) { + t.Parallel() + + db, ps := dbtestutil.NewDB(t) + server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{ + Anthropic: "test-anthropic-key", + }) + + ctx := chatdTestContext(t) + user, model := seedInternalChatDeps(ctx, t, db) + parent, err := server.CreateChat(ctx, CreateOptions{ + OwnerID: user.ID, + Title: "parent-desktop-disabled", + ModelConfigID: model.ID, + InitialUserContent: []codersdk.ChatMessagePart{codersdk.ChatMessageText("hello")}, + }) + require.NoError(t, err) + parentChat, err := db.GetChatByID(ctx, parent.ID) + require.NoError(t, err) + + tools := server.subagentTools(ctx, func() database.Chat { return parentChat }) + tool := findToolByName(tools, "spawn_computer_use_agent") + assert.Nil(t, tool, "spawn_computer_use_agent tool must be omitted when desktop is disabled") +} + func TestSpawnComputerUseAgent_UsesComputerUseModelNotParent(t *testing.T) { t.Parallel() db, ps := dbtestutil.NewDB(t) + require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true)) // Provide an Anthropic key so the tool can proceed. server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{ Anthropic: "test-anthropic-key", }) - ctx := testutil.Context(t, testutil.WaitLong) + ctx := chatdTestContext(t) user, model := seedInternalChatDeps(ctx, t, db) // The parent uses an OpenAI model. diff --git a/coderd/chats.go b/coderd/chats.go index d4f8b618c7..91d07fdebb 100644 --- a/coderd/chats.go +++ b/coderd/chats.go @@ -2554,6 +2554,49 @@ func (api *API) putChatSystemPrompt(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusNoContent) } +// EXPERIMENTAL: this endpoint is experimental and is subject to change. +// +//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler. +func (api *API) getChatDesktopEnabled(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + enabled, err := api.Database.GetChatDesktopEnabled(ctx) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching desktop setting.", + Detail: err.Error(), + }) + return + } + httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatDesktopEnabledResponse{ + EnableDesktop: enabled, + }) +} + +// EXPERIMENTAL: this endpoint is experimental and is subject to change. +func (api *API) putChatDesktopEnabled(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.UpdateChatDesktopEnabledRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + if err := api.Database.UpsertChatDesktopEnabled(ctx, req.EnableDesktop); httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } else if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error updating desktop setting.", + Detail: err.Error(), + }) + return + } + rw.WriteHeader(http.StatusNoContent) +} + // EXPERIMENTAL: this endpoint is experimental and is subject to change. // //nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler. diff --git a/coderd/chats_test.go b/coderd/chats_test.go index 5c6ada9e49..7e7a993779 100644 --- a/coderd/chats_test.go +++ b/coderd/chats_test.go @@ -4568,6 +4568,108 @@ func TestChatSystemPrompt(t *testing.T) { }) } +func TestChatDesktopEnabled(t *testing.T) { + t.Parallel() + + t.Run("ReturnsFalseWhenUnset", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + adminClient := newChatClient(t) + coderdtest.CreateFirstUser(t, adminClient) + + resp, err := adminClient.GetChatDesktopEnabled(ctx) + require.NoError(t, err) + require.False(t, resp.EnableDesktop) + }) + + t.Run("AdminCanSetTrue", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + adminClient := newChatClient(t) + coderdtest.CreateFirstUser(t, adminClient) + + err := adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{ + EnableDesktop: true, + }) + require.NoError(t, err) + + resp, err := adminClient.GetChatDesktopEnabled(ctx) + require.NoError(t, err) + require.True(t, resp.EnableDesktop) + }) + + t.Run("AdminCanSetFalse", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + adminClient := newChatClient(t) + coderdtest.CreateFirstUser(t, adminClient) + + // Set true first, then set false. + err := adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{ + EnableDesktop: true, + }) + require.NoError(t, err) + + err = adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{ + EnableDesktop: false, + }) + require.NoError(t, err) + + resp, err := adminClient.GetChatDesktopEnabled(ctx) + require.NoError(t, err) + require.False(t, resp.EnableDesktop) + }) + + t.Run("NonAdminCanRead", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + adminClient := newChatClient(t) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + memberClient, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + err := adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{ + EnableDesktop: true, + }) + require.NoError(t, err) + + resp, err := memberClient.GetChatDesktopEnabled(ctx) + require.NoError(t, err) + require.True(t, resp.EnableDesktop) + }) + + t.Run("NonAdminWriteFails", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + adminClient := newChatClient(t) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + memberClient, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + err := memberClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{ + EnableDesktop: true, + }) + requireSDKError(t, err, http.StatusForbidden) + }) + + t.Run("UnauthenticatedFails", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + adminClient := newChatClient(t) + coderdtest.CreateFirstUser(t, adminClient) + + anonClient := codersdk.New(adminClient.URL) + _, err := anonClient.GetChatDesktopEnabled(ctx) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode()) + }) +} + func requireSDKError(t *testing.T, err error, expectedStatus int) *codersdk.Error { t.Helper() diff --git a/coderd/coderd.go b/coderd/coderd.go index f1e8622a4b..247fef523f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1157,6 +1157,8 @@ func New(options *Options) *API { r.Route("/config", func(r chi.Router) { r.Get("/system-prompt", api.getChatSystemPrompt) r.Put("/system-prompt", api.putChatSystemPrompt) + r.Get("/desktop-enabled", api.getChatDesktopEnabled) + r.Put("/desktop-enabled", api.putChatDesktopEnabled) r.Get("/user-prompt", api.getUserChatCustomPrompt) r.Put("/user-prompt", api.putUserChatCustomPrompt) }) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index de456e5026..081982bacb 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2482,6 +2482,17 @@ func (q *querier) GetChatCostSummary(ctx context.Context, arg database.GetChatCo return q.db.GetChatCostSummary(ctx, arg) } +func (q *querier) GetChatDesktopEnabled(ctx context.Context) (bool, error) { + // The desktop-enabled flag is a deployment-wide setting read by any + // authenticated chat user and by chatd when deciding whether to expose + // computer-use tooling. We only require that an explicit actor is present + // in the context so unauthenticated calls fail closed. + if _, ok := ActorFromContext(ctx); !ok { + return false, ErrNoActor + } + return q.db.GetChatDesktopEnabled(ctx) +} + func (q *querier) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (database.ChatDiffStatus, error) { // Authorize read on the parent chat. _, err := q.GetChatByID(ctx, chatID) @@ -6548,6 +6559,13 @@ func (q *querier) UpsertBoundaryUsageStats(ctx context.Context, arg database.Ups return q.db.UpsertBoundaryUsageStats(ctx, arg) } +func (q *querier) UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { + return err + } + return q.db.UpsertChatDesktopEnabled(ctx, enableDesktop) +} + func (q *querier) UpsertChatDiffStatus(ctx context.Context, arg database.UpsertChatDiffStatusParams) (database.ChatDiffStatus, error) { // Authorize update on the parent chat. chat, err := q.db.GetChatByID(ctx, arg.ChatID) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 761d9cabf3..bb1659f054 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -641,6 +641,10 @@ func (s *MethodTestSuite) TestChats() { dbm.EXPECT().GetChatSystemPrompt(gomock.Any()).Return("prompt", nil).AnyTimes() check.Args().Asserts() })) + s.Run("GetChatDesktopEnabled", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetChatDesktopEnabled(gomock.Any()).Return(false, nil).AnyTimes() + check.Args().Asserts() + })) s.Run("GetEnabledChatModelConfigs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { configA := testutil.Fake(s.T(), faker, database.ChatModelConfig{}) configB := testutil.Fake(s.T(), faker, database.ChatModelConfig{}) @@ -850,6 +854,10 @@ func (s *MethodTestSuite) TestChats() { dbm.EXPECT().UpsertChatSystemPrompt(gomock.Any(), "").Return(nil).AnyTimes() check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) + s.Run("UpsertChatDesktopEnabled", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertChatDesktopEnabled(gomock.Any(), false).Return(nil).AnyTimes() + check.Args(false).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) + })) s.Run("GetUserChatSpendInPeriod", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { arg := database.GetUserChatSpendInPeriodParams{ UserID: uuid.New(), diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index e6769a697b..c7c9246fe9 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1048,6 +1048,14 @@ func (m queryMetricsStore) GetChatCostSummary(ctx context.Context, arg database. return r0, r1 } +func (m queryMetricsStore) GetChatDesktopEnabled(ctx context.Context) (bool, error) { + start := time.Now() + r0, r1 := m.s.GetChatDesktopEnabled(ctx) + m.queryLatencies.WithLabelValues("GetChatDesktopEnabled").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatDesktopEnabled").Inc() + return r0, r1 +} + func (m queryMetricsStore) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (database.ChatDiffStatus, error) { start := time.Now() r0, r1 := m.s.GetChatDiffStatusByChatID(ctx, chatID) @@ -4552,6 +4560,14 @@ func (m queryMetricsStore) UpsertBoundaryUsageStats(ctx context.Context, arg dat return r0, r1 } +func (m queryMetricsStore) UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error { + start := time.Now() + r0 := m.s.UpsertChatDesktopEnabled(ctx, enableDesktop) + m.queryLatencies.WithLabelValues("UpsertChatDesktopEnabled").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertChatDesktopEnabled").Inc() + return r0 +} + func (m queryMetricsStore) UpsertChatDiffStatus(ctx context.Context, arg database.UpsertChatDiffStatusParams) (database.ChatDiffStatus, error) { start := time.Now() r0, r1 := m.s.UpsertChatDiffStatus(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 45b6e95f89..8f950096e9 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1911,6 +1911,21 @@ func (mr *MockStoreMockRecorder) GetChatCostSummary(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatCostSummary", reflect.TypeOf((*MockStore)(nil).GetChatCostSummary), ctx, arg) } +// GetChatDesktopEnabled mocks base method. +func (m *MockStore) GetChatDesktopEnabled(ctx context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChatDesktopEnabled", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetChatDesktopEnabled indicates an expected call of GetChatDesktopEnabled. +func (mr *MockStoreMockRecorder) GetChatDesktopEnabled(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatDesktopEnabled", reflect.TypeOf((*MockStore)(nil).GetChatDesktopEnabled), ctx) +} + // GetChatDiffStatusByChatID mocks base method. func (m *MockStore) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (database.ChatDiffStatus, error) { m.ctrl.T.Helper() @@ -8510,6 +8525,20 @@ func (mr *MockStoreMockRecorder) UpsertBoundaryUsageStats(ctx, arg any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertBoundaryUsageStats", reflect.TypeOf((*MockStore)(nil).UpsertBoundaryUsageStats), ctx, arg) } +// UpsertChatDesktopEnabled mocks base method. +func (m *MockStore) UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertChatDesktopEnabled", ctx, enableDesktop) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertChatDesktopEnabled indicates an expected call of UpsertChatDesktopEnabled. +func (mr *MockStoreMockRecorder) UpsertChatDesktopEnabled(ctx, enableDesktop any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatDesktopEnabled", reflect.TypeOf((*MockStore)(nil).UpsertChatDesktopEnabled), ctx, enableDesktop) +} + // UpsertChatDiffStatus mocks base method. func (m *MockStore) UpsertChatDiffStatus(ctx context.Context, arg database.UpsertChatDiffStatusParams) (database.ChatDiffStatus, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index ea59c4775f..e93c8a4c04 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -234,6 +234,7 @@ type sqlcQuerier interface { // Aggregate cost summary for a single user within a date range. // Only counts assistant-role messages. GetChatCostSummary(ctx context.Context, arg GetChatCostSummaryParams) (GetChatCostSummaryRow, error) + GetChatDesktopEnabled(ctx context.Context) (bool, error) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (ChatDiffStatus, error) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds []uuid.UUID) ([]ChatDiffStatus, error) GetChatFileByID(ctx context.Context, id uuid.UUID) (ChatFile, error) @@ -865,6 +866,7 @@ type sqlcQuerier interface { // cumulative values for unique counts (accurate period totals). Request counts // are always deltas, accumulated in DB. Returns true if insert, false if update. UpsertBoundaryUsageStats(ctx context.Context, arg UpsertBoundaryUsageStatsParams) (bool, error) + UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error UpsertChatDiffStatus(ctx context.Context, arg UpsertChatDiffStatusParams) (ChatDiffStatus, error) UpsertChatDiffStatusReference(ctx context.Context, arg UpsertChatDiffStatusReferenceParams) (ChatDiffStatus, error) UpsertChatSystemPrompt(ctx context.Context, value string) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ad1eb7728d..08636649d2 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15409,6 +15409,18 @@ func (q *sqlQuerier) GetApplicationName(ctx context.Context) (string, error) { return value, err } +const getChatDesktopEnabled = `-- name: GetChatDesktopEnabled :one +SELECT + COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_desktop_enabled'), false) :: boolean AS enable_desktop +` + +func (q *sqlQuerier) GetChatDesktopEnabled(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, getChatDesktopEnabled) + var enable_desktop bool + err := row.Scan(&enable_desktop) + return enable_desktop, err +} + const getChatSystemPrompt = `-- name: GetChatSystemPrompt :one SELECT COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt @@ -15603,6 +15615,28 @@ func (q *sqlQuerier) UpsertApplicationName(ctx context.Context, value string) er return err } +const upsertChatDesktopEnabled = `-- name: UpsertChatDesktopEnabled :exec +INSERT INTO site_configs (key, value) +VALUES ( + 'agents_desktop_enabled', + CASE + WHEN $1::bool THEN 'true' + ELSE 'false' + END +) +ON CONFLICT (key) DO UPDATE +SET value = CASE + WHEN $1::bool THEN 'true' + ELSE 'false' +END +WHERE site_configs.key = 'agents_desktop_enabled' +` + +func (q *sqlQuerier) UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error { + _, err := q.db.ExecContext(ctx, upsertChatDesktopEnabled, enableDesktop) + return err +} + const upsertChatSystemPrompt = `-- name: UpsertChatSystemPrompt :exec INSERT INTO site_configs (key, value) VALUES ('agents_chat_system_prompt', $1) ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_system_prompt' diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql index 129f0271b9..4e33585c88 100644 --- a/coderd/database/queries/siteconfig.sql +++ b/coderd/database/queries/siteconfig.sql @@ -140,3 +140,23 @@ SELECT -- name: UpsertChatSystemPrompt :exec INSERT INTO site_configs (key, value) VALUES ('agents_chat_system_prompt', $1) ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_system_prompt'; + +-- name: GetChatDesktopEnabled :one +SELECT + COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_desktop_enabled'), false) :: boolean AS enable_desktop; + +-- name: UpsertChatDesktopEnabled :exec +INSERT INTO site_configs (key, value) +VALUES ( + 'agents_desktop_enabled', + CASE + WHEN sqlc.arg(enable_desktop)::bool THEN 'true' + ELSE 'false' + END +) +ON CONFLICT (key) DO UPDATE +SET value = CASE + WHEN sqlc.arg(enable_desktop)::bool THEN 'true' + ELSE 'false' +END +WHERE site_configs.key = 'agents_desktop_enabled'; diff --git a/codersdk/chats.go b/codersdk/chats.go index b1f28254e8..ce228bb492 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -320,6 +320,16 @@ type UserChatCustomPrompt struct { CustomPrompt string `json:"custom_prompt"` } +// ChatDesktopEnabledResponse is the response for getting the desktop setting. +type ChatDesktopEnabledResponse struct { + EnableDesktop bool `json:"enable_desktop"` +} + +// UpdateChatDesktopEnabledRequest is the request to update the desktop setting. +type UpdateChatDesktopEnabledRequest struct { + EnableDesktop bool `json:"enable_desktop"` +} + // ChatProviderConfigSource describes how a provider entry is sourced. type ChatProviderConfigSource string @@ -1270,6 +1280,33 @@ func (c *Client) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPro return resp, json.NewDecoder(res.Body).Decode(&resp) } +// GetChatDesktopEnabled returns the deployment-wide desktop setting. +func (c *Client) GetChatDesktopEnabled(ctx context.Context) (ChatDesktopEnabledResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/desktop-enabled", nil) + if err != nil { + return ChatDesktopEnabledResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ChatDesktopEnabledResponse{}, ReadBodyAsError(res) + } + var resp ChatDesktopEnabledResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// UpdateChatDesktopEnabled updates the deployment-wide desktop setting. +func (c *Client) UpdateChatDesktopEnabled(ctx context.Context, req UpdateChatDesktopEnabledRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/desktop-enabled", req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} + // UpdateUserChatCustomPrompt updates the user's custom chat prompt. func (c *Client) UpdateUserChatCustomPrompt(ctx context.Context, req UserChatCustomPrompt) (UserChatCustomPrompt, error) { res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/user-prompt", req) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 54eea1ddd8..03f729cacf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1170,6 +1170,14 @@ export interface ChatCostUsersResponse { readonly users: readonly ChatCostUserRollup[]; } +// From codersdk/chats.go +/** + * ChatDesktopEnabledResponse is the response for getting the desktop setting. + */ +export interface ChatDesktopEnabledResponse { + readonly enable_desktop: boolean; +} + // From codersdk/chats.go /** * ChatDiffContents represents the resolved diff text for a chat. @@ -6578,6 +6586,14 @@ export interface UpdateAppearanceConfig { readonly announcement_banners: readonly BannerConfig[]; } +// From codersdk/chats.go +/** + * UpdateChatDesktopEnabledRequest is the request to update the desktop setting. + */ +export interface UpdateChatDesktopEnabledRequest { + readonly enable_desktop: boolean; +} + // From codersdk/chats.go /** * UpdateChatModelConfigRequest updates a chat model config.