diff --git a/site/src/modules/management/AISettingsSidebarView.tsx b/site/src/modules/management/AISettingsSidebarView.tsx index b05cb2350c..66ec8c8867 100644 --- a/site/src/modules/management/AISettingsSidebarView.tsx +++ b/site/src/modules/management/AISettingsSidebarView.tsx @@ -1,4 +1,3 @@ -import { ArrowUpRightIcon } from "lucide-react"; import type { FC } from "react"; import { Sidebar as BaseSidebar, @@ -43,6 +42,11 @@ const AISettingsSidebarView: FC = ({ Instructions )} + {permissions.editDeploymentConfig && ( + + Coder Agents + + )} {permissions.editDeploymentConfig && ( Lifecycle @@ -58,13 +62,6 @@ const AISettingsSidebarView: FC = ({ MCP servers )} - {permissions.editDeploymentConfig && ( - -
- Manage Coder Agents -
-
- )} ); diff --git a/site/src/pages/AgentsPage/AgentSettingsAgentsPage.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx similarity index 94% rename from site/src/pages/AgentsPage/AgentSettingsAgentsPage.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx index 5f664afb15..06162afe73 100644 --- a/site/src/pages/AgentsPage/AgentSettingsAgentsPage.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx @@ -14,7 +14,8 @@ import { import type * as TypesGen from "#/api/typesGenerated"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { RequirePermission } from "#/modules/permissions/RequirePermission"; -import { AgentSettingsAgentsPageView } from "./AgentSettingsAgentsPageView"; +import { pageTitle } from "#/utils/page"; +import { CoderAgentsPageView } from "./CoderAgentsPageView"; const generalOverrideContext: TypesGen.ChatModelOverrideContext = "general"; const exploreOverrideContext: TypesGen.ChatModelOverrideContext = "explore"; @@ -45,7 +46,7 @@ const updateChatModelOverrideMutation = ( }, }); -const AgentSettingsAgentsPage: FC = () => { +const CoderAgentsPage: FC = () => { const { permissions } = useAuthenticated(); const queryClient = useQueryClient(); const canEditDeploymentConfig = permissions.editDeploymentConfig; @@ -85,7 +86,8 @@ const AgentSettingsAgentsPage: FC = () => { return ( - {pageTitle("Coder Agents", "AI Settings")} + { @@ -135,4 +137,4 @@ const AgentSettingsAgentsPage: FC = () => { ); }; -export default AgentSettingsAgentsPage; +export default CoderAgentsPage; diff --git a/site/src/pages/AgentsPage/AgentSettingsAgentsPageView.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx similarity index 90% rename from site/src/pages/AgentsPage/AgentSettingsAgentsPageView.stories.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx index 03bd43ef8f..643bae1df4 100644 --- a/site/src/pages/AgentsPage/AgentSettingsAgentsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx @@ -3,9 +3,9 @@ import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import type * as TypesGen from "#/api/typesGenerated"; import { MockChatModelConfig } from "#/testHelpers/chatModels"; import { - AgentSettingsAgentsPageView, - type AgentSettingsAgentsPageViewProps, -} from "./AgentSettingsAgentsPageView"; + CoderAgentsPageView, + type CoderAgentsPageViewProps, +} from "./CoderAgentsPageView"; const OVERRIDE_MALFORMED_WARNING = "The saved override is malformed and is being treated as unset. Click Save to clear it."; @@ -105,8 +105,8 @@ const allModelConfigs: TypesGen.ChatModelConfig[] = [ ]; const buildArgs = ( - overrides: Partial = {}, -): AgentSettingsAgentsPageViewProps => ({ + overrides: Partial = {}, +): CoderAgentsPageViewProps => ({ adminOverridesData: { allow_users: false }, adminOverridesError: undefined, onRetryAdminOverrides: fn(), @@ -138,13 +138,11 @@ const getSection = async ( ): Promise => { const canvas = within(canvasElement); const heading = await canvas.findByRole("heading", { name: headingName }); - const section = heading.closest("section"); - if (!(section instanceof HTMLElement)) { - throw new Error( - `Expected ${headingName} heading to live inside a section.`, - ); + const setting = heading.closest("form"); + if (!(setting instanceof HTMLElement)) { + throw new Error(`Expected ${headingName} heading to live inside a form.`); } - return section; + return setting; }; const selectModelInSection = async ( @@ -162,29 +160,36 @@ const selectModelInSection = async ( }; const meta = { - title: "pages/AgentsPage/AgentSettingsAgentsPageView", - component: AgentSettingsAgentsPageView, + title: "pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView", + component: CoderAgentsPageView, args: buildArgs(), -} satisfies Meta; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const AllOverridesUnset: Story = { args: buildArgs(), play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await canvas.findByText("Agents"); + expect( + await canvas.findByRole("heading", { name: "Coder Agents" }), + ).toBeVisible(); + expect( + canvas.getByText( + "Configure deployment-wide defaults for Coder Agents and agent-specific capabilities.", + ), + ).toBeVisible(); + expect(canvas.getByText("Allow personal model overrides")).toBeVisible(); const headings = await canvas.findAllByRole("heading", { level: 3 }); expect(headings.map((heading) => heading.textContent?.trim())).toEqual([ - "Enable users to define their personal overrides", "General model", "Title generation model", "Explore subagent model", ]); await canvas.findByText( - "Choose a model for generated chat titles. Leave unset to use Coder's default title algorithm, which currently tries fast title models for configured providers first, for example Claude Haiku, GPT-4o mini, and Gemini Flash, then falls back to the chat's current model. When a model is selected here, Coder uses only that model for title generation. Recommended title models are fast and low cost.", + "Leave unset to use Coder's title default, which prefers fast models from configured providers.", ); const unsetSections = [ @@ -204,8 +209,8 @@ export const AllOverridesUnset: Story = { within(section).getByRole("combobox", { name: placeholder }), ).toBeInTheDocument(); expect( - within(section).getByRole("button", { name: "Save" }), - ).toBeDisabled(); + within(section).queryByRole("button", { name: "Save" }), + ).not.toBeInTheDocument(); } }, }; @@ -217,7 +222,7 @@ export const PersonalOverridesDisabled: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const toggle = await canvas.findByRole("switch", { - name: "Enable users to define their personal overrides", + name: "Allow personal model overrides", }); expect(toggle).not.toBeChecked(); @@ -231,7 +236,7 @@ export const PersonalOverridesEnabled: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const toggle = await canvas.findByRole("switch", { - name: "Enable users to define their personal overrides", + name: "Allow personal model overrides", }); expect(toggle).toBeChecked(); diff --git a/site/src/pages/AgentsPage/AgentSettingsAgentsPageView.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx similarity index 61% rename from site/src/pages/AgentsPage/AgentSettingsAgentsPageView.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx index 46e2300975..0e25b2bdeb 100644 --- a/site/src/pages/AgentsPage/AgentSettingsAgentsPageView.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx @@ -1,10 +1,14 @@ import type { FC } from "react"; import type * as TypesGen from "#/api/typesGenerated"; +import { + SettingsHeader, + SettingsHeaderDescription, + SettingsHeaderTitle, +} from "#/components/SettingsHeader/SettingsHeader"; import { AdminPersonalModelOverridesSettings, type SavePersonalModelOverridesAdminSetting, } from "./components/AdminPersonalModelOverridesSettings"; -import { SectionHeader } from "./components/SectionHeader"; import { type MutationCallbacks, SubagentModelOverrideSettings, @@ -15,7 +19,7 @@ type SaveModelOverride = ( options?: MutationCallbacks, ) => void; -export interface AgentSettingsAgentsPageViewProps { +export interface CoderAgentsPageViewProps { adminOverridesData?: TypesGen.ChatPersonalModelOverridesAdminSettings; adminOverridesError?: unknown; onRetryAdminOverrides?: () => void; @@ -40,9 +44,7 @@ export interface AgentSettingsAgentsPageViewProps { isSaveExploreModelOverrideError: boolean; } -export const AgentSettingsAgentsPageView: FC< - AgentSettingsAgentsPageViewProps -> = ({ +export const CoderAgentsPageView: FC = ({ adminOverridesData, adminOverridesError, onRetryAdminOverrides, @@ -76,30 +78,28 @@ export const AgentSettingsAgentsPageView: FC< isSaveGeneralModelOverrideError; return ( -
- - - {showGeneralModelSection && onSaveGeneralModelOverride && ( -
- +
+ + Coder Agents + + Configure deployment-wide defaults for Coder Agents and agent-specific + capabilities. + + +
+ + {showGeneralModelSection && onSaveGeneralModelOverride && ( -
- )} -
- + )} -
-
- - Deployment-wide model override for read-only Explore subagents - launched through the spawn_agent tool with a - type=explore argument. - - } + description="Used for read-only codebase exploration before work returns to the main agent." modelOverrideData={exploreModelOverrideData} enabledModelConfigs={enabledModelConfigs} modelConfigsError={modelConfigsError} @@ -163,9 +135,8 @@ export const AgentSettingsAgentsPageView: FC< isSaving={isSavingExploreModelOverride} isSaveError={isSaveExploreModelOverrideError} saveErrorMessage="Failed to save Explore model override." - showHeader={false} /> -
+
); }; diff --git a/site/src/pages/AgentsPage/components/AdminPersonalModelOverridesSettings.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AdminPersonalModelOverridesSettings.stories.tsx similarity index 78% rename from site/src/pages/AgentsPage/components/AdminPersonalModelOverridesSettings.stories.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/components/AdminPersonalModelOverridesSettings.stories.tsx index 1ab37187ad..4ee4e75d12 100644 --- a/site/src/pages/AgentsPage/components/AdminPersonalModelOverridesSettings.stories.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AdminPersonalModelOverridesSettings.stories.tsx @@ -13,7 +13,8 @@ const baseArgs = { }; const meta = { - title: "pages/AgentsPage/components/AdminPersonalModelOverridesSettings", + title: + "pages/AISettingsPage/CoderAgentsPage/components/AdminPersonalModelOverridesSettings", component: AdminPersonalModelOverridesSettings, args: baseArgs, } satisfies Meta; @@ -25,16 +26,16 @@ export const FeatureDisabled: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const toggle = await canvas.findByRole("switch", { - name: "Enable users to define their personal overrides", + name: "Allow personal model overrides", }); expect( - await canvas.findByText( - "Enable users to define their personal overrides", - ), + await canvas.findByText("Allow personal model overrides"), ).toBeInTheDocument(); expect(toggle).not.toBeChecked(); - expect(canvas.getByRole("button", { name: "Save" })).toBeDisabled(); + expect( + canvas.queryByRole("button", { name: "Save" }), + ).not.toBeInTheDocument(); }, }; @@ -50,10 +51,12 @@ export const LoadingState: Story = { ).toBeInTheDocument(); expect( canvas.getByRole("switch", { - name: "Enable users to define their personal overrides", + name: "Allow personal model overrides", }), ).toBeDisabled(); - expect(canvas.getByRole("button", { name: "Save" })).toBeDisabled(); + expect( + canvas.queryByRole("button", { name: "Save" }), + ).not.toBeInTheDocument(); }, }; @@ -71,7 +74,9 @@ export const LoadError: Story = { expect( canvas.queryByText("Loading personal model override settings..."), ).not.toBeInTheDocument(); - expect(canvas.getByRole("button", { name: "Save" })).toBeDisabled(); + expect( + canvas.queryByRole("button", { name: "Save" }), + ).not.toBeInTheDocument(); await userEvent.click(canvas.getByRole("button", { name: "Retry" })); expect(args.onRetryAdminSettings).toHaveBeenCalled(); }, @@ -84,11 +89,13 @@ export const FeatureEnabled: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const toggle = await canvas.findByRole("switch", { - name: "Enable users to define their personal overrides", + name: "Allow personal model overrides", }); expect(toggle).toBeChecked(); - expect(canvas.getByRole("button", { name: "Save" })).toBeDisabled(); + expect( + canvas.queryByRole("button", { name: "Save" }), + ).not.toBeInTheDocument(); }, }; @@ -99,11 +106,11 @@ export const Saving: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const toggle = await canvas.findByRole("switch", { - name: "Enable users to define their personal overrides", + name: "Allow personal model overrides", }); expect(toggle).toBeDisabled(); - expect(canvas.getByRole("button", { name: "Save" })).toBeDisabled(); + expect(canvas.getByRole("button", { name: /save/i })).toBeDisabled(); }, }; @@ -126,11 +133,10 @@ export const SavesChangedSetting: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const toggle = await canvas.findByRole("switch", { - name: "Enable users to define their personal overrides", + name: "Allow personal model overrides", }); - const saveButton = canvas.getByRole("button", { name: "Save" }); - await userEvent.click(toggle); + const saveButton = await canvas.findByRole("button", { name: "Save" }); await waitFor(() => { expect(saveButton).toBeEnabled(); }); diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/components/AdminPersonalModelOverridesSettings.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AdminPersonalModelOverridesSettings.tsx new file mode 100644 index 0000000000..84a4b6a33f --- /dev/null +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AdminPersonalModelOverridesSettings.tsx @@ -0,0 +1,146 @@ +import { useFormik } from "formik"; +import type { FC } from "react"; +import type * as TypesGen from "#/api/typesGenerated"; +import { ErrorAlert } from "#/components/Alert/ErrorAlert"; +import { Button } from "#/components/Button/Button"; +import { Spinner } from "#/components/Spinner/Spinner"; +import { Switch } from "#/components/Switch/Switch"; +import { + TemporarySavedState, + useTemporarySavedState, +} from "#/components/TemporarySavedState/TemporarySavedState"; + +interface MutationCallbacks { + onSuccess?: () => void; + onError?: () => void; +} + +export type SavePersonalModelOverridesAdminSetting = ( + req: TypesGen.UpdateChatPersonalModelOverridesAdminSettingsRequest, + options?: MutationCallbacks, +) => void; + +interface AdminPersonalModelOverridesSettingsProps { + adminSettings: TypesGen.ChatPersonalModelOverridesAdminSettings | undefined; + adminSettingsError?: unknown; + onRetryAdminSettings?: () => void; + isRetryingAdminSettings?: boolean; + onSaveAdminSetting: SavePersonalModelOverridesAdminSetting; + isSavingAdminSetting: boolean; + isSaveAdminSettingError: boolean; +} + +export const AdminPersonalModelOverridesSettings: FC< + AdminPersonalModelOverridesSettingsProps +> = ({ + adminSettings, + adminSettingsError, + onRetryAdminSettings, + isRetryingAdminSettings = false, + onSaveAdminSetting, + isSavingAdminSetting, + isSaveAdminSettingError, +}) => { + const { isSavedVisible, showSavedState } = useTemporarySavedState(); + const hasLoadedAdminSettings = adminSettings !== undefined; + const hasAdminSettingsError = adminSettingsError != null; + const form = useFormik({ + enableReinitialize: true, + initialValues: { + allow_users: adminSettings?.allow_users ?? false, + }, + onSubmit: (values, { resetForm }) => { + onSaveAdminSetting( + { + allow_users: values.allow_users, + }, + { + onSuccess: () => { + showSavedState(); + resetForm({ values }); + }, + }, + ); + }, + }); + const isDisabled = isSavingAdminSetting || !hasLoadedAdminSettings; + const showSave = form.dirty || isSavingAdminSetting || isSavedVisible; + const showStatusArea = + hasAdminSettingsError || !hasLoadedAdminSettings || isSaveAdminSettingError; + + return ( +
+
+ { + void form.setFieldValue("allow_users", checked); + }} + aria-label="Allow personal model overrides" + type="button" + disabled={isDisabled} + className="mt-0.5" + /> +
+ Allow personal model overrides + + Saved user preferences are preserved but ignored while disabled. + +
+
+ {showSave && ( +
+ {isSavedVisible ? ( + + ) : ( + + )} +
+ )} + {showStatusArea && ( +
+ {hasAdminSettingsError && ( +
+ + {onRetryAdminSettings && ( + + )} +
+ )} + {!hasAdminSettingsError && !hasLoadedAdminSettings && ( +

+ Loading personal model override settings... +

+ )} + {isSaveAdminSettingError && ( +

+ Failed to save personal model override settings. +

+ )} +
+ )} +
+ ); +}; diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/components/AgentSettingLayout.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AgentSettingLayout.stories.tsx new file mode 100644 index 0000000000..e849340c2b --- /dev/null +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AgentSettingLayout.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, fn, within } from "storybook/test"; +import { Button } from "#/components/Button/Button"; +import { AgentSettingLayout } from "./AgentSettingLayout"; + +const meta = { + title: "pages/AISettingsPage/CoderAgentsPage/components/AgentSettingLayout", + component: AgentSettingLayout, + args: { + title: "General model", + description: + "Used by delegated agents that can edit files or run commands.", + showSave: false, + isSaving: false, + isSavedVisible: false, + saveDisabled: true, + onSubmit: fn(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: , + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect( + await canvas.findByRole("form", { name: "General model" }), + ).toBeVisible(); + expect(canvas.getByText("General model")).toBeVisible(); + expect(canvas.getByRole("button", { name: "Choose model" })).toBeVisible(); + }, +}; + +export const Saving: Story = { + args: { + showSave: true, + isSaving: true, + saveDisabled: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByRole("button", { name: /save/i })).toBeDisabled(); + }, +}; + +export const Saved: Story = { + args: { + showSave: false, + isSavedVisible: true, + saveDisabled: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText("Saved")).toBeVisible(); + }, +}; + +export const WithError: Story = { + args: { + error:

Failed to save setting.

, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText("Failed to save setting.")).toBeVisible(); + }, +}; diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/components/AgentSettingLayout.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AgentSettingLayout.tsx new file mode 100644 index 0000000000..6bec8cfb78 --- /dev/null +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/components/AgentSettingLayout.tsx @@ -0,0 +1,73 @@ +import type { FC, FormEventHandler, ReactNode } from "react"; +import { Button } from "#/components/Button/Button"; +import { Spinner } from "#/components/Spinner/Spinner"; +import { TemporarySavedState } from "#/components/TemporarySavedState/TemporarySavedState"; + +interface AgentSettingLayoutProps { + title: string; + description?: ReactNode; + children?: ReactNode; + error?: ReactNode; + showSave: boolean; + isSaving: boolean; + isSavedVisible: boolean; + saveDisabled: boolean; + onSubmit: FormEventHandler; +} + +export const AgentSettingLayout: FC = ({ + title, + description, + children, + error, + showSave, + isSaving, + isSavedVisible, + saveDisabled, + onSubmit, +}) => { + const shouldShowControls = + Boolean(children) || showSave || isSavedVisible || isSaving; + + return ( +
+
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ {shouldShowControls && ( +
+ {children} +
+ {(showSave || isSavedVisible || isSaving) && + (isSavedVisible ? ( + + ) : ( + + ))} +
+
+ )} + {error &&
{error}
} +
+ ); +}; diff --git a/site/src/pages/AgentsPage/components/SubagentModelOverrideSettings.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx similarity index 57% rename from site/src/pages/AgentsPage/components/SubagentModelOverrideSettings.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx index c7c0f7a5f4..06826f200a 100644 --- a/site/src/pages/AgentsPage/components/SubagentModelOverrideSettings.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx @@ -2,9 +2,11 @@ import { useFormik } from "formik"; import type { FC, ReactNode } from "react"; import type * as TypesGen from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; -import type { ModelSelectorOption } from "./ChatElements/ModelSelector"; -import { ModelSelector } from "./ChatElements/ModelSelector"; -import { ModelOverrideAlerts } from "./ModelOverrideAlerts"; +import { useTemporarySavedState } from "#/components/TemporarySavedState/TemporarySavedState"; +import type { ModelSelectorOption } from "#/pages/AgentsPage/components/ChatElements/ModelSelector"; +import { ModelSelector } from "#/pages/AgentsPage/components/ChatElements/ModelSelector"; +import { ModelOverrideAlerts } from "#/pages/AgentsPage/components/ModelOverrideAlerts"; +import { AgentSettingLayout } from "./AgentSettingLayout"; export interface MutationCallbacks { onSuccess?: () => void; @@ -36,7 +38,6 @@ interface SubagentModelOverrideSettingsProps { saveErrorMessage: string; unsetPlaceholder?: string; unavailableModelWarning?: string; - showHeader?: boolean; disabled?: boolean; } @@ -65,9 +66,9 @@ export const SubagentModelOverrideSettings: FC< saveErrorMessage, unsetPlaceholder = "Use chat default", unavailableModelWarning = "The saved model is no longer enabled and will be ignored until you choose a new override.", - showHeader = true, disabled = false, }) => { + const { isSavedVisible, showSavedState } = useTemporarySavedState(); const hasLoadedModelOverride = modelOverrideData !== undefined; const isMalformedOverride = modelOverrideData?.is_malformed ?? false; const enabledModelOptions = enabledModelConfigs.map(toModelSelectorOption); @@ -84,6 +85,7 @@ export const SubagentModelOverrideSettings: FC< }, { onSuccess: () => { + showSavedState(); resetForm({ values }); }, }, @@ -102,61 +104,55 @@ export const SubagentModelOverrideSettings: FC< ); return ( -
- {showHeader && ( - <> -

- {title} -

- {description && ( -

- {description} -

- )} - - )} - form.setFieldValue("model_config_id", value)} - disabled={isFormDisabled} - placeholder={ - isUnavailableSavedModel ? "Unavailable model" : unsetPlaceholder - } - emptyMessage={ - isLoading ? "Loading models..." : "No enabled models found." - } - className="h-10 w-full justify-between rounded-md border border-border border-solid bg-transparent px-3 text-sm shadow-sm" - contentClassName="min-w-[18rem]" - /> - -
- - + placeholder={ + isUnavailableSavedModel ? "Unavailable model" : unsetPlaceholder + } + emptyMessage={ + isLoading ? "Loading models..." : "No enabled models found." + } + className="h-10 w-full justify-between rounded-md border border-border border-solid bg-transparent px-3 text-sm" + contentClassName="min-w-[18rem]" + /> +
- {isSaveError && ( -

- {saveErrorMessage} -

- )} - + + ); }; diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 298c1f856c..a4ffe51671 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -27,9 +27,9 @@ import { withAuthProvider, withDashboardProvider, } from "#/testHelpers/storybook"; +import { CoderAgentsPageView } from "../AISettingsPage/CoderAgentsPage/CoderAgentsPageView"; import AgentAnalyticsPage from "./AgentAnalyticsPage"; import AgentCreatePage from "./AgentCreatePage"; -import { AgentSettingsAgentsPageView } from "./AgentSettingsAgentsPageView"; import AgentSettingsCompactionPage from "./AgentSettingsCompactionPage"; import AgentSettingsExperimentsPage from "./AgentSettingsExperimentsPage"; import AgentSettingsGeneralPage from "./AgentSettingsGeneralPage"; @@ -165,7 +165,7 @@ const buildChat = (overrides: Partial = {}): Chat => ({ const fixedNow = dayjs("2026-03-12T12:00:00"); const AgentsRouteElement = () => ( - { - expect(screen.getByText("Spend limits and usage")).toBeInTheDocument(); + expect( + screen.getByText("Opt in to experimental features."), + ).toBeInTheDocument(); }); }, }; diff --git a/site/src/pages/AgentsPage/components/AdminPersonalModelOverridesSettings.tsx b/site/src/pages/AgentsPage/components/AdminPersonalModelOverridesSettings.tsx deleted file mode 100644 index 727d0f0289..0000000000 --- a/site/src/pages/AgentsPage/components/AdminPersonalModelOverridesSettings.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useFormik } from "formik"; -import type { FC } from "react"; -import type * as TypesGen from "#/api/typesGenerated"; -import { ErrorAlert } from "#/components/Alert/ErrorAlert"; -import { Button } from "#/components/Button/Button"; -import { Switch } from "#/components/Switch/Switch"; - -interface MutationCallbacks { - onSuccess?: () => void; - onError?: () => void; -} - -export type SavePersonalModelOverridesAdminSetting = ( - req: TypesGen.UpdateChatPersonalModelOverridesAdminSettingsRequest, - options?: MutationCallbacks, -) => void; - -interface AdminPersonalModelOverridesSettingsProps { - adminSettings: TypesGen.ChatPersonalModelOverridesAdminSettings | undefined; - adminSettingsError?: unknown; - onRetryAdminSettings?: () => void; - isRetryingAdminSettings?: boolean; - onSaveAdminSetting: SavePersonalModelOverridesAdminSetting; - isSavingAdminSetting: boolean; - isSaveAdminSettingError: boolean; -} - -export const AdminPersonalModelOverridesSettings: FC< - AdminPersonalModelOverridesSettingsProps -> = ({ - adminSettings, - adminSettingsError, - onRetryAdminSettings, - isRetryingAdminSettings = false, - onSaveAdminSetting, - isSavingAdminSetting, - isSaveAdminSettingError, -}) => { - const hasLoadedAdminSettings = adminSettings !== undefined; - const hasAdminSettingsError = adminSettingsError != null; - const form = useFormik({ - enableReinitialize: true, - initialValues: { - allow_users: adminSettings?.allow_users ?? false, - }, - onSubmit: (values, { resetForm }) => { - onSaveAdminSetting( - { - allow_users: values.allow_users, - }, - { - onSuccess: () => { - resetForm({ values }); - }, - }, - ); - }, - }); - const isDisabled = isSavingAdminSetting || !hasLoadedAdminSettings; - - return ( -
-
-
-

- Enable users to define their personal overrides -

-

- Lets users choose personal models for root chats, General subagents, - and Explore subagents. When disabled, saved user settings remain - stored but are ignored at runtime. -

-
- { - void form.setFieldValue("allow_users", checked); - }} - aria-label="Enable users to define their personal overrides" - type="button" - disabled={isDisabled} - /> -
- {hasAdminSettingsError ? ( -
- - {onRetryAdminSettings && ( - - )} -
- ) : ( - !hasLoadedAdminSettings && ( -

- Loading personal model override settings... -

- ) - )} -
- -
- {isSaveAdminSettingError && ( -

- Failed to save personal model override settings. -

- )} -
- ); -}; diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx index 7664b7af5b..ade9a403cf 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx @@ -2238,7 +2238,7 @@ export const SettingsUserAgentsAdmin: Story = { }, }; -export const SettingsAdminAgentsEntryPreserved: Story = { +export const SettingsAdminCoderAgentsEntryPreserved: Story = { args: { chats: [], isAdmin: true, @@ -2251,8 +2251,13 @@ export const SettingsAdminAgentsEntryPreserved: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const agentsLink = canvas.getByRole("link", { name: "Agents" }); - await expect(agentsLink).toHaveAttribute("aria-current", "page"); + const coderAgentsLink = canvas.getByRole("link", { + name: "Coder Agents", + }); + expect(coderAgentsLink).toHaveAttribute( + "href", + "/ai/settings/coder-agents", + ); expect(canvas.getByText("Manage agents")).toBeInTheDocument(); }, }; diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx index 24936e2394..b22bdddca6 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx @@ -3,7 +3,6 @@ import { ArrowUpRightIcon, BotIcon, BoxesIcon, - ChevronRightIcon, CoinsIcon, FlaskConicalIcon, KeyIcon, @@ -147,10 +146,9 @@ export const SettingsPanel: FC = ({ )} @@ -158,10 +156,10 @@ export const SettingsPanel: FC = ({