From 08b5be149e545471c0d5af88a16b7d26735f1a0d Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Aug 2026 12:00:42 +0000 Subject: [PATCH 1/6] fix(site): preserve organization in model setup links --- .../ModelsPage/organizationModels.test.tsx | 7 +++ .../ModelsPage/organizationModels.tsx | 7 ++- .../AgentsPage/AgentChatPageView.stories.tsx | 6 ++- .../components/AgentChatInput.stories.tsx | 8 +++- .../AgentsPage/components/AgentChatInput.tsx | 7 +++ .../components/AgentCreateForm.stories.tsx | 46 ++++++++++++++++++- .../components/AgentSetupNotice.stories.tsx | 13 ++++-- .../components/AgentSetupNotice.tsx | 11 ++++- 8 files changed, 93 insertions(+), 12 deletions(-) diff --git a/site/src/pages/AISettingsPage/ModelsPage/organizationModels.test.tsx b/site/src/pages/AISettingsPage/ModelsPage/organizationModels.test.tsx index b27116e4469..f9bf044be7b 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/organizationModels.test.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/organizationModels.test.tsx @@ -10,6 +10,7 @@ import { } from "#/testHelpers/entities"; import { organizationAddModelPath, + organizationModelsPath, selectModelOrganization, useAccessibleModelOrganizations, } from "./organizationModels"; @@ -67,6 +68,12 @@ describe("selectModelOrganization", () => { }); }); + it("builds an organization-scoped models path", () => { + expect(organizationModelsPath(MockOrganization2)).toBe( + `/ai/settings/models?org=${MockOrganization2.name}`, + ); + }); + it("preserves auxiliary parameters in organization model paths", () => { const params = new URLSearchParams({ provider: "openai", diff --git a/site/src/pages/AISettingsPage/ModelsPage/organizationModels.tsx b/site/src/pages/AISettingsPage/ModelsPage/organizationModels.tsx index 7a4033b6e1f..dacb7e37f8b 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/organizationModels.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/organizationModels.tsx @@ -149,6 +149,11 @@ const organizationModelSettingsPath = ( return `/ai/settings/models${suffix}?${next.toString()}`; }; +export const organizationModelsPath = ( + organization: Organization, + searchParams?: URLSearchParams, +): string => organizationModelSettingsPath(organization, "", searchParams); + export const organizationAddModelPath = ( organization: Organization, searchParams?: URLSearchParams, @@ -168,5 +173,5 @@ export const organizationModelPath = ( export const useOrganizationModelsPath = (): string => { const { organization } = useOrganizationModels(); const [searchParams] = useSearchParams(); - return organizationModelSettingsPath(organization, "", searchParams); + return organizationModelsPath(organization, searchParams); }; diff --git a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx index aa5488c072a..1892cdb02d8 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx @@ -724,6 +724,7 @@ export const MissingProviderAndModelSetup: Story = { render: () => ( ( = { title: "pages/AgentsPage/AgentChatInput", component: AgentChatInput, - decorators: [withProxyProvider()], + decorators: [withDashboardProvider, withProxyProvider()], parameters: { queries: [ { diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index 81c266cc445..c822e42e9be 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -60,6 +60,7 @@ import { ModelSelector, type ModelSelectorOption, } from "#/modules/aiModels/ModelSelector"; +import { useDashboard } from "#/modules/dashboard/useDashboard"; import { countInvisibleCharacters } from "#/utils/invisibleUnicode"; import { isBelowMdViewport, isMobileViewport } from "#/utils/mobile"; import { chatWidthClass, useChatFullWidth } from "../hooks/useChatFullWidth"; @@ -426,6 +427,10 @@ export const AgentChatInput: FC = ({ preferencesQuery.isLoading, ); const [chatFullWidth] = useChatFullWidth(); + const { organizations } = useDashboard(); + const chatOrganization = organizations.find( + (organization) => organization.id === chatOrganizationId, + ); const showAgentSetupNotice = aiGatewayDisabled || (canConfigureAgentSetup @@ -1098,6 +1103,7 @@ export const AgentChatInput: FC = ({ isAdmin providerCount={providerCount ?? 0} modelCount={modelCount ?? 0} + organization={chatOrganization} unsupportedProviderNames={unsupportedProviderNames} aiGatewayDisabled={aiGatewayDisabled} /> @@ -1106,6 +1112,7 @@ export const AgentChatInput: FC = ({ isAdmin={false} providerCount={0} modelCount={0} + organization={chatOrganization} unsupportedProviderNames={unsupportedProviderNames} aiGatewayDisabled={aiGatewayDisabled} /> diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index bd8cdae918e..0b8d2bd198c 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -1169,7 +1169,51 @@ export const MissingProviderAndModelSetup: Story = { ); expect(canvas.getByRole("link", { name: "model" })).toHaveAttribute( "href", - "/ai/settings/models", + `/ai/settings/models?org=${MockDefaultOrganization.name}`, + ); + }, +}; + +export const LocalOrganizationMissingProviderAndModelSetup: Story = { + parameters: { + showOrganizations: true, + organizations: [MockOrganization2], + queries: [ + { + key: permittedOrgsKey, + data: [MockOrganization2], + }, + { + key: organizationChatModelsKey(MockOrganization2.id), + data: emptyModelCatalog, + }, + { + key: userChatProviderConfigsKey, + data: defaultUserProviderConfigs, + }, + { key: aiProvidersListKey, data: [] }, + ], + }, + args: { + ...defaultArgs, + canConfigureAgentSetup: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor(() => { + expect( + canvas.getAllByText((_content, element) => { + return ( + element?.textContent === + "To chat with Coder Agents, set up a provider then add a model." + ); + })[0], + ).toBeVisible(); + }); + expect(canvas.getByRole("link", { name: "model" })).toHaveAttribute( + "href", + `/ai/settings/models?org=${MockOrganization2.name}`, ); }, }; diff --git a/site/src/pages/AgentsPage/components/AgentSetupNotice.stories.tsx b/site/src/pages/AgentsPage/components/AgentSetupNotice.stories.tsx index a551f9c1d7b..9dfe588fa31 100644 --- a/site/src/pages/AgentsPage/components/AgentSetupNotice.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentSetupNotice.stories.tsx @@ -1,10 +1,14 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, within } from "storybook/test"; +import { MockDefaultOrganization } from "#/testHelpers/entities"; import { AgentSetupNotice } from "./AgentSetupNotice"; const meta: Meta = { title: "pages/AgentsPage/AgentSetupNotice", component: AgentSetupNotice, + args: { + organization: MockDefaultOrganization, + }, }; export default meta; @@ -22,9 +26,10 @@ export const AdminNoProvider: Story = { await expect( canvas.getByRole("link", { name: "provider" }), ).toHaveAttribute("href", "/ai/settings/providers"); - await expect( - canvas.getByRole("link", { name: "model" }), - ).toBeInTheDocument(); + await expect(canvas.getByRole("link", { name: "model" })).toHaveAttribute( + "href", + `/ai/settings/models?org=${MockDefaultOrganization.name}`, + ); }, }; @@ -39,7 +44,7 @@ export const AdminNoModel: Story = { const canvas = within(canvasElement); await expect(canvas.getByRole("link", { name: "model" })).toHaveAttribute( "href", - "/ai/settings/models", + `/ai/settings/models?org=${MockDefaultOrganization.name}`, ); await expect( canvas.queryByRole("link", { name: "provider" }), diff --git a/site/src/pages/AgentsPage/components/AgentSetupNotice.tsx b/site/src/pages/AgentsPage/components/AgentSetupNotice.tsx index 1a95b074606..9fb0c5eaf2f 100644 --- a/site/src/pages/AgentsPage/components/AgentSetupNotice.tsx +++ b/site/src/pages/AgentsPage/components/AgentSetupNotice.tsx @@ -1,11 +1,14 @@ import type { FC, ReactNode } from "react"; import { Link } from "react-router"; +import type { Organization } from "#/api/typesGenerated"; +import { organizationModelsPath } from "#/pages/AISettingsPage/ModelsPage/organizationModels"; import { docs } from "#/utils/docs"; interface AgentSetupNoticeProps { isAdmin: boolean; providerCount: number; modelCount: number; + organization?: Organization; // Names of configured providers the harness cannot use, populated by // the page only when no supported provider is configured. unsupportedProviderNames?: readonly string[]; @@ -26,12 +29,16 @@ export const AgentSetupNotice: FC = ({ isAdmin, providerCount, modelCount, + organization, unsupportedProviderNames = [], aiGatewayDisabled, }) => { const hasProvider = providerCount > 0; const hasModel = modelCount > 0; const hasUnsupportedProviderNames = unsupportedProviderNames.length > 0; + const modelsPath = organization + ? organizationModelsPath(organization) + : "/ai/settings/models"; // AI Gateway can be disabled even when providers/models exist in the DB // catalog, so check it before the provider/model counts below. Unlike @@ -115,7 +122,7 @@ export const AgentSetupNotice: FC = ({ {" "} then add a{" "} model @@ -132,7 +139,7 @@ export const AgentSetupNotice: FC = ({ To chat with Coder Agents, set up a{" "} model From dfd22a2e43bed954f29946b02584be02530cf86e Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Aug 2026 13:14:45 +0000 Subject: [PATCH 2/6] test(site): exercise organization-scoped model link --- .../components/AgentCreateForm.stories.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index 0b8d2bd198c..9d599d7b730 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -2,6 +2,7 @@ import type { Decorator, Meta, StoryObj } from "@storybook/react-vite"; import { delay } from "msw"; import { type ComponentProps, useEffect, useState } from "react"; import { QueryClient, QueryClientProvider, useQueryClient } from "react-query"; +import { useLocation } from "react-router"; import { expect, fn, @@ -11,6 +12,7 @@ import { waitFor, within, } from "storybook/test"; +import { reactRouterParameters } from "storybook-addon-remix-react-router"; import { API } from "#/api/api"; import { aiProvidersListKey } from "#/api/queries/aiProviders"; import { @@ -51,6 +53,16 @@ let pendingOrganizationAuthorization: Deferred< >; let capturedQueryClient: QueryClient | undefined; +const LocationProbe = () => { + const location = useLocation(); + return ( + + {location.pathname} + {location.search} + + ); +}; + const permittedOrgsKey = permittedOrganizationsKey({ object: { resource_type: "chat", owner_id: "me" }, action: "create", @@ -1178,6 +1190,13 @@ export const LocalOrganizationMissingProviderAndModelSetup: Story = { parameters: { showOrganizations: true, organizations: [MockOrganization2], + reactRouter: reactRouterParameters({ + location: { path: "/agents" }, + routing: [ + { path: "/agents", useStoryElement: true }, + { path: "/ai/settings/models", element: }, + ], + }), queries: [ { key: permittedOrgsKey, @@ -1211,10 +1230,10 @@ export const LocalOrganizationMissingProviderAndModelSetup: Story = { })[0], ).toBeVisible(); }); - expect(canvas.getByRole("link", { name: "model" })).toHaveAttribute( - "href", - `/ai/settings/models?org=${MockOrganization2.name}`, - ); + await userEvent.click(canvas.getByRole("link", { name: "model" })); + await expect( + await canvas.findByRole("status", { name: "Current location" }), + ).toHaveTextContent(`/ai/settings/models?org=${MockOrganization2.name}`); }, }; From 63e6986bdb264822c83e14ca92ab06bff6a55bad Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 8 Sep 2026 00:21:05 +0000 Subject: [PATCH 3/6] fix(site/src/modules/management): preserve organization in AI settings sidebar links --- .../AISettingsSidebarView.stories.tsx | 60 +++++++++++++++++++ .../management/AISettingsSidebarView.tsx | 52 ++++++++++++---- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/site/src/modules/management/AISettingsSidebarView.stories.tsx b/site/src/modules/management/AISettingsSidebarView.stories.tsx index 05d5ccba5c1..40415da982f 100644 --- a/site/src/modules/management/AISettingsSidebarView.stories.tsx +++ b/site/src/modules/management/AISettingsSidebarView.stories.tsx @@ -69,6 +69,37 @@ export const ModelsActive: Story = { }, }; +export const OrganizationParamPreserved: Story = { + parameters: { + reactRouter: reactRouterParameters({ + location: { + path: "/ai/settings/models", + searchParams: { org: "my organization" }, + }, + routing: [{ path: "/ai/settings/models", useStoryElement: true }], + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole("link", { name: "Models" })).toHaveAttribute( + "href", + "/ai/settings/models?org=my+organization", + ); + await expect( + canvas.getByRole("link", { name: "Coder Agents" }), + ).toHaveAttribute("href", "/ai/settings/coder-agents?org=my+organization"); + await expect( + canvas.getByRole("link", { name: "MCP servers" }), + ).toHaveAttribute("href", "/ai/settings/mcp-servers?org=my+organization"); + await expect( + canvas.getByRole("link", { name: "Providers" }), + ).toHaveAttribute("href", "/ai/settings/providers"); + await expect( + canvas.getByRole("link", { name: "Templates" }), + ).toHaveAttribute("href", "/ai/settings/templates"); + }, +}; + export const ModelsActiveOnOrganizationRoute: Story = { parameters: { reactRouter: reactRouterParameters({ @@ -299,6 +330,35 @@ export const MCPServersForCreateOnlyAdmin: Story = { }, }; +export const MCPServersForCreateOnlyAdminPreservesOrganization: Story = { + args: { + permissions: { + ...MockNoPermissions, + createAnyMCPServerConfig: true, + }, + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { + path: "/ai/settings/mcp-servers/add", + searchParams: { org: MockDefaultOrganization.name }, + }, + routing: [ + { path: "/ai/settings/mcp-servers/add", useStoryElement: true }, + ], + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.getByRole("link", { name: "MCP servers" }), + ).toHaveAttribute( + "href", + `/ai/settings/mcp-servers/add?org=${MockDefaultOrganization.name}`, + ); + }, +}; + export const MCPServersHiddenWithoutPermission: Story = { args: { permissions: { diff --git a/site/src/modules/management/AISettingsSidebarView.tsx b/site/src/modules/management/AISettingsSidebarView.tsx index 2c4faf9912c..836317e4edc 100644 --- a/site/src/modules/management/AISettingsSidebarView.tsx +++ b/site/src/modules/management/AISettingsSidebarView.tsx @@ -1,6 +1,6 @@ import { cn } from "cn"; import type { FC, ReactNode } from "react"; -import { Link, NavLink, useMatch } from "react-router"; +import { Link, NavLink, useMatch, useSearchParams } from "react-router"; import { Sidebar as BaseSidebar, SettingsSidebarNavItem as SidebarNavItem, @@ -9,6 +9,7 @@ import { canAccessAnyChatModelConfig, type Permissions, } from "#/modules/permissions"; +import { modelOrganizationSearchParam } from "#/pages/AISettingsPage/ModelsPage/organizationModels"; interface AISettingsSidebarViewProps { /** Site-wide permissions. */ @@ -36,7 +37,19 @@ const SubNavItem: FC<{ href: string; children?: ReactNode }> = ({ ); -const ModelsSidebarNavItem: FC = () => { +const organizationScopedPath = ( + pathname: string, + organizationName: string | null, +): string => { + if (!organizationName) { + return pathname; + } + const next = new URLSearchParams(); + next.set(modelOrganizationSearchParam, organizationName); + return `${pathname}?${next.toString()}`; +}; + +const ModelsSidebarNavItem: FC<{ href: string }> = ({ href }) => { const legacyMatch = useMatch("/ai/settings/models/*"); const organizationMatch = useMatch( "/ai/settings/organizations/:organization/models/*", @@ -45,7 +58,7 @@ const ModelsSidebarNavItem: FC = () => { return ( = ({ canAccessOrganizationModels = false, canShareOrganizationMCPServers = false, }) => { + const [searchParams] = useSearchParams(); + const organizationName = searchParams.get(modelOrganizationSearchParam); + const modelsPath = organizationScopedPath( + "/ai/settings/models", + organizationName, + ); + const coderAgentsPath = organizationScopedPath( + "/ai/settings/coder-agents", + organizationName, + ); + const mcpServersPath = organizationScopedPath( + "/ai/settings/mcp-servers", + organizationName, + ); + const addMCPServerPath = organizationScopedPath( + "/ai/settings/mcp-servers/add", + organizationName, + ); + return (
@@ -81,15 +113,15 @@ const AISettingsSidebarView: FC = ({ )} {(canAccessAnyChatModelConfig(permissions) || - canAccessOrganizationModels) && } + canAccessOrganizationModels) && ( + + )} {(permissions.editDeploymentConfig || canAccessOrganizationModels) && ( - - Coder Agents - + Coder Agents )} {permissions.editDeploymentConfig && (
- MCP servers + MCP servers {permissions.updateAnyTemplate && ( Templates )} @@ -112,8 +144,8 @@ const AISettingsSidebarView: FC = ({ permissions.updateAnyMCPServerConfig || permissions.deleteAnyMCPServerConfig || canShareOrganizationMCPServers - ? "/ai/settings/mcp-servers" - : "/ai/settings/mcp-servers/add" + ? mcpServersPath + : addMCPServerPath } > MCP servers From 3fa19efeb6637e09c9ecb43ba0eb850dcf5ae7bb Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 9 Sep 2026 03:02:06 +0000 Subject: [PATCH 4/6] fix(site/src): build organization-scoped sidebar links as router To objects --- site/src/components/Sidebar/Sidebar.tsx | 4 +-- .../management/AISettingsSidebarView.tsx | 28 +++++++++++-------- .../AgentsPage/AgentChatPageView.stories.tsx | 4 +-- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index 200dbbd3624..8b7dc511e3b 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -1,6 +1,6 @@ import { cn } from "cn"; import type { FC, ReactNode } from "react"; -import { NavLink } from "react-router"; +import { NavLink, type To } from "react-router"; interface SidebarProps { children?: ReactNode; @@ -15,7 +15,7 @@ export const Sidebar: FC = ({ className, children }) => { interface SettingsSidebarNavItemProps { children?: ReactNode; - href: string; + href: To; end?: boolean; } diff --git a/site/src/modules/management/AISettingsSidebarView.tsx b/site/src/modules/management/AISettingsSidebarView.tsx index 836317e4edc..38f627dc444 100644 --- a/site/src/modules/management/AISettingsSidebarView.tsx +++ b/site/src/modules/management/AISettingsSidebarView.tsx @@ -1,6 +1,12 @@ import { cn } from "cn"; import type { FC, ReactNode } from "react"; -import { Link, NavLink, useMatch, useSearchParams } from "react-router"; +import { + Link, + NavLink, + type To, + useMatch, + useSearchParams, +} from "react-router"; import { Sidebar as BaseSidebar, SettingsSidebarNavItem as SidebarNavItem, @@ -18,7 +24,7 @@ interface AISettingsSidebarViewProps { canShareOrganizationMCPServers?: boolean; } -const SubNavItem: FC<{ href: string; children?: ReactNode }> = ({ +const SubNavItem: FC<{ href: To; children?: ReactNode }> = ({ href, children, }) => ( @@ -40,16 +46,16 @@ const SubNavItem: FC<{ href: string; children?: ReactNode }> = ({ const organizationScopedPath = ( pathname: string, organizationName: string | null, -): string => { - if (!organizationName) { - return pathname; - } - const next = new URLSearchParams(); - next.set(modelOrganizationSearchParam, organizationName); - return `${pathname}?${next.toString()}`; -}; +): To => ({ + pathname, + search: organizationName + ? new URLSearchParams({ + [modelOrganizationSearchParam]: organizationName, + }).toString() + : "", +}); -const ModelsSidebarNavItem: FC<{ href: string }> = ({ href }) => { +const ModelsSidebarNavItem: FC<{ href: To }> = ({ href }) => { const legacyMatch = useMatch("/ai/settings/models/*"); const organizationMatch = useMatch( "/ai/settings/organizations/:organization/models/*", diff --git a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx index 1892cdb02d8..fde033c788d 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx @@ -724,7 +724,7 @@ export const MissingProviderAndModelSetup: Story = { render: () => ( ( Date: Wed, 9 Sep 2026 03:19:45 +0000 Subject: [PATCH 5/6] test(site/src/modules/management): navigate organization-scoped sidebar links in stories --- .../AISettingsSidebarView.stories.tsx | 157 ++++++++++-------- 1 file changed, 86 insertions(+), 71 deletions(-) diff --git a/site/src/modules/management/AISettingsSidebarView.stories.tsx b/site/src/modules/management/AISettingsSidebarView.stories.tsx index 40415da982f..6e3d14996cf 100644 --- a/site/src/modules/management/AISettingsSidebarView.stories.tsx +++ b/site/src/modules/management/AISettingsSidebarView.stories.tsx @@ -1,6 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useLocation } from "react-router"; import { expect, userEvent, waitFor, within } from "storybook/test"; -import { reactRouterParameters } from "storybook-addon-remix-react-router"; +import { + type RouterRoute, + reactRouterParameters, +} from "storybook-addon-remix-react-router"; import { organizationsPermissions } from "#/api/queries/organizations"; import { MockDefaultOrganization, @@ -16,27 +20,68 @@ import { import { AISettingsSidebar } from "./AISettingsSidebar"; import AISettingsSidebarView from "./AISettingsSidebarView"; +const LocationProbe = () => { + const location = useLocation(); + return ( +

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

+ ); +}; + +const aiSettingsRoutes: [RouterRoute, ...RouterRoute[]] = [ + { path: "/ai/settings/governance", useStoryElement: true }, + { path: "/ai/settings/gateway-keys", useStoryElement: true }, + { path: "/ai/settings/providers", useStoryElement: true }, + { path: "/ai/settings/coder-agents", useStoryElement: true }, + { path: "/ai/settings/models", useStoryElement: true }, + { + path: "/ai/settings/organizations/:organization/models", + useStoryElement: true, + }, + { path: "/ai/settings/mcp-servers", useStoryElement: true }, + { path: "/ai/settings/mcp-servers/add", useStoryElement: true }, + { path: "/ai/settings/templates", useStoryElement: true }, + { path: "/ai/settings/instructions", useStoryElement: true }, + { path: "/ai/settings/lifecycle", useStoryElement: true }, +]; + +const atLocation = (path: string, searchParams?: Record) => + reactRouterParameters({ + location: { path, searchParams }, + routing: aiSettingsRoutes, + }); + +const followLink = async ( + canvas: ReturnType, + name: string, + expectedLocation: string, +) => { + await userEvent.click(canvas.getByRole("link", { name })); + await waitFor(() => + expect( + canvas.getByRole("status", { name: "Current location" }).textContent, + ).toBe(expectedLocation), + ); +}; + const meta: Meta = { title: "modules/management/AISettingsSidebarView", component: AISettingsSidebarView, + decorators: [ + (Story) => ( + <> + + + + ), + ], args: { permissions: MockPermissions, }, parameters: { - reactRouter: reactRouterParameters({ - location: { path: "/ai/settings/coder-agents" }, - routing: [ - { path: "/ai/settings/governance", useStoryElement: true }, - { path: "/ai/settings/gateway-keys", useStoryElement: true }, - { path: "/ai/settings/providers", useStoryElement: true }, - { path: "/ai/settings/coder-agents", useStoryElement: true }, - { path: "/ai/settings/models", useStoryElement: true }, - { path: "/ai/settings/mcp-servers", useStoryElement: true }, - { path: "/ai/settings/templates", useStoryElement: true }, - { path: "/ai/settings/instructions", useStoryElement: true }, - { path: "/ai/settings/lifecycle", useStoryElement: true }, - ], - }), + reactRouter: atLocation("/ai/settings/coder-agents"), }, }; @@ -55,10 +100,7 @@ export const CoderAgentsActive: Story = { export const ModelsActive: Story = { parameters: { - reactRouter: reactRouterParameters({ - location: { path: "/ai/settings/models" }, - routing: [{ path: "/ai/settings/models", useStoryElement: true }], - }), + reactRouter: atLocation("/ai/settings/models"), }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -71,48 +113,34 @@ export const ModelsActive: Story = { export const OrganizationParamPreserved: Story = { parameters: { - reactRouter: reactRouterParameters({ - location: { - path: "/ai/settings/models", - searchParams: { org: "my organization" }, - }, - routing: [{ path: "/ai/settings/models", useStoryElement: true }], - }), + reactRouter: atLocation("/ai/settings/models", { org: "my organization" }), }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect(canvas.getByRole("link", { name: "Models" })).toHaveAttribute( - "href", + await followLink( + canvas, + "Coder Agents", + "/ai/settings/coder-agents?org=my+organization", + ); + await followLink( + canvas, + "MCP servers", + "/ai/settings/mcp-servers?org=my+organization", + ); + await followLink( + canvas, + "Models", "/ai/settings/models?org=my+organization", ); - await expect( - canvas.getByRole("link", { name: "Coder Agents" }), - ).toHaveAttribute("href", "/ai/settings/coder-agents?org=my+organization"); - await expect( - canvas.getByRole("link", { name: "MCP servers" }), - ).toHaveAttribute("href", "/ai/settings/mcp-servers?org=my+organization"); - await expect( - canvas.getByRole("link", { name: "Providers" }), - ).toHaveAttribute("href", "/ai/settings/providers"); - await expect( - canvas.getByRole("link", { name: "Templates" }), - ).toHaveAttribute("href", "/ai/settings/templates"); + await followLink(canvas, "Providers", "/ai/settings/providers"); }, }; export const ModelsActiveOnOrganizationRoute: Story = { parameters: { - reactRouter: reactRouterParameters({ - location: { - path: "/ai/settings/organizations/my-organization/models", - }, - routing: [ - { - path: "/ai/settings/organizations/:organization/models", - useStoryElement: true, - }, - ], - }), + reactRouter: atLocation( + "/ai/settings/organizations/my-organization/models", + ), }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -125,19 +153,13 @@ export const ModelsActiveOnOrganizationRoute: Story = { export const LifecycleActive: Story = { parameters: { - reactRouter: reactRouterParameters({ - location: { path: "/ai/settings/lifecycle" }, - routing: [{ path: "/ai/settings/lifecycle", useStoryElement: true }], - }), + reactRouter: atLocation("/ai/settings/lifecycle"), }, }; export const ProvidersActive: Story = { parameters: { - reactRouter: reactRouterParameters({ - location: { path: "/ai/settings/providers" }, - routing: [{ path: "/ai/settings/providers", useStoryElement: true }], - }), + reactRouter: atLocation("/ai/settings/providers"), }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -338,22 +360,15 @@ export const MCPServersForCreateOnlyAdminPreservesOrganization: Story = { }, }, parameters: { - reactRouter: reactRouterParameters({ - location: { - path: "/ai/settings/mcp-servers/add", - searchParams: { org: MockDefaultOrganization.name }, - }, - routing: [ - { path: "/ai/settings/mcp-servers/add", useStoryElement: true }, - ], + reactRouter: atLocation("/ai/settings/mcp-servers", { + org: MockDefaultOrganization.name, }), }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect( - canvas.getByRole("link", { name: "MCP servers" }), - ).toHaveAttribute( - "href", + await followLink( + canvas, + "MCP servers", `/ai/settings/mcp-servers/add?org=${MockDefaultOrganization.name}`, ); }, From a802a35b57628ba3c448a97238a140386cd6fcfc Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 9 Sep 2026 05:05:57 +0000 Subject: [PATCH 6/6] fix(site/src/pages/AgentsPage/components): mount DashboardProvider in ChatPageContent stories --- .../AgentsPage/components/ChatPageContent.stories.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatPageContent.stories.tsx b/site/src/pages/AgentsPage/components/ChatPageContent.stories.tsx index 714c1d4423b..035ef1dbf91 100644 --- a/site/src/pages/AgentsPage/components/ChatPageContent.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatPageContent.stories.tsx @@ -16,7 +16,10 @@ import { MockUserOwner, MockUserPreferenceSettings, } from "#/testHelpers/entities"; -import { withAuthProvider } from "#/testHelpers/storybook"; +import { + withAuthProvider, + withDashboardProvider, +} from "#/testHelpers/storybook"; import { ChatWorkspaceContext } from "../context/ChatWorkspaceContext"; import { createChatStore } from "./ChatConversation/chatStore"; import { FIXTURE_NOW } from "./ChatConversation/storyFixtures"; @@ -42,7 +45,7 @@ const StoryChatPageTimeline: FC<{ const meta = { title: "pages/AgentsPage/ChatPageContent", - decorators: [withAuthProvider], + decorators: [withAuthProvider, withDashboardProvider], parameters: { user: MockUserOwner, queries: [