From 509784a2a37521201293b9f38fd41ee51c2de1e7 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 16 Apr 2026 19:46:26 +0700 Subject: [PATCH] chore: improve design of agents behavior settings page (#24324) closes CODAGT-153 FIgma design: https://www.figma.com/design/oRDrsFh4guBgQUzLhRTPSe/agent-thoughts?node-id=60-8699&m=dev --- .../AgentSettingsAPIKeysPageView.tsx | 6 +- .../AgentSettingsBehaviorPageView.stories.tsx | 11 +- .../AgentSettingsBehaviorPageView.tsx | 17 +- .../AgentSettingsTemplatesPageView.tsx | 2 +- .../AgentsPage/components/AdminBadge.tsx | 6 +- .../components/ChatFullWidthSettings.tsx | 6 +- .../ChatModelAdminPanel.tsx | 4 +- .../PersonalInstructionsSettings.tsx | 64 ++++-- .../components/RetentionPeriodSettings.tsx | 66 +++--- .../AgentsPage/components/SectionHeader.tsx | 19 +- .../components/SystemInstructionsSettings.tsx | 66 ++++-- .../components/TemporarySavedState.tsx | 44 ++++ ...serCompactionThresholdSettings.stories.tsx | 28 +-- .../UserCompactionThresholdSettings.tsx | 202 ++++++++++-------- .../components/VirtualDesktopSettings.tsx | 52 ++--- .../components/WorkspaceAutostopSettings.tsx | 59 +++-- 16 files changed, 394 insertions(+), 258 deletions(-) create mode 100644 site/src/pages/AgentsPage/components/TemporarySavedState.tsx diff --git a/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx index 9497c82137..8cb7462195 100644 --- a/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx @@ -246,13 +246,13 @@ export const AgentSettingsAPIKeysPageView: FC< onRemove, }) => { return ( -
-
+
+
-
+
{error ? ( ) : isLoading ? ( diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx index 6d2f289fae..ce53f5bc0d 100644 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx @@ -230,10 +230,12 @@ export const DefaultAutostopSave: Story = { expect(saveButton).toBeEnabled(); }); - // Clearing to 0 should disable Save because toggle is still ON. + // Clearing back to the original value hides Save (pristine form). await userEvent.clear(durationInput); await waitFor(() => { - expect(saveButton).toBeDisabled(); + expect( + within(ttlForm).queryByRole("button", { name: "Save" }), + ).toBeNull(); }); }, }; @@ -303,10 +305,7 @@ export const DefaultAutostopSaveDisabled: Story = { expect(durationInput).toHaveValue("2"); const ttlForm = durationInput.closest("form")!; - const saveButton = within(ttlForm).getByRole("button", { - name: "Save", - }); - expect(saveButton).toBeDisabled(); + expect(within(ttlForm).queryByRole("button", { name: "Save" })).toBeNull(); }, }; diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx index 550f8b725e..26eb9b118e 100644 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx @@ -134,12 +134,11 @@ export const AgentSettingsBehaviorPageView: FC< isSavingSystemPrompt || isSavingUserPrompt || isSavingPlanModeInstructions; return ( - <> +
- - -
- -
-
-
-
- -
- -
)} - +
); }; diff --git a/site/src/pages/AgentsPage/AgentSettingsTemplatesPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsTemplatesPageView.tsx index 95754aae34..6ca0dff2a9 100644 --- a/site/src/pages/AgentsPage/AgentSettingsTemplatesPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsTemplatesPageView.tsx @@ -80,7 +80,7 @@ export const AgentSettingsTemplatesPageView: FC< }; return ( -
+
( - + - + - Admin + Admin only diff --git a/site/src/pages/AgentsPage/components/ChatFullWidthSettings.tsx b/site/src/pages/AgentsPage/components/ChatFullWidthSettings.tsx index bc000913fd..4a58f77f25 100644 --- a/site/src/pages/AgentsPage/components/ChatFullWidthSettings.tsx +++ b/site/src/pages/AgentsPage/components/ChatFullWidthSettings.tsx @@ -6,12 +6,12 @@ export const ChatFullWidthSettings: FC = () => { const [enabled, setEnabled] = useChatFullWidth(); return ( -
-

+
+

Chat Layout

-

+

Use full-width layout for agent chat messages, removing the default max-width constraint.

diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.tsx b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.tsx index 05bafae5ee..67cb16e0b3 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.tsx +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.tsx @@ -285,7 +285,7 @@ export const ChatModelAdminPanel: FC = ({ const modelConfigsUnavailable = modelConfigsData === null; return ( -
+
{isLoading && (
@@ -294,7 +294,7 @@ export const ChatModelAdminPanel: FC = ({ )} {/* Content */} -
+
{section === "providers" ? ( void; @@ -29,10 +34,12 @@ export const PersonalInstructionsSettings: FC< > = ({ userPromptData, onSaveUserPrompt, + isSavingUserPrompt, isSaveUserPromptError, isAnyPromptSaving, }) => { const [isUserPromptOverflowing, setIsUserPromptOverflowing] = useState(false); + const { isSavedVisible, showSavedState } = useTemporarySavedState(); const form = useFormik({ initialValues: { @@ -42,7 +49,12 @@ export const PersonalInstructionsSettings: FC< onSubmit: (values, helpers) => { onSaveUserPrompt( { custom_prompt: values.custom_prompt }, - { onSuccess: () => helpers.resetForm() }, + { + onSuccess: () => { + showSavedState(); + helpers.resetForm(); + }, + }, ); }, }); @@ -52,16 +64,16 @@ export const PersonalInstructionsSettings: FC< ); return ( -
-

+ +

Personal Instructions

-

+

Applied to all your conversations. Only visible to you.

)} -
- - +
+ {(form.dirty || isSavedVisible || isSavingUserPrompt) && + (isSavedVisible ? ( + + ) : ( + <> + + + + ))}
{isSaveUserPromptError && (

diff --git a/site/src/pages/AgentsPage/components/RetentionPeriodSettings.tsx b/site/src/pages/AgentsPage/components/RetentionPeriodSettings.tsx index 2450887701..d1e06103ab 100644 --- a/site/src/pages/AgentsPage/components/RetentionPeriodSettings.tsx +++ b/site/src/pages/AgentsPage/components/RetentionPeriodSettings.tsx @@ -4,8 +4,14 @@ import { useState } from "react"; import * as Yup from "yup"; import type * as TypesGen from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; +import { Input } from "#/components/Input/Input"; +import { Spinner } from "#/components/Spinner/Spinner"; import { Switch } from "#/components/Switch/Switch"; import { AdminBadge } from "./AdminBadge"; +import { + TemporarySavedState, + useTemporarySavedState, +} from "./TemporarySavedState"; interface MutationCallbacks { onSuccess?: () => void; @@ -44,6 +50,7 @@ export const RetentionPeriodSettings: FC = ({ const [retentionToggled, setRetentionToggled] = useState( null, ); + const { isSavedVisible, showSavedState } = useTemporarySavedState(); const serverRetentionDays = retentionDaysData?.retention_days ?? 30; const isRetentionEnabled = retentionToggled ?? serverRetentionDays > 0; @@ -57,6 +64,7 @@ export const RetentionPeriodSettings: FC = ({ { retention_days: values.retention_days }, { onSuccess: () => { + showSavedState(); setRetentionToggled(null); helpers.resetForm(); }, @@ -96,18 +104,14 @@ export const RetentionPeriodSettings: FC = ({ }; return ( - -

-

- Conversation Retention Period -

- -
+
-

- Archived conversations and orphaned files older than this are - automatically deleted. -

+
+

+ Conversation Retention Period +

+ +
= ({ disabled={isSavingRetentionDays || isRetentionDaysLoading} />
+

+ Archived conversations and orphaned files older than this are + automatically deleted. +

{isRetentionEnabled && ( <> - = ({ value={form.values.retention_days} onChange={form.handleChange} disabled={isSavingRetentionDays || isRetentionDaysLoading} - className="w-full rounded-lg border border-border bg-surface-primary px-4 py-2 text-[13px] text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30" + className="w-full" /> {form.errors.retention_days && form.touched.retention_days && (

{form.errors.retention_days}

)} -
- +
+ {(form.dirty || isSavedVisible || isSavingRetentionDays) && + (isSavedVisible ? ( + + ) : ( + + ))}
)} diff --git a/site/src/pages/AgentsPage/components/SectionHeader.tsx b/site/src/pages/AgentsPage/components/SectionHeader.tsx index 1ab7793f31..fa0608f21b 100644 --- a/site/src/pages/AgentsPage/components/SectionHeader.tsx +++ b/site/src/pages/AgentsPage/components/SectionHeader.tsx @@ -28,18 +28,15 @@ export const SectionHeader: FC = ({ : "m-0 mt-0.5 text-sm text-content-secondary"; return ( - <> -
-
-
- {label} - {badge} -
- {description &&

{description}

} +
+
+
+ {label} + {badge}
- {action} + {description &&

{description}

}
-
- + {action} +
); }; diff --git a/site/src/pages/AgentsPage/components/SystemInstructionsSettings.tsx b/site/src/pages/AgentsPage/components/SystemInstructionsSettings.tsx index 397fafe31d..479ebc726d 100644 --- a/site/src/pages/AgentsPage/components/SystemInstructionsSettings.tsx +++ b/site/src/pages/AgentsPage/components/SystemInstructionsSettings.tsx @@ -5,10 +5,15 @@ import TextareaAutosize from "react-textarea-autosize"; import type * as TypesGen from "#/api/typesGenerated"; import { Alert, AlertDescription } from "#/components/Alert/Alert"; import { Button } from "#/components/Button/Button"; +import { Spinner } from "#/components/Spinner/Spinner"; import { Switch } from "#/components/Switch/Switch"; import { cn } from "#/utils/cn"; import { countInvisibleCharacters } from "#/utils/invisibleUnicode"; import { AdminBadge } from "./AdminBadge"; +import { + TemporarySavedState, + useTemporarySavedState, +} from "./TemporarySavedState"; import { TextPreviewDialog } from "./TextPreviewDialog"; interface MutationCallbacks { @@ -22,6 +27,7 @@ interface SystemInstructionsSettingsProps { req: TypesGen.UpdateChatSystemPromptRequest, options?: MutationCallbacks, ) => void; + isSavingSystemPrompt: boolean; isSaveSystemPromptError: boolean; isAnyPromptSaving: boolean; } @@ -31,6 +37,7 @@ export const SystemInstructionsSettings: FC< > = ({ systemPromptData, onSaveSystemPrompt, + isSavingSystemPrompt, isSaveSystemPromptError, isAnyPromptSaving, }) => { @@ -38,6 +45,7 @@ export const SystemInstructionsSettings: FC< useState(false); const [isSystemPromptOverflowing, setIsSystemPromptOverflowing] = useState(false); + const { isSavedVisible, showSavedState } = useTemporarySavedState(); const hasLoadedSystemPrompt = systemPromptData !== undefined; const defaultSystemPrompt = systemPromptData?.default_system_prompt ?? ""; @@ -52,6 +60,7 @@ export const SystemInstructionsSettings: FC< onSubmit: (values, { resetForm }) => { onSaveSystemPrompt(values, { onSuccess: () => { + showSavedState(); resetForm(); }, }); @@ -65,9 +74,9 @@ export const SystemInstructionsSettings: FC< return ( <> - +
-

+

System Instructions

@@ -102,7 +111,7 @@ export const SystemInstructionsSettings: FC<

)} -
- - +
+ {(form.dirty || isSavedVisible || isSavingSystemPrompt) && + (isSavedVisible ? ( + + ) : ( + <> + + + + ))}
{isSaveSystemPromptError && (

diff --git a/site/src/pages/AgentsPage/components/TemporarySavedState.tsx b/site/src/pages/AgentsPage/components/TemporarySavedState.tsx new file mode 100644 index 0000000000..a19ba31a66 --- /dev/null +++ b/site/src/pages/AgentsPage/components/TemporarySavedState.tsx @@ -0,0 +1,44 @@ +import { CheckIcon } from "lucide-react"; +import type { FC } from "react"; +import { useEffect, useRef, useState } from "react"; + +export const useTemporarySavedState = ( + durationMs = 2500, +): { + isSavedVisible: boolean; + showSavedState: () => void; +} => { + const [isSavedVisible, setIsSavedVisible] = useState(false); + const timeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const showSavedState = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setIsSavedVisible(true); + timeoutRef.current = setTimeout(() => { + setIsSavedVisible(false); + timeoutRef.current = null; + }, durationMs); + }; + + return { isSavedVisible, showSavedState }; +}; + +export const TemporarySavedState: FC = () => ( +

+ + Saved +
+); diff --git a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx index 126007fa41..3d6cf8b84d 100644 --- a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx +++ b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx @@ -70,7 +70,7 @@ type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const gpt4oInput = await canvas.findByRole("spinbutton", { + const gpt4oInput = await canvas.findByRole("textbox", { name: /GPT-4o compaction threshold/i, }); @@ -96,10 +96,10 @@ export const Default: Story = { export const SaveAll: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const gpt4oInput = await canvas.findByRole("spinbutton", { + const gpt4oInput = await canvas.findByRole("textbox", { name: /GPT-4o compaction threshold/i, }); - const claudeInput = await canvas.findByRole("spinbutton", { + const claudeInput = await canvas.findByRole("textbox", { name: /Claude Sonnet compaction threshold/i, }); @@ -129,15 +129,15 @@ export const WithOverrides: Story = { }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const gpt4oInput = await canvas.findByRole("spinbutton", { + const gpt4oInput = await canvas.findByRole("textbox", { name: /GPT-4o compaction threshold/i, }); - const claudeInput = await canvas.findByRole("spinbutton", { + const claudeInput = await canvas.findByRole("textbox", { name: /Claude Sonnet compaction threshold/i, }); - expect(gpt4oInput).toHaveValue(90); - expect(claudeInput).toHaveValue(50); + expect(gpt4oInput).toHaveValue("90"); + expect(claudeInput).toHaveValue("50"); // Reset buttons should be visible for both overridden models const resetButtons = canvas.getAllByRole("button", { @@ -155,7 +155,7 @@ export const WithOverrides: Story = { export const CancelChanges: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const gpt4oInput = await canvas.findByRole("spinbutton", { + const gpt4oInput = await canvas.findByRole("textbox", { name: /GPT-4o compaction threshold/i, }); @@ -171,7 +171,7 @@ export const CancelChanges: Story = { }); // Input should be cleared back to empty (no override) - expect(gpt4oInput).toHaveValue(null); + expect(gpt4oInput).toHaveValue(""); }, }; @@ -179,11 +179,11 @@ export const InvalidDraftShowsFooter: Story = { name: "Invalid Draft Shows Footer", play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const gpt4oInput = await canvas.findByRole("spinbutton", { + const gpt4oInput = await canvas.findByRole("textbox", { name: /GPT-4o compaction threshold/i, }); - // Type an out-of-range value (number inputs reject non-numeric chars) + // Type an out-of-range value. await userEvent.type(gpt4oInput, "150"); // Input should be marked invalid @@ -205,7 +205,7 @@ export const DisableCompactionWarning: Story = { name: "100% Disable Compaction Warning", play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const gpt4oInput = await canvas.findByRole("spinbutton", { + const gpt4oInput = await canvas.findByRole("textbox", { name: /GPT-4o compaction threshold/i, }); @@ -239,10 +239,10 @@ export const PartialSaveFailure: Story = { }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const gpt4oInput = await canvas.findByRole("spinbutton", { + const gpt4oInput = await canvas.findByRole("textbox", { name: /GPT-4o compaction threshold/i, }); - const claudeInput = await canvas.findByRole("spinbutton", { + const claudeInput = await canvas.findByRole("textbox", { name: /Claude Sonnet compaction threshold/i, }); diff --git a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx index e35c6a766b..ec0f93f2d8 100644 --- a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx +++ b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx @@ -2,6 +2,7 @@ import { RotateCcwIcon } from "lucide-react"; import { type FC, useState } from "react"; import { getErrorMessage } from "#/api/errors"; import type * as TypesGen from "#/api/typesGenerated"; +import { Badge } from "#/components/Badge/Badge"; import { Button } from "#/components/Button/Button"; import { Input } from "#/components/Input/Input"; import { Spinner } from "#/components/Spinner/Spinner"; @@ -20,6 +21,11 @@ import { TooltipTrigger, } from "#/components/Tooltip/Tooltip"; import { cn } from "#/utils/cn"; +import { ProviderIcon } from "./ChatModelAdminPanel/ProviderIcon"; +import { + TemporarySavedState, + useTemporarySavedState, +} from "./TemporarySavedState"; interface UserCompactionThresholdSettingsProps { modelConfigs: readonly TypesGen.ChatModelConfig[]; @@ -49,6 +55,18 @@ const parseThresholdDraft = (value: string): number | null => { return parsedValue; }; +const ContextCompactionHeader: FC = () => ( +
+

+ Context Compaction +

+

+ Control when conversation context is automatically summarized for each + model. Setting 100% means the conversation will never auto-compact. +

+
+); + export const UserCompactionThresholdSettings: FC< UserCompactionThresholdSettingsProps > = ({ @@ -64,6 +82,7 @@ export const UserCompactionThresholdSettings: FC< const [drafts, setDrafts] = useState>({}); const [rowErrors, setRowErrors] = useState>({}); const [pendingModels, setPendingModels] = useState>(new Set()); + const { isSavedVisible, showSavedState } = useTemporarySavedState(); const enabledModelConfigs = modelConfigs.filter((config) => config.enabled); const overridesByModelID = new Map( @@ -149,6 +168,7 @@ export const UserCompactionThresholdSettings: FC< .then(() => { clearDraft(modelConfigId); clearRowError(modelConfigId); + return true; }) .catch((error: unknown) => { setRowErrors((currentErrors) => ({ @@ -158,12 +178,17 @@ export const UserCompactionThresholdSettings: FC< "Failed to save compaction threshold.", ), })); + return false; }) .finally(() => { removePending(modelConfigId); }); }); - void Promise.allSettled(saves); + void Promise.all(saves).then((results) => { + if (results.length > 0 && results.every(Boolean)) { + showSavedState(); + } + }); }; const handleCancelAll = () => { @@ -174,17 +199,13 @@ export const UserCompactionThresholdSettings: FC< const hasAnyPending = pendingModels.size > 0; const hasAnyErrors = Object.keys(rowErrors).length > 0; const hasAnyDrafts = Object.keys(drafts).length > 0; + const shouldShowActions = + hasAnyDrafts || hasAnyErrors || hasAnyPending || dirtyRows.length > 0; if (isThresholdsLoading) { return ( -
-

- Context Compaction -

-

- Control when conversation context is automatically summarized for each - model. Setting 100% means the conversation will never auto-compact. -

+
+
Loading thresholds... @@ -195,14 +216,8 @@ export const UserCompactionThresholdSettings: FC< if (thresholdsError != null) { return ( -
-

- Context Compaction -

-

- Control when conversation context is automatically summarized for each - model. Setting 100% means the conversation will never auto-compact. -

+
+

{getErrorMessage( thresholdsError, @@ -214,14 +229,8 @@ export const UserCompactionThresholdSettings: FC< } return ( -

-

- Context Compaction -

-

- Control when conversation context is automatically summarized for each - model. Setting 100% means the conversation will never auto-compact. -

+
+ {isLoadingModelConfigs ? (
@@ -243,7 +252,7 @@ export const UserCompactionThresholdSettings: FC< - Model + Model Default Threshold @@ -270,8 +279,14 @@ export const UserCompactionThresholdSettings: FC< return ( - - {modelName} + + + + {modelName} + {rowError && (

- { - setDrafts((currentDrafts) => ({ - ...currentDrafts, - [modelConfig.id]: event.target.value, - })); - clearRowError(modelConfig.id); - }} - disabled={isThisModelMutating} - /> +

+ { + setDrafts((currentDrafts) => ({ + ...currentDrafts, + [modelConfig.id]: event.target.value, + })); + clearRowError(modelConfig.id); + }} + disabled={isThisModelMutating} + /> + +
{(isInvalid || isDraftDisablingCompaction) && ( @@ -322,7 +346,6 @@ export const UserCompactionThresholdSettings: FC< )} - % - {dirtyRows.length > 0 && ( - - )} - -
-
- - )} + + + +
+ {isSavedVisible ? ( + + ) : ( + shouldShowActions && ( + <> + + {dirtyRows.length > 0 && ( + + )} + + ) + )} +
+
+
+
)}
diff --git a/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx b/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx index a2260da34c..0e88edcc04 100644 --- a/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx +++ b/site/src/pages/AgentsPage/components/VirtualDesktopSettings.tsx @@ -1,5 +1,7 @@ +import { TriangleAlertIcon } from "lucide-react"; import type { FC } from "react"; import type * as TypesGen from "#/api/typesGenerated"; +import { Badge } from "#/components/Badge/Badge"; import { Link } from "#/components/Link/Link"; import { Switch } from "#/components/Switch/Switch"; import { AdminBadge } from "./AdminBadge"; @@ -28,32 +30,17 @@ export const VirtualDesktopSettings: FC = ({ const desktopEnabled = desktopEnabledData?.enable_desktop ?? false; return ( -
-
-

- 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 a work-in-progress feature, and you're likely to - encounter bugs if you enable it. -

+
+

+ Virtual Desktop +

+ + + + Experimental feature +
= ({ disabled={isSavingDesktopEnabled} />
+
+

+ 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. +

+
{isSaveDesktopEnabledError && (

Failed to save desktop setting. diff --git a/site/src/pages/AgentsPage/components/WorkspaceAutostopSettings.tsx b/site/src/pages/AgentsPage/components/WorkspaceAutostopSettings.tsx index 86f7c9cc4b..affc379af2 100644 --- a/site/src/pages/AgentsPage/components/WorkspaceAutostopSettings.tsx +++ b/site/src/pages/AgentsPage/components/WorkspaceAutostopSettings.tsx @@ -4,9 +4,14 @@ import { useState } from "react"; import * as Yup from "yup"; import type * as TypesGen from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; +import { Spinner } from "#/components/Spinner/Spinner"; import { Switch } from "#/components/Switch/Switch"; import { AdminBadge } from "./AdminBadge"; import { DurationField } from "./DurationField/DurationField"; +import { + TemporarySavedState, + useTemporarySavedState, +} from "./TemporarySavedState"; interface MutationCallbacks { onSuccess?: () => void; @@ -37,6 +42,7 @@ export const WorkspaceAutostopSettings: FC = ({ }) => { // ── Toggle state (fires immediate mutations, not a form submit) ── const [autostopToggled, setAutostopToggled] = useState(null); + const { isSavedVisible, showSavedState } = useTemporarySavedState(); // ── Derived state ── const serverTTLMs = workspaceTTLData?.workspace_ttl_ms ?? 0; @@ -63,6 +69,7 @@ export const WorkspaceAutostopSettings: FC = ({ { workspace_ttl_ms: values.workspace_ttl_ms }, { onSuccess: () => { + showSavedState(); setAutostopToggled(null); helpers.resetForm(); }, @@ -111,19 +118,14 @@ export const WorkspaceAutostopSettings: FC = ({ const fieldError = form.errors.workspace_ttl_ms; return ( - -

-

- Workspace Autostop Fallback -

- -
+
-

- Set a default autostop for agent-created workspaces that don't have - one defined in their template. Template-defined autostop rules always - take precedence. Active conversations will extend the stop time. -

+
+

+ Workspace Autostop Fallback +

+ +
= ({ disabled={isSavingWorkspaceTTL || isWorkspaceTTLLoading} />
+

+ Set a default autostop for agent-created workspaces that don't have one + defined in their template. Template-defined autostop rules always take + precedence. Active conversations will extend the stop time. +

{isAutostopEnabled && ( = ({ /> )} {isAutostopEnabled && ( -
- +
+ {(form.dirty || isSavedVisible || isSavingWorkspaceTTL) && + (isSavedVisible ? ( + + ) : ( + + ))}
)} {isSaveWorkspaceTTLError && (