From 05e41511636482ed5a8425fa3a353141dd1cfff9 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Aug 2026 13:28:59 +0000 Subject: [PATCH] fix(site): show Manage agents link to organization model admins The Agents settings panel gated the Manage agents link on editDeploymentConfig, but the Coder Agents page it links to already admits organization model admins. Gate the link on a new canAccessCoderAgentsSettings helper (deployment edit access or any chat model config access) threaded as a separate prop, leaving isAdmin untouched for API-key visibility and provider queries. --- site/src/modules/permissions/index.ts | 16 +++++++++ .../AgentsPage/AgentsPageLayout.stories.tsx | 20 +++++++++++ .../src/pages/AgentsPage/AgentsPageLayout.tsx | 3 ++ .../ChatsSidebar/ChatsSidebar.stories.tsx | 35 +++++++++++++++++++ .../components/ChatsSidebar/ChatsSidebar.tsx | 9 ++++- .../ChatsSidebar/settings/SettingsPanel.tsx | 6 ++-- 6 files changed, 85 insertions(+), 4 deletions(-) diff --git a/site/src/modules/permissions/index.ts b/site/src/modules/permissions/index.ts index c6f64421cd1..96bfc31ecaa 100644 --- a/site/src/modules/permissions/index.ts +++ b/site/src/modules/permissions/index.ts @@ -28,6 +28,22 @@ export const canAccessAnyChatModelConfig = ( ); }; +/** + * Whether the user can open the Coder Agents settings page at + * /ai/settings/coder-agents. Deployment admins manage deployment-wide + * agent settings, and organization model admins manage their + * organizations' model configurations on the same page. + */ +export const canAccessCoderAgentsSettings = ( + permissions: Permissions | undefined, +): boolean => { + return ( + permissions !== undefined && + (permissions.editDeploymentConfig || + canAccessAnyChatModelConfig(permissions)) + ); +}; + export const canViewDeploymentSettings = ( permissions: Permissions | undefined, ): permissions is Permissions => { diff --git a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx index b00152a7589..7adbfc2eec2 100644 --- a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx @@ -1206,6 +1206,26 @@ export const OpensSettingsForNonAdmins: Story = { }, }; +export const OpensSettingsForOrgModelAdmins: Story = { + parameters: { + permissions: { + ...MockNoPermissions, + editAnyChatModelConfig: true, + }, + }, + play: async ({ canvasElement }) => { + await openSettingsView(canvasElement); + + const manageAgentsLink = await screen.findByRole("link", { + name: "Manage agents", + }); + expect(manageAgentsLink).toHaveAttribute( + "href", + "/ai/settings/coder-agents", + ); + }, +}; + export const OpensAISettingsFromManageAgentsOnMobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" }, diff --git a/site/src/pages/AgentsPage/AgentsPageLayout.tsx b/site/src/pages/AgentsPage/AgentsPageLayout.tsx index 5ebb75fe3f3..68dbdb91eae 100644 --- a/site/src/pages/AgentsPage/AgentsPageLayout.tsx +++ b/site/src/pages/AgentsPage/AgentsPageLayout.tsx @@ -59,6 +59,7 @@ import { getDefaultOrganizationName, useDashboard, } from "#/modules/dashboard/useDashboard"; +import { canAccessCoderAgentsSettings } from "#/modules/permissions"; import { cn } from "#/utils/cn"; import { pageTitle } from "#/utils/page"; import { createReconnectingWebSocket } from "#/utils/reconnectingWebSocket"; @@ -170,6 +171,7 @@ const AgentsPageLayout: FC = () => { const personalOverridesOrganizationId = defaultOrganizationId || (organizations[0]?.id ?? ""); const isAgentsAdmin = permissions.editDeploymentConfig; + const canManageAgentSettings = canAccessCoderAgentsSettings(permissions); const [sidebarFilters, setSidebarFilters] = getAgentSidebarFilters( searchParams, @@ -791,6 +793,7 @@ const AgentsPageLayout: FC = () => { personalModelOverridesQuery.data?.enabled } isAdmin={isAgentsAdmin} + canManageAgentSettings={canManageAgentSettings} />
{ + const canvas = within(canvasElement); + const manageAgentsLink = await canvas.findByRole("link", { + name: "Manage agents", + }); + expect(manageAgentsLink).toHaveAttribute( + "href", + "/ai/settings/coder-agents", + ); + // API-key visibility stays tied to isAdmin and configured providers. + expect( + canvas.queryByRole("link", { name: "Secrets (API keys)" }), + ).not.toBeInTheDocument(); + }, +}; + export const PreservesArchivedFilterOnSettingsNavigation: Story = { args: { chats: [ diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx index 513db7d06b8..0a795c6a1cb 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx @@ -49,6 +49,12 @@ interface ChatsSidebarProps { onCollapse?: () => void; isPersonalModelOverridesEnabled?: boolean; isAdmin?: boolean; + /** + * Whether the user can open the Coder Agents settings page. Broader + * than isAdmin: organization model admins qualify without deployment + * config access. + */ + canManageAgentSettings?: boolean; currentUserId: string; } @@ -85,6 +91,7 @@ export const ChatsSidebar: FC = (props) => { onCollapse, isPersonalModelOverridesEnabled = false, isAdmin = false, + canManageAgentSettings = false, currentUserId, } = props; const { agentId, chatId } = useParams<{ @@ -156,7 +163,7 @@ export const ChatsSidebar: FC = (props) => { settingsSection={settingsSection} showApiKeysItem={showApiKeysItem} isPersonalModelOverridesEnabled={isPersonalModelOverridesEnabled} - isAdmin={isAdmin} + canManageAgentSettings={canManageAgentSettings} location={location} onCollapse={onCollapse} /> diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx index f957fabdee2..16573f30114 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/settings/SettingsPanel.tsx @@ -20,7 +20,7 @@ interface SettingsPanelProps { readonly settingsSection: string | undefined; readonly showApiKeysItem: boolean; readonly isPersonalModelOverridesEnabled: boolean; - readonly isAdmin: boolean; + readonly canManageAgentSettings: boolean; readonly location: Location; readonly onCollapse?: () => void; } @@ -30,7 +30,7 @@ export const SettingsPanel: FC = ({ settingsSection, showApiKeysItem, isPersonalModelOverridesEnabled, - isAdmin, + canManageAgentSettings, location, onCollapse, }) => { @@ -113,7 +113,7 @@ export const SettingsPanel: FC = ({ state={location.state} /> )} - {isAdmin && ( + {canManageAgentSettings && (