fix(site): link manage agents to AI settings (#26912)

This commit is contained in:
Danielle Maywood
2026-07-01 12:55:04 +01:00
committed by GitHub
parent e3ac65aa5f
commit 2839cd2427
7 changed files with 89 additions and 212 deletions
@@ -2,17 +2,13 @@ import type { FC } from "react";
import { Outlet, useLocation } from "react-router";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import { AgentPageHeader } from "./components/AgentPageHeader";
import { sidebarViewFromPath } from "./components/ChatsSidebar/sidebarView";
const AgentSettingsPage: FC = () => {
const location = useLocation();
const match = location.pathname.match(/\/agents\/settings\/(.+)/);
const section = match?.[1];
const sidebarView = sidebarViewFromPath(location.pathname);
const mobileBack = section
? sidebarView.panel === "settings-admin"
? { to: "/agents/settings/admin", label: "Manage agents" }
: { to: "/agents/settings", label: "Settings" }
? { to: "/agents/settings", label: "Settings" }
: undefined;
return (
@@ -228,12 +228,15 @@ const agentsRouting = {
},
{
path: "admin",
element: <Navigate to="/agents/settings/coder-agents" replace />,
element: <Navigate to="/ai/settings/coder-agents" replace />,
},
{ path: "coder-agents", element: <AgentsRouteElement /> },
{
path: "agents",
element: <Navigate to="/agents/settings/coder-agents" replace />,
element: <Navigate to="/ai/settings/coder-agents" replace />,
},
{
path: "coder-agents",
element: <Navigate to="/ai/settings/coder-agents" replace />,
},
{
path: "spend",
@@ -252,8 +255,11 @@ const agentsRouting = {
};
const aiSettingsRouting = {
path: "/ai/settings/spend",
element: <div>Spend limits and usage</div>,
path: "/ai/settings",
children: [
{ path: "coder-agents", element: <AgentsRouteElement /> },
{ path: "spend", element: <div>Spend limits and usage</div> },
],
};
const setInnerWidthForStory = (width: number) => {
@@ -1160,7 +1166,7 @@ export const OpensSettingsForNonAdmins: Story = {
},
};
export const OpensCoderAgentsFromManageAgentsOnMobile: Story = {
export const OpensAISettingsFromManageAgentsOnMobile: Story = {
args: {
isAgentsAdmin: true,
},
@@ -1172,10 +1178,16 @@ export const OpensCoderAgentsFromManageAgentsOnMobile: Story = {
}),
},
play: async () => {
await userEvent.click(
await screen.findByRole("link", { name: "Manage agents" }),
const manageAgentsLink = await screen.findByRole("link", {
name: "Manage agents",
});
expect(manageAgentsLink).toHaveAttribute(
"href",
"/ai/settings/coder-agents",
);
await userEvent.click(manageAgentsLink);
await expect(
await screen.findByRole("heading", { name: "Coder Agents" }),
).toBeInTheDocument();
@@ -1196,10 +1208,16 @@ export const SettingsViewCoderAgentsLink: Story = {
).toBeInTheDocument();
});
await userEvent.click(
await screen.findByRole("link", { name: "Manage agents" }),
const manageAgentsLink = await screen.findByRole("link", {
name: "Manage agents",
});
expect(manageAgentsLink).toHaveAttribute(
"href",
"/ai/settings/coder-agents",
);
await userEvent.click(manageAgentsLink);
await waitFor(() => {
expect(
screen.getByText(
@@ -77,6 +77,7 @@ const agentsRouting = [
];
const settingsRouting = [
{ path: "/ai/settings/coder-agents", useStoryElement: true },
{ path: "/agents/settings/:section", useStoryElement: true },
{ path: "/agents/settings", useStoryElement: true },
...agentsRouting,
@@ -2271,33 +2272,13 @@ export const SettingsUserAgentsAdmin: Story = {
const canvas = within(canvasElement);
const agentsLink = canvas.getByRole("link", { name: "Agents" });
await expect(agentsLink).toHaveAttribute("aria-current", "page");
expect(
canvas.getByRole("link", { name: "Manage agents" }),
).toBeInTheDocument();
},
};
export const SettingsAdminCoderAgentsEntryPreserved: Story = {
args: {
chats: [],
isAdmin: true,
},
parameters: {
reactRouter: reactRouterParameters({
location: { path: "/agents/settings/agents" },
routing: settingsRouting,
}),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const coderAgentsLink = canvas.getByRole("link", {
name: "Coder Agents",
const manageAgentsLink = canvas.getByRole("link", {
name: "Manage agents",
});
expect(coderAgentsLink).toHaveAttribute(
expect(manageAgentsLink).toHaveAttribute(
"href",
"/ai/settings/coder-agents",
);
expect(canvas.getByText("Manage agents")).toBeInTheDocument();
},
};
@@ -98,14 +98,7 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
const location = useLocation();
const sidebarView = sidebarViewFromPath(location.pathname);
const isSettingsPanel = isSettingsView(sidebarView);
const isFallbackToUserPanel =
sidebarView.panel === "settings-admin" && !isAdmin;
const settingsPanel =
sidebarView.panel === "settings-admin" && isAdmin
? "settings-admin"
: "settings";
const settingsSection =
isSettingsPanel && !isFallbackToUserPanel ? sidebarView.section : undefined;
const settingsSection = isSettingsPanel ? sidebarView.section : undefined;
const providerConfigsQuery = useQuery({
...userChatProviderConfigs(),
enabled: isSettingsPanel && !isAdmin,
@@ -164,7 +157,6 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
/>
<SettingsPanel
isSettingsPanel={isSettingsPanel}
settingsPanel={settingsPanel}
settingsSection={settingsSection}
showApiKeysItem={showApiKeysItem}
isPersonalModelOverridesEnabled={isPersonalModelOverridesEnabled}
@@ -2,15 +2,9 @@ import {
ArrowLeftIcon,
ArrowUpRightIcon,
BotIcon,
BoxesIcon,
CoinsIcon,
KeyIcon,
LayoutTemplateIcon,
PanelLeftCloseIcon,
PlugIcon,
ReceiptTextIcon,
RefreshCwIcon,
ServerIcon,
Settings2Icon,
ShrinkIcon,
UserIcon,
@@ -23,7 +17,6 @@ import { SettingsNavItem } from "./SettingsNavItem";
interface SettingsPanelProps {
readonly isSettingsPanel: boolean;
readonly settingsPanel: "settings" | "settings-admin";
readonly settingsSection: string | undefined;
readonly showApiKeysItem: boolean;
readonly isPersonalModelOverridesEnabled: boolean;
@@ -34,7 +27,6 @@ interface SettingsPanelProps {
export const SettingsPanel: FC<SettingsPanelProps> = ({
isSettingsPanel,
settingsPanel,
settingsSection,
showApiKeysItem,
isPersonalModelOverridesEnabled,
@@ -42,9 +34,6 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
location,
onCollapse,
}) => {
const subNavTitle =
settingsPanel === "settings-admin" ? "Manage agents" : "Settings";
return (
<div
className={cn(
@@ -57,34 +46,18 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
<div className="border-b border-border-default px-2 pb-2 pt-3 sm:py-2">
<div className="relative flex items-center">
<span className="pointer-events-none absolute inset-0 flex items-center justify-center text-sm font-medium text-content-primary">
{subNavTitle}
Settings
</span>
<Button
asChild
variant="subtle"
size="icon"
aria-label={
settingsPanel === "settings-admin"
? "Back to settings"
: "Back to agents"
}
aria-label="Back to agents"
className="relative z-10 size-7 min-w-0 text-content-secondary hover:text-content-primary"
>
{settingsPanel === "settings-admin" ? (
<Link
to="/agents/settings/general"
state={location.state}
aria-label="Back to settings"
>
<ArrowLeftIcon />
</Link>
) : (
<Link
to={(location.state as { from?: string })?.from || "/agents"}
>
<ArrowLeftIcon />
</Link>
)}
<Link to={(location.state as { from?: string })?.from || "/agents"}>
<ArrowLeftIcon />
</Link>
</Button>
<div className="flex-1" />
{onCollapse && (
@@ -100,117 +73,56 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
)}
</div>
</div>
{settingsPanel === "settings" ? (
<nav className="flex flex-col gap-0.5 px-2 py-2">
<SettingsNavItem
icon={UserIcon}
label="General"
active={!settingsSection || settingsSection === "general"}
to="/agents/settings/general"
state={location.state}
/>
{isPersonalModelOverridesEnabled && (
<SettingsNavItem
icon={BotIcon}
label="Agents"
active={settingsSection === "user-agents"}
to="/agents/settings/user-agents"
state={location.state}
/>
)}
<SettingsNavItem
icon={ReceiptTextIcon}
label="Personal skills"
active={settingsSection === "personal-skills"}
to="/agents/settings/personal-skills"
state={location.state}
/>
<SettingsNavItem
icon={ShrinkIcon}
label="Compaction"
active={settingsSection === "compaction"}
to="/agents/settings/compaction"
state={location.state}
/>
{showApiKeysItem && (
<SettingsNavItem
icon={KeyIcon}
label="Secrets (API keys)"
active={settingsSection === "api-keys"}
to="/agents/settings/api-keys"
state={location.state}
/>
)}
{isAdmin && (
<SettingsNavItem
icon={Settings2Icon}
label="Manage agents"
active={settingsSection === "coder-agents"}
to="/agents/settings/coder-agents"
state={location.state}
/>
)}
</nav>
) : (
<nav className="flex flex-col gap-0.5 px-2 py-2">
<nav className="flex flex-col gap-0.5 px-2 py-2">
<SettingsNavItem
icon={UserIcon}
label="General"
active={!settingsSection || settingsSection === "general"}
to="/agents/settings/general"
state={location.state}
/>
{isPersonalModelOverridesEnabled && (
<SettingsNavItem
icon={BotIcon}
label="Coder Agents"
label="Agents"
active={settingsSection === "user-agents"}
to="/agents/settings/user-agents"
state={location.state}
/>
)}
<SettingsNavItem
icon={ReceiptTextIcon}
label="Personal skills"
active={settingsSection === "personal-skills"}
to="/agents/settings/personal-skills"
state={location.state}
/>
<SettingsNavItem
icon={ShrinkIcon}
label="Compaction"
active={settingsSection === "compaction"}
to="/agents/settings/compaction"
state={location.state}
/>
{showApiKeysItem && (
<SettingsNavItem
icon={KeyIcon}
label="Secrets (API keys)"
active={settingsSection === "api-keys"}
to="/agents/settings/api-keys"
state={location.state}
/>
)}
{isAdmin && (
<SettingsNavItem
icon={Settings2Icon}
label="Manage agents"
active={false}
to="/ai/settings/coder-agents"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={PlugIcon}
label="Providers"
active={false}
to="/ai/settings/providers"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={BoxesIcon}
label="Models"
active={false}
to="/ai/settings/models"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={ServerIcon}
label="MCP servers"
active={false}
to="/ai/settings/mcp-servers"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={LayoutTemplateIcon}
label="Templates"
active={false}
to="/ai/settings/templates"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={CoinsIcon}
label="Spend"
active={false}
to="/ai/settings/spend"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={ReceiptTextIcon}
label="Instructions"
active={false}
to="/ai/settings/instructions"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={RefreshCwIcon}
label="Lifecycle"
active={false}
to="/ai/settings/lifecycle"
trailingIcon={ArrowUpRightIcon}
/>
</nav>
)}
)}
</nav>
</div>
);
};
@@ -46,10 +46,10 @@ describe("sidebarViewFromPath", () => {
});
});
it("normalizes the admin index route to an undefined section", () => {
it("keeps legacy admin redirect paths on the settings panel", () => {
expect(sidebarViewFromPath("/agents/settings/admin")).toEqual({
panel: "settings-admin",
section: undefined,
panel: "settings",
section: "admin",
});
});
@@ -93,12 +93,6 @@ describe("isSettingsView", () => {
);
});
it("returns true for the admin settings panel", () => {
expect(
isSettingsView({ panel: "settings-admin", section: "providers" }),
).toBe(true);
});
it("returns false for chats", () => {
expect(isSettingsView({ panel: "chats" })).toBe(false);
});
@@ -1,15 +1,8 @@
type SidebarView =
| { panel: "chats" }
| { panel: "settings"; section: string | undefined }
| { panel: "settings-admin"; section: string | undefined }
| { panel: "analytics" };
const ADMIN_SETTINGS_SECTIONS = new Set([
"coder-agents",
"mcp-servers",
"spend",
]);
/**
* Derive the current sidebar view from the URL pathname.
*/
@@ -19,22 +12,13 @@ export function sidebarViewFromPath(pathname: string): SidebarView {
}
const settingsMatch = pathname.match(/^\/agents\/settings(?:\/([^/]+))?/);
if (settingsMatch) {
const section = settingsMatch[1];
if (section === "admin") {
return { panel: "settings-admin", section: undefined };
}
return {
panel: ADMIN_SETTINGS_SECTIONS.has(section ?? "")
? "settings-admin"
: "settings",
section,
};
return { panel: "settings", section: settingsMatch[1] };
}
return { panel: "chats" };
}
export function isSettingsView(
view: SidebarView,
): view is Extract<SidebarView, { panel: "settings" | "settings-admin" }> {
return view.panel === "settings" || view.panel === "settings-admin";
): view is Extract<SidebarView, { panel: "settings" }> {
return view.panel === "settings";
}