From 4968eb2ddbfeab3bb5f89a44bf8c973059e40261 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Mon, 27 Apr 2026 15:30:22 -0700 Subject: [PATCH 1/3] clean up the create pages, get rid of unnecessary dialogs, add list view for agents Signed-off-by: Peter Jausovec --- ui/cypress/e2e/smoke.cy.ts | 19 +- ui/public/mockServiceWorker.js | 2 +- ui/src/app/actions/servers.ts | 6 +- .../agents/[namespace]/[name]/chat/page.tsx | 12 +- ui/src/app/agents/new/page.tsx | 1159 +++++++---------- ui/src/app/login/page.tsx | 29 +- ui/src/app/mcp/new/page.tsx | 101 ++ ui/src/app/mcp/page.tsx | 47 + ui/src/app/models/new/page.tsx | 80 +- ui/src/app/models/page.tsx | 451 ++++--- .../app/prompts/[namespace]/[name]/page.tsx | 103 +- ui/src/app/prompts/new/page.tsx | 69 +- ui/src/app/prompts/page.tsx | 63 +- ui/src/app/servers/page.tsx | 283 ---- ui/src/app/tools/page.tsx | 316 ----- ui/src/components/AgentList.tsx | 111 +- ui/src/components/AgentListView.tsx | 400 ++++++ ui/src/components/AppInitializer.tsx | 23 +- ui/src/components/Header.tsx | 24 +- ui/src/components/NamespaceCombobox.tsx | 9 +- .../agent-form/AgentSkillsFormSection.tsx | 265 ++++ .../agent-form/ByoDeploymentFields.tsx | 308 +++++ .../agent-form/ServiceAccountNameField.tsx | 50 + .../components/agent-form/agent-form-types.ts | 15 + .../agent-form/focusFirstFormError.ts | 73 ++ .../components/agent-form/form-primitives.tsx | 61 + ui/src/components/chat/ChatInterface.tsx | 26 +- ui/src/components/create/MemorySection.tsx | 19 +- .../create/ModelSelectionSection.tsx | 13 +- .../create/PromptInstructionsTextarea.tsx | 4 + .../components/create/SelectToolsDialog.tsx | 6 +- .../components/create/SystemPromptSection.tsx | 8 +- ui/src/components/create/ToolsSection.tsx | 4 - ui/src/components/layout/AppPageFrame.tsx | 56 + ui/src/components/layout/PageHeader.tsx | 77 ++ .../McpServerForm.tsx} | 175 ++- ui/src/components/mcp/McpServersView.tsx | 384 ++++++ ui/src/lib/__tests__/countAgentTools.test.ts | 64 + ui/src/lib/__tests__/formatTimeAgo.test.ts | 39 + ui/src/lib/countAgentTools.ts | 37 + ui/src/lib/formatTimeAgo.ts | 50 + ui/src/lib/skipToContent.ts | 3 + ui/src/types/index.ts | 5 + 43 files changed, 3239 insertions(+), 1810 deletions(-) create mode 100644 ui/src/app/mcp/new/page.tsx create mode 100644 ui/src/app/mcp/page.tsx delete mode 100644 ui/src/app/servers/page.tsx delete mode 100644 ui/src/app/tools/page.tsx create mode 100644 ui/src/components/AgentListView.tsx create mode 100644 ui/src/components/agent-form/AgentSkillsFormSection.tsx create mode 100644 ui/src/components/agent-form/ByoDeploymentFields.tsx create mode 100644 ui/src/components/agent-form/ServiceAccountNameField.tsx create mode 100644 ui/src/components/agent-form/agent-form-types.ts create mode 100644 ui/src/components/agent-form/focusFirstFormError.ts create mode 100644 ui/src/components/agent-form/form-primitives.tsx create mode 100644 ui/src/components/layout/AppPageFrame.tsx create mode 100644 ui/src/components/layout/PageHeader.tsx rename ui/src/components/{AddServerDialog.tsx => mcp/McpServerForm.tsx} (85%) create mode 100644 ui/src/components/mcp/McpServersView.tsx create mode 100644 ui/src/lib/__tests__/countAgentTools.test.ts create mode 100644 ui/src/lib/__tests__/formatTimeAgo.test.ts create mode 100644 ui/src/lib/countAgentTools.ts create mode 100644 ui/src/lib/formatTimeAgo.ts create mode 100644 ui/src/lib/skipToContent.ts diff --git a/ui/cypress/e2e/smoke.cy.ts b/ui/cypress/e2e/smoke.cy.ts index ec5cb082c..f295c3f45 100644 --- a/ui/cypress/e2e/smoke.cy.ts +++ b/ui/cypress/e2e/smoke.cy.ts @@ -35,22 +35,19 @@ describe('Main page', () => { cy.contains('h1', 'Agents').should('be.visible'); cy.visit('/agents/new') - cy.contains('h1', 'Create New Agent').should('be.visible'); + cy.contains('h1', 'New Agent').should('be.visible'); cy.wait(1000) cy.visit('/models') cy.contains('h1', 'Models').should('be.visible'); cy.visit('/models/new') - cy.contains('h1', 'Create New Model').should('be.visible'); + cy.contains('h1', 'New Model').should('be.visible'); cy.wait(1000) - cy.visit('/tools') - cy.contains('h1', 'Tools Library').should('be.visible'); - - cy.wait(1000) - cy.visit('/servers') - cy.contains('h1', 'MCP Servers').should('be.visible'); + cy.visit('/mcp') + cy.contains('h1', 'MCP & tools').should('be.visible'); + cy.get('#mcp-search').should('be.visible'); }) }) @@ -64,7 +61,11 @@ describe('Regressions', () => { cy.visit('/models') cy.contains('h1', 'Models').should('be.visible'); - cy.get('[data-test="edit-model-default/default-model-config"]').should('be.visible').click(); + // `model.ref` (e.g. default/default-model-config) is embedded in data-test; use prefix to avoid exact-ref coupling + cy.get('[data-test^="edit-model-"]') + .first() + .should('be.visible') + .click(); cy.contains('h1', 'Edit Model').should('be.visible'); cy.get('[data-test="edit-model-name-button"]').should('be.visible').click(); diff --git a/ui/public/mockServiceWorker.js b/ui/public/mockServiceWorker.js index b17fcd650..2c0248801 100644 --- a/ui/public/mockServiceWorker.js +++ b/ui/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.14' +const PACKAGE_VERSION = '2.13.4' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/ui/src/app/actions/servers.ts b/ui/src/app/actions/servers.ts index 5f0b22558..7441187b0 100644 --- a/ui/src/app/actions/servers.ts +++ b/ui/src/app/actions/servers.ts @@ -36,7 +36,8 @@ export async function deleteServer(serverName: string): Promise(error, "Error creating MCP server"); diff --git a/ui/src/app/agents/[namespace]/[name]/chat/page.tsx b/ui/src/app/agents/[namespace]/[name]/chat/page.tsx index bd33137aa..3ee1bc56e 100644 --- a/ui/src/app/agents/[namespace]/[name]/chat/page.tsx +++ b/ui/src/app/agents/[namespace]/[name]/chat/page.tsx @@ -71,8 +71,16 @@ export default function ChatAgentPage({ params }: { params: Promise<{ name: stri if (gate === "loading") { return ( -
- +
+
+ + Preparing chat… +
); } diff --git a/ui/src/app/agents/new/page.tsx b/ui/src/app/agents/new/page.tsx index 93e81dfc7..bc6868b0c 100644 --- a/ui/src/app/agents/new/page.tsx +++ b/ui/src/app/agents/new/page.tsx @@ -1,10 +1,9 @@ "use client"; import React, { useState, useEffect, Suspense, useCallback, useMemo } from "react"; +import { Loader2 } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Brain, GitBranch, Loader2, Settings2, PlusCircle, Trash2, Layers } from "lucide-react"; import { formAgentTypeFromApi, formUsesByoSections, formUsesDeclarativeSections } from "@/lib/agentFormLayout"; import { ModelConfig, AgentType, ContextConfig } from "@/types"; import { SystemPromptSection } from "@/components/create/SystemPromptSection"; @@ -17,20 +16,14 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useAgents } from "@/components/AgentsProvider"; import { LoadingState } from "@/components/LoadingState"; import { ErrorState } from "@/components/ErrorState"; -import KagentLogo from "@/components/kagent-logo"; import { AgentFormData } from "@/components/AgentsProvider"; import { Tool, EnvVar } from "@/types"; import { toast } from "sonner"; import { NamespaceCombobox } from "@/components/NamespaceCombobox"; import { MAX_SKILLS_PER_SOURCE, - applyGitSkillUrlPathChange, formRowsToGitRepos, gitRepoToFormRow, - gitSkillRowUrlIssues, - isDuplicateGitSkillFormRow, - isDuplicateOciSkillRef, - isValidSkillContainerImage, newEmptyGitSkillRow, validateDeclarativeAgentSkills, type GitSkillFormRow, @@ -38,21 +31,13 @@ import { import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -interface ValidationErrors { - name?: string; - namespace?: string; - description?: string; - type?: string; - systemPrompt?: string; - model?: string; - knowledgeSources?: string; - tools?: string; - skills?: string; - memoryModel?: string; - memoryTtl?: string; - serviceAccountName?: string; - promptSources?: string; -} +import { FormSection, FieldRoot, FieldLabel, FieldHint, FieldError } from "@/components/agent-form/form-primitives"; +import { ByoDeploymentFields } from "@/components/agent-form/ByoDeploymentFields"; +import { AgentSkillsFormSection } from "@/components/agent-form/AgentSkillsFormSection"; +import { ServiceAccountNameField } from "@/components/agent-form/ServiceAccountNameField"; +import { AgentFormValidationErrors } from "@/components/agent-form/agent-form-types"; +import { focusFirstFormError } from "@/components/agent-form/focusFirstFormError"; +import { PageHeader } from "@/components/layout/PageHeader"; interface AgentPageContentProps { isEditMode: boolean; @@ -73,7 +58,6 @@ const DEFAULT_SYSTEM_PROMPT = `You're a helpful agent, made by the kagent team. - Your response will include a summary of actions you took and an explanation of the result - If you created any artifacts such as files or resources, you will include those in your response as well` -// Inner component that uses useSearchParams, wrapped in Suspense function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageContentProps) { const router = useRouter(); const { models, loading, error, createNewAgent, updateAgent, getAgent, validateAgentData } = useAgents(); @@ -106,9 +90,11 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo promptSourceRows: PromptSourceRow[]; isSubmitting: boolean; isLoading: boolean; - errors: ValidationErrors; + errors: AgentFormValidationErrors; } + const [formDirty, setFormDirty] = useState(false); + const [state, setState] = useState({ name: "", namespace: "default", @@ -140,6 +126,18 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo const useDeclarativeAgentFields = formUsesDeclarativeSections(state.agentType, state.byoImage); const showByoFields = formUsesByoSections(state.agentType, state.byoImage); + const disabled = state.isSubmitting || state.isLoading; + + useEffect(() => { + if (!formDirty) { + return; + } + const onBeforeUnload = (e: BeforeUnloadEvent) => { + e.preventDefault(); + }; + window.addEventListener("beforeunload", onBeforeUnload); + return () => window.removeEventListener("beforeunload", onBeforeUnload); + }, [formDirty]); const resolvedGitSkillRepos = useMemo( () => formRowsToGitRepos(state.skillGitRepos || []), @@ -173,23 +171,21 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo [state.promptSourceRows], ); - // Fetch existing agent data if in edit mode useEffect(() => { const fetchAgentData = async () => { if (isEditMode && agentName && agentNamespace) { try { - setState(prev => ({ ...prev, isLoading: true })); + setState((prev) => ({ ...prev, isLoading: true })); const agentResponse = await getAgent(agentName, agentNamespace); if (!agentResponse) { toast.error("Agent not found"); - setState(prev => ({ ...prev, isLoading: false })); + setState((prev) => ({ ...prev, isLoading: false })); return; } const agent = agentResponse.agent; if (agent) { try { - // Populate form with existing agent data const baseUpdates: Partial = { name: agent.metadata.name || "", namespace: agent.metadata.namespace || "", @@ -199,7 +195,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo const useDeclarativeForm = agent.spec.type === "Declarative"; if (useDeclarativeForm) { const decl = agent.spec?.declarative; - const memorySpec = decl?.memory ?? agent.spec?.memory; + const memorySpec = decl?.memory ?? (agent.spec as { memory?: { modelConfig: string; ttlDays?: number } })?.memory; const memoryModelConfig = memorySpec?.modelConfig ? `${agent.metadata.namespace}/${memorySpec.modelConfig}` : ""; @@ -210,21 +206,25 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo name: ds.name || "", alias: ds.alias || "", })) ?? [newPromptSourceRow()]; - setState(prev => ({ + setState((prev) => ({ ...prev, ...baseUpdates, systemPrompt: decl?.systemMessage || "", promptSourceRows: srcRows.length > 0 ? srcRows : [newPromptSourceRow()], - selectedTools: (decl?.tools && agentResponse.tools) ? agentResponse.tools : [], - selectedModel: agentResponse.modelConfigRef ? { ref: agentResponse.modelConfigRef, spec: { model: agentResponse.model || "", provider: "" } } : null, - skillRefs: (agent.spec?.skills?.refs && agent.spec.skills.refs.length > 0) ? agent.spec.skills.refs : [""], + selectedTools: decl?.tools && agentResponse.tools ? agentResponse.tools : [], + selectedModel: agentResponse.modelConfigRef + ? { ref: agentResponse.modelConfigRef, spec: { model: agentResponse.model || "", provider: "" } } + : null, + skillRefs: agent.spec?.skills?.refs && agent.spec.skills.refs.length > 0 ? agent.spec.skills.refs : [""], skillGitRepos: agent.spec?.skills?.gitRefs && agent.spec.skills.gitRefs.length > 0 ? agent.spec.skills.gitRefs.map(gitRepoToFormRow) : [newEmptyGitSkillRow()], skillsGitAuthSecretName: agent.spec?.skills?.gitAuthSecretRef?.name || "", stream: decl?.stream ?? false, - selectedMemoryModel: memoryModelConfig ? { ref: memoryModelConfig, spec: { model: memorySpec?.modelConfig || "", provider: "" } } : null, + selectedMemoryModel: memoryModelConfig + ? { ref: memoryModelConfig, spec: { model: memorySpec?.modelConfig || "", provider: "" } } + : null, memoryTtlDays: memorySpec?.ttlDays ? String(memorySpec.ttlDays) : "", contextConfig: decl?.context, serviceAccountName: decl?.deployment?.serviceAccountName || "", @@ -233,7 +233,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo byoArgs: "", })); } else { - setState(prev => ({ + setState((prev) => ({ ...prev, ...baseUpdates, systemPrompt: "", @@ -246,16 +246,27 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo byoArgs: (agent.spec?.byo?.deployment?.args || []).join(" "), replicas: agent.spec?.byo?.deployment?.replicas !== undefined ? String(agent.spec?.byo?.deployment?.replicas) : "", imagePullPolicy: agent.spec?.byo?.deployment?.imagePullPolicy || "", - imagePullSecrets: (agent.spec?.byo?.deployment?.imagePullSecrets || []).map((s: { name: string }) => s.name).concat((agent.spec?.byo?.deployment?.imagePullSecrets || []).length === 0 ? [""] : []), - envPairs: (agent.spec?.byo?.deployment?.env || []).map((e: EnvVar) => ( - e?.valueFrom?.secretKeyRef - ? { name: e.name || "", isSecret: true, secretName: e.valueFrom.secretKeyRef.name || "", secretKey: e.valueFrom.secretKeyRef.key || "", optional: e.valueFrom.secretKeyRef.optional } - : { name: e.name || "", value: e.value || "", isSecret: false } - )).concat((agent.spec?.byo?.deployment?.env || []).length === 0 ? [{ name: "", value: "", isSecret: false }] : []), + imagePullSecrets: (agent.spec?.byo?.deployment?.imagePullSecrets || []) + .map((s: { name: string }) => s.name) + .concat((agent.spec?.byo?.deployment?.imagePullSecrets || []).length === 0 ? [""] : []), + envPairs: (agent.spec?.byo?.deployment?.env || []) + .map((e: EnvVar) => + e?.valueFrom?.secretKeyRef + ? { + name: e.name || "", + isSecret: true, + secretName: e.valueFrom.secretKeyRef.name || "", + secretKey: e.valueFrom.secretKeyRef.key || "", + optional: e.valueFrom.secretKeyRef.optional, + } + : { name: e.name || "", value: e.value || "", isSecret: false }, + ) + .concat((agent.spec?.byo?.deployment?.env || []).length === 0 + ? [{ name: "", value: "", isSecret: false }] + : []), serviceAccountName: agent.spec?.byo?.deployment?.serviceAccountName || "", })); } - } catch (extractError) { console.error("Error extracting assistant data:", extractError); toast.error("Failed to extract agent data"); @@ -263,11 +274,11 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo } else { toast.error("Agent not found"); } - } catch (error) { - console.error("Error fetching agent:", error); + } catch (e) { + console.error("Error fetching agent:", e); toast.error("Failed to load agent data"); } finally { - setState(prev => ({ ...prev, isLoading: false })); + setState((prev) => ({ ...prev, isLoading: false })); } } }; @@ -289,9 +300,9 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo byoImage: state.byoImage, memory: memoryEnabled ? { - modelConfig: state.selectedMemoryModel?.ref || "", - ttlDays: state.memoryTtlDays ? parseInt(state.memoryTtlDays, 10) : undefined, - } + modelConfig: state.selectedMemoryModel?.ref || "", + ttlDays: state.memoryTtlDays ? parseInt(state.memoryTtlDays, 10) : undefined, + } : undefined, context: state.contextConfig, serviceAccountName: state.serviceAccountName, @@ -310,27 +321,44 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo } } - setState(prev => ({ ...prev, errors: newErrors })); - return Object.keys(newErrors).length === 0; + setState((prev) => ({ ...prev, errors: newErrors })); + const valid = Object.keys(newErrors).length === 0; + if (!valid) { + requestAnimationFrame(() => { + focusFirstFormError(newErrors, { byoSectionsActive: showByoFields }); + }); + } + return valid; }; - // Add field-level validation functions // eslint-disable-next-line @typescript-eslint/no-explicit-any - const validateField = (fieldName: keyof ValidationErrors, value: any) => { + const validateField = (fieldName: keyof AgentFormValidationErrors, value: any) => { const formData: Partial = {}; - const memoryEnabled = !!(state.selectedMemoryModel?.ref || state.memoryTtlDays); - // Set only the field being validated switch (fieldName) { - case 'name': formData.name = value; break; - case 'namespace': formData.namespace = value; break; - case 'description': formData.description = value; break; - case 'type': formData.type = value; break; - case 'systemPrompt': formData.systemPrompt = value; break; - case 'model': formData.modelName = value; break; - case 'tools': formData.tools = value; break; - case 'memoryModel': + case "name": + formData.name = value; + break; + case "namespace": + formData.namespace = value; + break; + case "description": + formData.description = value; + break; + case "type": + formData.type = value; + break; + case "systemPrompt": + formData.systemPrompt = value; + break; + case "model": + formData.modelName = value; + break; + case "tools": + formData.tools = value; + break; + case "memoryModel": if (memoryEnabled || value) { formData.memory = { modelConfig: value, @@ -338,7 +366,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo }; } break; - case 'memoryTtl': + case "memoryTtl": if (memoryEnabled || value) { formData.memory = { modelConfig: state.selectedMemoryModel?.ref || "", @@ -346,18 +374,19 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo }; } break; - case 'serviceAccountName': formData.serviceAccountName = value; break; + case "serviceAccountName": + formData.serviceAccountName = value; + break; } const fieldErrors = validateAgentData(formData); - const valueForField = (fieldErrors as Record)[fieldName as string]; - setState(prev => ({ + setState((prev) => ({ ...prev, errors: { ...prev.errors, [fieldName]: valueForField, - } + }, })); }; @@ -367,8 +396,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo } try { - - setState(prev => ({ ...prev, isSubmitting: true })); + setState((prev) => ({ ...prev, isSubmitting: true })); if (useDeclarativeAgentFields && !state.selectedModel) { throw new Error("Model is required for this agent type."); } @@ -391,28 +419,34 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo useDeclarativeAgentFields && (state.skillsGitAuthSecretName || "").trim() ? (state.skillsGitAuthSecretName || "").trim() : undefined, - memory: useDeclarativeAgentFields && memoryEnabled - ? { - modelConfig: state.selectedMemoryModel?.ref || "", - ttlDays: state.memoryTtlDays ? parseInt(state.memoryTtlDays, 10) : undefined, - } - : undefined, + memory: + useDeclarativeAgentFields && memoryEnabled + ? { + modelConfig: state.selectedMemoryModel?.ref || "", + ttlDays: state.memoryTtlDays ? parseInt(state.memoryTtlDays, 10) : undefined, + } + : undefined, context: useDeclarativeAgentFields ? state.contextConfig : undefined, - // BYO byoImage: state.byoImage, byoCmd: state.byoCmd || undefined, byoArgs: state.byoArgs ? state.byoArgs.split(/\s+/).filter(Boolean) : undefined, replicas: state.replicas ? parseInt(state.replicas, 10) : undefined, imagePullPolicy: state.imagePullPolicy || undefined, - imagePullSecrets: (state.imagePullSecrets || []).filter(n => n.trim()).map(n => ({ name: n.trim() })), + imagePullSecrets: (state.imagePullSecrets || []) + .filter((n) => n.trim()) + .map((n) => ({ name: n.trim() })), env: (state.envPairs || []) - .map(ev => { + .map((ev) => { const name = (ev.name || "").trim(); - if (!name) return null; + if (!name) { + return null; + } if (ev.isSecret) { const secName = (ev.secretName || "").trim(); const secKey = (ev.secretKey || "").trim(); - if (!secName || !secKey) return null; + if (!secName || !secKey) { + return null; + } return { name, valueFrom: { @@ -433,10 +467,8 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo let result; if (isEditMode && agentName && agentNamespace) { - // Update existing agent result = await updateAgent(agentData); } else { - // Create new agent result = await createNewAgent(agentData); } @@ -444,662 +476,363 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo throw new Error(result.error); } + setFormDirty(false); router.push(`/agents`); - return; - } catch (error) { - console.error(`Error ${isEditMode ? "updating" : "creating"} agent:`, error); - const errorMessage = error instanceof Error ? error.message : `Failed to ${isEditMode ? "update" : "create"} agent. Please try again.`; + } catch (e) { + console.error(`Error ${isEditMode ? "updating" : "creating"} agent:`, e); + const errorMessage = + e instanceof Error ? e.message : `Failed to ${isEditMode ? "update" : "create"} agent. Please try again.`; toast.error(errorMessage); - setState(prev => ({ ...prev, isSubmitting: false })); + setState((prev) => ({ ...prev, isSubmitting: false })); } }; - const renderPageContent = () => { - if (state.isSubmitting) { - return ; - } + const clearSkillsError = useCallback(() => { + setState((prev) => ({ ...prev, errors: { ...prev.errors, skills: undefined } })); + }, []); + const renderPageContent = () => { if (error) { return ; } return ( -
-
-

{isEditMode ? "Edit Agent" : "Create New Agent"}

+
+ + Skip to form + +
+ + +
+
{ + if (!formDirty) { + setFormDirty(true); + } + }} + onSubmit={(e) => { + e.preventDefault(); + void handleSaveAgent(); + }} + > +

+ {state.isSubmitting + ? isEditMode + ? "Saving…" + : "Creating…" + : ""} +

+ + + Agent name + Resource name in the cluster (shown in the UI and in refs). + setState((prev) => ({ ...prev, name: e.target.value }))} + onBlur={() => validateField("name", state.name)} + className={state.errors.name ? "border-destructive" : ""} + placeholder="e.g. my-assistant" + autoComplete="off" + spellCheck={false} + translate="no" + disabled={disabled || isEditMode} + aria-invalid={!!state.errors.name} + /> + {state.errors.name} + + + + Namespace + Must exist and match where ModelConfigs and tools are resolved. + { + setState((prev) => ({ ...prev, selectedModel: null, namespace: value })); + validateField("namespace", value); + }} + disabled={disabled || isEditMode} + /> + + + + Agent type + Declarative and sandbox (without a custom image) use the built-in model runtime. BYO or sandbox with a custom image adds container settings. + + + + + Description (optional) + Internal note only; not sent to the model as instructions. +