diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 6e0144a435..1e1df13600 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -3079,6 +3079,21 @@ class ApiMethods {
await this.axios.put("/api/experimental/chats/config/system-prompt", req);
};
+ getChatDesktopEnabled =
+ async (): Promise => {
+ const response =
+ await this.axios.get(
+ "/api/experimental/chats/config/desktop-enabled",
+ );
+ return response.data;
+ };
+
+ updateChatDesktopEnabled = async (
+ req: TypesGen.UpdateChatDesktopEnabledRequest,
+ ): Promise => {
+ await this.axios.put("/api/experimental/chats/config/desktop-enabled", req);
+ };
+
getUserChatCustomPrompt =
async (): Promise => {
const response = await this.axios.get(
diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts
index e9b01f53d7..a8067348e4 100644
--- a/site/src/api/queries/chats.ts
+++ b/site/src/api/queries/chats.ts
@@ -395,6 +395,22 @@ export const updateChatSystemPrompt = (queryClient: QueryClient) => ({
},
});
+const chatDesktopEnabledKey = ["chat-desktop-enabled"] as const;
+
+export const chatDesktopEnabled = () => ({
+ queryKey: chatDesktopEnabledKey,
+ queryFn: () => API.getChatDesktopEnabled(),
+});
+
+export const updateChatDesktopEnabled = (queryClient: QueryClient) => ({
+ mutationFn: API.updateChatDesktopEnabled,
+ onSuccess: async () => {
+ await queryClient.invalidateQueries({
+ queryKey: chatDesktopEnabledKey,
+ });
+ },
+});
+
const chatUserCustomPromptKey = ["chat-user-custom-prompt"] as const;
export const chatUserCustomPrompt = () => ({
diff --git a/site/src/pages/AgentsPage/AgentDetail.tsx b/site/src/pages/AgentsPage/AgentDetail.tsx
index be14a44d70..0c01d375dd 100644
--- a/site/src/pages/AgentsPage/AgentDetail.tsx
+++ b/site/src/pages/AgentsPage/AgentDetail.tsx
@@ -2,6 +2,7 @@ import { API, watchWorkspace } from "api/api";
import { isApiError } from "api/errors";
import {
chat,
+ chatDesktopEnabled,
chatMessagesForInfiniteScroll,
chatModelConfigs,
chatModels,
@@ -317,6 +318,7 @@ const AgentDetail: FC = () => {
}, [workspaceId, queryClient]);
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
+ const desktopEnabledQuery = useQuery(chatDesktopEnabled());
const sshConfigQuery = useQuery(deploymentSSHConfig());
const workspace = workspaceQuery.data;
const workspaceAgent = getWorkspaceAgent(workspace, undefined);
@@ -901,7 +903,9 @@ const AgentDetail: FC = () => {
hasMoreMessages={chatMessagesQuery.hasNextPage ?? false}
isFetchingMoreMessages={chatMessagesQuery.isFetchingNextPage}
onFetchMoreMessages={chatMessagesQuery.fetchNextPage}
- desktopChatId={agentId}
+ desktopChatId={
+ desktopEnabledQuery.data?.enable_desktop ? agentId : undefined
+ }
/>
);
};
diff --git a/site/src/pages/AgentsPage/SettingsPageContent.stories.tsx b/site/src/pages/AgentsPage/SettingsPageContent.stories.tsx
new file mode 100644
index 0000000000..61ce58062b
--- /dev/null
+++ b/site/src/pages/AgentsPage/SettingsPageContent.stories.tsx
@@ -0,0 +1,70 @@
+import { MockUserOwner } from "testHelpers/entities";
+import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { API } from "api/api";
+import dayjs from "dayjs";
+import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
+import { SettingsPageContent } from "./SettingsPageContent";
+
+const meta = {
+ title: "pages/AgentsPage/SettingsPageContent",
+ component: SettingsPageContent,
+ decorators: [withAuthProvider, withDashboardProvider],
+ args: {
+ activeSection: "behavior",
+ canManageChatModelConfigs: false,
+ canSetSystemPrompt: true,
+ now: dayjs("2026-03-12T00:00:00Z"),
+ },
+ parameters: {
+ user: MockUserOwner,
+ layout: "fullscreen",
+ },
+ beforeEach: () => {
+ spyOn(API, "getChatSystemPrompt").mockResolvedValue({
+ system_prompt: "",
+ });
+ spyOn(API, "updateChatSystemPrompt").mockResolvedValue();
+ spyOn(API, "getChatDesktopEnabled").mockResolvedValue({
+ enable_desktop: false,
+ });
+ spyOn(API, "updateChatDesktopEnabled").mockResolvedValue();
+ spyOn(API, "getUserChatCustomPrompt").mockResolvedValue({
+ custom_prompt: "",
+ });
+ spyOn(API, "updateUserChatCustomPrompt").mockResolvedValue({
+ custom_prompt: "",
+ });
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const DesktopSetting: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await canvas.findByText("Virtual Desktop");
+ await canvas.findByText(
+ /Allow agents to use a virtual, graphical desktop/i,
+ );
+ await canvas.findByRole("switch", { name: "Enable" });
+ },
+};
+
+export const TogglesDesktop: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const toggle = await canvas.findByRole("switch", {
+ name: "Enable",
+ });
+
+ await userEvent.click(toggle);
+ await waitFor(() => {
+ expect(API.updateChatDesktopEnabled).toHaveBeenCalledWith({
+ enable_desktop: true,
+ });
+ });
+ },
+};
diff --git a/site/src/pages/AgentsPage/SettingsPageContent.tsx b/site/src/pages/AgentsPage/SettingsPageContent.tsx
index d8d819ef1a..8cc82ad465 100644
--- a/site/src/pages/AgentsPage/SettingsPageContent.tsx
+++ b/site/src/pages/AgentsPage/SettingsPageContent.tsx
@@ -2,18 +2,22 @@ import { getErrorMessage } from "api/errors";
import {
chatCostSummary,
chatCostUsers,
+ chatDesktopEnabled,
chatSystemPrompt,
chatUserCustomPrompt,
+ updateChatDesktopEnabled,
updateChatSystemPrompt,
updateUserChatCustomPrompt,
} from "api/queries/chats";
import type * as TypesGen from "api/typesGenerated";
import { AvatarData } from "components/Avatar/AvatarData";
import { Button } from "components/Button/Button";
+import { Link } from "components/Link/Link";
import { PaginationAmount } from "components/PaginationWidget/PaginationAmount";
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase";
import { SearchField } from "components/SearchField/SearchField";
import { Spinner } from "components/Spinner/Spinner";
+import { Switch } from "components/Switch/Switch";
import {
Table,
TableBody,
@@ -31,7 +35,7 @@ import {
import dayjs from "dayjs";
import { useDebouncedValue } from "hooks/debounce";
import { useClickableTableRow } from "hooks/useClickableTableRow";
-import { ShieldIcon } from "lucide-react";
+import { FlaskConicalIcon, ShieldIcon } from "lucide-react";
import { type FC, type FormEvent, useCallback, useMemo, useState } from "react";
import {
keepPreviousData,
@@ -63,6 +67,20 @@ const AdminBadge: FC = () => (
);
+const ExperimentBadge: FC = () => (
+
+
+
+
+
+ Experiment
+
+
+ Experimental feature.
+
+
+);
+
const pageSize = 10;
const UserRow: FC<{
@@ -355,6 +373,13 @@ export const SettingsPageContent: FC = ({
isError: isSaveUserPromptError,
} = useMutation(updateUserChatCustomPrompt(queryClient));
+ const desktopEnabledQuery = useQuery(chatDesktopEnabled());
+ const {
+ mutate: saveDesktopEnabled,
+ isPending: isSavingDesktopEnabled,
+ isError: isSaveDesktopEnabledError,
+ } = useMutation(updateChatDesktopEnabled(queryClient));
+
const serverPrompt = systemPromptQuery.data?.system_prompt ?? "";
const [localEdit, setLocalEdit] = useState(null);
const systemPromptDraft = localEdit ?? serverPrompt;
@@ -366,7 +391,9 @@ export const SettingsPageContent: FC = ({
const isSystemPromptDirty = localEdit !== null && localEdit !== serverPrompt;
const isUserPromptDirty =
localUserEdit !== null && localUserEdit !== serverUserPrompt;
- const isDisabled = isSavingSystemPrompt || isSavingUserPrompt;
+ const desktopEnabled = desktopEnabledQuery.data?.enable_desktop ?? false;
+ const isDisabled =
+ isSavingSystemPrompt || isSavingUserPrompt || isSavingDesktopEnabled;
const handleSaveSystemPrompt = useCallback(
(event: FormEvent) => {
@@ -495,6 +522,50 @@ export const SettingsPageContent: FC = ({
)}
+
+
+
+
+ Virtual Desktop
+
+
+
+
+
+
+
+ Allow agents to use a virtual, graphical desktop within
+ workspaces. Requires the{" "}
+
+ portabledesktop module
+ {" "}
+ to be installed in the workspace and the Anthropic
+ provider to be configured.
+
+
+ Warning: This is an experimental, in-progress feature,
+ and you’re likely to encounter bugs if you enable it.
+
+
+
+ saveDesktopEnabled({ enable_desktop: checked })
+ }
+ aria-label="Enable"
+ disabled={isDisabled}
+ />
+
+ {isSaveDesktopEnabledError && (
+
+ Failed to save desktop setting.
+
+ )}
+
>
)}
>
diff --git a/site/src/pages/AgentsPage/SidebarTabView.stories.tsx b/site/src/pages/AgentsPage/SidebarTabView.stories.tsx
index 936cc5380a..6105c49ca5 100644
--- a/site/src/pages/AgentsPage/SidebarTabView.stories.tsx
+++ b/site/src/pages/AgentsPage/SidebarTabView.stories.tsx
@@ -74,6 +74,13 @@ export const EmptyState: Story = {
},
};
+export const DesktopHidden: Story = {
+ args: {
+ tabs: [],
+ desktopChatId: undefined,
+ },
+};
+
export const ExpandedWithTitle: Story = {
args: {
tabs: [gitTab],