diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6e832fb..2173eae 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,3 +58,16 @@ jobs: with: args: > -Dsonar.qualitygate.wait=false + + - name: Notify OpenClaw for Review + if: success() + run: | + curl -s -X POST "https://openclaw.soludev.tech/hooks/agent" \ + -H "Authorization: Bearer ${{ secrets.OPENCLAW_WEBHOOK_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "CI terminée pour PR \"${{ github.event.pull_request.title }}\" - ${{ github.event.pull_request.html_url }}. Fais une review du code de la PR. Analyse les changements, ensuite poste un commentaire de review sur la PR. Donne un score de 1 à 10 pour la qualité du code, et suggère des améliorations si nécessaire.", + "deliver": true, + "channel": "telegram", + "to": "1988394825" + }' diff --git a/bun.lock b/bun.lock index a2ec038..1d78c36 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@microsoft/fetch-event-source": "^2.0.1", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", @@ -16,6 +17,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", @@ -26,10 +28,11 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "framer-motion": "^12.38.0", + "js-yaml": "^4.1.1", "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-hook-form": "^7.72.1", + "react-hook-form": "^7.73.1", "react-markdown": "^10.1.0", "react-router-dom": "^7.14.0", "remark-gfm": "^4.0.1", @@ -45,6 +48,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/js-yaml": "^4.0.9", "@types/node": "^25.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -182,12 +186,16 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -232,6 +240,8 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], @@ -388,6 +398,8 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -456,6 +468,8 @@ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], @@ -700,6 +714,8 @@ "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsdom": ["jsdom@25.0.1", "", { "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.12", "parse5": "^7.1.2", "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^2.11.2" }, "optionalPeers": ["canvas"] }, "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -912,7 +928,7 @@ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - "react-hook-form": ["react-hook-form@7.72.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig=="], + "react-hook-form": ["react-hook-form@7.73.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], diff --git a/package.json b/package.json index 0c427c3..ad3ec99 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@microsoft/fetch-event-source": "^2.0.1", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", @@ -29,6 +30,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", @@ -39,10 +41,11 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "framer-motion": "^12.38.0", + "js-yaml": "^4.1.1", "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-hook-form": "^7.72.1", + "react-hook-form": "^7.73.1", "react-markdown": "^10.1.0", "react-router-dom": "^7.14.0", "remark-gfm": "^4.0.1", @@ -58,6 +61,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/js-yaml": "^4.0.9", "@types/node": "^25.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/src/application/components/agent/AgentCard.tsx b/src/application/components/agent/AgentCard.tsx index f673b52..fd318a3 100644 --- a/src/application/components/agent/AgentCard.tsx +++ b/src/application/components/agent/AgentCard.tsx @@ -2,8 +2,8 @@ import type { AgentConfigMetadata } from "@/domain/entities/agent/agentConfigMet import StatusBadge from "@/application/components/shared/StatusBadge"; interface AgentCardProps { - agent: AgentConfigMetadata; - onConfigure: (name: string) => void; + readonly agent: AgentConfigMetadata; + readonly onConfigure: (name: string) => void; } const AGENT_ICONS: Record = { diff --git a/src/application/components/agent/AgentConfigForm.tsx b/src/application/components/agent/AgentConfigForm.tsx new file mode 100644 index 0000000..bb24f65 --- /dev/null +++ b/src/application/components/agent/AgentConfigForm.tsx @@ -0,0 +1,519 @@ +import { useForm, useFieldArray } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { AgentConfig, SubAgentConfig } from "@/domain/entities/agent/agentConfig"; +import { BackendType, MiddlewareType } from "@/domain/entities/agent/agentConfig"; +import { McpTransportType } from "@/domain/entities/agent/mcpServerConfig"; +import type { McpServerConfig } from "@/domain/entities/agent/mcpServerConfig"; +import { + agentConfigSchema, + type AgentConfigFormData, +} from "@/domain/entities/agent/agentConfigSchema"; +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, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/application/components/ui/accordion"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/application/components/ui/select"; +import { Separator } from "@/application/components/ui/separator"; +import StringListEditor from "./StringListEditor"; +import McpServerEditor from "./McpServerEditor"; +import SubAgentEditor from "./SubAgentEditor"; +import HITLEditor from "./HITLEditor"; +import ResponseFormatEditor from "./ResponseFormatEditor"; + +// ─── Constants ────────────────────────────────────────────────────────────── + +const ACCORDION_SECTIONS = [ + "general", + "system-prompt", + "tools-middleware", + "backend", + "hitl", + "memory-skills", + "mcp-servers", + "subagents", + "response-format", +] as const; + +const MIDDLEWARE_OPTIONS: { label: string; value: MiddlewareType }[] = [ + { value: MiddlewareType.TODO_LIST, label: "todo_list" }, + { value: MiddlewareType.FILESYSTEM, label: "filesystem" }, + { value: MiddlewareType.SUB_AGENT, label: "sub_agent" }, +]; + +const BACKEND_OPTIONS: { label: string; value: BackendType }[] = [ + { value: BackendType.STATE, label: "state" }, + { value: BackendType.STORE, label: "store" }, + { value: BackendType.FILESYSTEM, label: "filesystem" }, + { value: BackendType.COMPOSITE, label: "composite" }, +]; + +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: "", + system_prompt: "", + system_prompt_file: "", + tools: [], + middleware: [], + backend: { type: BackendType.STATE }, + hitl: { rules: {} }, + memory: [], + skills: [], + subagents: [], + mcp_servers: [], + response_format: undefined, + debug: false, +}; + +function getDefaultValues( + mode: "create" | "edit", + initialData?: AgentConfig, +): AgentConfigFormData { + if (mode === "edit" && initialData) { + return { + ...DEFAULT_FORM_VALUES, + ...initialData, + system_prompt: initialData.system_prompt ?? "", + system_prompt_file: initialData.system_prompt_file ?? "", + response_format: initialData.response_format ?? undefined, + }; + } + return { ...DEFAULT_FORM_VALUES }; +} + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +function cn(...classes: (string | false | undefined)[]): string { + return classes.filter(Boolean).join(" "); +} + +function buildAccordionTriggerClass(active?: boolean): string { + return cn( + "text-xs font-bold font-headline uppercase tracking-widest hover:no-underline", + active + ? "text-secondary-brand" + : "text-on-surface-variant", + ); +} + +// ─── Props ────────────────────────────────────────────────────────────────── + +interface AgentConfigFormProps { + mode: "create" | "edit"; + initialData?: AgentConfig; + onSubmit: (data: AgentConfigFormData) => void; + onCancel: () => void; + isPending?: boolean; +} + +// ─── Component ──────────────────────────────────────────────────────────── + +export default function AgentConfigForm({ + mode, + initialData, + onSubmit, + onCancel, + isPending = false, +}: Readonly) { + const { + register, + handleSubmit, + control, + setValue, + getValues, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(agentConfigSchema), + defaultValues: getDefaultValues(mode, initialData), + }); + + const mcpServersArray = useFieldArray({ control, name: "mcp_servers" }); + const subagentsArray = useFieldArray({ control, name: "subagents" }); + + // eslint-disable-next-line react-hooks/incompatible-library + const backendType = watch("backend.type"); + const middlewareValues = watch("middleware"); + + function handleFormSubmit(data: AgentConfigFormData) { + const cleaned: AgentConfigFormData = { + ...data, + system_prompt: data.system_prompt || undefined, + system_prompt_file: data.system_prompt_file || undefined, + response_format: data.response_format ?? undefined, + }; + onSubmit(cleaned); + } + + function addMcpServer() { + mcpServersArray.append({ ...EMPTY_MCP_SERVER }); + } + + function addSubagent() { + subagentsArray.append({ ...EMPTY_SUBAGENT }); + } + + function toggleMiddleware(mw: MiddlewareType, isChecked: boolean) { + const current = getValues("middleware"); + if (isChecked) { + setValue("middleware", [...current, mw]); + } else { + setValue( + "middleware", + current.filter((m) => m !== mw), + ); + } + } + + function renderSubmitLabel(): string { + if (isPending) return "Saving..."; + return mode === "create" ? "Create" : "Update"; + } + + return ( +
+
+
+ + {/* General */} + + + General + + + + + + + + +
+ + setValue("debug", v)} + /> +
+
+
+ + {/* System Prompt */} + + + System Prompt + + + +