diff --git a/README.md b/README.md index 6784471..5e69d0c 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,15 @@ tests/ | `/rag` | RagPage | Browse MinIO folders and files with breadcrumb navigation | | `/settings` | SettingsPage | Theme, chat, LLM provider, and reset preferences (persisted to `localStorage`) | +## Agent Configuration + +The agent creation/edit form (in `CreateAgentDialog`) reflects the current backend schema. The following UI changes have been made: + +- **General section** — A new **Description** text input is available alongside the agent name. Tools are no longer managed here; they are configured exclusively via MCP servers (see the MCP servers section of the form). +- **Debug toggle removed** — The Debug toggle has been removed from the form. +- **Tools section removed** — Tools are managed via MCP servers only. The dedicated "Tools" section no longer appears in the form. +- **Subagents section** — A new **"Add from existing agents"** dropdown lets you select an existing agent as a subagent reference (populating `agent_ref`). When a subagent references an existing agent, a **`ref:`** badge is displayed next to its name and the name field becomes read-only (the name is derived from the referenced agent). + ## RAG File Browser The `/rag` page provides a MinIO-backed file browser with three tabs: Browse, Query, and Classical. The browse tab uses breadcrumb navigation driven by `useFolders` and `useFiles`, and supports the following file/folder management actions alongside read and upload: diff --git a/src/application/App.tsx b/src/application/App.tsx index 28558a6..6bcf95a 100644 --- a/src/application/App.tsx +++ b/src/application/App.tsx @@ -8,6 +8,7 @@ const AgentsPage = lazy(() => import("@/application/pages/AgentsPage")); const RagPage = lazy(() => import("@/application/pages/RagPage")); const SkillsPage = lazy(() => import("@/application/pages/SkillsPage")); const MemoriesPage = lazy(() => import("@/application/pages/MemoriesPage")); +const McpRegistryPage = lazy(() => import("@/application/pages/McpRegistryPage")); function PageFallback() { return ( @@ -28,6 +29,7 @@ function App() { } /> } /> } /> + } /> } /> ); diff --git a/src/application/components/agent/AgentCard.tsx b/src/application/components/agent/AgentCard.tsx index ce47a6f..33e9c47 100644 --- a/src/application/components/agent/AgentCard.tsx +++ b/src/application/components/agent/AgentCard.tsx @@ -1,6 +1,5 @@ import { Bot, Settings, Trash2 } from "lucide-react"; import type { AgentConfigMetadata } from "@/domain/entities/agent/agentConfigMetadata"; -import StatusBadge from "@/application/components/shared/StatusBadge"; import { Button } from "@/application/components/ui/button"; interface AgentCardProps { @@ -22,7 +21,6 @@ export default function AgentCard({ agent, onConfigure, onDelete }: Readonly -

