feat(settings): move utility agents to main agents page (#436)

* feat(settings): move utility agents to main agents page

- Extract utility agents section into reusable component
- Add utility agents section to main agents settings page
- Remove separate utility agents page and sidebar link
- Consolidate all agent-related settings in one location

* style: format utility-agents-section.tsx with prettier

* fix: add error handling for utility agents operations

- Add try/catch with rollback for handleDefaultChange
- Add try/catch for onDelete to prevent UI inconsistency on failure

Addresses CodeRabbit review comments.
This commit is contained in:
João Salavisa
2026-03-14 10:10:16 +00:00
committed by GitHub
parent fabe2f71a3
commit 50e19bcdac
3 changed files with 55 additions and 51 deletions
+3
View File
@@ -29,6 +29,7 @@ import { useAgentDiscovery } from "@/hooks/domains/settings/use-agent-discovery"
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
import { AgentLogo } from "@/components/agent-logo";
import { AddTUIAgentDialog } from "@/components/settings/add-tui-agent-dialog";
import { UtilityAgentsSection } from "@/components/settings/utility-agents-section";
import { toAgentProfileOption } from "@/lib/state/slices/settings/types";
import type {
AgentDiscovery,
@@ -565,6 +566,8 @@ export default function AgentsSettingsPage() {
<ConfigChatAgentSection />
<UtilityAgentsSection />
<AddTUIAgentDialog
open={tuiDialogOpen}
onOpenChange={setTuiDialogOpen}
@@ -14,7 +14,6 @@ import {
IconMessageCircle,
IconBrandGithub,
IconSparkles,
IconWand,
} from "@tabler/icons-react";
import {
Sidebar,
@@ -299,16 +298,6 @@ export function SettingsAppSidebar() {
</SidebarMenuButton>
</SidebarMenuItem>
{/* Utility Agents */}
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/settings/utility"}>
<Link href="/settings/utility">
<IconWand className="h-4 w-4" />
<span>Utility Agents</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<ExecutorsSidebarSection pathname={pathname} executors={executors} />
<SecretsSidebarSection pathname={pathname} />
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Separator } from "@kandev/ui/separator";
import {
listUtilityAgents,
@@ -43,7 +43,7 @@ async function handleBuiltinChange(
);
}
export default function UtilityAgentsSettingsPage() {
export function UtilityAgentsSection() {
const [agents, setAgents] = useState<UtilityAgent[]>([]);
const [inferenceAgents, setInferenceAgents] = useState<InferenceAgent[]>([]);
const [defaultAgentId, setDefaultAgentId] = useState("");
@@ -75,14 +75,17 @@ export default function UtilityAgentsSettingsPage() {
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleDefaultChange = async (agentId: string, model: string) => {
const prevAgentId = defaultAgentId;
const prevModel = defaultModel;
setDefaultAgentId(agentId);
setDefaultModel(model);
await updateUserSettings({ default_utility_agent_id: agentId, default_utility_model: model });
try {
await updateUserSettings({ default_utility_agent_id: agentId, default_utility_model: model });
} catch {
setDefaultAgentId(prevAgentId);
setDefaultModel(prevModel);
}
};
const openEditDialog = (agent: UtilityAgent | null) => {
@@ -96,46 +99,55 @@ export default function UtilityAgentsSettingsPage() {
fetchData();
};
if (loading)
return <div className="py-8 text-center text-muted-foreground text-sm">Loading...</div>;
useEffect(() => {
fetchData();
}, [fetchData]);
if (loading) return null;
return (
<div className="space-y-8">
<header>
<h2 className="text-xl font-semibold">Utility Agents</h2>
<p className="text-sm text-muted-foreground">
One-shot AI helpers for commits, PRs, and prompts.
</p>
</header>
<Separator />
<DefaultModelSection
inferenceAgents={inferenceAgents}
defaultAgentId={defaultAgentId}
defaultModel={defaultModel}
onDefaultChange={handleDefaultChange}
/>
<PerActionOverridesSection
builtins={builtins}
allModels={allModels}
defaultModel={defaultModel}
onModelChange={(agent, value) => handleBuiltinChange(agent, value, setAgents)}
onEdit={openEditDialog}
/>
<CustomAgentsSection
agents={customAgents}
onAdd={() => openEditDialog(null)}
onEdit={openEditDialog}
onDelete={async (agent) => {
await deleteUtilityAgent(agent.id);
setAgents((prev) => prev.filter((a) => a.id !== agent.id));
}}
/>
<>
<div className="space-y-4">
<Separator />
<div>
<h3 className="text-lg font-semibold">Utility Agents</h3>
<p className="text-sm text-muted-foreground">
One-shot AI helpers for commits, PRs, and prompts.
</p>
</div>
<DefaultModelSection
inferenceAgents={inferenceAgents}
defaultAgentId={defaultAgentId}
defaultModel={defaultModel}
onDefaultChange={handleDefaultChange}
/>
<PerActionOverridesSection
builtins={builtins}
allModels={allModels}
defaultModel={defaultModel}
onModelChange={(agent, value) => handleBuiltinChange(agent, value, setAgents)}
onEdit={openEditDialog}
/>
<CustomAgentsSection
agents={customAgents}
onAdd={() => openEditDialog(null)}
onEdit={openEditDialog}
onDelete={async (agent) => {
try {
await deleteUtilityAgent(agent.id);
setAgents((prev) => prev.filter((a) => a.id !== agent.id));
} catch {
// Error already logged by API layer
}
}}
/>
</div>
<UtilityAgentDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
agent={editingAgent}
onSuccess={closeDialog}
/>
</div>
</>
);
}