From 5f969c76c87df1eb7976d41c961de27ac54f6ed5 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 24 Aug 2026 13:20:53 +0000 Subject: [PATCH 1/4] fix: move organization model overrides from Models to Coder Agents settings The Defaults & overrides tab under AI Settings > Models mixed agent override controls into the reusable model catalog and gated them behind the Models layout, while the Coder Agents page was gated wholesale by editDeploymentConfig and blocked organization model administrators. Relocate the organization override controls (General, Explore, title generation, compaction, and Advisor) into an Organization settings section on the Coder Agents page with an organization picker, and keep deployment-wide controls in a separate Deployment settings section visible only with editDeploymentConfig. The page and sidebar entry are now accessible with either deployment or organization model access. The legacy /ai/settings/models/defaults route redirects to /ai/settings/coder-agents preserving query parameters. The Models page drops its now-single tab UI and the unused defaults path helpers. --- docs/ai-coder/agents/models.md | 21 +- .../agents/platform-controls/advisor.md | 8 +- .../AISettingsSidebarView.stories.tsx | 4 +- .../management/AISettingsSidebarView.tsx | 32 ++- .../CoderAgentsPage/CoderAgentsPage.tsx | 71 ++++++- .../CoderAgentsPageView.stories.tsx | 125 +++++++++++- .../CoderAgentsPage/CoderAgentsPageView.tsx | 187 ++++++++++++++---- .../OrganizationAgentSettings.stories.tsx | 111 +++++++++++ .../OrganizationAgentSettings.tsx} | 87 ++++---- ...OrganizationAgentSettingsView.stories.tsx} | 10 +- .../OrganizationAgentSettingsView.tsx} | 20 +- .../AddModelPage/AddModelPageView.stories.tsx | 1 - .../DefaultsPage/DefaultsPage.stories.tsx | 65 ------ .../ModelsPage/ModelDefaultsRedirect.test.tsx | 33 ++++ .../ModelsPage/ModelDefaultsRedirect.tsx | 11 ++ .../ModelsPage/ModelsPageView.stories.tsx | 1 - .../OrganizationModelsLayout.stories.tsx | 71 +++---- .../ModelsPage/OrganizationModelsLayout.tsx | 30 +-- .../UpdateModelPageView.stories.tsx | 1 - .../components/ModelForm.stories.tsx | 1 - .../ModelsPage/organizationModels.tsx | 14 +- .../AgentsPage/AgentsPageLayout.stories.tsx | 6 + .../AgentsPage/components/AdvisorSettings.tsx | 9 +- site/src/router.tsx | 9 +- 24 files changed, 618 insertions(+), 310 deletions(-) create mode 100644 site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings.stories.tsx rename site/src/pages/AISettingsPage/{ModelsPage/DefaultsPage/DefaultsPage.tsx => CoderAgentsPage/OrganizationAgentSettings.tsx} (61%) rename site/src/pages/AISettingsPage/{ModelsPage/DefaultsPage/DefaultsPageView.stories.tsx => CoderAgentsPage/OrganizationAgentSettingsView.stories.tsx} (94%) rename site/src/pages/AISettingsPage/{ModelsPage/DefaultsPage/DefaultsPageView.tsx => CoderAgentsPage/OrganizationAgentSettingsView.tsx} (86%) delete mode 100644 site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPage.stories.tsx create mode 100644 site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx create mode 100644 site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.tsx diff --git a/docs/ai-coder/agents/models.md b/docs/ai-coder/agents/models.md index 855bbedc6ab..ce8884214fa 100644 --- a/docs/ai-coder/agents/models.md +++ b/docs/ai-coder/agents/models.md @@ -297,19 +297,14 @@ contact an administrator. ## Model overrides -Beyond the chat-level model picker, Coder Agents supports two override -layers. Both are stored per organization and resolve from the chat's -organization: - -- **Admin overrides** (per organization): Pin specific contexts to a - particular model. Configure them on the **Defaults & overrides** tab - under **AI Settings** > **Models** for the selected organization. -- **Personal overrides** (per user and organization, opt-in by admin): - Let users override the model for their own root chats and delegated - subagents. Admins enable the deployment-wide toggle under - **AI Settings** > **Coder Agents**; once on, each user sees an - **Agents** tab in their personal **Agents** > **Settings**. Users in - more than one organization pick which organization to configure. +Beyond the chat-level model picker, Coder Agents supports two override layers. +Both are stored per organization and resolve from the chat's organization: + +- **Admin overrides** (per organization): Pin specific contexts to a particular model. + Configure them under **AI Settings** > **Coder Agents** > **Organization settings** for the selected organization. +- **Personal overrides** (per user and organization, opt-in by admin): Let users override the model for their own root chats and delegated subagents. + Admins enable the deployment-wide toggle under **AI Settings** > **Coder Agents**; once enabled, each user sees an **Agents** tab in their personal **Agents** > **Settings**. + Users in more than one organization pick which organization to configure. > [!IMPORTANT] > When a deployment upgrades from the older deployment-wide override diff --git a/docs/ai-coder/agents/platform-controls/advisor.md b/docs/ai-coder/agents/platform-controls/advisor.md index aabdaae0875..9a5bae6487d 100644 --- a/docs/ai-coder/agents/platform-controls/advisor.md +++ b/docs/ai-coder/agents/platform-controls/advisor.md @@ -38,11 +38,9 @@ deployment-wide. | Max uses per turn | `0` (unlimited) | Caps how many times the root agent can call the advisor in a single chat turn. Must be a non-negative integer. | | Max output tokens | `0` (server default) | Caps the advisor model's response length. `0` uses the server default of 16,384 tokens. Must be a non-negative integer. | -The advisor model and its reasoning effort are organization-scoped -[model overrides](../models.md#model-overrides). Configure them on the -**Defaults & overrides** tab under **AI Settings** > **Models** for the -selected organization. When no override is set, the advisor reuses the -root agent's model. +The advisor model and its reasoning effort are organization-scoped [model overrides](../models.md#model-overrides). +Configure them under **AI Settings** > **Coder Agents** > **Organization settings** for the selected organization. +When no override is set, the advisor reuses the root agent's model. The advisor is not available in plan mode or to subagents. Failed advisor invocations refund the per-turn budget. diff --git a/site/src/modules/management/AISettingsSidebarView.stories.tsx b/site/src/modules/management/AISettingsSidebarView.stories.tsx index 3f302edadf3..ea871982da2 100644 --- a/site/src/modules/management/AISettingsSidebarView.stories.tsx +++ b/site/src/modules/management/AISettingsSidebarView.stories.tsx @@ -180,8 +180,8 @@ export const OrganizationOnlyRoleCanAccessModels: Story = { const canvas = within(canvasElement); await expect(canvas.getByRole("link", { name: "Models" })).toBeVisible(); await expect( - canvas.queryByRole("link", { name: "Coder Agents" }), - ).not.toBeInTheDocument(); + canvas.getByRole("link", { name: "Coder Agents" }), + ).toBeVisible(); }, }; diff --git a/site/src/modules/management/AISettingsSidebarView.tsx b/site/src/modules/management/AISettingsSidebarView.tsx index 7b76847cec1..2a636f79734 100644 --- a/site/src/modules/management/AISettingsSidebarView.tsx +++ b/site/src/modules/management/AISettingsSidebarView.tsx @@ -80,24 +80,22 @@ const AISettingsSidebarView: FC = ({ )} {(canAccessAnyChatModelConfig(permissions) || canAccessOrganizationModels) && } + {(permissions.editDeploymentConfig || canAccessOrganizationModels) && ( + + Coder Agents + + )} {permissions.editDeploymentConfig && ( - <> - - Coder Agents - -
- - MCP servers - - {permissions.updateTemplates && ( - Templates - )} - - Instructions - - Lifecycle -
- +
+ MCP servers + {permissions.updateTemplates && ( + Templates + )} + + Instructions + + Lifecycle +
)} {!permissions.editDeploymentConfig && (permissions.viewAnyMCPServerConfigs || diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx index 029dfe24e6a..3df7f735472 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx @@ -1,5 +1,6 @@ import type { FC } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; +import { useSearchParams } from "react-router"; import { chatAdvisorConfig, chatComputerUseProvider, @@ -8,17 +9,42 @@ import { updateChatComputerUseProvider, updateChatPersonalModelOverridesAdminSettings, } from "#/api/queries/chats"; +import { organizationsPermissions } from "#/api/queries/organizations"; +import { Loader } from "#/components/Loader/Loader"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { RequirePermission } from "#/modules/permissions/RequirePermission"; +import { + modelOrganizationSearchParam, + selectModelOrganization, + useAccessibleModelOrganizations, +} from "#/pages/AISettingsPage/ModelsPage/organizationModels"; import { pageTitle } from "#/utils/page"; import { CoderAgentsPageView } from "./CoderAgentsPageView"; +import { OrganizationAgentSettings } from "./OrganizationAgentSettings"; const CoderAgentsPage: FC = () => { const { permissions } = useAuthenticated(); - const { experiments } = useDashboard(); + const { experiments, organizations } = useDashboard(); const queryClient = useQueryClient(); + const [searchParams, setSearchParams] = useSearchParams(); const canEditDeploymentConfig = permissions.editDeploymentConfig; + const accessibleOrganizationsQuery = + useAccessibleModelOrganizations(organizations); + const organizationSelection = selectModelOrganization( + accessibleOrganizationsQuery.organizations, + searchParams.get(modelOrganizationSearchParam), + ); + const activeOrganization = organizationSelection.organization; + const organizationPermissionsQuery = useQuery({ + ...organizationsPermissions( + activeOrganization ? [activeOrganization.id] : undefined, + ), + enabled: activeOrganization !== undefined, + }); + const activeOrganizationPermissions = activeOrganization + ? organizationPermissionsQuery.data?.[activeOrganization.id] + : undefined; const showAdvisorSettings = experiments.includes("chat-advisor"); const showVirtualDesktopSettings = experiments.includes( "chat-virtual-desktop", @@ -44,11 +70,52 @@ const CoderAgentsPage: FC = () => { const saveComputerUseProviderMutation = useMutation( updateChatComputerUseProvider(queryClient), ); + const canAccessOrganizationSettings = + accessibleOrganizationsQuery.organizations.length > 0; + const isFeatureVisible = + canEditDeploymentConfig || + canAccessOrganizationSettings || + accessibleOrganizationsQuery.isLoading || + accessibleOrganizationsQuery.error !== null; + const organizationSettings = + activeOrganization && + organizationPermissionsQuery.data === undefined && + organizationPermissionsQuery.error == null ? ( + + ) : activeOrganization && + organizationPermissionsQuery.data !== undefined ? ( + + ) : undefined; return ( - + Codestin Search App { + const next = new URLSearchParams(searchParams); + next.set(modelOrganizationSearchParam, organization.name); + setSearchParams(next); + }} + organizationAccessError={ + accessibleOrganizationsQuery.partialError ?? + accessibleOrganizationsQuery.error + } + organizationPermissionsError={organizationPermissionsQuery.error} + requestedOrganizationDenied={ + organizationSelection.requestedOrganizationDenied + } + isOrganizationAccessLoading={accessibleOrganizationsQuery.isLoading} + organizationSettings={organizationSettings} + canEditDeploymentConfig={canEditDeploymentConfig} adminOverridesData={personalOverridesQuery.data} adminOverridesError={personalOverridesQuery.error} onRetryAdminOverrides={() => void personalOverridesQuery.refetch()} diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx index f02de8013a2..015145dd6dc 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx @@ -1,12 +1,23 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, fn, userEvent, waitFor, within } from "storybook/test"; +import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; +import { + MockDefaultOrganization, + MockOrganization2, +} from "#/testHelpers/entities"; import { CoderAgentsPageView, type CoderAgentsPageViewProps, } from "./CoderAgentsPageView"; const defaultArgs: CoderAgentsPageViewProps = { + organization: MockDefaultOrganization, + organizations: [MockDefaultOrganization, MockOrganization2], + onSelectOrganization: fn(), + requestedOrganizationDenied: false, + isOrganizationAccessLoading: false, + organizationSettings:
Organization override controls
, + canEditDeploymentConfig: true, adminOverridesData: { allow_users: true }, onSaveAdminOverrides: fn(), isSavingAdminOverrides: false, @@ -51,7 +62,10 @@ export const Default: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await expect( - canvas.getAllByRole("link", { name: "Defaults & overrides" })[0], + canvas.getByRole("heading", { name: "Organization settings" }), + ).toBeVisible(); + await expect( + canvas.getByRole("heading", { name: "Deployment settings" }), ).toBeVisible(); await expect(canvas.getByText("Advisor")).toBeVisible(); const maxUses = canvas.getByRole("spinbutton", { name: "Uses / turn" }); @@ -76,3 +90,110 @@ export const WithoutAdvisor: Story = { await expect(canvas.queryByText("Advisor")).not.toBeInTheDocument(); }, }; + +export const SelectOrganization: Story = { + args: { onSelectOrganization: fn() }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { + name: new RegExp(MockDefaultOrganization.display_name, "i"), + }), + ); + await userEvent.click( + await screen.findByRole("option", { + name: new RegExp(MockOrganization2.display_name, "i"), + }), + ); + await expect(args.onSelectOrganization).toHaveBeenCalledWith( + MockOrganization2, + ); + }, +}; + +export const OrganizationOnly: Story = { + args: { canEditDeploymentConfig: false }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.getByRole("heading", { name: "Organization settings" }), + ).toBeVisible(); + await expect( + canvas.queryByRole("heading", { name: "Deployment settings" }), + ).not.toBeInTheDocument(); + }, +}; + +export const DeploymentOnly: Story = { + args: { + organization: undefined, + organizations: [], + organizationSettings: undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.queryByRole("heading", { name: "Organization settings" }), + ).not.toBeInTheDocument(); + await expect( + canvas.getByRole("heading", { name: "Deployment settings" }), + ).toBeVisible(); + }, +}; + +export const SingleOrganization: Story = { + args: { organizations: [MockDefaultOrganization] }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.queryByRole("button", { + name: new RegExp(MockDefaultOrganization.display_name, "i"), + }), + ).not.toBeInTheDocument(); + }, +}; + +export const InaccessibleRequestedOrganization: Story = { + args: { requestedOrganizationDenied: true }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole("alert")).toHaveTextContent( + "requested organization is not available", + ); + await expect( + canvas.getByText("Organization override controls"), + ).toBeVisible(); + }, +}; + +export const IndependentErrors: Story = { + args: { + organizationAccessError: new Error("Failed to load another organization"), + adminOverridesError: new Error("Failed to load deployment setting"), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.getByText("Failed to load another organization"), + ).toBeVisible(); + await expect( + canvas.getByText("Failed to load deployment setting"), + ).toBeVisible(); + await expect( + canvas.getByText("Organization override controls"), + ).toBeVisible(); + }, +}; + +export const Mobile: Story = { + parameters: { viewport: { defaultViewport: "mobile1" } }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.getByRole("heading", { name: "Organization settings" }), + ).toBeVisible(); + await expect( + canvas.getByRole("heading", { name: "Deployment settings" }), + ).toBeVisible(); + }, +}; diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx index b1db5f679da..26fbd285060 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx @@ -1,7 +1,13 @@ -import type { FC } from "react"; +import type { FC, ReactNode } from "react"; import type { UseMutateFunction } from "react-query"; -import { Link } from "react-router"; import type * as TypesGen from "#/api/typesGenerated"; +import { Alert } from "#/components/Alert/Alert"; +import { ErrorAlert } from "#/components/Alert/ErrorAlert"; +import { Loader } from "#/components/Loader/Loader"; +import { + getOrganizationLabel, + OrganizationAutocomplete, +} from "#/components/OrganizationAutocomplete/OrganizationAutocomplete"; import { SettingsHeader, SettingsHeaderDescription, @@ -16,6 +22,15 @@ import { import type { MutationCallbacks } from "./components/SubagentModelOverrideSettings"; export interface CoderAgentsPageViewProps { + organization?: TypesGen.Organization; + organizations: readonly TypesGen.Organization[]; + onSelectOrganization: (organization: TypesGen.Organization) => void; + organizationAccessError?: unknown; + organizationPermissionsError?: unknown; + requestedOrganizationDenied: boolean; + isOrganizationAccessLoading: boolean; + organizationSettings?: ReactNode; + canEditDeploymentConfig: boolean; adminOverridesData?: TypesGen.ChatPersonalModelOverridesAdminSettings; adminOverridesError?: unknown; onRetryAdminOverrides?: () => void; @@ -49,6 +64,15 @@ export interface CoderAgentsPageViewProps { } export const CoderAgentsPageView: FC = ({ + organization, + organizations, + onSelectOrganization, + organizationAccessError, + organizationPermissionsError, + requestedOrganizationDenied, + isOrganizationAccessLoading, + organizationSettings, + canEditDeploymentConfig, adminOverridesData, adminOverridesError, onRetryAdminOverrides, @@ -71,47 +95,124 @@ export const CoderAgentsPageView: FC = ({ onSaveComputerUseProvider, isSavingComputerUseProvider, computerUseProviderSaveError, -}) => ( -
- - Coder Agents - - Configure deployment-wide Coder Agents capabilities. Model defaults are - configured per organization in{" "} - Defaults & overrides. - - -
- - {showVirtualDesktopSettings && ( - +}) => { + return ( +
+ + Coder Agents + + Configure organization model choices and deployment-wide Coder Agents + capabilities. + + + + {isOrganizationAccessLoading && !organization && } + {organizationAccessError != null && !organization && ( + )} - {showAdvisorSettings && ( - + {organization && ( +
+
+
+

+ Organization settings +

+

+ Choose model and reasoning defaults for each Coder Agents + context. +

+
+ {organizations.length > 1 && ( + { + if (nextOrganization) { + onSelectOrganization(nextOrganization); + } + }} + /> + )} +
+ {requestedOrganizationDenied && ( + + The requested organization is not available. Showing settings for{" "} + {organization.display_name || organization.name} instead. + + )} + {organizationAccessError != null && ( + + )} + {organizationPermissionsError != null && ( + + )} + {organizationSettings} +
+ )} + + {canEditDeploymentConfig && ( +
+
+

+ Deployment settings +

+

+ Configure Coder Agents capabilities that apply to every + organization. +

+
+
+ + {showVirtualDesktopSettings && ( + + )} + {showAdvisorSettings && ( + + )} +
+
)}
-
-); + ); +}; diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings.stories.tsx new file mode 100644 index 00000000000..d057b527cbc --- /dev/null +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings.stories.tsx @@ -0,0 +1,111 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + expect, + screen, + spyOn, + userEvent, + waitFor, + within, +} from "storybook/test"; +import { API } from "#/api/api"; +import { + MockChatModel, + MockChatModelProviderDescriptor, +} from "#/testHelpers/chatModels"; +import { MockDefaultOrganization } from "#/testHelpers/entities"; +import { OrganizationAgentSettings } from "./OrganizationAgentSettings"; + +const meta: Meta = { + title: "pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings", + component: OrganizationAgentSettings, + args: { + organization: MockDefaultOrganization, + canEdit: true, + showAdvisor: true, + }, +}; +export default meta; +type Story = StoryObj; + +export const ClearableWhenModelCatalogFails: Story = { + beforeEach: () => { + spyOn(API.experimental, "getChatModels").mockRejectedValue( + new Error("failed to load models"), + ); + spyOn( + API.experimental, + "getOrganizationChatModelOverrides", + ).mockResolvedValue({ + overrides: [{ context: "general", model_config_id: "model-gone" }], + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + // A failed model catalog fetch must not hide the override rows: an + // admin must still be able to clear a stale override without it. + await canvas.findAllByText(/failed to load models/i); + const generalSection = await canvas.findByRole("form", { + name: "General subagent", + }); + const clear = within(generalSection).getByRole("button", { + name: "Clear", + }); + await expect(clear).toBeEnabled(); + await userEvent.click(clear); + await waitFor(() => + expect( + within(generalSection).getByRole("button", { name: "Save" }), + ).toBeEnabled(), + ); + }, +}; + +export const SavesOverrideForSelectedOrganization: Story = { + beforeEach: () => { + spyOn(API.experimental, "getChatModels").mockResolvedValue({ + models: [ + { + ...MockChatModel, + organization_id: MockDefaultOrganization.id, + }, + ], + providers: [MockChatModelProviderDescriptor], + unsupported_providers: [], + }); + spyOn( + API.experimental, + "getOrganizationChatModelOverrides", + ).mockResolvedValue({ overrides: [] }); + spyOn( + API.experimental, + "updateOrganizationChatModelOverride", + ).mockResolvedValue({ + context: "explore", + model_config_id: MockChatModel.id, + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const exploreSection = await canvas.findByRole("form", { + name: "Explore subagent", + }); + await userEvent.click( + within(exploreSection).getByRole("combobox", { name: "Use default" }), + ); + await userEvent.click( + await screen.findByRole("option", { + name: new RegExp(MockChatModel.model), + }), + ); + const save = within(exploreSection).getByRole("button", { name: "Save" }); + await waitFor(() => expect(save).toBeEnabled()); + await userEvent.click(save); + await waitFor(() => { + expect( + API.experimental.updateOrganizationChatModelOverride, + ).toHaveBeenCalledWith(MockDefaultOrganization.id, "explore", { + model_config_id: MockChatModel.id, + }); + }); + }, +}; diff --git a/site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPage.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings.tsx similarity index 61% rename from site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPage.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings.tsx index a93f14d5cb7..16d240c3f23 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPage.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettings.tsx @@ -5,18 +5,18 @@ import { organizationChatModelOverrides, updateOrganizationChatModelOverride, } from "#/api/queries/chats"; -import type { ChatModelOverrideContext } from "#/api/typesGenerated"; -import { useDashboard } from "#/modules/dashboard/useDashboard"; +import type { + ChatModelOverrideContext, + Organization, +} from "#/api/typesGenerated"; import { filterModelsWithEnabledProvider, providerInfoByIDFromDescriptors, } from "#/pages/AgentsPage/utils/modelOptions"; -import { pageTitle } from "#/utils/page"; -import { - splitModelQueryErrors, - useOrganizationModels, -} from "../organizationModels"; -import DefaultsPageView, { type SaveModelOverride } from "./DefaultsPageView"; +import { splitModelQueryErrors } from "../ModelsPage/organizationModels"; +import OrganizationAgentSettingsView, { + type SaveModelOverride, +} from "./OrganizationAgentSettingsView"; const contexts: readonly ChatModelOverrideContext[] = [ "general", @@ -26,17 +26,31 @@ const contexts: readonly ChatModelOverrideContext[] = [ "advisor", ]; -const DefaultsPage: FC = () => { - const { organization } = useOrganizationModels(); - // Remount on organization change so mutation state (pending saves, - // errors) from one organization never renders on another's form. - return ; -}; +interface OrganizationAgentSettingsProps { + organization: Organization; + canEdit: boolean; + showAdvisor: boolean; +} -const DefaultsPageContent: FC = () => { +export const OrganizationAgentSettings: FC = ({ + organization, + canEdit, + showAdvisor, +}) => ( + +); + +const OrganizationAgentSettingsContent: FC = ({ + organization, + canEdit, + showAdvisor, +}) => { const queryClient = useQueryClient(); - const { experiments } = useDashboard(); - const { organization, permissions } = useOrganizationModels(); const modelsQuery = useQuery(chatModels(organization.id)); const overridesQuery = useQuery( organizationChatModelOverrides(organization.id), @@ -104,27 +118,22 @@ const DefaultsPageContent: FC = () => { } return ( - <> - Codestin Search App - mutations[index]?.isPending)) - } - errorContexts={ - new Set(contexts.filter((_, index) => mutations[index]?.isError)) - } - /> - + mutations[index]?.isPending)) + } + errorContexts={ + new Set(contexts.filter((_, index) => mutations[index]?.isError)) + } + /> ); }; - -export default DefaultsPage; diff --git a/site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPageView.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettingsView.stories.tsx similarity index 94% rename from site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPageView.stories.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettingsView.stories.tsx index 78c976a78de..b0a06ccd3c6 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettingsView.stories.tsx @@ -2,7 +2,7 @@ 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 { MockChatModel } from "#/testHelpers/chatModels"; -import DefaultsPageView from "./DefaultsPageView"; +import OrganizationAgentSettingsView from "./OrganizationAgentSettingsView"; const model: TypesGen.ChatModel = { ...MockChatModel, @@ -32,9 +32,9 @@ const saveByContext = new Map< ["advisor", fn()], ]); -const meta: Meta = { - title: "pages/AISettingsPage/ModelsPage/DefaultsPageView", - component: DefaultsPageView, +const meta: Meta = { + title: "pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettingsView", + component: OrganizationAgentSettingsView, args: { overrides, enabledModels: [model, alternateModel], @@ -55,7 +55,7 @@ const meta: Meta = { }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const SetAndUnset: Story = { beforeEach: () => { diff --git a/site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPageView.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettingsView.tsx similarity index 86% rename from site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPageView.tsx rename to site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettingsView.tsx index 91075e4cea5..80342b733cb 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPageView.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/OrganizationAgentSettingsView.tsx @@ -1,11 +1,6 @@ import type { FC } from "react"; import type * as TypesGen from "#/api/typesGenerated"; import { ErrorAlert } from "#/components/Alert/ErrorAlert"; -import { - SettingsHeader, - SettingsHeaderDescription, - SettingsHeaderTitle, -} from "#/components/SettingsHeader/SettingsHeader"; import type { ProviderInfo } from "#/pages/AgentsPage/utils/modelOptions"; import { SubagentModelOverrideSettings } from "#/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings"; @@ -14,7 +9,7 @@ export type SaveModelOverride = ( options?: { onSuccess?: () => void; onError?: () => void }, ) => void; -interface DefaultsPageViewProps { +interface OrganizationAgentSettingsViewProps { overrides: readonly TypesGen.ChatModelOverrideResponse[] | undefined; enabledModels: readonly TypesGen.ChatModel[]; providerInfoByID: ReadonlyMap; @@ -69,7 +64,7 @@ const settings: readonly { }, ]; -const DefaultsPageView: FC = ({ +const OrganizationAgentSettingsView: FC = ({ overrides, enabledModels, providerInfoByID, @@ -90,14 +85,7 @@ const DefaultsPageView: FC = ({ ); return ( -
- - Defaults & overrides - - Choose organization defaults for each Coder Agents context. Leave a - model unset to use the default model selection. - - +
{refetchError != null && } {enabledModels.length === 0 && !isLoading && refetchError == null && (

@@ -138,4 +126,4 @@ const DefaultsPageView: FC = ({ ); }; -export default DefaultsPageView; +export default OrganizationAgentSettingsView; diff --git a/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.stories.tsx index b0036a4d651..0fb858b0094 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.stories.tsx @@ -23,7 +23,6 @@ const meta: Meta = { = { - title: "pages/AISettingsPage/ModelsPage/DefaultsPage", - component: DefaultsPage, - decorators: [ - (Story) => ( - - - - ), - withDashboardProvider, - ], -}; -export default meta; -type Story = StoryObj; - -export const ClearableWhenModelCatalogFails: Story = { - beforeEach: () => { - spyOn(API.experimental, "getChatModels").mockRejectedValue( - new Error("failed to load models"), - ); - spyOn( - API.experimental, - "getOrganizationChatModelOverrides", - ).mockResolvedValue({ - overrides: [{ context: "general", model_config_id: "model-gone" }], - }); - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - // A failed model catalog fetch must not hide the override rows: an - // admin must still be able to clear a stale override without it. - await canvas.findAllByText(/failed to load models/i); - const generalSection = await canvas.findByRole("form", { - name: "General subagent", - }); - const clear = within(generalSection).getByRole("button", { - name: "Clear", - }); - await expect(clear).toBeEnabled(); - await userEvent.click(clear); - await waitFor(() => - expect( - within(generalSection).getByRole("button", { name: "Save" }), - ).toBeEnabled(), - ); - }, -}; diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx new file mode 100644 index 00000000000..feca204240a --- /dev/null +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx @@ -0,0 +1,33 @@ +import { render, waitFor } from "@testing-library/react"; +import { createMemoryRouter, RouterProvider } from "react-router"; +import { expect, it } from "vitest"; +import { ModelDefaultsRedirect } from "./ModelDefaultsRedirect"; + +it("redirects to Coder Agents and preserves the query string", async () => { + const router = createMemoryRouter( + [ + { + path: "/ai/settings/models/defaults", + element: , + }, + { + path: "/ai/settings/coder-agents", + element:

Coder Agents
, + }, + ], + { + initialEntries: [ + "/ai/settings/models/defaults?org=engineering&source=bookmark", + ], + }, + ); + + render(); + + await waitFor(() => { + expect(router.state.location.pathname).toBe("/ai/settings/coder-agents"); + expect(router.state.location.search).toBe( + "?org=engineering&source=bookmark", + ); + }); +}); diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.tsx new file mode 100644 index 00000000000..9d9363a3f31 --- /dev/null +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.tsx @@ -0,0 +1,11 @@ +import { Navigate, useLocation } from "react-router"; + +export const ModelDefaultsRedirect = () => { + const location = useLocation(); + return ( + + ); +}; diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx index 76d910dd444..924ab98fe2c 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx @@ -29,7 +29,6 @@ const meta: Meta = { = { key: chatModels(MockOrganization2.id).queryKey, data: { models: [], providers: [], unsupported_providers: [] }, }, + { + key: organizationsPermissions([ + MockDefaultOrganization.id, + MockOrganization2.id, + ]).queryKey, + data: { + [MockDefaultOrganization.id]: MockOrganizationPermissions, + [MockOrganization2.id]: MockOrganizationPermissions, + }, + }, { key: organizationsPermissions([MockDefaultOrganization.id]).queryKey, data: { @@ -121,46 +131,6 @@ export const SwitchOrganizationPreservesAuxiliaryParameters: Story = { }, }; -export const DefaultsTabNavigatesToOrganizationDefaults: Story = { - beforeEach: () => { - // Mock at the API layer so a background refetch cannot replace the - // seeded permissions. - spyOn(API, "checkAuthorization").mockImplementation(async ({ checks }) => - Object.fromEntries(Object.keys(checks).map((id) => [id, true])), - ); - }, - parameters: { - reactRouter: reactRouterParameters({ - location: { - path: "/ai/settings/models", - searchParams: { org: MockOrganization2.name }, - }, - routing: [{ path: "*", useStoryElement: true }], - }), - }, - render: () => ( - <> - - - - ), - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const defaultsTab = await canvas.findByRole("link", { - name: "Defaults & overrides", - }); - await userEvent.click(defaultsTab); - await waitFor(() => { - expect(screen.getByTestId("location-probe")).toHaveTextContent( - `/ai/settings/models/defaults?org=${MockOrganization2.name}`, - ); - expect( - canvas.getByRole("link", { name: "Defaults & overrides" }), - ).toHaveAttribute("aria-current", "page"); - }); - }, -}; - export const InvalidRequestedOrganizationFallsBackToDefault: Story = { parameters: { reactRouter: reactRouterParameters({ @@ -208,6 +178,16 @@ export const InvalidRequestedOrganizationDeniesAdd: Story = { key: chatModels(MockOrganization2.id).queryKey, data: { models: [], providers: [], unsupported_providers: [] }, }, + { + key: organizationsPermissions([ + MockDefaultOrganization.id, + MockOrganization2.id, + ]).queryKey, + data: { + [MockDefaultOrganization.id]: MockOrganizationPermissions, + [MockOrganization2.id]: MockOrganizationPermissions, + }, + }, { key: organizationsPermissions([MockDefaultOrganization.id]).queryKey, data: { @@ -282,6 +262,16 @@ export const DuplicateDisplayNamesAreDisambiguated: Story = { key: chatModels(duplicateNameOrganization.id).queryKey, data: { models: [], providers: [], unsupported_providers: [] }, }, + { + key: organizationsPermissions([ + MockDefaultOrganization.id, + duplicateNameOrganization.id, + ]).queryKey, + data: { + [MockDefaultOrganization.id]: MockOrganizationPermissions, + [duplicateNameOrganization.id]: MockOrganizationPermissions, + }, + }, { key: organizationsPermissions([MockDefaultOrganization.id]).queryKey, data: { @@ -383,6 +373,7 @@ export const Loading: Story = { export const NoReadableOrganizationIsNotFound: Story = { beforeEach: () => { + spyOn(API, "checkAuthorization").mockResolvedValue({}); spyOn(API.experimental, "getChatModels").mockRejectedValue({ isAxiosError: true, response: { status: 403 }, diff --git a/site/src/pages/AISettingsPage/ModelsPage/OrganizationModelsLayout.tsx b/site/src/pages/AISettingsPage/ModelsPage/OrganizationModelsLayout.tsx index 18f47fb7e0c..da62e0dc8b3 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/OrganizationModelsLayout.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/OrganizationModelsLayout.tsx @@ -13,14 +13,11 @@ import { getOrganizationLabel, OrganizationAutocomplete, } from "#/components/OrganizationAutocomplete/OrganizationAutocomplete"; -import { LinkTabs, LinkTabsList, TabLink } from "#/components/Tabs/Tabs"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import NotFoundPage from "#/pages/NotFoundPage/NotFoundPage"; import { modelOrganizationSearchParam, OrganizationModelsContext, - organizationModelDefaultsPath, - organizationModelsPath, selectModelOrganization, useAccessibleModelOrganizations, } from "./organizationModels"; @@ -76,7 +73,6 @@ const OrganizationModelsLayout: FC = () => { { />
)} - - - - Models - - - Defaults & overrides - - - {accessibleOrganizations.length > 1 && (
{ } const next = new URLSearchParams(searchParams); next.set(modelOrganizationSearchParam, organization.name); - const pathname = location.pathname.split("?", 1)[0]; - void navigate(`${pathname}?${next.toString()}`); + void navigate(`${location.pathname}?${next.toString()}`); }} />
diff --git a/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx index 81338947902..63e750dd6c7 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx @@ -35,7 +35,6 @@ const meta: Meta = { ( organizationModelSettingsPath(organization, "", searchParams); - -export const organizationModelDefaultsPath = ( - organization: Organization, - searchParams?: URLSearchParams, -): string => - organizationModelSettingsPath(organization, "/defaults", searchParams); - export const organizationAddModelPath = ( organization: Organization, searchParams?: URLSearchParams, @@ -154,5 +142,5 @@ export const organizationModelPath = ( export const useOrganizationModelsPath = (): string => { const { organization } = useOrganizationModels(); const [searchParams] = useSearchParams(); - return organizationModelsPath(organization, searchParams); + return organizationModelSettingsPath(organization, "", searchParams); }; diff --git a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx index cf860c8ba97..03c60fd5d5f 100644 --- a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx @@ -106,6 +106,12 @@ const buildChat = (overrides: Partial = {}): Chat => ({ const AgentsRouteElement = () => ( = ({ return ( - Cap advisor usage per turn. Configure its model in{" "} - Defaults & overrides. - Set limits to 0 for unlimited. - - } + description="Cap advisor usage per turn. Configure its model in Organization settings above. Set limits to 0 for unlimited." showSave={canSave} isSaving={isSavingAdvisorConfig} isSavedVisible={isSavedVisible} diff --git a/site/src/router.tsx b/site/src/router.tsx index f078fa47b34..2630b693a33 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -14,6 +14,7 @@ import { RequireAuth } from "./contexts/auth/RequireAuth"; import { DashboardLayout } from "./modules/dashboard/DashboardLayout"; import { aiTasksEnabled } from "./modules/tasks/useAITasksEnabled"; import { AISettingsIndexRedirect } from "./pages/AISettingsPage/AISettingsIndexRedirect"; +import { ModelDefaultsRedirect } from "./pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect"; import AuditPage from "./pages/AuditPage/AuditPage"; import ConnectionLogPage from "./pages/ConnectionLogPage/ConnectionLogPage"; import { HealthLayout } from "./pages/HealthPage/HealthLayout"; @@ -436,9 +437,6 @@ const AISettingsGatewayKeysPage = lazy( const AISettingsModelsPage = lazy( () => import("./pages/AISettingsPage/ModelsPage/ModelsPage"), ); -const AISettingsModelDefaultsPage = lazy( - () => import("./pages/AISettingsPage/ModelsPage/DefaultsPage/DefaultsPage"), -); const AISettingsOrganizationModelsLayout = lazy( () => import("./pages/AISettingsPage/ModelsPage/OrganizationModelsLayout"), ); @@ -743,15 +741,12 @@ export const router = createBrowserRouter( element={} /> } /> + } /> } > } /> - } - /> } /> } /> From 8e33eacf00eb61c3be4efda3d7289a78dd2ea4f6 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 24 Aug 2026 13:36:40 +0000 Subject: [PATCH 2/4] fix(site): drop removed organizations field from ModelForm story context --- .../ModelsPage/components/ModelFormProviderConfig.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx index eca85164935..5964cb8955d 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx @@ -17,7 +17,6 @@ const withOrganizationModels = (Story: React.FC) => ( Date: Mon, 24 Aug 2026 15:34:58 +0000 Subject: [PATCH 3/4] docs(docs/ai-coder/agents): defer override relocation docs to follow-up PR --- docs/ai-coder/agents/models.md | 21 ++++++++++++------- .../agents/platform-controls/advisor.md | 8 ++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/ai-coder/agents/models.md b/docs/ai-coder/agents/models.md index ce8884214fa..855bbedc6ab 100644 --- a/docs/ai-coder/agents/models.md +++ b/docs/ai-coder/agents/models.md @@ -297,14 +297,19 @@ contact an administrator. ## Model overrides -Beyond the chat-level model picker, Coder Agents supports two override layers. -Both are stored per organization and resolve from the chat's organization: - -- **Admin overrides** (per organization): Pin specific contexts to a particular model. - Configure them under **AI Settings** > **Coder Agents** > **Organization settings** for the selected organization. -- **Personal overrides** (per user and organization, opt-in by admin): Let users override the model for their own root chats and delegated subagents. - Admins enable the deployment-wide toggle under **AI Settings** > **Coder Agents**; once enabled, each user sees an **Agents** tab in their personal **Agents** > **Settings**. - Users in more than one organization pick which organization to configure. +Beyond the chat-level model picker, Coder Agents supports two override +layers. Both are stored per organization and resolve from the chat's +organization: + +- **Admin overrides** (per organization): Pin specific contexts to a + particular model. Configure them on the **Defaults & overrides** tab + under **AI Settings** > **Models** for the selected organization. +- **Personal overrides** (per user and organization, opt-in by admin): + Let users override the model for their own root chats and delegated + subagents. Admins enable the deployment-wide toggle under + **AI Settings** > **Coder Agents**; once on, each user sees an + **Agents** tab in their personal **Agents** > **Settings**. Users in + more than one organization pick which organization to configure. > [!IMPORTANT] > When a deployment upgrades from the older deployment-wide override diff --git a/docs/ai-coder/agents/platform-controls/advisor.md b/docs/ai-coder/agents/platform-controls/advisor.md index 9a5bae6487d..aabdaae0875 100644 --- a/docs/ai-coder/agents/platform-controls/advisor.md +++ b/docs/ai-coder/agents/platform-controls/advisor.md @@ -38,9 +38,11 @@ deployment-wide. | Max uses per turn | `0` (unlimited) | Caps how many times the root agent can call the advisor in a single chat turn. Must be a non-negative integer. | | Max output tokens | `0` (server default) | Caps the advisor model's response length. `0` uses the server default of 16,384 tokens. Must be a non-negative integer. | -The advisor model and its reasoning effort are organization-scoped [model overrides](../models.md#model-overrides). -Configure them under **AI Settings** > **Coder Agents** > **Organization settings** for the selected organization. -When no override is set, the advisor reuses the root agent's model. +The advisor model and its reasoning effort are organization-scoped +[model overrides](../models.md#model-overrides). Configure them on the +**Defaults & overrides** tab under **AI Settings** > **Models** for the +selected organization. When no override is set, the advisor reuses the +root agent's model. The advisor is not available in plan mode or to subagents. Failed advisor invocations refund the per-turn budget. From bd09ccbdaee0c45dac5ebfce36dabc246f013b76 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 24 Aug 2026 16:21:20 +0000 Subject: [PATCH 4/4] fix(site): stabilize organization settings discovery --- .../CoderAgentsPageView.stories.tsx | 19 +++++++ .../CoderAgentsPage/CoderAgentsPageView.tsx | 12 ++--- .../ModelDefaultsRedirect.stories.tsx | 50 +++++++++++++++++++ .../ModelsPage/ModelDefaultsRedirect.test.tsx | 33 ------------ 4 files changed, 75 insertions(+), 39 deletions(-) create mode 100644 site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.stories.tsx delete mode 100644 site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx index 015145dd6dc..a5e60955b17 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx @@ -124,6 +124,25 @@ export const OrganizationOnly: Story = { }, }; +export const OrganizationDiscoveryLoading: Story = { + args: { isOrganizationAccessLoading: true }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.getByRole("status", { name: "Loading organization settings" }), + ).toBeVisible(); + await expect( + canvas.queryByRole("heading", { name: "Organization settings" }), + ).not.toBeInTheDocument(); + await expect( + canvas.queryByText("Organization override controls"), + ).not.toBeInTheDocument(); + await expect( + canvas.getByRole("heading", { name: "Deployment settings" }), + ).toBeVisible(); + }, +}; + export const DeploymentOnly: Story = { args: { organization: undefined, diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx index 26fbd285060..fcfc15c7f1e 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx @@ -106,11 +106,9 @@ export const CoderAgentsPageView: FC = ({ - {isOrganizationAccessLoading && !organization && } - {organizationAccessError != null && !organization && ( - - )} - {organization && ( + {isOrganizationAccessLoading ? ( + + ) : organization ? (
= ({ )} {organizationSettings}
- )} + ) : organizationAccessError != null ? ( + + ) : null} {canEditDeploymentConfig && (
{ + const location = useLocation(); + return ( +

+ {location.pathname} + {location.search} +

+ ); +}; + +const meta: Meta = { + title: "pages/AISettingsPage/ModelDefaultsRedirect", + component: ModelDefaultsRedirect, + parameters: { + reactRouter: reactRouterParameters({ + location: { + path: "/ai/settings/models/defaults", + searchParams: { org: "engineering", source: "bookmark" }, + }, + routing: [ + { + path: "/ai/settings/models/defaults", + useStoryElement: true, + }, + { + path: "/ai/settings/coder-agents", + element: , + }, + ], + }), + }, +}; +export default meta; +type Story = StoryObj; + +export const PreservesQueryParameters: Story = { + play: async () => { + await expect( + await screen.findByRole("status", { name: "Current location" }), + ).toHaveTextContent( + "/ai/settings/coder-agents?org=engineering&source=bookmark", + ); + }, +}; diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx deleted file mode 100644 index feca204240a..00000000000 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelDefaultsRedirect.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { render, waitFor } from "@testing-library/react"; -import { createMemoryRouter, RouterProvider } from "react-router"; -import { expect, it } from "vitest"; -import { ModelDefaultsRedirect } from "./ModelDefaultsRedirect"; - -it("redirects to Coder Agents and preserves the query string", async () => { - const router = createMemoryRouter( - [ - { - path: "/ai/settings/models/defaults", - element: , - }, - { - path: "/ai/settings/coder-agents", - element:
Coder Agents
, - }, - ], - { - initialEntries: [ - "/ai/settings/models/defaults?org=engineering&source=bookmark", - ], - }, - ); - - render(); - - await waitFor(() => { - expect(router.state.location.pathname).toBe("/ai/settings/coder-agents"); - expect(router.state.location.search).toBe( - "?org=engineering&source=bookmark", - ); - }); -});