diff --git a/src/application/components/agent/AgentConfigForm.tsx b/src/application/components/agent/AgentConfigForm.tsx index ffae6c4..984f99d 100644 --- a/src/application/components/agent/AgentConfigForm.tsx +++ b/src/application/components/agent/AgentConfigForm.tsx @@ -1,7 +1,6 @@ -import { useCallback, memo } from "react"; +import { useCallback, memo, useMemo } from "react"; import { useForm, useFieldArray, useWatch, type Control } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Plus } from "lucide-react"; import type { AgentConfig, SubAgentConfig } from "@/domain/entities/agent/agentConfig"; import { BackendType } from "@/domain/entities/agent/agentConfig"; import { McpTransportType } from "@/domain/entities/agent/mcpServerConfig"; @@ -14,7 +13,6 @@ import { import { Button } from "@/application/components/ui/button"; import { Input } from "@/application/components/ui/input"; import { Label } from "@/application/components/ui/label"; -import { Switch } from "@/application/components/ui/switch"; import { Textarea } from "@/application/components/ui/textarea"; import { Accordion, @@ -29,18 +27,21 @@ import { SelectTrigger, SelectValue, } from "@/application/components/ui/select"; -import StringListEditor from "./StringListEditor"; +import PillMultiSelect, { + type PillMultiSelectOption, +} from "@/application/components/shared/PillMultiSelect"; import { SkillPillMultiSelect, MemoryPillMultiSelect } from "./SkillMemorySelects"; -import McpServerEditor from "./McpServerEditor"; import SubAgentEditor from "./SubAgentEditor"; import HITLEditor from "./HITLEditor"; import ResponseFormatEditor from "./ResponseFormatEditor"; +import { useMcpRegistry } from "@/application/hooks/mcpServer/useMcpRegistry"; +import { useAgents } from "@/application/hooks/agent/useAgents"; +import { mcpRegistryApi } from "@/infrastructure/api/mcpServer/mcpRegistryApi"; import { AGENT_CONFIG_FORM_ID as FORM_ID } from "./formConstants"; type SectionValue = | "general" | "system-prompt" - | "tools" | "backend" | "hitl" | "memory-skills" @@ -53,7 +54,6 @@ const DEFAULT_OPEN_SECTIONS: SectionValue[] = ["general", "system-prompt"]; const SECTION_META: Record = { general: { label: "General" }, "system-prompt": { label: "System Prompt" }, - tools: { label: "Tools" }, backend: { label: "Backend" }, hitl: { label: "HITL" }, "memory-skills": { label: "Memory & Skills" }, @@ -67,28 +67,6 @@ const BACKEND_STORAGE_OPTIONS: { label: string; value: "memory" | "postgres" }[] { value: "postgres", label: "postgres" }, ]; -const EMPTY_MCP_SERVER: McpServerConfig = { - name: "", - transport: McpTransportType.STDIO, - command: undefined, - args: [], - url: undefined, - headers: {}, - env: {}, - auth_token: undefined, -}; - -const EMPTY_SUBAGENT: SubAgentConfig = { - name: "", - description: "", - instructions: undefined, - model: undefined, - tools: [], - skills: [], - mcp_servers: [], - response_format: undefined, -}; - const DEFAULT_FORM_VALUES: AgentConfigFormData = { name: "", model: "", @@ -150,6 +128,8 @@ export default function AgentConfigForm({ const subagentsArray = useFieldArray({ control, name: "subagents" }); const checkpointBackend = useWatch({ control, name: "backend.checkpoint_backend" }); + const currentAgentName = useWatch({ control, name: "name" }); + const { data: existingAgents } = useAgents(); function handleFormSubmit(data: AgentConfigFormData) { const cleaned: AgentConfigFormData = { @@ -161,8 +141,20 @@ export default function AgentConfigForm({ onSubmit(cleaned); } - function addSubagent() { - subagentsArray.append({ ...EMPTY_SUBAGENT }); + function addFromExistingAgent(agentName: string) { + const existing = (existingAgents ?? []).find((a) => a.name === agentName); + if (!existing) return; + subagentsArray.append({ + name: agentName, + agent_ref: agentName, + description: existing.description ?? "", + instructions: undefined, + model: undefined, + tools: [], + skills: [], + mcp_servers: [], + response_format: undefined, + }); } return ( @@ -185,6 +177,14 @@ export default function AgentConfigForm({ aria-invalid={!!errors.name} /> + + + -
- - setValue("debug", v)} - /> -
@@ -232,18 +222,6 @@ export default function AgentConfigForm({ - - - - setValue("tools", tools)} - placeholder="Add tool…" - /> - - - @@ -309,6 +287,33 @@ export default function AgentConfigForm({ + {(existingAgents ?? []).length > 0 && ( +
+ + +
+ )} {subagentsArray.fields.map((field, index) => ( subagentsArray.remove(index)} /> ))} -
@@ -382,25 +386,15 @@ function FormField({ id, label, error, children }: Readonly) { ); } -interface AddButtonProps { - onClick: () => void; - label: string; -} - -function AddButton({ onClick, label }: Readonly) { - return ( - - ); -} - -// Isolated MCP servers accordion item: owns its useFieldArray AND the -// AccordionItem so the parent form's re-renders (triggered by useWatch on -// unrelated fields) do NOT remount the McpServerEditor instances and steal -// input focus. The whole AccordionItem is memoized so it only re-renders when -// the MCP servers array itself changes. +// Isolated MCP servers accordion item. MCP servers are now managed in their +// own screen (MCP Registry page), so this section only lets users pick +// registered servers via toggle pills — exactly like the Skills/Memories +// sections. Selecting a pill reveals the server's config and embeds a full +// McpServerConfig (the agent schema still requires the complete object) into +// the form; deselecting removes the entry by name. +// +// The AccordionItem is memoized so parent form re-renders (from useWatch on +// unrelated fields) do not recompute the registry options. const McpServersAccordionItem = memo(function McpServersAccordionItem({ control, defaultValue, @@ -409,32 +403,76 @@ const McpServersAccordionItem = memo(function McpServersAccordionItem({ defaultValue: SectionValue[]; }>) { const mcpServersArray = useFieldArray({ control, name: "mcp_servers" }); + const { data: registryServers } = useMcpRegistry(); const isOpen = defaultValue.includes("mcp-servers"); - const addMcpServer = useCallback( - () => mcpServersArray.append({ ...EMPTY_MCP_SERVER }), - [mcpServersArray], - ); - const updateMcpServer = useCallback( - (index: number, v: McpServerConfig) => mcpServersArray.update(index, v), - [mcpServersArray], + + const fields = mcpServersArray.fields as unknown as McpServerConfig[]; + const registryNames = useMemo( + () => new Set((registryServers ?? []).map((s) => s.name)), + [registryServers], ); - const removeMcpServer = useCallback( - (index: number) => mcpServersArray.remove(index), - [mcpServersArray], + // Names currently embedded in the form: registry-selected + any legacy + // custom entry (name not in the registry). Legacy entries are surfaced as + // extra active pills so they can be removed — no invisible form data. + const selectedNames = fields.map((f) => f.name).filter((n) => n.length > 0); + + const options = useMemo(() => { + const fromRegistry: PillMultiSelectOption[] = (registryServers ?? []).map((s) => ({ + value: s.name, + label: s.name, + description: s.openapi_url ?? s.url ?? undefined, + })); + const legacy: PillMultiSelectOption[] = fields + .map((f) => f.name) + .filter((n) => n.length > 0 && !registryNames.has(n)) + .map((n) => ({ value: n, label: `${n} (custom)` })); + return [...fromRegistry, ...legacy]; + }, [registryServers, fields, registryNames]); + + const handleRegistryChange = useCallback( + (next: string[]) => { + const prev = selectedNames; + // Newly selected names that exist in the registry → reveal + embed. + next + .filter((name) => !prev.includes(name) && registryNames.has(name)) + .forEach((name) => { + void mcpRegistryApi.reveal(name).then((revealed) => { + mcpServersArray.append({ + name: revealed.name, + transport: McpTransportType.HTTP, + command: undefined, + args: [], + url: revealed.url, + headers: revealed.headers, + env: revealed.env, + auth_token: revealed.auth_token ?? undefined, + }); + }); + }); + // Deselected names → remove the embedded entry (registry or legacy). + prev + .filter((name) => !next.includes(name)) + .forEach((name) => { + const idx = fields.findIndex((f) => f.name === name); + if (idx >= 0) mcpServersArray.remove(idx); + }); + }, + [selectedNames, registryNames, fields, mcpServersArray], ); + return ( - {mcpServersArray.fields.map((field, index) => ( - updateMcpServer(index, v)} - onRemove={() => removeMcpServer(index)} +
+ + - ))} - +
); diff --git a/src/application/components/agent/AgentConfigViewer.tsx b/src/application/components/agent/AgentConfigViewer.tsx index a90b17c..5014e12 100644 --- a/src/application/components/agent/AgentConfigViewer.tsx +++ b/src/application/components/agent/AgentConfigViewer.tsx @@ -7,7 +7,7 @@ import { useDeleteAgent } from "@/application/hooks/agent/useDeleteAgent"; import { serializeAgentConfig, agentConfigToYamlFile } from "@/application/lib/yaml"; import type { AgentConfig } from "@/domain/entities/agent/agentConfig"; import type { AgentConfigFormData } from "@/domain/entities/agent/agentConfigSchema"; -import StatusBadge from "@/application/components/shared/StatusBadge"; +import { Badge } from "@/application/components/ui/badge"; import ToolTag from "@/application/components/shared/ToolTag"; import { SkillPillMultiSelect, MemoryPillMultiSelect } from "./SkillMemorySelects"; import { @@ -122,7 +122,7 @@ export default function AgentConfigViewer({ -
+
{mode === "edit" && config ? ( <> Model

{config.model}

+
+ + {config.description && (
- Debug - + Description +

{config.description}

- + )} {config.system_prompt && (
@@ -190,17 +193,6 @@ export default function AgentConfigViewer({
)} - {config.tools.length > 0 && ( -
- Tools ({config.tools.length}) -
- {config.tools.map((tool) => ( - - ))} -
-
- )} -
Backend
@@ -274,7 +266,12 @@ export default function AgentConfigViewer({ key={sub.name} className="border border-border-soft bg-surface-warm p-3" > -

{sub.name}

+
+

{sub.name}

+ {sub.agent_ref && ( + ref: {sub.agent_ref} + )} +

{sub.description}

))} diff --git a/src/application/components/agent/CreateAgentDialog.tsx b/src/application/components/agent/CreateAgentDialog.tsx index b80e612..cea261b 100644 --- a/src/application/components/agent/CreateAgentDialog.tsx +++ b/src/application/components/agent/CreateAgentDialog.tsx @@ -123,7 +123,7 @@ export default function CreateAgentDialog({
-
+
{mode === "form" ? ( setKeyInput(e.target.value)} placeholder={keyPlaceholder} - className="flex-1" + className="flex-1 min-w-0" aria-label={`${label} key`} /> setValueInput(e.target.value)} placeholder={valuePlaceholder} - className="flex-1" + className="flex-1 min-w-0" aria-label={`${label} value`} /> -
- -
-
- - updateLocal({ name: e.target.value })} - onBlur={commit} - placeholder="my-server" - /> -
-
- - -
-
- - {isStdio && ( - <> - -
- - updateLocal({ command: e.target.value || undefined })} - onBlur={commit} - placeholder="npx" - /> -
- { - updateLocal({ args }); - onChange({ ...draft, args }); - }} - placeholder="Add argument…" - /> - - )} - - {!isStdio && ( - <> - -
- - updateLocal({ url: e.target.value || undefined })} - onBlur={commit} - placeholder="http://localhost:3000/mcp" - /> -
- { - updateLocal({ headers }); - onChange({ ...draft, headers }); - }} - keyPlaceholder="Header name" - valuePlaceholder="Header value" - /> - - )} - - - { - updateLocal({ env }); - onChange({ ...draft, env }); - }} - keyPlaceholder="Variable name" - valuePlaceholder="Variable value" - /> - -
- - updateLocal({ auth_token: e.target.value || undefined })} - onBlur={commit} - placeholder="Optional auth token" - /> -
-
- ); -} - -export const McpServerEditor = memo(McpServerEditorImpl); - -export default McpServerEditor; \ No newline at end of file diff --git a/src/application/components/agent/StringListEditor.tsx b/src/application/components/agent/StringListEditor.tsx index 9c146af..3cc4af2 100644 --- a/src/application/components/agent/StringListEditor.tsx +++ b/src/application/components/agent/StringListEditor.tsx @@ -51,7 +51,7 @@ export default function StringListEditor({ onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder={placeholder} - className="flex-1" + className="flex-1 min-w-0" name={`${label.toLowerCase().replace(/\s+/g, "-")}-input`} /> -
-
- - update({ name: e.target.value })} - placeholder="researcher" - /> -
-
- - update({ model: e.target.value || undefined })} - placeholder="openai:gpt-4o (optional)" - /> -
+
+ +
-
+
update({ description: e.target.value })} placeholder="Describe what this subagent does" />
- -
- -