From cc2b8d537049e2e83e2a4640f059c6613bc445e6 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:51:42 +0000 Subject: [PATCH 01/22] refactor(site/src): split agent settings pages --- .../AgentsPage/AgentSettingsBehaviorPage.tsx | 124 +---- .../AgentSettingsBehaviorPageView.stories.tsx | 475 ------------------ .../AgentSettingsBehaviorPageView.tsx | 203 -------- .../AgentSettingsCompactionPage.tsx | 48 ++ ...gentSettingsCompactionPageView.stories.tsx | 47 ++ .../AgentSettingsCompactionPageView.tsx | 50 ++ .../AgentSettingsExperimentsPage.tsx | 34 ++ ...entSettingsExperimentsPageView.stories.tsx | 24 + .../AgentSettingsExperimentsPageView.tsx | 45 ++ .../AgentsPage/AgentSettingsGeneralPage.tsx | 26 + .../AgentSettingsGeneralPageView.stories.tsx | 26 + .../AgentSettingsGeneralPageView.tsx | 48 ++ .../AgentsPage/AgentSettingsLifecyclePage.tsx | 51 ++ ...AgentSettingsLifecyclePageView.stories.tsx | 32 ++ .../AgentSettingsLifecyclePageView.tsx | 75 +++ .../AgentSettingsSystemInstructionsPage.tsx | 52 ++ ...ingsSystemInstructionsPageView.stories.tsx | 35 ++ ...gentSettingsSystemInstructionsPageView.tsx | 69 +++ .../AgentsPage/AgentsPageView.stories.tsx | 122 ++--- .../components/Sidebar/AgentsSidebar.tsx | 42 +- site/src/router.tsx | 31 +- 21 files changed, 785 insertions(+), 874 deletions(-) delete mode 100644 site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx delete mode 100644 site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsExperimentsPage.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsGeneralPage.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsLifecyclePage.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPage.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx create mode 100644 site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx index 49ca5609aadae..f298e47a1484e 100644 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx @@ -1,128 +1,8 @@ import type { FC } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { - chatDesktopEnabled, - chatModelConfigs, - chatPlanModeInstructions, - chatRetentionDays, - chatSystemPrompt, - chatUserCustomPrompt, - chatWorkspaceTTL, - deleteUserCompactionThreshold, - updateChatDesktopEnabled, - updateChatPlanModeInstructions, - updateChatRetentionDays, - updateChatSystemPrompt, - updateChatWorkspaceTTL, - updateUserChatCustomPrompt, - updateUserCompactionThreshold, - userCompactionThresholds, -} from "#/api/queries/chats"; -import { useAuthenticated } from "#/hooks/useAuthenticated"; -import { AgentSettingsBehaviorPageView } from "./AgentSettingsBehaviorPageView"; +import { Navigate } from "react-router"; const AgentSettingsBehaviorPage: FC = () => { - const { permissions } = useAuthenticated(); - const queryClient = useQueryClient(); - - const systemPromptQuery = useQuery({ - ...chatSystemPrompt(), - enabled: permissions.editDeploymentConfig, - }); - const saveSystemPromptMutation = useMutation( - updateChatSystemPrompt(queryClient), - ); - const planModeInstructionsQuery = useQuery({ - ...chatPlanModeInstructions(), - enabled: permissions.editDeploymentConfig, - }); - const savePlanModeInstructionsMutation = useMutation( - updateChatPlanModeInstructions(queryClient), - ); - - const userPromptQuery = useQuery(chatUserCustomPrompt()); - const saveUserPromptMutation = useMutation( - updateUserChatCustomPrompt(queryClient), - ); - - const desktopEnabledQuery = useQuery(chatDesktopEnabled()); - const saveDesktopEnabledMutation = useMutation( - updateChatDesktopEnabled(queryClient), - ); - - const workspaceTTLQuery = useQuery(chatWorkspaceTTL()); - const saveWorkspaceTTLMutation = useMutation( - updateChatWorkspaceTTL(queryClient), - ); - - const retentionDaysQuery = useQuery(chatRetentionDays()); - const saveRetentionDaysMutation = useMutation( - updateChatRetentionDays(queryClient), - ); - - const modelConfigsQuery = useQuery(chatModelConfigs()); - - const thresholdsQuery = useQuery(userCompactionThresholds()); - const saveThresholdMutation = useMutation( - updateUserCompactionThreshold(queryClient), - ); - const resetThresholdMutation = useMutation( - deleteUserCompactionThreshold(queryClient), - ); - - const handleSaveThreshold = ( - modelConfigId: string, - thresholdPercent: number, - ) => - saveThresholdMutation.mutateAsync({ - modelConfigId, - req: { threshold_percent: thresholdPercent }, - }); - - const handleResetThreshold = (modelConfigId: string) => - resetThresholdMutation.mutateAsync(modelConfigId); - - return ( - - ); + return ; }; export default AgentSettingsBehaviorPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx deleted file mode 100644 index 31c1f55e31806..0000000000000 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.stories.tsx +++ /dev/null @@ -1,475 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, fn, userEvent, waitFor, within } from "storybook/test"; -import type * as TypesGen from "#/api/typesGenerated"; -import { AgentSettingsBehaviorPageView } from "./AgentSettingsBehaviorPageView"; - -const mockDefaultSystemPrompt = "You are Coder, an AI coding assistant..."; - -// Baseline props shared across stories. Only primitives and simple -// objects here to avoid the composeStory deep-merge hang (see vault -// entry storybook-composestory-hang). -const baseProps = { - canSetSystemPrompt: true as boolean, - systemPromptData: { - system_prompt: "", - include_default_system_prompt: true, - default_system_prompt: mockDefaultSystemPrompt, - } as TypesGen.ChatSystemPromptResponse, - planModeInstructionsData: { - plan_mode_instructions: "", - } as TypesGen.ChatPlanModeInstructionsResponse, - userPromptData: { custom_prompt: "" } as TypesGen.UserChatCustomPrompt, - desktopEnabledData: { - enable_desktop: false, - } as TypesGen.ChatDesktopEnabledResponse, - workspaceTTLData: { - workspace_ttl_ms: 0, - } as TypesGen.ChatWorkspaceTTLResponse, - isWorkspaceTTLLoading: false, - isWorkspaceTTLLoadError: false, - retentionDaysData: { - retention_days: 30, - } as TypesGen.ChatRetentionDaysResponse, - isRetentionDaysLoading: false, - isRetentionDaysLoadError: false, - modelConfigsData: [] as TypesGen.ChatModelConfig[], - modelConfigsError: undefined as unknown, - isLoadingModelConfigs: false, - thresholds: [] as readonly TypesGen.UserChatCompactionThreshold[], - isThresholdsLoading: false, - thresholdsError: undefined as unknown, - isSavingSystemPrompt: false, - isSaveSystemPromptError: false, - isSavingPlanModeInstructions: false, - isSavePlanModeInstructionsError: false, - isSavingUserPrompt: false, - isSaveUserPromptError: false, - isSavingDesktopEnabled: false, - isSaveDesktopEnabledError: false, - isSavingWorkspaceTTL: false, - isSaveWorkspaceTTLError: false, - isSavingRetentionDays: false, - isSaveRetentionDaysError: false, -}; - -const meta = { - title: "pages/AgentsPage/AgentSettingsBehaviorPageView", - component: AgentSettingsBehaviorPageView, - args: { - ...baseProps, - onSaveSystemPrompt: fn(), - onSavePlanModeInstructions: fn(), - onSaveUserPrompt: fn(), - onSaveDesktopEnabled: fn(), - onSaveWorkspaceTTL: fn(), - onSaveRetentionDays: fn(), - onSaveThreshold: fn(), - onResetThreshold: fn(), - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -// ── Desktop ──────────────────────────────────────────────────── - -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, args }) => { - const canvas = within(canvasElement); - const toggle = await canvas.findByRole("switch", { name: "Enable" }); - - await userEvent.click(toggle); - await waitFor(() => { - expect(args.onSaveDesktopEnabled).toHaveBeenCalledWith({ - enable_desktop: true, - }); - }); - }, -}; - -// ── System prompt ────────────────────────────────────────────── - -export const AdminWithDefaultToggleOn: Story = { - args: { - systemPromptData: { - system_prompt: "Always use TypeScript for code examples.", - include_default_system_prompt: true, - default_system_prompt: mockDefaultSystemPrompt, - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const body = within(canvasElement.ownerDocument.body); - - const toggle = await canvas.findByRole("switch", { - name: "Include Coder Agents default system prompt", - }); - expect(toggle).toBeChecked(); - expect( - await canvas.findByDisplayValue( - "Always use TypeScript for code examples.", - ), - ).toBeInTheDocument(); - expect( - canvas.getByText(/built-in Coder Agents prompt is prepended/i), - ).toBeInTheDocument(); - - // Preview dialog opens and closes. - await userEvent.click(canvas.getByRole("button", { name: "Preview" })); - expect(await body.findByText("Default System Prompt")).toBeInTheDocument(); - expect(body.getByText(mockDefaultSystemPrompt)).toBeInTheDocument(); - await userEvent.keyboard("{Escape}"); - await waitFor(() => { - expect(body.queryByText("Default System Prompt")).not.toBeInTheDocument(); - }); - - // Toggle off include_default and save. - await userEvent.click(toggle); - const promptForm = canvas - .getByDisplayValue("Always use TypeScript for code examples.") - .closest("form")!; - const saveButton = within(promptForm).getByRole("button", { - name: "Save", - }); - await waitFor(() => { - expect(saveButton).toBeEnabled(); - }); - }, -}; - -export const AdminWithDefaultToggleOff: Story = { - args: { - systemPromptData: { - system_prompt: "You are a custom assistant.", - include_default_system_prompt: false, - default_system_prompt: mockDefaultSystemPrompt, - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const toggle = await canvas.findByRole("switch", { - name: "Include Coder Agents default system prompt", - }); - expect(toggle).not.toBeChecked(); - expect( - await canvas.findByDisplayValue("You are a custom assistant."), - ).toBeInTheDocument(); - expect( - canvas.getByText(/only the additional instructions below are used/i), - ).toBeInTheDocument(); - }, -}; - -// ── Autostop ─────────────────────────────────────────────────── - -export const DefaultAutostopDefault: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await canvas.findByText("Workspace Autostop Fallback"); - await canvas.findByText( - /set a default autostop for agent-created workspaces/i, - ); - - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - expect(toggle).not.toBeChecked(); - expect(canvas.queryByLabelText("Autostop Fallback")).toBeNull(); - }, -}; - -export const DefaultAutostopCustomValue: Story = { - args: { - workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - expect(toggle).toBeChecked(); - - const durationInput = await canvas.findByLabelText("Autostop Fallback"); - expect(durationInput).toHaveValue("2"); - }, -}; - -export const DefaultAutostopSave: Story = { - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - // Toggle ON — fires immediate save with 1h default. - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - await userEvent.click(toggle); - - await waitFor(() => { - expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( - { workspace_ttl_ms: 3_600_000 }, - expect.anything(), - ); - }); - - const durationInput = await canvas.findByLabelText("Autostop Fallback"); - expect(durationInput).toHaveValue("1"); - - // Change to 3 hours. - await userEvent.clear(durationInput); - await userEvent.type(durationInput, "3"); - - const ttlForm = durationInput.closest("form")!; - const saveButton = within(ttlForm).getByRole("button", { - name: "Save", - }); - await waitFor(() => { - expect(saveButton).toBeEnabled(); - }); - - // Clearing back to the original value hides Save (pristine form). - await userEvent.clear(durationInput); - await waitFor(() => { - expect( - within(ttlForm).queryByRole("button", { name: "Save" }), - ).toBeNull(); - }); - }, -}; - -export const DefaultAutostopExceedsMax: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - await userEvent.click(toggle); - - const durationInput = await canvas.findByLabelText("Autostop Fallback"); - const ttlForm = durationInput.closest("form")!; - - // 721 hours exceeds the 30-day / 720h limit. - await userEvent.clear(durationInput); - await userEvent.type(durationInput, "721"); - - await waitFor(() => { - expect(canvas.getByText(/must not exceed 30 days/i)).toBeInTheDocument(); - }); - - const saveButton = within(ttlForm).getByRole("button", { - name: "Save", - }); - expect(saveButton).toBeDisabled(); - }, -}; - -export const DefaultAutostopToggleOff: Story = { - args: { - workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - expect(toggle).toBeChecked(); - - await userEvent.click(toggle); - await waitFor(() => { - expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( - { workspace_ttl_ms: 0 }, - expect.anything(), - ); - }); - }, -}; - -export const DefaultAutostopSaveDisabled: Story = { - args: { - workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - expect(toggle).toBeChecked(); - - const durationInput = await canvas.findByLabelText("Autostop Fallback"); - expect(durationInput).toHaveValue("2"); - - const ttlForm = durationInput.closest("form")!; - expect(within(ttlForm).queryByRole("button", { name: "Save" })).toBeNull(); - }, -}; - -export const DefaultAutostopToggleFailure: Story = { - args: { - isSaveWorkspaceTTLError: true, - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - expect(toggle).not.toBeChecked(); - - await userEvent.click(toggle); - - await waitFor(() => { - expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( - { workspace_ttl_ms: 3_600_000 }, - expect.anything(), - ); - }); - - // Error message should be visible. - expect( - canvas.getByText("Failed to save autostop setting."), - ).toBeInTheDocument(); - }, -}; - -export const DefaultAutostopToggleOffFailure: Story = { - args: { - workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, - isSaveWorkspaceTTLError: true, - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const toggle = await canvas.findByRole("switch", { - name: "Enable default autostop", - }); - expect(toggle).toBeChecked(); - - const durationInput = await canvas.findByLabelText("Autostop Fallback"); - expect(durationInput).toHaveValue("2"); - - await userEvent.click(toggle); - - await waitFor(() => { - expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( - { workspace_ttl_ms: 0 }, - expect.anything(), - ); - }); - - // Error message should be visible. - expect( - canvas.getByText("Failed to save autostop setting."), - ).toBeInTheDocument(); - }, -}; - -export const DefaultAutostopNotVisibleToNonAdmin: Story = { - args: { - canSetSystemPrompt: false, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Personal Instructions should be visible. - await canvas.findByText("Personal Instructions"); - - // Admin-only sections should not be present. - expect(canvas.queryByText("Workspace Autostop Fallback")).toBeNull(); - expect(canvas.queryByText("Virtual Desktop")).toBeNull(); - expect(canvas.queryByText("System Instructions")).toBeNull(); - }, -}; - -// ── Invisible Unicode warnings ───────────────────────────────── - -export const InvisibleUnicodeWarningSystemPrompt: Story = { - args: { - systemPromptData: { - system_prompt: - "Normal prompt text\u200b\u200b\u200b\u200bhidden instruction", - include_default_system_prompt: true, - default_system_prompt: mockDefaultSystemPrompt, - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText("System Instructions"); - - const alert = await canvas.findByText(/invisible Unicode/); - expect(alert).toBeInTheDocument(); - expect(alert.textContent).toContain("4"); - }, -}; - -export const InvisibleUnicodeWarningUserPrompt: Story = { - args: { - userPromptData: { - custom_prompt: "My custom prompt\u200b\u200c\u200dhidden", - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText("Personal Instructions"); - - const alert = await canvas.findByText(/invisible Unicode/); - expect(alert).toBeInTheDocument(); - expect(alert.textContent).toContain("2"); - }, -}; - -export const InvisibleUnicodeWarningOnType: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const textarea = await canvas.findByPlaceholderText( - "Additional behavior, style, and tone preferences", - ); - - // No warning initially. - expect(canvas.queryByText(/invisible Unicode/)).toBeNull(); - - // Type a string containing a ZWS character. - await userEvent.type(textarea, "hello\u200bworld"); - - await waitFor(() => { - expect(canvas.getByText(/invisible Unicode/)).toBeInTheDocument(); - }); - }, -}; - -export const NoWarningForCleanPrompt: Story = { - args: { - systemPromptData: { - system_prompt: "You are a helpful coding assistant.", - include_default_system_prompt: true, - default_system_prompt: mockDefaultSystemPrompt, - }, - userPromptData: { - custom_prompt: "Be concise and use TypeScript.", - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText("Personal Instructions"); - await canvas.findByText("System Instructions"); - - expect(canvas.queryByText(/invisible Unicode/)).toBeNull(); - }, -}; diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx deleted file mode 100644 index 26eb9b118eee0..0000000000000 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import type { FC } from "react"; -import type * as TypesGen from "#/api/typesGenerated"; -import { ChatFullWidthSettings } from "./components/ChatFullWidthSettings"; -import { PersonalInstructionsSettings } from "./components/PersonalInstructionsSettings"; -import { PlanModeInstructionsSettings } from "./components/PlanModeInstructionsSettings"; -import { RetentionPeriodSettings } from "./components/RetentionPeriodSettings"; -import { SectionHeader } from "./components/SectionHeader"; -import { SystemInstructionsSettings } from "./components/SystemInstructionsSettings"; -import { UserCompactionThresholdSettings } from "./components/UserCompactionThresholdSettings"; -import { VirtualDesktopSettings } from "./components/VirtualDesktopSettings"; -import { WorkspaceAutostopSettings } from "./components/WorkspaceAutostopSettings"; - -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} - -interface AgentSettingsBehaviorPageViewProps { - canSetSystemPrompt: boolean; - - // Raw query data - systemPromptData: TypesGen.ChatSystemPromptResponse | undefined; - planModeInstructionsData: - | TypesGen.ChatPlanModeInstructionsResponse - | undefined; - userPromptData: TypesGen.UserChatCustomPrompt | undefined; - desktopEnabledData: TypesGen.ChatDesktopEnabledResponse | undefined; - workspaceTTLData: TypesGen.ChatWorkspaceTTLResponse | undefined; - isWorkspaceTTLLoading: boolean; - isWorkspaceTTLLoadError: boolean; - retentionDaysData: TypesGen.ChatRetentionDaysResponse | undefined; - isRetentionDaysLoading: boolean; - isRetentionDaysLoadError: boolean; - modelConfigsData: TypesGen.ChatModelConfig[] | undefined; - modelConfigsError: unknown; - isLoadingModelConfigs: boolean; - - // Thresholds (passed through to child component) - thresholds: readonly TypesGen.UserChatCompactionThreshold[] | undefined; - isThresholdsLoading: boolean; - thresholdsError: unknown; - onSaveThreshold: ( - modelConfigId: string, - thresholdPercent: number, - ) => Promise; - onResetThreshold: (modelConfigId: string) => Promise; - - // Mutation handlers - onSaveSystemPrompt: ( - req: TypesGen.UpdateChatSystemPromptRequest, - options?: MutationCallbacks, - ) => void; - isSavingSystemPrompt: boolean; - isSaveSystemPromptError: boolean; - - onSavePlanModeInstructions: ( - req: TypesGen.UpdateChatPlanModeInstructionsRequest, - options?: MutationCallbacks, - ) => void; - isSavingPlanModeInstructions: boolean; - isSavePlanModeInstructionsError: boolean; - - onSaveUserPrompt: ( - req: TypesGen.UserChatCustomPrompt, - options?: MutationCallbacks, - ) => void; - isSavingUserPrompt: boolean; - isSaveUserPromptError: boolean; - - onSaveDesktopEnabled: ( - req: TypesGen.UpdateChatDesktopEnabledRequest, - options?: MutationCallbacks, - ) => void; - isSavingDesktopEnabled: boolean; - isSaveDesktopEnabledError: boolean; - - onSaveWorkspaceTTL: ( - req: TypesGen.UpdateChatWorkspaceTTLRequest, - options?: MutationCallbacks, - ) => void; - isSavingWorkspaceTTL: boolean; - isSaveWorkspaceTTLError: boolean; - - onSaveRetentionDays: ( - req: TypesGen.UpdateChatRetentionDaysRequest, - options?: MutationCallbacks, - ) => void; - isSavingRetentionDays: boolean; - isSaveRetentionDaysError: boolean; -} - -export const AgentSettingsBehaviorPageView: FC< - AgentSettingsBehaviorPageViewProps -> = ({ - canSetSystemPrompt, - systemPromptData, - planModeInstructionsData, - userPromptData, - desktopEnabledData, - workspaceTTLData, - isWorkspaceTTLLoading, - isWorkspaceTTLLoadError, - retentionDaysData, - isRetentionDaysLoading, - isRetentionDaysLoadError, - modelConfigsData, - modelConfigsError, - isLoadingModelConfigs, - thresholds, - isThresholdsLoading, - thresholdsError, - onSaveThreshold, - onResetThreshold, - onSaveSystemPrompt, - isSavingSystemPrompt, - isSaveSystemPromptError, - onSavePlanModeInstructions, - isSavingPlanModeInstructions, - isSavePlanModeInstructionsError, - onSaveUserPrompt, - isSavingUserPrompt, - isSaveUserPromptError, - onSaveDesktopEnabled, - isSavingDesktopEnabled, - isSaveDesktopEnabledError, - onSaveWorkspaceTTL, - isSavingWorkspaceTTL, - isSaveWorkspaceTTLError, - onSaveRetentionDays, - isSavingRetentionDays, - isSaveRetentionDaysError, -}) => { - const isAnyPromptSaving = - isSavingSystemPrompt || isSavingUserPrompt || isSavingPlanModeInstructions; - - return ( -
- - - - - - {/* ── Admin-only settings ── */} - {canSetSystemPrompt && ( - <> - - - - - - - )} -
- ); -}; diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx new file mode 100644 index 0000000000000..1109ee8f9d949 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx @@ -0,0 +1,48 @@ +import type { FC } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { + chatModelConfigs, + deleteUserCompactionThreshold, + updateUserCompactionThreshold, + userCompactionThresholds, +} from "#/api/queries/chats"; +import { AgentSettingsCompactionPageView } from "./AgentSettingsCompactionPageView"; + +const AgentSettingsCompactionPage: FC = () => { + const queryClient = useQueryClient(); + const modelConfigsQuery = useQuery(chatModelConfigs()); + const thresholdsQuery = useQuery(userCompactionThresholds()); + const saveThresholdMutation = useMutation( + updateUserCompactionThreshold(queryClient), + ); + const resetThresholdMutation = useMutation( + deleteUserCompactionThreshold(queryClient), + ); + + const handleSaveThreshold = ( + modelConfigId: string, + thresholdPercent: number, + ) => + saveThresholdMutation.mutateAsync({ + modelConfigId, + req: { threshold_percent: thresholdPercent }, + }); + + const handleResetThreshold = (modelConfigId: string) => + resetThresholdMutation.mutateAsync(modelConfigId); + + return ( + + ); +}; + +export default AgentSettingsCompactionPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx new file mode 100644 index 0000000000000..99fce00ba50c0 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import type * as TypesGen from "#/api/typesGenerated"; +import { + AgentSettingsCompactionPageView, + type AgentSettingsCompactionPageViewProps, +} from "./AgentSettingsCompactionPageView"; + +const baseArgs: AgentSettingsCompactionPageViewProps = { + modelConfigsData: [ + { + id: "model-config-1", + provider: "openai", + model: "gpt-4.1-mini", + display_name: "GPT 4.1 Mini", + enabled: true, + is_default: false, + context_limit: 1_000_000, + compression_threshold: 70, + created_at: "2026-03-12T12:00:00.000Z", + updated_at: "2026-03-12T12:00:00.000Z", + }, + ] as TypesGen.ChatModelConfig[], + modelConfigsError: undefined, + isLoadingModelConfigs: false, + thresholds: [ + { + model_config_id: "model-config-1", + threshold_percent: 60, + }, + ], + isThresholdsLoading: false, + thresholdsError: undefined, + onSaveThreshold: fn(async () => undefined), + onResetThreshold: fn(async () => undefined), +}; + +const meta = { + title: "pages/AgentsPage/AgentSettingsCompactionPageView", + component: AgentSettingsCompactionPageView, + args: baseArgs, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx new file mode 100644 index 0000000000000..4dd47ee35a91d --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx @@ -0,0 +1,50 @@ +import type { FC } from "react"; +import type * as TypesGen from "#/api/typesGenerated"; +import { SectionHeader } from "./components/SectionHeader"; +import { UserCompactionThresholdSettings } from "./components/UserCompactionThresholdSettings"; + +export interface AgentSettingsCompactionPageViewProps { + modelConfigsData: TypesGen.ChatModelConfig[] | undefined; + modelConfigsError: unknown; + isLoadingModelConfigs: boolean; + thresholds: readonly TypesGen.UserChatCompactionThreshold[] | undefined; + isThresholdsLoading: boolean; + thresholdsError: unknown; + onSaveThreshold: ( + modelConfigId: string, + thresholdPercent: number, + ) => Promise; + onResetThreshold: (modelConfigId: string) => Promise; +} + +export const AgentSettingsCompactionPageView: FC< + AgentSettingsCompactionPageViewProps +> = ({ + modelConfigsData, + modelConfigsError, + isLoadingModelConfigs, + thresholds, + isThresholdsLoading, + thresholdsError, + onSaveThreshold, + onResetThreshold, +}) => { + return ( +
+ + +
+ ); +}; diff --git a/site/src/pages/AgentsPage/AgentSettingsExperimentsPage.tsx b/site/src/pages/AgentsPage/AgentSettingsExperimentsPage.tsx new file mode 100644 index 0000000000000..88fc772940f87 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsExperimentsPage.tsx @@ -0,0 +1,34 @@ +import type { FC } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { + chatDesktopEnabled, + updateChatDesktopEnabled, +} from "#/api/queries/chats"; +import { useAuthenticated } from "#/hooks/useAuthenticated"; +import { RequirePermission } from "#/modules/permissions/RequirePermission"; +import { AgentSettingsExperimentsPageView } from "./AgentSettingsExperimentsPageView"; + +const AgentSettingsExperimentsPage: FC = () => { + const { permissions } = useAuthenticated(); + const queryClient = useQueryClient(); + const desktopEnabledQuery = useQuery({ + ...chatDesktopEnabled(), + enabled: permissions.editDeploymentConfig, + }); + const saveDesktopEnabledMutation = useMutation( + updateChatDesktopEnabled(queryClient), + ); + + return ( + + + + ); +}; + +export default AgentSettingsExperimentsPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx new file mode 100644 index 0000000000000..60d000cae3827 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { + AgentSettingsExperimentsPageView, + type AgentSettingsExperimentsPageViewProps, +} from "./AgentSettingsExperimentsPageView"; + +const baseArgs: AgentSettingsExperimentsPageViewProps = { + desktopEnabledData: { enable_desktop: false }, + onSaveDesktopEnabled: fn(), + isSavingDesktopEnabled: false, + isSaveDesktopEnabledError: false, +}; + +const meta = { + title: "pages/AgentsPage/AgentSettingsExperimentsPageView", + component: AgentSettingsExperimentsPageView, + args: baseArgs, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx new file mode 100644 index 0000000000000..0c4de32633f0b --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx @@ -0,0 +1,45 @@ +import type { FC } from "react"; +import type * as TypesGen from "#/api/typesGenerated"; +import { AdminBadge } from "./components/AdminBadge"; +import { SectionHeader } from "./components/SectionHeader"; +import { VirtualDesktopSettings } from "./components/VirtualDesktopSettings"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} + +export interface AgentSettingsExperimentsPageViewProps { + desktopEnabledData: TypesGen.ChatDesktopEnabledResponse | undefined; + onSaveDesktopEnabled: ( + req: TypesGen.UpdateChatDesktopEnabledRequest, + options?: MutationCallbacks, + ) => void; + isSavingDesktopEnabled: boolean; + isSaveDesktopEnabledError: boolean; +} + +export const AgentSettingsExperimentsPageView: FC< + AgentSettingsExperimentsPageViewProps +> = ({ + desktopEnabledData, + onSaveDesktopEnabled, + isSavingDesktopEnabled, + isSaveDesktopEnabledError, +}) => { + return ( +
+ } + /> + +
+ ); +}; diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPage.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPage.tsx new file mode 100644 index 0000000000000..9675446f653c7 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPage.tsx @@ -0,0 +1,26 @@ +import type { FC } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { + chatUserCustomPrompt, + updateUserChatCustomPrompt, +} from "#/api/queries/chats"; +import { AgentSettingsGeneralPageView } from "./AgentSettingsGeneralPageView"; + +const AgentSettingsGeneralPage: FC = () => { + const queryClient = useQueryClient(); + const userPromptQuery = useQuery(chatUserCustomPrompt()); + const saveUserPromptMutation = useMutation( + updateUserChatCustomPrompt(queryClient), + ); + + return ( + + ); +}; + +export default AgentSettingsGeneralPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx new file mode 100644 index 0000000000000..1fdd1aec490ea --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { + AgentSettingsGeneralPageView, + type AgentSettingsGeneralPageViewProps, +} from "./AgentSettingsGeneralPageView"; + +const baseArgs: AgentSettingsGeneralPageViewProps = { + userPromptData: { + custom_prompt: "Prefer concise answers with clear next steps.", + }, + onSaveUserPrompt: fn(), + isSavingUserPrompt: false, + isSaveUserPromptError: false, +}; + +const meta = { + title: "pages/AgentsPage/AgentSettingsGeneralPageView", + component: AgentSettingsGeneralPageView, + args: baseArgs, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx new file mode 100644 index 0000000000000..beaa94cb24cda --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx @@ -0,0 +1,48 @@ +import type { FC } from "react"; +import type * as TypesGen from "#/api/typesGenerated"; +import { ChatFullWidthSettings } from "./components/ChatFullWidthSettings"; +import { PersonalInstructionsSettings } from "./components/PersonalInstructionsSettings"; +import { SectionHeader } from "./components/SectionHeader"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} + +export interface AgentSettingsGeneralPageViewProps { + userPromptData: TypesGen.UserChatCustomPrompt | undefined; + onSaveUserPrompt: ( + req: TypesGen.UserChatCustomPrompt, + options?: MutationCallbacks, + ) => void; + isSavingUserPrompt: boolean; + isSaveUserPromptError: boolean; +} + +export const AgentSettingsGeneralPageView: FC< + AgentSettingsGeneralPageViewProps +> = ({ + userPromptData, + onSaveUserPrompt, + isSavingUserPrompt, + isSaveUserPromptError, +}) => { + const isAnyPromptSaving = isSavingUserPrompt; + + return ( +
+ + + +
+ ); +}; diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePage.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePage.tsx new file mode 100644 index 0000000000000..0dd07eef4542f --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePage.tsx @@ -0,0 +1,51 @@ +import type { FC } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { + chatRetentionDays, + chatWorkspaceTTL, + updateChatRetentionDays, + updateChatWorkspaceTTL, +} from "#/api/queries/chats"; +import { useAuthenticated } from "#/hooks/useAuthenticated"; +import { RequirePermission } from "#/modules/permissions/RequirePermission"; +import { AgentSettingsLifecyclePageView } from "./AgentSettingsLifecyclePageView"; + +const AgentSettingsLifecyclePage: FC = () => { + const { permissions } = useAuthenticated(); + const queryClient = useQueryClient(); + const workspaceTTLQuery = useQuery({ + ...chatWorkspaceTTL(), + enabled: permissions.editDeploymentConfig, + }); + const retentionDaysQuery = useQuery({ + ...chatRetentionDays(), + enabled: permissions.editDeploymentConfig, + }); + const saveWorkspaceTTLMutation = useMutation( + updateChatWorkspaceTTL(queryClient), + ); + const saveRetentionDaysMutation = useMutation( + updateChatRetentionDays(queryClient), + ); + + return ( + + + + ); +}; + +export default AgentSettingsLifecyclePage; diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx new file mode 100644 index 0000000000000..909a2a770e2f5 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { + AgentSettingsLifecyclePageView, + type AgentSettingsLifecyclePageViewProps, +} from "./AgentSettingsLifecyclePageView"; + +const baseArgs: AgentSettingsLifecyclePageViewProps = { + workspaceTTLData: { workspace_ttl_ms: 3_600_000 }, + isWorkspaceTTLLoading: false, + isWorkspaceTTLLoadError: false, + onSaveWorkspaceTTL: fn(), + isSavingWorkspaceTTL: false, + isSaveWorkspaceTTLError: false, + retentionDaysData: { retention_days: 30 }, + isRetentionDaysLoading: false, + isRetentionDaysLoadError: false, + onSaveRetentionDays: fn(), + isSavingRetentionDays: false, + isSaveRetentionDaysError: false, +}; + +const meta = { + title: "pages/AgentsPage/AgentSettingsLifecyclePageView", + component: AgentSettingsLifecyclePageView, + args: baseArgs, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx new file mode 100644 index 0000000000000..e6e78c9b711ca --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx @@ -0,0 +1,75 @@ +import type { FC } from "react"; +import type * as TypesGen from "#/api/typesGenerated"; +import { AdminBadge } from "./components/AdminBadge"; +import { RetentionPeriodSettings } from "./components/RetentionPeriodSettings"; +import { SectionHeader } from "./components/SectionHeader"; +import { WorkspaceAutostopSettings } from "./components/WorkspaceAutostopSettings"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} + +export interface AgentSettingsLifecyclePageViewProps { + workspaceTTLData: TypesGen.ChatWorkspaceTTLResponse | undefined; + isWorkspaceTTLLoading: boolean; + isWorkspaceTTLLoadError: boolean; + onSaveWorkspaceTTL: ( + req: TypesGen.UpdateChatWorkspaceTTLRequest, + options?: MutationCallbacks, + ) => void; + isSavingWorkspaceTTL: boolean; + isSaveWorkspaceTTLError: boolean; + retentionDaysData: TypesGen.ChatRetentionDaysResponse | undefined; + isRetentionDaysLoading: boolean; + isRetentionDaysLoadError: boolean; + onSaveRetentionDays: ( + req: TypesGen.UpdateChatRetentionDaysRequest, + options?: MutationCallbacks, + ) => void; + isSavingRetentionDays: boolean; + isSaveRetentionDaysError: boolean; +} + +export const AgentSettingsLifecyclePageView: FC< + AgentSettingsLifecyclePageViewProps +> = ({ + workspaceTTLData, + isWorkspaceTTLLoading, + isWorkspaceTTLLoadError, + onSaveWorkspaceTTL, + isSavingWorkspaceTTL, + isSaveWorkspaceTTLError, + retentionDaysData, + isRetentionDaysLoading, + isRetentionDaysLoadError, + onSaveRetentionDays, + isSavingRetentionDays, + isSaveRetentionDaysError, +}) => { + return ( +
+ } + /> + + +
+ ); +}; diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPage.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPage.tsx new file mode 100644 index 0000000000000..e9ae44eb18cfa --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPage.tsx @@ -0,0 +1,52 @@ +import type { FC } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { + chatPlanModeInstructions, + chatSystemPrompt, + updateChatPlanModeInstructions, + updateChatSystemPrompt, +} from "#/api/queries/chats"; +import { useAuthenticated } from "#/hooks/useAuthenticated"; +import { RequirePermission } from "#/modules/permissions/RequirePermission"; +import { AgentSettingsSystemInstructionsPageView } from "./AgentSettingsSystemInstructionsPageView"; + +const AgentSettingsSystemInstructionsPage: FC = () => { + const { permissions } = useAuthenticated(); + const queryClient = useQueryClient(); + + const systemPromptQuery = useQuery({ + ...chatSystemPrompt(), + enabled: permissions.editDeploymentConfig, + }); + const planModeInstructionsQuery = useQuery({ + ...chatPlanModeInstructions(), + enabled: permissions.editDeploymentConfig, + }); + const saveSystemPromptMutation = useMutation( + updateChatSystemPrompt(queryClient), + ); + const savePlanModeInstructionsMutation = useMutation( + updateChatPlanModeInstructions(queryClient), + ); + + return ( + + + + ); +}; + +export default AgentSettingsSystemInstructionsPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx new file mode 100644 index 0000000000000..e6360590e5a00 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { + AgentSettingsSystemInstructionsPageView, + type AgentSettingsSystemInstructionsPageViewProps, +} from "./AgentSettingsSystemInstructionsPageView"; + +const baseArgs: AgentSettingsSystemInstructionsPageViewProps = { + systemPromptData: { + system_prompt: "Always explain tradeoffs before proposing a change.", + include_default_system_prompt: true, + default_system_prompt: "You are Coder, an AI coding assistant.", + }, + planModeInstructionsData: { + plan_mode_instructions: + "Use a numbered checklist for implementation plans.", + }, + onSaveSystemPrompt: fn(), + isSavingSystemPrompt: false, + isSaveSystemPromptError: false, + onSavePlanModeInstructions: fn(), + isSavingPlanModeInstructions: false, + isSavePlanModeInstructionsError: false, +}; + +const meta = { + title: "pages/AgentsPage/AgentSettingsSystemInstructionsPageView", + component: AgentSettingsSystemInstructionsPageView, + args: baseArgs, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx new file mode 100644 index 0000000000000..b1fd1d1e5b284 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx @@ -0,0 +1,69 @@ +import type { FC } from "react"; +import type * as TypesGen from "#/api/typesGenerated"; +import { AdminBadge } from "./components/AdminBadge"; +import { PlanModeInstructionsSettings } from "./components/PlanModeInstructionsSettings"; +import { SectionHeader } from "./components/SectionHeader"; +import { SystemInstructionsSettings } from "./components/SystemInstructionsSettings"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} + +export interface AgentSettingsSystemInstructionsPageViewProps { + systemPromptData: TypesGen.ChatSystemPromptResponse | undefined; + planModeInstructionsData: + | TypesGen.ChatPlanModeInstructionsResponse + | undefined; + onSaveSystemPrompt: ( + req: TypesGen.UpdateChatSystemPromptRequest, + options?: MutationCallbacks, + ) => void; + isSavingSystemPrompt: boolean; + isSaveSystemPromptError: boolean; + onSavePlanModeInstructions: ( + req: TypesGen.UpdateChatPlanModeInstructionsRequest, + options?: MutationCallbacks, + ) => void; + isSavingPlanModeInstructions: boolean; + isSavePlanModeInstructionsError: boolean; +} + +export const AgentSettingsSystemInstructionsPageView: FC< + AgentSettingsSystemInstructionsPageViewProps +> = ({ + systemPromptData, + planModeInstructionsData, + onSaveSystemPrompt, + isSavingSystemPrompt, + isSaveSystemPromptError, + onSavePlanModeInstructions, + isSavingPlanModeInstructions, + isSavePlanModeInstructionsError, +}) => { + const isAnyPromptSaving = + isSavingSystemPrompt || isSavingPlanModeInstructions; + + return ( +
+ } + /> + + +
+ ); +}; diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index d1c72d1d0d4f7..cb62d07ffe30e 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -16,7 +16,6 @@ import { API } from "#/api/api"; import type * as TypesGen from "#/api/typesGenerated"; import type { Chat } from "#/api/typesGenerated"; import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog"; -import { useAuthenticated } from "#/hooks/useAuthenticated"; import { MockNoPermissions, MockPermissions, @@ -29,9 +28,13 @@ import { import AgentAnalyticsPage from "./AgentAnalyticsPage"; import AgentCreatePage from "./AgentCreatePage"; import { AgentSettingsAgentsPageView } from "./AgentSettingsAgentsPageView"; -import { AgentSettingsBehaviorPageView } from "./AgentSettingsBehaviorPageView"; +import AgentSettingsCompactionPage from "./AgentSettingsCompactionPage"; +import AgentSettingsExperimentsPage from "./AgentSettingsExperimentsPage"; +import AgentSettingsGeneralPage from "./AgentSettingsGeneralPage"; +import AgentSettingsLifecyclePage from "./AgentSettingsLifecyclePage"; import AgentSettingsPage from "./AgentSettingsPage"; import AgentSettingsSpendPage from "./AgentSettingsSpendPage"; +import AgentSettingsSystemInstructionsPage from "./AgentSettingsSystemInstructionsPage"; import { AgentsPageView } from "./AgentsPageView"; import type { ModelSelectorOption } from "./components/ChatElements"; @@ -151,59 +154,6 @@ const buildChat = (overrides: Partial = {}): Chat => ({ // across timezones. const fixedNow = dayjs("2026-03-12T12:00:00"); -// Renders the real PageView components with mock data so the -// visual snapshots match the actual UI. -const BehaviorRouteElement = () => { - const { permissions } = useAuthenticated(); - return ( - undefined)} - onResetThreshold={fn(async () => undefined)} - /> - ); -}; - const AgentsRouteElement = () => ( , children: [ - { index: true, element: }, - { path: "behavior", element: }, + { index: true, element: }, + { path: "general", element: }, + { + path: "behavior", + element: , + }, + { path: "compaction", element: }, + { + path: "system-instructions", + element: , + }, + { path: "experiments", element: }, + { path: "lifecycle", element: }, { path: "agents", element: }, { path: "spend", element: }, { @@ -349,10 +310,39 @@ const meta: Meta = { spyOn(API.experimental, "getChatDesktopEnabled").mockResolvedValue({ enable_desktop: false, }); + spyOn(API.experimental, "updateChatDesktopEnabled").mockResolvedValue(); + spyOn(API.experimental, "getChatPlanModeInstructions").mockResolvedValue({ + plan_mode_instructions: "", + }); + spyOn( + API.experimental, + "updateChatPlanModeInstructions", + ).mockResolvedValue(); + spyOn( + API.experimental, + "getUserChatCompactionThresholds", + ).mockResolvedValue({ + thresholds: [], + }); + spyOn( + API.experimental, + "updateUserChatCompactionThreshold", + ).mockResolvedValue({ + model_config_id: defaultModelConfigID, + threshold_percent: 70, + }); + spyOn( + API.experimental, + "deleteUserChatCompactionThreshold", + ).mockResolvedValue(); spyOn(API.experimental, "getChatWorkspaceTTL").mockResolvedValue({ workspace_ttl_ms: 0, }); spyOn(API.experimental, "updateChatWorkspaceTTL").mockResolvedValue(); + spyOn(API.experimental, "getChatRetentionDays").mockResolvedValue({ + retention_days: 30, + }); + spyOn(API.experimental, "updateChatRetentionDays").mockResolvedValue(); spyOn(API.experimental, "getChatUsageLimitConfig").mockResolvedValue({ spend_limit_micros: null, period: "month", @@ -669,9 +659,7 @@ export const OpensSettingsForAdmins: Story = { await waitFor(() => { expect( - screen.getByText( - "Custom instructions that shape how the agent responds in your conversations.", - ), + screen.getByText("Personal preferences for your chat experience."), ).toBeInTheDocument(); }); }, @@ -689,9 +677,7 @@ export const OpensSettingsForNonAdmins: Story = { await waitFor(() => { expect( - screen.getByText( - "Custom instructions that shape how the agent responds in your conversations.", - ), + screen.getByText("Personal preferences for your chat experience."), ).toBeInTheDocument(); }); }, @@ -707,9 +693,7 @@ export const SettingsViewResets: Story = { await waitFor(() => { expect( - screen.getByText( - "Custom instructions that shape how the agent responds in your conversations.", - ), + screen.getByText("Personal preferences for your chat experience."), ).toBeInTheDocument(); }); @@ -727,13 +711,11 @@ export const SettingsViewResets: Story = { const backButton = screen.getByLabelText("Back to Agents"); await userEvent.click(backButton); - // Re-open settings, should reset to Behavior + // Re-open settings, should reset to General await openSettingsView(canvasElement); await waitFor(() => { expect( - screen.getByText( - "Custom instructions that shape how the agent responds in your conversations.", - ), + screen.getByText("Personal preferences for your chat experience."), ).toBeInTheDocument(); }); }, diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index 1cd990c8ae50d..a0062fcf3fa1e 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -27,7 +27,9 @@ import { ChevronLeftIcon, ChevronRightIcon, EllipsisIcon, + FileTextIcon, FilterIcon, + FlaskConicalIcon, GitMergeIcon, GitPullRequestArrowIcon, GitPullRequestClosedIcon, @@ -43,6 +45,7 @@ import { SettingsIcon, ShieldIcon, SquarePenIcon, + TimerIcon, Trash2Icon, UserIcon, WalletIcon, @@ -1309,11 +1312,20 @@ export const AgentsSidebar: FC = (props) => { diff --git a/site/src/router.tsx b/site/src/router.tsx index b08c15ba50a8d..32c3a585bacd8 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -360,6 +360,21 @@ const AgentSettingsPage = lazy( const AgentSettingsBehaviorPage = lazy( () => import("./pages/AgentsPage/AgentSettingsBehaviorPage"), ); +const AgentSettingsGeneralPage = lazy( + () => import("./pages/AgentsPage/AgentSettingsGeneralPage"), +); +const AgentSettingsCompactionPage = lazy( + () => import("./pages/AgentsPage/AgentSettingsCompactionPage"), +); +const AgentSettingsSystemInstructionsPage = lazy( + () => import("./pages/AgentsPage/AgentSettingsSystemInstructionsPage"), +); +const AgentSettingsExperimentsPage = lazy( + () => import("./pages/AgentsPage/AgentSettingsExperimentsPage"), +); +const AgentSettingsLifecyclePage = lazy( + () => import("./pages/AgentsPage/AgentSettingsLifecyclePage"), +); const AgentSettingsAgentsPage = lazy( () => import("./pages/AgentsPage/AgentSettingsAgentsPage"), ); @@ -710,8 +725,22 @@ export const router = createBrowserRouter( > } /> }> - } /> + } /> + } /> } /> + } + /> + } + /> + } + /> + } /> } /> } /> } /> From 2a897aac81902d095c1b7e84937cf4c509173b8f Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:02:25 +0000 Subject: [PATCH 02/22] refactor(site/src/pages/AgentsPage): extract MutationCallbacks to shared types --- .../AgentsPage/AgentSettingsExperimentsPageView.tsx | 6 +----- .../pages/AgentsPage/AgentSettingsGeneralPageView.tsx | 10 ++-------- .../AgentsPage/AgentSettingsLifecyclePageView.tsx | 6 +----- .../AgentSettingsSystemInstructionsPageView.tsx | 6 +----- site/src/pages/AgentsPage/types.ts | 4 ++++ 5 files changed, 9 insertions(+), 23 deletions(-) create mode 100644 site/src/pages/AgentsPage/types.ts diff --git a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx index 0c4de32633f0b..d3fbbca9b8639 100644 --- a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx @@ -3,11 +3,7 @@ import type * as TypesGen from "#/api/typesGenerated"; import { AdminBadge } from "./components/AdminBadge"; import { SectionHeader } from "./components/SectionHeader"; import { VirtualDesktopSettings } from "./components/VirtualDesktopSettings"; - -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} +import type { MutationCallbacks } from "./types"; export interface AgentSettingsExperimentsPageViewProps { desktopEnabledData: TypesGen.ChatDesktopEnabledResponse | undefined; diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx index beaa94cb24cda..a53d57563996d 100644 --- a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx @@ -3,11 +3,7 @@ import type * as TypesGen from "#/api/typesGenerated"; import { ChatFullWidthSettings } from "./components/ChatFullWidthSettings"; import { PersonalInstructionsSettings } from "./components/PersonalInstructionsSettings"; import { SectionHeader } from "./components/SectionHeader"; - -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} +import type { MutationCallbacks } from "./types"; export interface AgentSettingsGeneralPageViewProps { userPromptData: TypesGen.UserChatCustomPrompt | undefined; @@ -27,8 +23,6 @@ export const AgentSettingsGeneralPageView: FC< isSavingUserPrompt, isSaveUserPromptError, }) => { - const isAnyPromptSaving = isSavingUserPrompt; - return (
diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx index e6e78c9b711ca..f017f1fe051c3 100644 --- a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx @@ -4,11 +4,7 @@ import { AdminBadge } from "./components/AdminBadge"; import { RetentionPeriodSettings } from "./components/RetentionPeriodSettings"; import { SectionHeader } from "./components/SectionHeader"; import { WorkspaceAutostopSettings } from "./components/WorkspaceAutostopSettings"; - -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} +import type { MutationCallbacks } from "./types"; export interface AgentSettingsLifecyclePageViewProps { workspaceTTLData: TypesGen.ChatWorkspaceTTLResponse | undefined; diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx index b1fd1d1e5b284..984c1ad98ab7e 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx @@ -4,11 +4,7 @@ import { AdminBadge } from "./components/AdminBadge"; import { PlanModeInstructionsSettings } from "./components/PlanModeInstructionsSettings"; import { SectionHeader } from "./components/SectionHeader"; import { SystemInstructionsSettings } from "./components/SystemInstructionsSettings"; - -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} +import type { MutationCallbacks } from "./types"; export interface AgentSettingsSystemInstructionsPageViewProps { systemPromptData: TypesGen.ChatSystemPromptResponse | undefined; diff --git a/site/src/pages/AgentsPage/types.ts b/site/src/pages/AgentsPage/types.ts new file mode 100644 index 0000000000000..64f443a9f05fa --- /dev/null +++ b/site/src/pages/AgentsPage/types.ts @@ -0,0 +1,4 @@ +export interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} From 0a0cab315381a6f8f9fa658862666e12e78faead Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:05:02 +0000 Subject: [PATCH 03/22] test(site/src/pages/AgentsPage): restore Storybook interaction coverage for split settings pages --- ...gentSettingsCompactionPageView.stories.tsx | 3 + ...entSettingsExperimentsPageView.stories.tsx | 27 ++- .../AgentSettingsGeneralPageView.stories.tsx | 82 ++++++- ...AgentSettingsLifecyclePageView.stories.tsx | 209 +++++++++++++++++- ...ingsSystemInstructionsPageView.stories.tsx | 114 +++++++++- 5 files changed, 429 insertions(+), 6 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx index 99fce00ba50c0..7eba9abc11708 100644 --- a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx @@ -44,4 +44,7 @@ const meta = { export default meta; type Story = StoryObj; +// Interaction coverage for threshold save and reset lives in +// UserCompactionThresholdSettings.stories.tsx because this page view only wraps +// that component with a section header. export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx index 60d000cae3827..13390a824043c 100644 --- a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; +import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import { AgentSettingsExperimentsPageView, type AgentSettingsExperimentsPageViewProps, @@ -22,3 +22,28 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +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 within workspaces./i, + ); + await canvas.findByRole("switch", { name: "Enable" }); + }, +}; + +export const TogglesDesktop: Story = { + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { name: "Enable" }); + + await userEvent.click(toggle); + await waitFor(() => { + expect(args.onSaveDesktopEnabled).toHaveBeenCalledWith({ + enable_desktop: true, + }); + }); + }, +}; diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx index 1fdd1aec490ea..9758ae17d0212 100644 --- a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; +import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import { AgentSettingsGeneralPageView, type AgentSettingsGeneralPageViewProps, @@ -24,3 +24,83 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +// These warning stories moved here because PersonalInstructionsSettings now +// lives on the General page. +export const InvisibleUnicodeWarningUserPrompt: Story = { + args: { + userPromptData: { + custom_prompt: "My custom prompt\u200b\u200c\u200dhidden", + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText("Personal Instructions"); + const alert = await canvas.findByText(/invisible Unicode/); + expect(alert).toBeInTheDocument(); + expect(alert.textContent).toContain("2"); + }, +}; + +export const InvisibleUnicodeWarningOnType: Story = { + args: { + userPromptData: { + custom_prompt: "", + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const textarea = await canvas.findByPlaceholderText( + "Additional behavior, style, and tone preferences", + ); + + expect(canvas.queryByText(/invisible Unicode/)).toBeNull(); + await userEvent.type(textarea, "hello\u200bworld"); + + await waitFor(() => { + expect(canvas.getByText(/invisible Unicode/)).toBeInTheDocument(); + }); + }, +}; + +export const SavesUserPrompt: Story = { + args: { + userPromptData: { + custom_prompt: "", + }, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const textarea = await canvas.findByPlaceholderText( + "Additional behavior, style, and tone preferences", + ); + + expect(canvas.queryByText(/invisible Unicode/)).toBeNull(); + await userEvent.type( + textarea, + "Prefer concise answers with clear next steps.", + ); + + const promptForm = textarea.closest("form"); + if (!(promptForm instanceof HTMLFormElement)) { + throw new Error( + "Expected personal instructions textarea to live inside a form.", + ); + } + const saveButton = within(promptForm).getByRole("button", { + name: "Save", + }); + await waitFor(() => { + expect(saveButton).toBeEnabled(); + }); + await userEvent.click(saveButton); + + await waitFor(() => { + expect(args.onSaveUserPrompt).toHaveBeenCalledWith( + { custom_prompt: "Prefer concise answers with clear next steps." }, + expect.anything(), + ); + }); + }, +}; diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx index 909a2a770e2f5..f8f60bdc3e277 100644 --- a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx @@ -1,12 +1,12 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; +import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import { AgentSettingsLifecyclePageView, type AgentSettingsLifecyclePageViewProps, } from "./AgentSettingsLifecyclePageView"; const baseArgs: AgentSettingsLifecyclePageViewProps = { - workspaceTTLData: { workspace_ttl_ms: 3_600_000 }, + workspaceTTLData: { workspace_ttl_ms: 0 }, isWorkspaceTTLLoading: false, isWorkspaceTTLLoadError: false, onSaveWorkspaceTTL: fn(), @@ -30,3 +30,208 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const DefaultAutostopDefault: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByText("Workspace Autostop Fallback"); + await canvas.findByText( + /Set a default autostop for agent-created workspaces/i, + ); + + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + expect(toggle).not.toBeChecked(); + expect(canvas.queryByLabelText("Autostop Fallback")).toBeNull(); + }, +}; + +export const DefaultAutostopCustomValue: Story = { + args: { + workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + expect(toggle).toBeChecked(); + + const durationInput = await canvas.findByLabelText("Autostop Fallback"); + expect(durationInput).toHaveValue("2"); + }, +}; + +export const DefaultAutostopSave: Story = { + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + await userEvent.click(toggle); + + await waitFor(() => { + expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( + { workspace_ttl_ms: 3_600_000 }, + expect.anything(), + ); + }); + + const durationInput = await canvas.findByLabelText("Autostop Fallback"); + expect(durationInput).toHaveValue("1"); + + await userEvent.clear(durationInput); + await userEvent.type(durationInput, "3"); + + const ttlForm = durationInput.closest("form"); + if (!(ttlForm instanceof HTMLFormElement)) { + throw new Error( + "Expected autostop duration input to live inside a form.", + ); + } + const saveButton = within(ttlForm).getByRole("button", { + name: "Save", + }); + await waitFor(() => { + expect(saveButton).toBeEnabled(); + }); + + await userEvent.clear(durationInput); + await waitFor(() => { + expect( + within(ttlForm).queryByRole("button", { name: "Save" }), + ).toBeNull(); + }); + }, +}; + +export const DefaultAutostopExceedsMax: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + await userEvent.click(toggle); + + const durationInput = await canvas.findByLabelText("Autostop Fallback"); + const ttlForm = durationInput.closest("form"); + if (!(ttlForm instanceof HTMLFormElement)) { + throw new Error( + "Expected autostop duration input to live inside a form.", + ); + } + + await userEvent.clear(durationInput); + await userEvent.type(durationInput, "721"); + + await waitFor(() => { + expect(canvas.getByText(/must not exceed 30 days/i)).toBeInTheDocument(); + }); + + const saveButton = within(ttlForm).getByRole("button", { + name: "Save", + }); + expect(saveButton).toBeDisabled(); + }, +}; + +export const DefaultAutostopToggleOff: Story = { + args: { + workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + expect(toggle).toBeChecked(); + + await userEvent.click(toggle); + await waitFor(() => { + expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( + { workspace_ttl_ms: 0 }, + expect.anything(), + ); + }); + }, +}; + +export const DefaultAutostopSaveDisabled: Story = { + args: { + workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + expect(toggle).toBeChecked(); + + const durationInput = await canvas.findByLabelText("Autostop Fallback"); + expect(durationInput).toHaveValue("2"); + + const ttlForm = durationInput.closest("form"); + if (!(ttlForm instanceof HTMLFormElement)) { + throw new Error( + "Expected autostop duration input to live inside a form.", + ); + } + expect(within(ttlForm).queryByRole("button", { name: "Save" })).toBeNull(); + }, +}; + +export const DefaultAutostopToggleFailure: Story = { + args: { + isSaveWorkspaceTTLError: true, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + expect(toggle).not.toBeChecked(); + + await userEvent.click(toggle); + await waitFor(() => { + expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( + { workspace_ttl_ms: 3_600_000 }, + expect.anything(), + ); + }); + expect( + canvas.getByText("Failed to save autostop setting."), + ).toBeInTheDocument(); + }, +}; + +export const DefaultAutostopToggleOffFailure: Story = { + args: { + workspaceTTLData: { workspace_ttl_ms: 7_200_000 }, + isSaveWorkspaceTTLError: true, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Enable default autostop", + }); + expect(toggle).toBeChecked(); + + const durationInput = await canvas.findByLabelText("Autostop Fallback"); + expect(durationInput).toHaveValue("2"); + + await userEvent.click(toggle); + await waitFor(() => { + expect(args.onSaveWorkspaceTTL).toHaveBeenCalledWith( + { workspace_ttl_ms: 0 }, + expect.anything(), + ); + }); + expect( + canvas.getByText("Failed to save autostop setting."), + ).toBeInTheDocument(); + }, +}; + +// DefaultAutostopNotVisibleToNonAdmin is intentionally not ported because the +// split Lifecycle page is already gated by RequirePermission. diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx index e6360590e5a00..39c4fc9d03d0e 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx @@ -1,15 +1,17 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; +import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import { AgentSettingsSystemInstructionsPageView, type AgentSettingsSystemInstructionsPageViewProps, } from "./AgentSettingsSystemInstructionsPageView"; +const mockDefaultSystemPrompt = "You are Coder, an AI coding assistant."; + const baseArgs: AgentSettingsSystemInstructionsPageViewProps = { systemPromptData: { system_prompt: "Always explain tradeoffs before proposing a change.", include_default_system_prompt: true, - default_system_prompt: "You are Coder, an AI coding assistant.", + default_system_prompt: mockDefaultSystemPrompt, }, planModeInstructionsData: { plan_mode_instructions: @@ -33,3 +35,111 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const AdminWithDefaultToggleOn: Story = { + args: { + systemPromptData: { + system_prompt: "Always use TypeScript for code examples.", + include_default_system_prompt: true, + default_system_prompt: mockDefaultSystemPrompt, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(canvasElement.ownerDocument.body); + + const toggle = await canvas.findByRole("switch", { + name: "Include Coder Agents default system prompt", + }); + expect(toggle).toBeChecked(); + const promptInput = await canvas.findByDisplayValue( + "Always use TypeScript for code examples.", + ); + expect(promptInput).toBeInTheDocument(); + expect( + canvas.getByText(/built-in Coder Agents prompt is prepended/i), + ).toBeInTheDocument(); + + await userEvent.click(canvas.getByRole("button", { name: "Preview" })); + expect(await body.findByText("Default System Prompt")).toBeInTheDocument(); + expect(body.getByText(mockDefaultSystemPrompt)).toBeInTheDocument(); + await userEvent.keyboard("{Escape}"); + await waitFor(() => { + expect(body.queryByText("Default System Prompt")).not.toBeInTheDocument(); + }); + + await userEvent.click(toggle); + const promptForm = promptInput.closest("form"); + if (!(promptForm instanceof HTMLFormElement)) { + throw new Error("Expected system prompt textarea to live inside a form."); + } + const saveButton = within(promptForm).getByRole("button", { + name: "Save", + }); + await waitFor(() => { + expect(saveButton).toBeEnabled(); + }); + }, +}; + +export const AdminWithDefaultToggleOff: Story = { + args: { + systemPromptData: { + system_prompt: "You are a custom assistant.", + include_default_system_prompt: false, + default_system_prompt: mockDefaultSystemPrompt, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const toggle = await canvas.findByRole("switch", { + name: "Include Coder Agents default system prompt", + }); + expect(toggle).not.toBeChecked(); + expect( + await canvas.findByDisplayValue("You are a custom assistant."), + ).toBeInTheDocument(); + expect( + canvas.getByText(/only the additional instructions below are used/i), + ).toBeInTheDocument(); + }, +}; + +export const InvisibleUnicodeWarningSystemPrompt: Story = { + args: { + systemPromptData: { + system_prompt: + "Normal prompt text\u200b\u200b\u200b\u200bhidden instruction", + include_default_system_prompt: true, + default_system_prompt: mockDefaultSystemPrompt, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText("System Instructions"); + const alert = await canvas.findByText(/invisible Unicode/); + expect(alert).toBeInTheDocument(); + expect(alert.textContent).toContain("4"); + }, +}; + +// The deleted combined story covered both prompt editors on one page. After +// the split, this story covers the system prompt half and the General page +// stories cover the personal instructions half. +export const NoWarningForCleanPrompt: Story = { + args: { + systemPromptData: { + system_prompt: "You are a helpful coding assistant.", + include_default_system_prompt: true, + default_system_prompt: mockDefaultSystemPrompt, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText("System Instructions"); + await canvas.findByDisplayValue("You are a helpful coding assistant."); + expect(canvas.queryByText(/invisible Unicode/)).toBeNull(); + }, +}; From d4fb08dcec4538b0c191250bf3162e0f660e79d2 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:16:37 +0000 Subject: [PATCH 04/22] docs(site/src/pages/AgentsPage): clarify backward-compat redirect and prop pass-through --- site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx | 6 ++++++ site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx | 1 + 2 files changed, 7 insertions(+) diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx index f298e47a1484e..c5cb5e3bd5de4 100644 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx @@ -1,3 +1,9 @@ +// This file exists only as a backward-compat redirect for +// /agents/settings/behavior. +// The old Behavior page was split into General, Compaction, +// System Instructions, Experiments, and Lifecycle pages. +// It now redirects to /agents/settings/general, so keep this as a +// bookmark-preserving alias that should not grow. import type { FC } from "react"; import { Navigate } from "react-router"; diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx index a53d57563996d..2c5993715294e 100644 --- a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx @@ -34,6 +34,7 @@ export const AgentSettingsGeneralPageView: FC< onSaveUserPrompt={onSaveUserPrompt} isSavingUserPrompt={isSavingUserPrompt} isSaveUserPromptError={isSaveUserPromptError} + // This shared prop kept its old multi-prompt name, and General has one prompt, so this single saving flag is correct. isAnyPromptSaving={isSavingUserPrompt} /> From a119f6dd8c5c4eb114af4a1f75fc32bae0102cba Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:40:29 +0000 Subject: [PATCH 05/22] refactor(site/src): nest agent admin settings sidebar --- .../AgentsPage/AgentsPageView.stories.tsx | 17 +- site/src/pages/AgentsPage/AgentsPageView.tsx | 8 +- .../AgentsPage/components/AgentPageHeader.tsx | 5 +- .../components/Sidebar/AgentsSidebar.tsx | 318 +++++++++++------- site/src/router.tsx | 6 +- 5 files changed, 220 insertions(+), 134 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index cb62d07ffe30e..0618bb89967a5 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -182,9 +182,13 @@ const agentsRouting = { }, { path: "compaction", element: }, { - path: "system-instructions", + path: "instructions", element: , }, + { + path: "system-instructions", + element: , + }, { path: "experiments", element: }, { path: "lifecycle", element: }, { path: "agents", element: }, @@ -697,7 +701,8 @@ export const SettingsViewResets: Story = { ).toBeInTheDocument(); }); - // Navigate to Spend section + // Navigate to the admin panel, then open the Spend section. + await userEvent.click(screen.getByText("Agent admin")); await userEvent.click(screen.getByText("Spend")); await waitFor(() => { expect( @@ -707,9 +712,11 @@ export const SettingsViewResets: Story = { ).toBeInTheDocument(); }); - // Go back to conversations - const backButton = screen.getByLabelText("Back to Agents"); - await userEvent.click(backButton); + // Step back to the top-level settings panel, then back to conversations. + const backToSettingsButton = screen.getByLabelText("Back to Settings"); + await userEvent.click(backToSettingsButton); + const backToAgentsButton = screen.getByLabelText("Back to Agents"); + await userEvent.click(backToAgentsButton); // Re-open settings, should reset to General await openSettingsView(canvasElement); diff --git a/site/src/pages/AgentsPage/AgentsPageView.tsx b/site/src/pages/AgentsPage/AgentsPageView.tsx index 6d25b8c36278f..844e7d34bf25a 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.tsx @@ -117,10 +117,10 @@ export const AgentsPageView: FC = ({ // Mobile can't fit the sidebar nav and content side by side, // so we show one or the other depending on the route depth. - const isSettingsIndex = - sidebarView.panel === "settings" && !sidebarView.section; - const isSettingsDetail = - sidebarView.panel === "settings" && Boolean(sidebarView.section); + const isSettingsPanel = + sidebarView.panel === "settings" || sidebarView.panel === "settings-admin"; + const isSettingsIndex = isSettingsPanel && !sidebarView.section; + const isSettingsDetail = isSettingsPanel && Boolean(sidebarView.section); const isAnalytics = sidebarView.panel === "analytics"; // The sidebar expects plain string error messages, but the outlet diff --git a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx index 85fbd9f9187a7..d72464b4899bc 100644 --- a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx +++ b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx @@ -32,6 +32,9 @@ export const AgentPageHeader: FC = ({ const location = useLocation(); const sidebarView = sidebarViewFromPath(location.pathname); + const isSettingsPanel = + sidebarView.panel === "settings" || sidebarView.panel === "settings-admin"; + return (
{mobileBack ? ( @@ -74,7 +77,7 @@ export const AgentPageHeader: FC = ({ aria-label="Settings" className={cn( "h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary", - sidebarView.panel === "settings" && "text-content-primary", + isSettingsPanel && "text-content-primary", )} > diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index a0062fcf3fa1e..dc65a342b1fda 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -26,29 +26,33 @@ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, + CoinsIcon, EllipsisIcon, - FileTextIcon, FilterIcon, FlaskConicalIcon, GitMergeIcon, GitPullRequestArrowIcon, GitPullRequestClosedIcon, GitPullRequestDraftIcon, - KeyRoundIcon, + KeyIcon, LayoutTemplateIcon, Loader2Icon, PanelLeftCloseIcon, PauseIcon, PinIcon, PinOffIcon, + PlugIcon, + ReceiptTextIcon, + RefreshCwIcon, ServerIcon, + Settings2Icon, SettingsIcon, ShieldIcon, + ShrinkIcon, + SparklesIcon, SquarePenIcon, - TimerIcon, Trash2Icon, - UserIcon, - WalletIcon, + UsersIcon, } from "lucide-react"; import { createContext, @@ -110,8 +114,23 @@ import { RenameChatDialog } from "./RenameChatDialog"; type SidebarView = | { panel: "chats" } | { panel: "settings"; section: string | undefined } + | { panel: "settings-admin"; section: string | undefined } | { panel: "analytics" }; +const ADMIN_SETTINGS_SECTIONS = new Set([ + "agents", + "templates", + "providers", + "models", + "mcp-servers", + "spend", + "insights", + "instructions", + "system-instructions", + "experiments", + "lifecycle", +]); + /** * Derive the current sidebar view from the URL pathname. */ @@ -121,7 +140,13 @@ export function sidebarViewFromPath(pathname: string): SidebarView { } const settingsMatch = pathname.match(/^\/agents\/settings(?:\/([^/]+))?/); if (settingsMatch) { - return { panel: "settings", section: settingsMatch[1] }; + const section = settingsMatch[1]; + return { + panel: ADMIN_SETTINGS_SECTIONS.has(section ?? "") + ? "settings-admin" + : "settings", + section, + }; } return { panel: "chats" }; } @@ -819,12 +844,18 @@ export const AgentsSidebar: FC = (props) => { const { appearance, buildInfo } = useDashboard(); const location = useLocation(); const sidebarView = sidebarViewFromPath(location.pathname); + const isSettingsPanel = + sidebarView.panel === "settings" || sidebarView.panel === "settings-admin"; + const settingsPanel = + sidebarView.panel === "settings-admin" && isAdmin + ? "settings-admin" + : "settings"; + const settingsSection = isSettingsPanel ? sidebarView.section : undefined; const providerConfigsQuery = useQuery({ ...userChatProviderConfigs(), - enabled: sidebarView.panel === "settings" && !isAdmin, + enabled: isSettingsPanel && !isAdmin, }); - const isApiKeysSection = - sidebarView.panel === "settings" && sidebarView.section === "api-keys"; + const isApiKeysSection = isSettingsPanel && settingsSection === "api-keys"; const showApiKeysItem = isAdmin || isApiKeysSection || Boolean(providerConfigsQuery.data?.length); const normalizedSearch = ""; @@ -1028,17 +1059,18 @@ export const AgentsSidebar: FC = (props) => { onOpenRenameDialog: onRenameTitle ? setChatPendingRename : undefined, }; - const subNavTitle = "Settings"; + const subNavTitle = + settingsPanel === "settings-admin" ? "Agent admin" : "Settings"; return (
{/* ── Panel 1: Chats ── */}
@@ -1057,7 +1089,7 @@ export const AgentsSidebar: FC = (props) => { aria-label="Settings" className={cn( "h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary", - sidebarView.panel === "settings" && "text-content-primary", + isSettingsPanel && "text-content-primary", )} > @@ -1269,10 +1301,10 @@ export const AgentsSidebar: FC = (props) => {
{/* Back header */}
@@ -1284,14 +1316,28 @@ export const AgentsSidebar: FC = (props) => { asChild variant="subtle" size="icon" - aria-label="Back to Agents" + aria-label={ + settingsPanel === "settings-admin" + ? "Back to Settings" + : "Back to Agents" + } className="relative z-10 h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary" > - - - + {settingsPanel === "settings-admin" ? ( + + + + ) : ( + + + + )}
{onCollapse && ( @@ -1308,112 +1354,122 @@ export const AgentsSidebar: FC = (props) => {
{/* Sub-navigation items */} - {sidebarView.panel === "settings" && ( + {settingsPanel === "settings" ? ( + ) : ( + )}
{onRenameTitle && ( @@ -1436,6 +1492,7 @@ type SettingsNavItemProps = { active: boolean; adminOnly?: boolean; disabled?: boolean; + trailingIcon?: FC<{ className?: string }>; } & ( | { to: string; replace?: boolean; state?: unknown; onClick?: () => void } | { to?: never; replace?: never; state?: never; onClick: () => void } @@ -1454,22 +1511,26 @@ const NavItemContent: FC<{ icon: FC<{ className?: string }>; label: string; adminOnly?: boolean; -}> = ({ icon: Icon, label, adminOnly }) => ( + trailingIcon?: FC<{ className?: string }>; +}> = ({ icon: Icon, label, adminOnly, trailingIcon: TrailingIcon }) => ( <> - - {label} - {adminOnly && ( - - - - - - - Admin only - - )} - + {label} + {(adminOnly || TrailingIcon) && ( + + {adminOnly && ( + + + + + + + Admin only + + )} + {TrailingIcon && } + + )} ); @@ -1479,6 +1540,7 @@ const SettingsNavItem: FC = ({ active, adminOnly, disabled, + trailingIcon, ...rest }) => { if (rest.to != null) { @@ -1492,7 +1554,12 @@ const SettingsNavItem: FC = ({ aria-current={active ? "page" : undefined} tabIndex={disabled ? -1 : undefined} > - + ); } @@ -1505,7 +1572,12 @@ const SettingsNavItem: FC = ({ className={navItemClassName(active, disabled)} aria-current={active ? "page" : undefined} > - + ); }; diff --git a/site/src/router.tsx b/site/src/router.tsx index 32c3a585bacd8..efbb8d94deef9 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -733,9 +733,13 @@ export const router = createBrowserRouter( element={} /> } /> + } + /> } From fffc09fe904b19b1862c7d91e37d17d8750d3f6e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:53:23 +0000 Subject: [PATCH 06/22] docs(site/src): align page headers with updated settings sidebar labels --- site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx | 2 +- .../AgentsPage/AgentSettingsSystemInstructionsPageView.tsx | 2 +- site/src/router.tsx | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx index 8cb746219571a..a9b57dc4117cc 100644 --- a/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx @@ -249,7 +249,7 @@ export const AgentSettingsAPIKeysPageView: FC<
diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx index 984c1ad98ab7e..b344fa23991d5 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx @@ -43,7 +43,7 @@ export const AgentSettingsSystemInstructionsPageView: FC< return (
} /> diff --git a/site/src/router.tsx b/site/src/router.tsx index efbb8d94deef9..9f67d7ea02fb5 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -736,6 +736,8 @@ export const router = createBrowserRouter( path="instructions" element={} /> + {/* Permanent backward-compat for bookmarks to the old URL. Do not remove. */} + {/* If /agents/settings/instructions is renamed, update this redirect target. */} } From 2e2c852fded4529c2930f61506a52a65ef231fa3 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:09:08 +0000 Subject: [PATCH 07/22] chore: nudge CI From 1f72ce7f75fdba69c46e5210b0b1749f1a5276e8 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:47:18 +0000 Subject: [PATCH 08/22] test(site/src/pages/AgentsPage/components/Sidebar): fix settings sidebar stories --- .../components/Sidebar/AgentsSidebar.stories.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx index e80240e272a40..1cce646b7357c 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx @@ -772,7 +772,7 @@ export const RenameChatGenerateLateResponseDoesNotClobberOtherChat: Story = { }); expect(inputB).toHaveValue("Chat B"); await userEvent.clear(inputB); - await userEvent.type(inputB, "User edit for B"); + await userEvent.paste("User edit for B"); await new Promise((resolve) => setTimeout(resolve, 250)); expect(inputB).toHaveValue("User edit for B"); @@ -833,7 +833,7 @@ export const RenameChatGenerateLateResponseDoesNotClobberSameChatReopen: Story = }); expect(input).toHaveValue("Chat same"); await userEvent.clear(input); - await userEvent.type(input, "User edit"); + await userEvent.paste("User edit"); await new Promise((resolve) => setTimeout(resolve, 250)); expect(input).toHaveValue("User edit"); @@ -1510,7 +1510,9 @@ export const SettingsAPIKeysAdmin: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect(canvas.getByText("API Keys")).toBeInTheDocument(); + await expect( + canvas.getByRole("link", { name: "Secrets (API keys)" }), + ).toBeInTheDocument(); }, }; @@ -1541,6 +1543,8 @@ export const SettingsAPIKeysNonAdmin: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect(canvas.getByText("API Keys")).toBeInTheDocument(); + await expect( + canvas.getByRole("link", { name: "Secrets (API keys)" }), + ).toBeInTheDocument(); }, }; From 7b62af959505cac277b374cc20e9821343dcdabf Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:59:51 +0000 Subject: [PATCH 09/22] test(site/src/pages/AgentsPage): harden settings reset story --- site/src/pages/AgentsPage/AgentsPageView.stories.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 0618bb89967a5..10c765b9e26a9 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -702,8 +702,8 @@ export const SettingsViewResets: Story = { }); // Navigate to the admin panel, then open the Spend section. - await userEvent.click(screen.getByText("Agent admin")); - await userEvent.click(screen.getByText("Spend")); + await userEvent.click(screen.getByRole("link", { name: "Agent admin" })); + await userEvent.click(screen.getByRole("link", { name: "Spend" })); await waitFor(() => { expect( screen.getByText( @@ -713,9 +713,13 @@ export const SettingsViewResets: Story = { }); // Step back to the top-level settings panel, then back to conversations. - const backToSettingsButton = screen.getByLabelText("Back to Settings"); + const backToSettingsButton = screen.getByRole("link", { + name: "Back to Settings", + }); await userEvent.click(backToSettingsButton); - const backToAgentsButton = screen.getByLabelText("Back to Agents"); + const backToAgentsButton = screen.getByRole("link", { + name: "Back to Agents", + }); await userEvent.click(backToAgentsButton); // Re-open settings, should reset to General From d3393e4b79fab79b3a07ce7a0c818395c014a83b Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:50:10 +0000 Subject: [PATCH 10/22] refactor(site/src): rename agent admin sub-panel and link from deployment settings --- site/src/modules/management/DeploymentSidebarView.tsx | 5 +++++ site/src/pages/AgentsPage/AgentsPageView.stories.tsx | 2 +- .../pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/site/src/modules/management/DeploymentSidebarView.tsx b/site/src/modules/management/DeploymentSidebarView.tsx index 30336d746da3b..82a038fa8e30f 100644 --- a/site/src/modules/management/DeploymentSidebarView.tsx +++ b/site/src/modules/management/DeploymentSidebarView.tsx @@ -106,6 +106,11 @@ export const DeploymentSidebarView: FC = ({ {!hasPremiumLicense && ( Premium )} + {permissions.editDeploymentConfig && ( + + Manage Coder Agents + + )}
); diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 10c765b9e26a9..29ded435d25d4 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -702,7 +702,7 @@ export const SettingsViewResets: Story = { }); // Navigate to the admin panel, then open the Spend section. - await userEvent.click(screen.getByRole("link", { name: "Agent admin" })); + await userEvent.click(screen.getByRole("link", { name: "Manage agents" })); await userEvent.click(screen.getByRole("link", { name: "Spend" })); await waitFor(() => { expect( diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index dc65a342b1fda..48629705adb6d 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -1060,7 +1060,7 @@ export const AgentsSidebar: FC = (props) => { }; const subNavTitle = - settingsPanel === "settings-admin" ? "Agent admin" : "Settings"; + settingsPanel === "settings-admin" ? "Manage agents" : "Settings"; return (
{/* ── Panel 1: Chats ── */} @@ -1386,7 +1386,7 @@ export const AgentsSidebar: FC = (props) => { {isAdmin && ( Date: Wed, 22 Apr 2026 10:06:40 +0000 Subject: [PATCH 11/22] feat(site/src/modules/management): add external-link arrow to Manage Coder Agents --- site/src/modules/management/DeploymentSidebarView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/modules/management/DeploymentSidebarView.tsx b/site/src/modules/management/DeploymentSidebarView.tsx index 82a038fa8e30f..75d6528e7bd6c 100644 --- a/site/src/modules/management/DeploymentSidebarView.tsx +++ b/site/src/modules/management/DeploymentSidebarView.tsx @@ -108,7 +108,9 @@ export const DeploymentSidebarView: FC = ({ )} {permissions.editDeploymentConfig && ( - Manage Coder Agents + + Manage Coder Agents + )}
From 3c47c3b3a813da18bdf872b5ddd7dcf3355ec320 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:27:03 +0000 Subject: [PATCH 12/22] refactor(site/src): address PR review feedback --- .../pages/AgentsPage/AgentSettingsBehaviorPage.tsx | 14 -------------- .../AgentSettingsCompactionPageView.stories.tsx | 3 --- .../AgentSettingsExperimentsPageView.tsx | 6 +++++- .../AgentSettingsGeneralPageView.stories.tsx | 2 -- .../AgentsPage/AgentSettingsGeneralPageView.tsx | 7 +++++-- .../AgentSettingsLifecyclePageView.stories.tsx | 3 --- .../AgentsPage/AgentSettingsLifecyclePageView.tsx | 6 +++++- .../AgentSettingsSystemInstructionsPageView.tsx | 6 +++++- .../pages/AgentsPage/AgentsPageView.stories.tsx | 10 +--------- .../components/Sidebar/AgentsSidebar.tsx | 10 +++------- site/src/pages/AgentsPage/types.ts | 4 ---- site/src/router.tsx | 10 ---------- 12 files changed, 24 insertions(+), 57 deletions(-) delete mode 100644 site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx delete mode 100644 site/src/pages/AgentsPage/types.ts diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx deleted file mode 100644 index c5cb5e3bd5de4..0000000000000 --- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPage.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// This file exists only as a backward-compat redirect for -// /agents/settings/behavior. -// The old Behavior page was split into General, Compaction, -// System Instructions, Experiments, and Lifecycle pages. -// It now redirects to /agents/settings/general, so keep this as a -// bookmark-preserving alias that should not grow. -import type { FC } from "react"; -import { Navigate } from "react-router"; - -const AgentSettingsBehaviorPage: FC = () => { - return ; -}; - -export default AgentSettingsBehaviorPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx index 7eba9abc11708..99fce00ba50c0 100644 --- a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx @@ -44,7 +44,4 @@ const meta = { export default meta; type Story = StoryObj; -// Interaction coverage for threshold save and reset lives in -// UserCompactionThresholdSettings.stories.tsx because this page view only wraps -// that component with a section header. export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx index d3fbbca9b8639..0c4de32633f0b 100644 --- a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx @@ -3,7 +3,11 @@ import type * as TypesGen from "#/api/typesGenerated"; import { AdminBadge } from "./components/AdminBadge"; import { SectionHeader } from "./components/SectionHeader"; import { VirtualDesktopSettings } from "./components/VirtualDesktopSettings"; -import type { MutationCallbacks } from "./types"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} export interface AgentSettingsExperimentsPageViewProps { desktopEnabledData: TypesGen.ChatDesktopEnabledResponse | undefined; diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx index 9758ae17d0212..c65c225457202 100644 --- a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx @@ -25,8 +25,6 @@ type Story = StoryObj; export const Default: Story = {}; -// These warning stories moved here because PersonalInstructionsSettings now -// lives on the General page. export const InvisibleUnicodeWarningUserPrompt: Story = { args: { userPromptData: { diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx index 2c5993715294e..c371d0381df85 100644 --- a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx @@ -3,7 +3,11 @@ import type * as TypesGen from "#/api/typesGenerated"; import { ChatFullWidthSettings } from "./components/ChatFullWidthSettings"; import { PersonalInstructionsSettings } from "./components/PersonalInstructionsSettings"; import { SectionHeader } from "./components/SectionHeader"; -import type { MutationCallbacks } from "./types"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} export interface AgentSettingsGeneralPageViewProps { userPromptData: TypesGen.UserChatCustomPrompt | undefined; @@ -34,7 +38,6 @@ export const AgentSettingsGeneralPageView: FC< onSaveUserPrompt={onSaveUserPrompt} isSavingUserPrompt={isSavingUserPrompt} isSaveUserPromptError={isSaveUserPromptError} - // This shared prop kept its old multi-prompt name, and General has one prompt, so this single saving flag is correct. isAnyPromptSaving={isSavingUserPrompt} /> diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx index f8f60bdc3e277..21c46e27a045d 100644 --- a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.stories.tsx @@ -232,6 +232,3 @@ export const DefaultAutostopToggleOffFailure: Story = { ).toBeInTheDocument(); }, }; - -// DefaultAutostopNotVisibleToNonAdmin is intentionally not ported because the -// split Lifecycle page is already gated by RequirePermission. diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx index f017f1fe051c3..e6e78c9b711ca 100644 --- a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx @@ -4,7 +4,11 @@ import { AdminBadge } from "./components/AdminBadge"; import { RetentionPeriodSettings } from "./components/RetentionPeriodSettings"; import { SectionHeader } from "./components/SectionHeader"; import { WorkspaceAutostopSettings } from "./components/WorkspaceAutostopSettings"; -import type { MutationCallbacks } from "./types"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} export interface AgentSettingsLifecyclePageViewProps { workspaceTTLData: TypesGen.ChatWorkspaceTTLResponse | undefined; diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx index b344fa23991d5..b08c115e4004a 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx @@ -4,7 +4,11 @@ import { AdminBadge } from "./components/AdminBadge"; import { PlanModeInstructionsSettings } from "./components/PlanModeInstructionsSettings"; import { SectionHeader } from "./components/SectionHeader"; import { SystemInstructionsSettings } from "./components/SystemInstructionsSettings"; -import type { MutationCallbacks } from "./types"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} export interface AgentSettingsSystemInstructionsPageViewProps { systemPromptData: TypesGen.ChatSystemPromptResponse | undefined; diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 29ded435d25d4..0ec7831c1f178 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -176,19 +176,11 @@ const agentsRouting = { children: [ { index: true, element: }, { path: "general", element: }, - { - path: "behavior", - element: , - }, { path: "compaction", element: }, { path: "instructions", element: , }, - { - path: "system-instructions", - element: , - }, { path: "experiments", element: }, { path: "lifecycle", element: }, { path: "agents", element: }, @@ -702,7 +694,7 @@ export const SettingsViewResets: Story = { }); // Navigate to the admin panel, then open the Spend section. - await userEvent.click(screen.getByRole("link", { name: "Manage agents" })); + await userEvent.click(screen.getByRole("link", { name: "Manage Agents" })); await userEvent.click(screen.getByRole("link", { name: "Spend" })); await waitFor(() => { expect( diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index 48629705adb6d..3a27b687f0e80 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -1060,7 +1060,7 @@ export const AgentsSidebar: FC = (props) => { }; const subNavTitle = - settingsPanel === "settings-admin" ? "Manage agents" : "Settings"; + settingsPanel === "settings-admin" ? "Manage Agents" : "Settings"; return (
{/* ── Panel 1: Chats ── */} @@ -1359,11 +1359,7 @@ export const AgentsSidebar: FC = (props) => { @@ -1386,7 +1382,7 @@ export const AgentsSidebar: FC = (props) => { {isAdmin && ( void; - onError?: () => void; -} diff --git a/site/src/router.tsx b/site/src/router.tsx index 9f67d7ea02fb5..d636ef293b87b 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -357,9 +357,6 @@ const AgentCreatePage = lazy( const AgentSettingsPage = lazy( () => import("./pages/AgentsPage/AgentSettingsPage"), ); -const AgentSettingsBehaviorPage = lazy( - () => import("./pages/AgentsPage/AgentSettingsBehaviorPage"), -); const AgentSettingsGeneralPage = lazy( () => import("./pages/AgentsPage/AgentSettingsGeneralPage"), ); @@ -727,7 +724,6 @@ export const router = createBrowserRouter( }> } /> } /> - } /> } @@ -736,12 +732,6 @@ export const router = createBrowserRouter( path="instructions" element={} /> - {/* Permanent backward-compat for bookmarks to the old URL. Do not remove. */} - {/* If /agents/settings/instructions is renamed, update this redirect target. */} - } - /> } From 8b464c012212aeebfbcf3cbe765593137b497545 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:39:55 +0000 Subject: [PATCH 13/22] refactor(site/src/pages/AgentsPage): type view props with UseMutateFunction --- .../AgentSettingsExperimentsPageView.tsx | 16 +++++------- .../AgentSettingsGeneralPageView.tsx | 16 +++++------- .../AgentSettingsLifecyclePageView.tsx | 26 +++++++++---------- ...gentSettingsSystemInstructionsPageView.tsx | 26 +++++++++---------- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx index 0c4de32633f0b..33d9ee58c6975 100644 --- a/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsExperimentsPageView.tsx @@ -1,20 +1,18 @@ import type { FC } from "react"; +import type { UseMutateFunction } from "react-query"; import type * as TypesGen from "#/api/typesGenerated"; import { AdminBadge } from "./components/AdminBadge"; import { SectionHeader } from "./components/SectionHeader"; import { VirtualDesktopSettings } from "./components/VirtualDesktopSettings"; -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} - export interface AgentSettingsExperimentsPageViewProps { desktopEnabledData: TypesGen.ChatDesktopEnabledResponse | undefined; - onSaveDesktopEnabled: ( - req: TypesGen.UpdateChatDesktopEnabledRequest, - options?: MutationCallbacks, - ) => void; + onSaveDesktopEnabled: UseMutateFunction< + void, + Error, + TypesGen.UpdateChatDesktopEnabledRequest, + unknown + >; isSavingDesktopEnabled: boolean; isSaveDesktopEnabledError: boolean; } diff --git a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx index c371d0381df85..200acf63bd410 100644 --- a/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsGeneralPageView.tsx @@ -1,20 +1,18 @@ import type { FC } from "react"; +import type { UseMutateFunction } from "react-query"; import type * as TypesGen from "#/api/typesGenerated"; import { ChatFullWidthSettings } from "./components/ChatFullWidthSettings"; import { PersonalInstructionsSettings } from "./components/PersonalInstructionsSettings"; import { SectionHeader } from "./components/SectionHeader"; -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} - export interface AgentSettingsGeneralPageViewProps { userPromptData: TypesGen.UserChatCustomPrompt | undefined; - onSaveUserPrompt: ( - req: TypesGen.UserChatCustomPrompt, - options?: MutationCallbacks, - ) => void; + onSaveUserPrompt: UseMutateFunction< + TypesGen.UserChatCustomPrompt, + Error, + TypesGen.UserChatCustomPrompt, + unknown + >; isSavingUserPrompt: boolean; isSaveUserPromptError: boolean; } diff --git a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx index e6e78c9b711ca..ad6fb5ed4e63b 100644 --- a/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsLifecyclePageView.tsx @@ -1,32 +1,32 @@ import type { FC } from "react"; +import type { UseMutateFunction } from "react-query"; import type * as TypesGen from "#/api/typesGenerated"; import { AdminBadge } from "./components/AdminBadge"; import { RetentionPeriodSettings } from "./components/RetentionPeriodSettings"; import { SectionHeader } from "./components/SectionHeader"; import { WorkspaceAutostopSettings } from "./components/WorkspaceAutostopSettings"; -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} - export interface AgentSettingsLifecyclePageViewProps { workspaceTTLData: TypesGen.ChatWorkspaceTTLResponse | undefined; isWorkspaceTTLLoading: boolean; isWorkspaceTTLLoadError: boolean; - onSaveWorkspaceTTL: ( - req: TypesGen.UpdateChatWorkspaceTTLRequest, - options?: MutationCallbacks, - ) => void; + onSaveWorkspaceTTL: UseMutateFunction< + void, + Error, + TypesGen.UpdateChatWorkspaceTTLRequest, + unknown + >; isSavingWorkspaceTTL: boolean; isSaveWorkspaceTTLError: boolean; retentionDaysData: TypesGen.ChatRetentionDaysResponse | undefined; isRetentionDaysLoading: boolean; isRetentionDaysLoadError: boolean; - onSaveRetentionDays: ( - req: TypesGen.UpdateChatRetentionDaysRequest, - options?: MutationCallbacks, - ) => void; + onSaveRetentionDays: UseMutateFunction< + void, + Error, + TypesGen.UpdateChatRetentionDaysRequest, + unknown + >; isSavingRetentionDays: boolean; isSaveRetentionDaysError: boolean; } diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx index b08c115e4004a..aa6b0b983622f 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx @@ -1,30 +1,30 @@ import type { FC } from "react"; +import type { UseMutateFunction } from "react-query"; import type * as TypesGen from "#/api/typesGenerated"; import { AdminBadge } from "./components/AdminBadge"; import { PlanModeInstructionsSettings } from "./components/PlanModeInstructionsSettings"; import { SectionHeader } from "./components/SectionHeader"; import { SystemInstructionsSettings } from "./components/SystemInstructionsSettings"; -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} - export interface AgentSettingsSystemInstructionsPageViewProps { systemPromptData: TypesGen.ChatSystemPromptResponse | undefined; planModeInstructionsData: | TypesGen.ChatPlanModeInstructionsResponse | undefined; - onSaveSystemPrompt: ( - req: TypesGen.UpdateChatSystemPromptRequest, - options?: MutationCallbacks, - ) => void; + onSaveSystemPrompt: UseMutateFunction< + void, + Error, + TypesGen.UpdateChatSystemPromptRequest, + unknown + >; isSavingSystemPrompt: boolean; isSaveSystemPromptError: boolean; - onSavePlanModeInstructions: ( - req: TypesGen.UpdateChatPlanModeInstructionsRequest, - options?: MutationCallbacks, - ) => void; + onSavePlanModeInstructions: UseMutateFunction< + void, + Error, + TypesGen.UpdateChatPlanModeInstructionsRequest, + unknown + >; isSavingPlanModeInstructions: boolean; isSavePlanModeInstructionsError: boolean; } From 8a7527a51de65128db92f01b12d0d816fca3b9f0 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:02:10 +0000 Subject: [PATCH 14/22] test(site/src/pages/AgentsPage): await sidebar sub-panel transitions in settings reset story --- site/src/pages/AgentsPage/AgentsPageView.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 0ec7831c1f178..97a0cf40a43d6 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -695,7 +695,7 @@ export const SettingsViewResets: Story = { // Navigate to the admin panel, then open the Spend section. await userEvent.click(screen.getByRole("link", { name: "Manage Agents" })); - await userEvent.click(screen.getByRole("link", { name: "Spend" })); + await userEvent.click(await screen.findByRole("link", { name: "Spend" })); await waitFor(() => { expect( screen.getByText( @@ -705,11 +705,11 @@ export const SettingsViewResets: Story = { }); // Step back to the top-level settings panel, then back to conversations. - const backToSettingsButton = screen.getByRole("link", { + const backToSettingsButton = await screen.findByRole("link", { name: "Back to Settings", }); await userEvent.click(backToSettingsButton); - const backToAgentsButton = screen.getByRole("link", { + const backToAgentsButton = await screen.findByRole("link", { name: "Back to Agents", }); await userEvent.click(backToAgentsButton); From cb2c80865e71ad682ffa7109457aa4583af5d53e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:30:57 +0000 Subject: [PATCH 15/22] refactor(site/src/pages/AgentsPage): remove dead system-instructions refs and add compaction story coverage --- ...gentSettingsCompactionPageView.stories.tsx | 63 ++++++++++++++++++- .../components/Sidebar/AgentsSidebar.tsx | 6 +- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx index 99fce00ba50c0..ffb10622c074a 100644 --- a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; +import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import type * as TypesGen from "#/api/typesGenerated"; import { AgentSettingsCompactionPageView, @@ -45,3 +45,64 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const SavesThreshold: Story = { + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const thresholdInput = await canvas.findByLabelText( + "GPT 4.1 Mini compaction threshold", + ); + + await userEvent.clear(thresholdInput); + await userEvent.type(thresholdInput, "80"); + + const saveButton = await canvas.findByRole("button", { + name: "Save 1 change", + }); + await waitFor(() => { + expect(saveButton).toBeEnabled(); + }); + await userEvent.click(saveButton); + + await waitFor(() => { + expect(args.onSaveThreshold).toHaveBeenCalledWith("model-config-1", 80); + }); + }, +}; + +export const ResetsThreshold: Story = { + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const resetButton = await canvas.findByLabelText( + "Reset GPT 4.1 Mini to default", + ); + + await waitFor(() => { + expect(resetButton).toBeEnabled(); + }); + await userEvent.click(resetButton); + + await waitFor(() => { + expect(args.onResetThreshold).toHaveBeenCalledWith("model-config-1"); + }); + }, +}; + +export const InvalidThresholdIsRejected: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const thresholdInput = await canvas.findByLabelText( + "GPT 4.1 Mini compaction threshold", + ); + + await userEvent.clear(thresholdInput); + await userEvent.type(thresholdInput, "150"); + + await waitFor(() => { + expect(thresholdInput).toHaveAttribute("aria-invalid", "true"); + expect( + canvas.queryByRole("button", { name: /save \d+ changes?/i }), + ).toBeNull(); + }); + }, +}; diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index 3a27b687f0e80..eb5ffc8851358 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -126,7 +126,6 @@ const ADMIN_SETTINGS_SECTIONS = new Set([ "spend", "insights", "instructions", - "system-instructions", "experiments", "lifecycle", ]); @@ -1437,10 +1436,7 @@ export const AgentsSidebar: FC = (props) => { From 7743b8cbd93a8670712f59a66036e104dd5bb62a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:56:05 +0000 Subject: [PATCH 16/22] fix(site/src/pages/AgentsPage): restore mobile admin sub-panel nav access --- .../AgentsPage/AgentsPageView.stories.tsx | 21 +++++++++++++++++++ .../components/Sidebar/AgentsSidebar.tsx | 7 +++++-- site/src/router.tsx | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 97a0cf40a43d6..1b79d26956c92 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -183,6 +183,7 @@ const agentsRouting = { }, { path: "experiments", element: }, { path: "lifecycle", element: }, + { path: "admin", element: }, { path: "agents", element: }, { path: "spend", element: }, { @@ -679,6 +680,26 @@ export const OpensSettingsForNonAdmins: Story = { }, }; +export const OpensAdminSubPanelOnMobile: Story = { + args: { + isAgentsAdmin: true, + }, + parameters: { + viewport: { defaultViewport: "mobile1" }, + }, + play: async ({ canvasElement }) => { + await openSettingsView(canvasElement); + await userEvent.click(screen.getByRole("link", { name: "Manage Agents" })); + + await expect( + await screen.findByRole("link", { name: "Providers" }), + ).toBeInTheDocument(); + await expect( + await screen.findByRole("link", { name: "Spend" }), + ).toBeInTheDocument(); + }, +}; + export const SettingsViewResets: Story = { args: { isAgentsAdmin: true, diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index eb5ffc8851358..56617bbfb193a 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -140,6 +140,9 @@ export function sidebarViewFromPath(pathname: string): SidebarView { const settingsMatch = pathname.match(/^\/agents\/settings(?:\/([^/]+))?/); if (settingsMatch) { const section = settingsMatch[1]; + if (section === "admin") { + return { panel: "settings-admin", section: undefined }; + } return { panel: ADMIN_SETTINGS_SECTIONS.has(section ?? "") ? "settings-admin" @@ -1383,7 +1386,7 @@ export const AgentsSidebar: FC = (props) => { icon={Settings2Icon} label="Manage Agents" active={sidebarView.panel === "settings-admin" && isAdmin} - to="/agents/settings/agents" + to="/agents/settings/admin" state={location.state} trailingIcon={ChevronRightIcon} /> @@ -1394,7 +1397,7 @@ export const AgentsSidebar: FC = (props) => { diff --git a/site/src/router.tsx b/site/src/router.tsx index d636ef293b87b..5db47c42bb2be 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -737,6 +737,7 @@ export const router = createBrowserRouter( element={} /> } /> + } /> } /> } /> } /> From 5175fa0a9a3e50fc58094a2efb2db782b1d6c346 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:17:27 +0000 Subject: [PATCH 17/22] refactor(site/src/pages/AgentsPage): polish sidebar and rename instructions page --- ....tsx => AgentSettingsInstructionsPage.tsx} | 8 +++---- ...tSettingsInstructionsPageView.stories.tsx} | 16 +++++++------- ... => AgentSettingsInstructionsPageView.tsx} | 6 ++--- .../AgentsPage/AgentsPageView.stories.tsx | 4 ++-- site/src/pages/AgentsPage/AgentsPageView.tsx | 4 ++-- .../AgentsPage/components/AgentPageHeader.tsx | 5 ++--- .../components/Sidebar/AgentsSidebar.tsx | 22 +++++++++++++------ site/src/router.tsx | 6 ++--- 8 files changed, 39 insertions(+), 32 deletions(-) rename site/src/pages/AgentsPage/{AgentSettingsSystemInstructionsPage.tsx => AgentSettingsInstructionsPage.tsx} (86%) rename site/src/pages/AgentsPage/{AgentSettingsSystemInstructionsPageView.stories.tsx => AgentSettingsInstructionsPageView.stories.tsx} (90%) rename site/src/pages/AgentsPage/{AgentSettingsSystemInstructionsPageView.tsx => AgentSettingsInstructionsPageView.tsx} (92%) diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPage.tsx b/site/src/pages/AgentsPage/AgentSettingsInstructionsPage.tsx similarity index 86% rename from site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPage.tsx rename to site/src/pages/AgentsPage/AgentSettingsInstructionsPage.tsx index e9ae44eb18cfa..ed423856c1778 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPage.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsInstructionsPage.tsx @@ -8,9 +8,9 @@ import { } from "#/api/queries/chats"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { RequirePermission } from "#/modules/permissions/RequirePermission"; -import { AgentSettingsSystemInstructionsPageView } from "./AgentSettingsSystemInstructionsPageView"; +import { AgentSettingsInstructionsPageView } from "./AgentSettingsInstructionsPageView"; -const AgentSettingsSystemInstructionsPage: FC = () => { +const AgentSettingsInstructionsPage: FC = () => { const { permissions } = useAuthenticated(); const queryClient = useQueryClient(); @@ -31,7 +31,7 @@ const AgentSettingsSystemInstructionsPage: FC = () => { return ( - { ); }; -export default AgentSettingsSystemInstructionsPage; +export default AgentSettingsInstructionsPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsInstructionsPageView.stories.tsx similarity index 90% rename from site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx rename to site/src/pages/AgentsPage/AgentSettingsInstructionsPageView.stories.tsx index 39c4fc9d03d0e..4812d8ae79aa0 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsInstructionsPageView.stories.tsx @@ -1,13 +1,13 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import { - AgentSettingsSystemInstructionsPageView, - type AgentSettingsSystemInstructionsPageViewProps, -} from "./AgentSettingsSystemInstructionsPageView"; + AgentSettingsInstructionsPageView, + type AgentSettingsInstructionsPageViewProps, +} from "./AgentSettingsInstructionsPageView"; const mockDefaultSystemPrompt = "You are Coder, an AI coding assistant."; -const baseArgs: AgentSettingsSystemInstructionsPageViewProps = { +const baseArgs: AgentSettingsInstructionsPageViewProps = { systemPromptData: { system_prompt: "Always explain tradeoffs before proposing a change.", include_default_system_prompt: true, @@ -26,13 +26,13 @@ const baseArgs: AgentSettingsSystemInstructionsPageViewProps = { }; const meta = { - title: "pages/AgentsPage/AgentSettingsSystemInstructionsPageView", - component: AgentSettingsSystemInstructionsPageView, + title: "pages/AgentsPage/AgentSettingsInstructionsPageView", + component: AgentSettingsInstructionsPageView, args: baseArgs, -} satisfies Meta; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsInstructionsPageView.tsx similarity index 92% rename from site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx rename to site/src/pages/AgentsPage/AgentSettingsInstructionsPageView.tsx index aa6b0b983622f..6468cc4c8ec26 100644 --- a/site/src/pages/AgentsPage/AgentSettingsSystemInstructionsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsInstructionsPageView.tsx @@ -6,7 +6,7 @@ import { PlanModeInstructionsSettings } from "./components/PlanModeInstructionsS import { SectionHeader } from "./components/SectionHeader"; import { SystemInstructionsSettings } from "./components/SystemInstructionsSettings"; -export interface AgentSettingsSystemInstructionsPageViewProps { +export interface AgentSettingsInstructionsPageViewProps { systemPromptData: TypesGen.ChatSystemPromptResponse | undefined; planModeInstructionsData: | TypesGen.ChatPlanModeInstructionsResponse @@ -29,8 +29,8 @@ export interface AgentSettingsSystemInstructionsPageViewProps { isSavePlanModeInstructionsError: boolean; } -export const AgentSettingsSystemInstructionsPageView: FC< - AgentSettingsSystemInstructionsPageViewProps +export const AgentSettingsInstructionsPageView: FC< + AgentSettingsInstructionsPageViewProps > = ({ systemPromptData, planModeInstructionsData, diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 1b79d26956c92..f842b7dd4137b 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -31,10 +31,10 @@ import { AgentSettingsAgentsPageView } from "./AgentSettingsAgentsPageView"; import AgentSettingsCompactionPage from "./AgentSettingsCompactionPage"; import AgentSettingsExperimentsPage from "./AgentSettingsExperimentsPage"; import AgentSettingsGeneralPage from "./AgentSettingsGeneralPage"; +import AgentSettingsInstructionsPage from "./AgentSettingsInstructionsPage"; import AgentSettingsLifecyclePage from "./AgentSettingsLifecyclePage"; import AgentSettingsPage from "./AgentSettingsPage"; import AgentSettingsSpendPage from "./AgentSettingsSpendPage"; -import AgentSettingsSystemInstructionsPage from "./AgentSettingsSystemInstructionsPage"; import { AgentsPageView } from "./AgentsPageView"; import type { ModelSelectorOption } from "./components/ChatElements"; @@ -179,7 +179,7 @@ const agentsRouting = { { path: "compaction", element: }, { path: "instructions", - element: , + element: , }, { path: "experiments", element: }, { path: "lifecycle", element: }, diff --git a/site/src/pages/AgentsPage/AgentsPageView.tsx b/site/src/pages/AgentsPage/AgentsPageView.tsx index 844e7d34bf25a..bf0de9efa217c 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.tsx @@ -6,6 +6,7 @@ import { pageTitle } from "#/utils/page"; import type { ModelSelectorOption } from "./components/ChatElements"; import { AgentsSidebar, + isSettingsView, sidebarViewFromPath, } from "./components/Sidebar/AgentsSidebar"; import type { ChatDetailError } from "./utils/usageLimitMessage"; @@ -117,8 +118,7 @@ export const AgentsPageView: FC = ({ // Mobile can't fit the sidebar nav and content side by side, // so we show one or the other depending on the route depth. - const isSettingsPanel = - sidebarView.panel === "settings" || sidebarView.panel === "settings-admin"; + const isSettingsPanel = isSettingsView(sidebarView); const isSettingsIndex = isSettingsPanel && !sidebarView.section; const isSettingsDetail = isSettingsPanel && Boolean(sidebarView.section); const isAnalytics = sidebarView.panel === "analytics"; diff --git a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx index d72464b4899bc..833665bab5b06 100644 --- a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx +++ b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx @@ -12,7 +12,7 @@ import { CoderIcon } from "#/components/Icons/CoderIcon"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { cn } from "#/utils/cn"; import type { AgentsOutletContext } from "../AgentsPageView"; -import { sidebarViewFromPath } from "./Sidebar/AgentsSidebar"; +import { isSettingsView, sidebarViewFromPath } from "./Sidebar/AgentsSidebar"; interface AgentPageHeaderProps { children?: ReactNode; @@ -32,8 +32,7 @@ export const AgentPageHeader: FC = ({ const location = useLocation(); const sidebarView = sidebarViewFromPath(location.pathname); - const isSettingsPanel = - sidebarView.panel === "settings" || sidebarView.panel === "settings-admin"; + const isSettingsPanel = isSettingsView(sidebarView); return (
diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index 56617bbfb193a..4fe24b6644b71 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -52,7 +52,7 @@ import { SparklesIcon, SquarePenIcon, Trash2Icon, - UsersIcon, + UserIcon, } from "lucide-react"; import { createContext, @@ -153,6 +153,12 @@ export function sidebarViewFromPath(pathname: string): SidebarView { return { panel: "chats" }; } +export function isSettingsView( + view: SidebarView, +): view is Extract { + return view.panel === "settings" || view.panel === "settings-admin"; +} + interface AgentsSidebarProps { chats: readonly Chat[]; chatErrorReasons: Record; @@ -846,13 +852,15 @@ export const AgentsSidebar: FC = (props) => { const { appearance, buildInfo } = useDashboard(); const location = useLocation(); const sidebarView = sidebarViewFromPath(location.pathname); - const isSettingsPanel = - sidebarView.panel === "settings" || sidebarView.panel === "settings-admin"; + const isSettingsPanel = isSettingsView(sidebarView); + const isFallbackToUserPanel = + sidebarView.panel === "settings-admin" && !isAdmin; const settingsPanel = sidebarView.panel === "settings-admin" && isAdmin ? "settings-admin" : "settings"; - const settingsSection = isSettingsPanel ? sidebarView.section : undefined; + const settingsSection = + isSettingsPanel && !isFallbackToUserPanel ? sidebarView.section : undefined; const providerConfigsQuery = useQuery({ ...userChatProviderConfigs(), enabled: isSettingsPanel && !isAdmin, @@ -1359,7 +1367,7 @@ export const AgentsSidebar: FC = (props) => { {settingsPanel === "settings" ? (