mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): link manage agents to AI settings (#26912)
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user