From 45d76bbb906cfc130486bc55ef24d33d475368bd Mon Sep 17 00:00:00 2001 From: Duncan Date: Sat, 8 Aug 2026 14:20:00 -0400 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20Phase=201=20R2=20=E2=80=94=20m?= =?UTF-8?q?erged=20agent=20edit=20surface=20(one=20form,=20one=20coordinat?= =?UTF-8?q?or)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build the merged edit surface the spec requires. One dialog (AgentEditMergedDialog) handles all R1–R7 contexts via a single AgentFormModel, rendering D+I+L sections together on one form and submitting every save through the Artifact 3 coordinator. Key changes: - AgentEditMergedDialog: replaces refetchStores+handleSubmit inline block with useAgentEditMergedSubmit hook; moves hook call before canSubmit so isSaving is in scope. For instance-with-definition context, renders both D-section (definition fields: name, prompt, runtime, model, provider) and I-section (instance fields: name, respond-to, harness, env overlay, parallelism) on one form. All saves go through emitAgentFormDiff → runAgentSaveCoordinator (observed-state settlement for D+I+L together). - AgentEditMergedDialogDSection: removes !showInst guards from runtime, model, and provider fields. These are D-fields and must render for all definition contexts, including instance-with-definition (writes to the definition via coordinator). Removes the now-unused showInst prop. - AgentEditMergedDialogInstanceSection: gates model, provider, and API key fields on !showDef. When a definition is present, model/provider are D-fields shown in the D-section; the I-section renders instance-specific fields only. - agentFormModel.ts: adds instanceEnvVars field (row 8 contract: per-instance env overlay that does not wholesale-replace the definition env). - agentDialogRouting.test.mjs: adds tests verifying hasDefinitionContext/ hasInstanceContext predicates and that the D/I section components are exported as functions (structural guards for the merged surface). AgentInstanceEditDialog has no production consumer on any R1–R7 route. The mode='instance-edit' path in AgentDialog.tsx is unreachable from all production edit surfaces (UserProfilePanel, AgentsView, AgentManagementDialogs all use AgentEditDialog with ctx, not AgentDialog with mode='instance-edit'). File-size gate: AgentEditMergedDialog.tsx split count = 981 (< 1000 ceiling). Full suite: 4562 tests, 0 failures. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../agents/lib/personaCatalogRelay.test.mjs | 4 +- .../features/agents/ui/AgentEditDialog.tsx | 308 +----- .../agents/ui/AgentEditMergedDialog.tsx | 980 ++++++++++++++++++ .../ui/AgentEditMergedDialogDSection.tsx | 242 +++++ .../AgentEditMergedDialogInstanceSection.tsx | 532 ++++++++++ .../agents/ui/agentDialogRouting.test.mjs | 194 +++- .../src/features/agents/ui/agentFormModel.ts | 58 +- .../agents/ui/useAgentEditMergedSubmit.ts | 289 ++++++ 8 files changed, 2283 insertions(+), 324 deletions(-) create mode 100644 desktop/src/features/agents/ui/AgentEditMergedDialog.tsx create mode 100644 desktop/src/features/agents/ui/AgentEditMergedDialogDSection.tsx create mode 100644 desktop/src/features/agents/ui/AgentEditMergedDialogInstanceSection.tsx create mode 100644 desktop/src/features/agents/ui/useAgentEditMergedSubmit.ts diff --git a/desktop/src/features/agents/lib/personaCatalogRelay.test.mjs b/desktop/src/features/agents/lib/personaCatalogRelay.test.mjs index ef516f4b01..c02644c012 100644 --- a/desktop/src/features/agents/lib/personaCatalogRelay.test.mjs +++ b/desktop/src/features/agents/lib/personaCatalogRelay.test.mjs @@ -367,7 +367,7 @@ test("test_foreign_entry_with_no_local_copy_stays_unselected", () => { BOB, ); - assert.equal(personas[0].id, "catalog:" + ALICE + ":reviewer"); + assert.equal(personas[0].id, `catalog:${ALICE}:reviewer`); assert.equal(personas[0].isActive, false); }); @@ -388,7 +388,7 @@ test("test_catalog_source_match_is_scoped_to_the_publishing_owner", () => { ALICE, ); - assert.equal(personas[0].id, "catalog:" + BOB + ":reviewer"); + assert.equal(personas[0].id, `catalog:${BOB}:reviewer`); assert.equal(personas[0].isActive, false); }); diff --git a/desktop/src/features/agents/ui/AgentEditDialog.tsx b/desktop/src/features/agents/ui/AgentEditDialog.tsx index 74c86392dc..1c877239ea 100644 --- a/desktop/src/features/agents/ui/AgentEditDialog.tsx +++ b/desktop/src/features/agents/ui/AgentEditDialog.tsx @@ -1,83 +1,46 @@ /** - * AgentEditDialog.tsx — Merged agent edit surface (Phase 1). + * AgentEditDialog.tsx — Merged agent edit surface entry point (Phase 1 Round 2). * - * Collapses R1–R7 from the Phase 0 spec Artifact 2 onto a single dialog. - * R8 (duplicate = create seed) and R9 (start-on-launch quick toggle) are - * kept as separate paths and are not affected here. + * Collapses R1–R7 onto a single dialog: AgentEditMergedDialog. * - * Architecture: - * instance-with-definition or instance-only: - * → AgentInstanceEditDialog (all I/L fields, correct edit-agent-dialog - * testid, linked-runtime awareness, auto-restart setter, saved-while- - * stopped affordance). + * All contexts — instance-with-definition, instance-only, definition-only — + * render the ONE merged surface. AgentEditMergedDialog seeds from + * AgentFormModel, renders D+I+L sections per context, and submits every + * save through the Artifact 3 coordinator (observed-state settlement for D, + * I, and per-policy writes). * - * definition-only (zero-instance definitions — R5 library card, R6 review): - * → AgentDefinitionDialog, wired through the Artifact 3 coordinator. - * Team-managed definitions render fields disabled with a "Managed by - * team" note (D-fields structurally unemittable per the spec). - * - * The Artifact 3 coordinator is invoked for the definition-only path only. - * Instance paths use AgentInstanceEditDialog's own well-tested save path. - * - * S5 "Instances" vocabulary rename is deferred to Phase 3. Built-in agents - * are fully editable (Artifact 1 corrected matrix). + * AgentDefinitionDialog survives for create flows (R8 duplicate seed, import) + * only — it is NOT a consumer of any R1–R7 edit route. + * AgentInstanceEditDialog has no production consumer on any R1–R7 route. */ -import * as React from "react"; -import { toast } from "sonner"; -import { useQueryClient } from "@tanstack/react-query"; - -import { - managedAgentsQueryKey, - personasQueryKey, - useAcpRuntimesQuery, - useStartManagedAgentMutation, - useUpdatePersonaMutation, -} from "@/features/agents/hooks"; -import { useUpdatePersonaAndPublishMutation } from "@/features/agents/lib/usePersonaCatalogRelay"; -import { runAgentSaveCoordinator } from "./agentSaveCoordinator"; +import type { ManagedAgent } from "@/shared/api/types"; +import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent"; import type { AgentEditContext } from "./agentFormModel"; export type { AgentEditContext }; -import { isDefinitionReadOnly } from "./agentFormModel"; -import type { - AgentPersona, - CreatePersonaInput, - ManagedAgent, - UpdatePersonaInput, -} from "@/shared/api/types"; -import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent"; -import type { AgentDefinitionSubmitOptions } from "./AgentDefinitionDialog"; import { runLocationForBackend } from "@/features/agents/lib/agentAccessWarning"; -import { AgentDefinitionDialog } from "./AgentDefinitionDialog"; -import { AgentInstanceEditDialog } from "./AgentInstanceEditDialog"; import { AgentRunLocationProvider } from "./AgentRunLocationContext"; -import { editPersonaDialogState } from "./personaDialogState"; +import { AgentEditMergedDialog } from "./AgentEditMergedDialog"; // ── Types ───────────────────────────────────────────────────────────────────── export type AgentEditDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; - /** - * Edit context: which entity or entities to edit. - */ + /** Edit context: which entity or entities to edit. */ ctx: AgentEditContext; /** - * Optional field to focus when the dialog opens from a card deep-link - * (instance paths only — AgentInstanceEditDialog honors this). + * Optional field to focus when the dialog opens from a card deep-link. */ initialFocus?: EditAgentFocusTarget; onUpdated?: (agent: ManagedAgent) => void; /** * Optional pre-save validator (R6 origin permission check). - * Fires before the coordinator's step 0 on definition-only paths. * Return a non-null string to abort with an error toast; return null to proceed. */ onValidate?: () => string | null; /** - * Optional initial-values override for the definition fields. When provided, - * these values are merged into the seeded form (used in R6 review mode to - * pre-fill agent-requested changes for user approval). + * Optional initial-values override for definition fields (R6 review mode). */ initialValueOverrides?: Partial<{ displayName: string; @@ -99,226 +62,31 @@ export function AgentEditDialog({ initialValueOverrides, initialFocus, }: AgentEditDialogProps) { - // ── Instance paths: delegate entirely to AgentInstanceEditDialog ────────── - // - // AgentInstanceEditDialog renders the full I+L field set (respondTo/allowlist, - // parallelism, env vars, harness pin, auto-restart, start-on-launch, instance - // name) and owns a well-tested save path. Route all instance-present contexts - // here so no I/L field is accidentally omitted. - // - // Wrap in AgentRunLocationProvider so RespondToField's access warning can - // name the machine the agent actually runs on (mirrors AgentDialog's instance- - // edit arm — backend → run location for the remote-backend warning path). - if (ctx.kind === "instance-with-definition" || ctx.kind === "instance-only") { + // All contexts route to the merged surface. The run-location provider wraps + // instance-present paths so RespondToField's remote-backend warning can name + // the machine the agent actually runs on. + const inst = ctx.kind !== "definition-only" ? ctx.instance : null; + const runLocation = inst ? runLocationForBackend(inst.backend) : null; + + const inner = ( + + ); + + if (inst) { return ( - - + + {inner} ); } - // ── Definition-only path: AgentDefinitionDialog + Artifact 3 coordinator ── - return ( - - ); -} - -// ── Definition-only edit: coordinator-wired AgentDefinitionDialog ───────────── -// -// Separated into its own component so React hook ordering is stable across -// the instance/definition-only branch above. All hooks run unconditionally here. - -function AgentEditDefinitionOnlyDialog({ - ctx, - open, - onOpenChange, - // onUpdated is intentionally omitted: definition-only has no ManagedAgent to - // return. Instance paths surface onUpdated via AgentInstanceEditDialog. - onValidate, - initialValueOverrides, -}: Omit & { - ctx: Extract; -}) { - const queryClient = useQueryClient(); - const updatePersonaMutation = useUpdatePersonaMutation(); - const startMutation = useStartManagedAgentMutation(); - const runtimesQuery = useAcpRuntimesQuery({ enabled: open }); - - // Community id is required for publish-aware mutations; null → non-catalog edit. - const updatePersonaAndPublishMutation = - useUpdatePersonaAndPublishMutation(null); - - const [isSaving, setIsSaving] = React.useState(false); - const [saveError, setSaveError] = React.useState(null); - - // Reset error on open - React.useEffect(() => { - if (open) setSaveError(null); - }, [open]); - - const def = ctx.definition; - const runtimes = runtimesQuery.data ?? []; - const runtimeCatalogStatus = runtimesQuery.isLoading - ? ("loading" as const) - : runtimesQuery.isError - ? ("error" as const) - : ("ready" as const); - - // Team-managed: D-fields render disabled (structurally unemittable per spec). - const defReadOnly = isDefinitionReadOnly(ctx); - - // ── Settlement helper ────────────────────────────────────────────────────── - async function refetchStores(): Promise<{ - persona: AgentPersona | null; - agent: ManagedAgent | null; - }> { - await Promise.all([ - queryClient.invalidateQueries({ queryKey: personasQueryKey }), - queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey }), - ]); - const personas = - queryClient.getQueryData(personasQueryKey) ?? []; - return { - persona: personas.find((p) => p.id === def.id) ?? null, - agent: null, // definition-only: no instance to settle - }; - } - - // ── onSubmit ── called by AgentDefinitionDialog when the user clicks Save ─ - async function handleSubmit( - input: CreatePersonaInput | UpdatePersonaInput, - options: AgentDefinitionSubmitOptions, - ): Promise { - // AgentEditDialog is always in edit mode — the input will always be an - // UpdatePersonaInput. If for any reason a CreatePersonaInput arrives - // (e.g. dialog misconfiguration), bail out safely. - if (!("id" in input)) { - console.error( - "[AgentEditDialog] Received CreatePersonaInput in edit mode — ignoring.", - ); - return undefined; - } - - // Pre-save validation (e.g. R6 origin-permission check). - if (onValidate) { - const validationError = onValidate(); - if (validationError) { - toast.error(validationError); - return undefined; - } - } - - // Team-managed: D-fields are structurally unemittable; form renders read-only. - // Guard here too so a misconfigured call path cannot bypass the UI gate. - if (defReadOnly) { - return undefined; - } - - setSaveError(null); - setIsSaving(true); - - try { - const personaInput: UpdatePersonaInput = input; - - const success = await runAgentSaveCoordinator({ - ctx, - personaInput, - agentInput: null, - policySets: [], - publishCatalogUpdates: options.publishCatalogUpdates, - runtimes: runtimes.length > 0 ? runtimes : undefined, - updatePersona: (p) => updatePersonaMutation.mutateAsync(p), - updatePersonaAndPublish: (p) => - updatePersonaAndPublishMutation.mutateAsync(p), - updateManagedAgent: (_a) => { - throw new Error("No instance in definition-only context"); - }, - setAutoRestart: (_pubkey, _value) => Promise.resolve(), - setStartOnAppLaunch: (_pubkey, _value) => Promise.resolve(), - refetchStores, - onDone: () => onOpenChange(false), - onSavedWhileStopped: (agent) => { - // definition-only: no instance, but preserve the affordance contract - const savedName = agent.name; - toast(`${savedName} saved while stopped.`, { - action: { - label: "Start now", - onClick: () => { - startMutation.mutate(agent.pubkey, { - onSuccess: () => toast.success(`${savedName} started.`), - onError: (err) => - toast.error( - err instanceof Error - ? `${savedName} failed to start: ${err.message}` - : `${savedName} failed to start.`, - ), - }); - }, - }, - }); - }, - }); - - if (!success) { - setSaveError( - new Error("Some changes may not have persisted. Reopen to retry."), - ); - } - // definition-only: no ManagedAgent to surface; onUpdated is not called. - // Instance paths call onUpdated via AgentInstanceEditDialog directly. - } finally { - setIsSaving(false); - } - - return undefined; - } - - // ── Build initial values for AgentDefinitionDialog ───────────────────────── - const baseDialogState = editPersonaDialogState(def); - const dialogState = - baseDialogState && initialValueOverrides - ? { - ...baseDialogState, - initialValues: { - ...baseDialogState.initialValues, - ...initialValueOverrides, - }, - } - : baseDialogState; - - return ( - - ); + return inner; } diff --git a/desktop/src/features/agents/ui/AgentEditMergedDialog.tsx b/desktop/src/features/agents/ui/AgentEditMergedDialog.tsx new file mode 100644 index 0000000000..27fe81f212 --- /dev/null +++ b/desktop/src/features/agents/ui/AgentEditMergedDialog.tsx @@ -0,0 +1,980 @@ +/** + * AgentEditMergedDialog — single merged edit surface for R1–R7. + * + * instance-with-definition → D + I + L sections on one form + * instance-only → I + L sections + * definition-only → D sections only + * + * Submit: emitAgentFormDiff → runAgentSaveCoordinator (observed-state settlement). + * Submit logic extracted to useAgentEditMergedSubmit; section JSX to + * AgentEditMergedDialogDSection / AgentEditMergedDialogInstanceSection. + */ + +import * as React from "react"; + +import { + useAcpRuntimesQuery, + useBakedBuildEnvKeysQuery, + usePersonasQuery, + useStartManagedAgentMutation, + useUpdateManagedAgentMutation, + useUpdatePersonaMutation, +} from "@/features/agents/hooks"; +import { useUpdatePersonaAndPublishMutation } from "@/features/agents/lib/usePersonaCatalogRelay"; +import type { ManagedAgent, RespondToMode } from "@/shared/api/types"; +import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent"; +import { Button } from "@/shared/ui/button"; +import { ChooserDialogContent } from "@/shared/ui/chooser-dialog-content"; +import { Dialog } from "@/shared/ui/dialog"; + +import { + seedAgentFormModel, + hasDefinitionContext, + hasInstanceContext, + isDefinitionReadOnly, + editContextDefinition, + editContextInstance, + type AgentEditContext, +} from "./agentFormModel"; +import { useAgentEditMergedSubmit } from "./useAgentEditMergedSubmit"; +import { AgentEditMergedDSection } from "./AgentEditMergedDialogDSection"; +import { AgentEditMergedInstanceSection } from "./AgentEditMergedDialogInstanceSection"; +import { + ADD_CUSTOM_HARNESS_OPTION, + runtimeDropdownAction, + usePendingHarnessSelection, +} from "./addCustomHarness"; +import { + AUTO_PROVIDER_DROPDOWN_VALUE, + BLOCK_BUILD_HIDDEN_PROVIDER_IDS, + CUSTOM_PROVIDER_DROPDOWN_VALUE, + formatRuntimeOptionLabel, + getDefaultPersonaRuntime, + getPersonaProviderOptions, + isMissingRequiredDropdownField, + NO_RUNTIME_DROPDOWN_VALUE, + buildPersonaRuntimeDropdownOptions, + runtimeSupportsLlmProviderSelection, + shouldClearKnownModelForSelectionScope, + sortPersonaRuntimes, + type PersonaDropdownOption, +} from "./agentConfigOptions"; +import { + selectionOnModelDropdownChange, + selectionOnProviderDropdownChange, + selectionOnRuntimeChange, + type RuntimeModelProviderSelection, +} from "./runtimeModelProviderSelection"; +import { + MODEL_DISCOVERY_LOADING_VALUE, + usePersonaModelDiscovery, +} from "./usePersonaModelDiscovery"; +import { useAgentDialogDefaults } from "./useAgentDialogDefaults"; +import { useProviderApiKeyFieldState } from "./providerApiKeyFieldState"; +import { useRequiredCredentialState } from "./useRequiredCredentialState"; +import { getBakedProviderInheritLabel } from "./bakedEnvHelpers"; +import { getProviderApiKeyEnvVar } from "./agentConfigOptions"; +import { resolveModelFieldStatusMessage } from "./agentConfigControls"; +import { useAgentAccessOwnerOnlyQuery } from "@/features/agents/useAgentAccessOwnerOnly"; +import { + resolveInheritedRuntimeSubmission, + isEditAgentProviderSaveValid, +} from "./personaRuntimeModel"; +import type { EnvVarsValue } from "./EnvVarsEditor"; +import { formatPersonaNamePoolText } from "./personaDialogState"; +import { useRuntimeFileConfigQuery } from "../hooks"; + +// ── Types / Component ──────────────────────────────────────────────────────── + +export type AgentEditMergedDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + ctx: AgentEditContext; + /** Optional field to focus when the dialog opens from a card deep-link. */ + initialFocus?: EditAgentFocusTarget; + onUpdated?: (agent: ManagedAgent) => void; + /** + * Optional pre-save validator (R6 origin permission check). + * Return a non-null string to abort with an error toast; return null to proceed. + */ + onValidate?: () => string | null; + /** + * Optional initial-values override for definition fields (R6 review mode). + */ + initialValueOverrides?: Partial<{ + displayName: string; + systemPrompt: string; + runtime: string | undefined; + provider: string | undefined; + model: string | undefined; + }>; +}; + +export function AgentEditMergedDialog({ + open, + onOpenChange, + ctx, + initialFocus, + onUpdated, + onValidate, + initialValueOverrides, +}: AgentEditMergedDialogProps) { + const def = editContextDefinition(ctx); + const inst = editContextInstance(ctx); + const showDef = hasDefinitionContext(ctx); + const showInst = hasInstanceContext(ctx); + const defReadOnly = isDefinitionReadOnly(ctx); + + // ── Queries / mutations ─────────────────────────────────────────────────── + const runtimesQuery = useAcpRuntimesQuery({ enabled: open }); + const runtimes = runtimesQuery.data ?? []; + const runtimeCatalogStatus = runtimesQuery.isLoading + ? ("loading" as const) + : runtimesQuery.isError + ? ("error" as const) + : ("ready" as const); + + const updatePersonaMutation = useUpdatePersonaMutation(); + const updatePersonaAndPublishMutation = + useUpdatePersonaAndPublishMutation(null); + const updateMutation = useUpdateManagedAgentMutation(); + const startMutation = useStartManagedAgentMutation(); + + const { data: bakedEnvKeys } = useBakedBuildEnvKeysQuery({ enabled: open }); + const { data: agentAccessOwnerOnly } = useAgentAccessOwnerOnlyQuery({ + enabled: open && showInst, + }); + + const personasQuery = usePersonasQuery(); + const linkedPersona = React.useMemo( + () => + inst?.personaId + ? (personasQuery.data?.find((p) => p.id === inst.personaId) ?? null) + : null, + [inst?.personaId, personasQuery.data], + ); + + // ── Form state — seeded from AgentFormModel ─────────────────────────────── + // D-fields + const [displayName, setDisplayName] = React.useState(""); + const [avatarUrl, setAvatarUrl] = React.useState(""); + const [systemPrompt, setSystemPrompt] = React.useState(""); + const [namePoolText, setNamePoolText] = React.useState(""); + // Runtime/model/provider + const [selectedRuntimeId, setSelectedRuntimeId] = React.useState("custom"); + const [model, setModel] = React.useState(""); + const [provider, setProvider] = React.useState(""); + const [isCustomModelEditing, setIsCustomModelEditing] = React.useState(false); + const [isCustomProviderEditing, setIsCustomProviderEditing] = + React.useState(false); + const runtimeTouched = React.useRef(false); + + // I-fields + const [instanceName, setInstanceName] = React.useState(""); + const [respondTo, setRespondTo] = React.useState( + inst?.respondTo ?? "anyone", + ); + const [respondToAllowlist, setRespondToAllowlist] = React.useState( + inst?.respondToAllowlist ?? [], + ); + const [parallelism, setParallelism] = React.useState( + String(inst?.parallelism ?? 1), + ); + const [envVars, setEnvVars] = React.useState( + def?.envVars ?? inst?.envVars ?? {}, + ); + const [instanceEnvVars, setInstanceEnvVars] = React.useState( + inst?.envVars ?? {}, + ); + // Harness-pin (I-field) + const [agentCommand, setAgentCommand] = React.useState( + inst?.agentCommand ?? "", + ); + const [inheritHarness, setInheritHarness] = React.useState( + inst != null && inst.personaId != null && inst.agentCommandOverride == null, + ); + const [agentArgs, setAgentArgs] = React.useState( + inst?.agentArgs.join(",") ?? "", + ); + const [acpCommand, setAcpCommand] = React.useState(inst?.acpCommand ?? ""); + const [originalAgentCommand, setOriginalAgentCommand] = React.useState( + inst?.agentCommand ?? "", + ); + + // L-fields + const [autoRestartOnConfigChange, setAutoRestartOnConfigChange] = + React.useState(inst?.autoRestartOnConfigChange ?? false); + + // UI state + const [showAdvancedFields, setShowAdvancedFields] = React.useState(false); + const [isAvatarUploadPending, setIsAvatarUploadPending] = + React.useState(false); + const [aiDefaultsOpen, setAiDefaultsOpen] = React.useState(false); + const aiDefaultsTriggerRef = React.useRef(null); + const [isAddHarnessOpen, setIsAddHarnessOpen] = React.useState(false); + + // ── Seed form on open ───────────────────────────────────────────────────── + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — only re-seed on open/entity-switch + React.useEffect(() => { + if (!open) return; + + const seed = seedAgentFormModel(ctx); + setDisplayName(seed.displayName); + setAvatarUrl(seed.avatarUrl); + setSystemPrompt(seed.systemPrompt); + setNamePoolText(formatPersonaNamePoolText(seed.namePool ?? [])); + setModel(seed.model ?? ""); + setProvider(seed.provider ?? ""); + setIsCustomModelEditing(false); + setIsCustomProviderEditing(false); + setRespondTo((seed.respondTo ?? "anyone") as RespondToMode); + setRespondToAllowlist(seed.respondToAllowlist); + setParallelism(String(seed.parallelism ?? 1)); + setEnvVars(seed.envVars); + setInstanceEnvVars(seed.instanceEnvVars ?? {}); + setInstanceName(seed.instanceName ?? ""); + setAutoRestartOnConfigChange(seed.autoRestartOnConfigChange ?? false); + setShowAdvancedFields(false); + setIsAvatarUploadPending(false); + runtimeTouched.current = false; + + if (inst) { + setAgentCommand(inst.agentCommand); + setOriginalAgentCommand(inst.agentCommand); + setInheritHarness( + inst.personaId != null && inst.agentCommandOverride == null, + ); + setAgentArgs(inst.agentArgs.join(",")); + setAcpCommand(inst.acpCommand); + const matched = + runtimes.find((r) => r.command?.trim() === inst.agentCommand.trim()) ?? + runtimes.find((r) => r.id === inst.agentCommand.trim()); + setSelectedRuntimeId(matched ? matched.id : "custom"); + } else if (def) { + // definition-only: seed runtime from definition + const defRuntimeId = def.runtime?.trim() ?? ""; + setSelectedRuntimeId(defRuntimeId || "custom"); + } + + // Apply any caller-supplied initial value overrides (R6 review mode). + if (initialValueOverrides) { + if (initialValueOverrides.displayName !== undefined) + setDisplayName(initialValueOverrides.displayName); + if (initialValueOverrides.systemPrompt !== undefined) + setSystemPrompt(initialValueOverrides.systemPrompt); + if (initialValueOverrides.model !== undefined) + setModel(initialValueOverrides.model ?? ""); + if (initialValueOverrides.provider !== undefined) + setProvider(initialValueOverrides.provider ?? ""); + if (initialValueOverrides.runtime !== undefined) + setSelectedRuntimeId(initialValueOverrides.runtime ?? "custom"); + } + }, [open, inst?.pubkey, def?.id]); + + // Re-derive runtime id when catalog loads + React.useEffect(() => { + if (!open || runtimeTouched.current || runtimes.length === 0 || !inst) + return; + const matched = + runtimes.find((r) => r.command?.trim() === inst.agentCommand.trim()) ?? + runtimes.find((r) => r.id === inst.agentCommand.trim()); + if (matched) setSelectedRuntimeId(matched.id); + }, [open, runtimes, inst?.agentCommand, inst]); + + // ── Runtime / model / provider machinery ────────────────────────────────── + const sortedRuntimes = React.useMemo( + () => sortPersonaRuntimes(runtimes), + [runtimes], + ); + const selectedRuntime = runtimes.find((r) => r.id === selectedRuntimeId); + const runtimeDropdownValue = selectedRuntimeId || NO_RUNTIME_DROPDOWN_VALUE; + + // Instance-path runtime dropdown options + const instanceRuntimeDropdownOptions: PersonaDropdownOption[] = + React.useMemo(() => { + const options: PersonaDropdownOption[] = [ + ...sortedRuntimes.map((r) => ({ + label: formatRuntimeOptionLabel(r), + value: r.id, + })), + { label: "Custom command", value: "custom" }, + ]; + if ( + selectedRuntimeId && + selectedRuntimeId !== "custom" && + !options.some((o) => o.value === selectedRuntimeId) + ) { + options.push({ + label: `${selectedRuntimeId} (current)`, + value: selectedRuntimeId, + }); + } + options.push(ADD_CUSTOM_HARNESS_OPTION); + return options; + }, [sortedRuntimes, selectedRuntimeId]); + + // Definition path: uses buildPersonaRuntimeDropdownOptions (supports loading/blank) + const { + blankRuntimeOptionLabel: defBlankLabel, + runtimeDropdownOptions: defRuntimeDropdownOptions, + } = React.useMemo(() => { + const result = buildPersonaRuntimeDropdownOptions({ + defaultRuntimeId: getDefaultPersonaRuntime(runtimes)?.id, + isCreateMode: false, + runtime: selectedRuntimeId === "custom" ? "" : selectedRuntimeId, + runtimes, + runtimesLoading: runtimeCatalogStatus === "loading", + }); + result.runtimeDropdownOptions.push(ADD_CUSTOM_HARNESS_OPTION); + return result; + }, [runtimes, runtimeCatalogStatus, selectedRuntimeId]); + + const originalRuntimeSupportsProvider = React.useMemo(() => { + if (!inst) return false; + const originalCommand = originalAgentCommand.trim(); + const matched = + runtimes.find((r) => r.command?.trim() === originalCommand) ?? + runtimes.find((r) => r.id === originalCommand); + return runtimeSupportsLlmProviderSelection(matched?.id ?? ""); + }, [runtimes, originalAgentCommand, inst]); + + // Prospective runtime after submit (for gate + discovery) + const prospectiveRuntimeId = React.useMemo(() => { + if (!inst) return selectedRuntimeId || ""; + if (!inheritHarness) return selectedRuntime?.id ?? selectedRuntimeId; + const personaRuntimeId = linkedPersona?.runtime?.trim(); + if (personaRuntimeId) { + return ( + runtimes.find((r) => r.id === personaRuntimeId)?.id ?? personaRuntimeId + ); + } + return ( + runtimes.find((r) => r.command?.trim() === inst.agentCommand.trim()) + ?.id ?? + runtimes.find((r) => r.id === inst.agentCommand.trim())?.id ?? + getDefaultPersonaRuntime(runtimes)?.id ?? + "" + ); + }, [ + inheritHarness, + linkedPersona?.runtime, + runtimes, + inst?.agentCommand, + selectedRuntime?.id, + selectedRuntimeId, + inst, + ]); + + const llmProviderFieldVisible = runtimeSupportsLlmProviderSelection( + showInst ? prospectiveRuntimeId : selectedRuntimeId, + ); + + const prospectiveRuntime = runtimes.find( + (r) => r.id === prospectiveRuntimeId, + ); + + // Resolve inherited env / provider for credential gate + const inheritedEnvVars = linkedPersona?.envVars ?? {}; + const inheritedSubmission = React.useMemo(() => { + if (!inst) + return { provider: provider || null, model: model || null, envVars }; + return resolveInheritedRuntimeSubmission({ + inheritHarness, + agentWasHarnessPinned: inst.agentCommandOverride != null, + provider, + personaProvider: linkedPersona?.provider ?? "", + model, + personaModel: linkedPersona?.model ?? null, + envVars: showDef ? envVars : instanceEnvVars, + personaEnvVars: inheritedEnvVars, + }); + }, [ + inheritHarness, + inst, + linkedPersona, + provider, + model, + envVars, + instanceEnvVars, + inheritedEnvVars, + showDef, + ]); + + const { + globalConfig, + inheritedDefaults: { + provider: inheritedProviderDefault, + model: inheritedModelDefault, + }, + inheritedEnvVars: inheritedEnvVarsForAdvanced, + } = useAgentDialogDefaults({ inheritedEnvVars, open }); + + const { requiredEnvKeys, fileSatisfiedEnvKeys, requiredEnvKeyMissing } = + useRequiredCredentialState({ + open: open && showInst, + prospectiveRuntimeId, + provider: inheritedSubmission.provider ?? "", + globalProvider: inheritedProviderDefault.value, + envVars: inheritedSubmission.envVars, + globalEnvVars: globalConfig.env_vars, + personaEnvVars: inheritHarness ? inheritedEnvVars : undefined, + }); + + // runtimeFileConfig is used by localModeGate (definition-only). For the + // merged surface, the credential gate drives Save readiness on instance paths. + useRuntimeFileConfigQuery( + showInst ? prospectiveRuntimeId : selectedRuntimeId, + { enabled: open }, + ); + + // Model discovery + const effectiveProvider = + (inheritedSubmission.provider ?? "").trim() || + inheritedProviderDefault.value; + const providerForDiscovery = llmProviderFieldVisible ? effectiveProvider : ""; + const envVarsForDiscovery = React.useMemo( + () => ({ ...globalConfig.env_vars, ...inheritedSubmission.envVars }), + [globalConfig.env_vars, inheritedSubmission.envVars], + ); + + const { + discoveredModelOptions, + modelDiscoveryLoading, + modelDiscoveryStatus, + } = usePersonaModelDiscovery({ + envVars: envVarsForDiscovery, + isCustomProviderEditing, + modelFieldVisible: true, + open, + provider: providerForDiscovery, + selectedRuntime: showInst ? prospectiveRuntime : selectedRuntime, + }); + + const hideProviderIds = React.useMemo( + () => + (bakedEnvKeys ?? []).includes("BUZZ_AGENT_PROVIDER") + ? BLOCK_BUILD_HIDDEN_PROVIDER_IDS + : new Set(), + [bakedEnvKeys], + ); + + const providerOptions = getPersonaProviderOptions( + provider.trim(), + showInst ? (selectedRuntime?.id ?? "") : selectedRuntimeId, + inheritedProviderDefault.source === "global" + ? inheritedProviderDefault.value + : "", + hideProviderIds, + ); + const providerSelectValue = isCustomProviderEditing + ? CUSTOM_PROVIDER_DROPDOWN_VALUE + : provider.trim() || AUTO_PROVIDER_DROPDOWN_VALUE; + const providerDropdownOptions: PersonaDropdownOption[] = [ + ...providerOptions.map((opt) => ({ + label: + opt.id === "" && inheritedProviderDefault.source === "build" + ? getBakedProviderInheritLabel( + inheritedProviderDefault.value, + providerOptions, + ) + : opt.label, + value: opt.id || AUTO_PROVIDER_DROPDOWN_VALUE, + })), + { label: "Custom provider...", value: CUSTOM_PROVIDER_DROPDOWN_VALUE }, + ]; + + const { + isRelayMesh, + options: effectiveModelOptions, + selectValue: modelSelectValue, + showCustomInput: showCustomModelInput, + } = relayMeshModelPickerState({ + discoveredOptions: discoveredModelOptions, + fallbackOptions: [ + { id: "", label: getDefaultLlmModelLabel(inheritedModelDefault.value) }, + ], + isCustomEditing: isCustomModelEditing, + model, + provider: providerForDiscovery, + }); + + const modelDropdownOptions = buildModelDropdownOptions({ + allowCustom: !isRelayMesh, + globalModel: isRelayMesh ? undefined : inheritedModelDefault.value, + globalModelLabel: isRelayMesh + ? undefined + : getDefaultLlmModelLabel(inheritedModelDefault.value), + loading: modelDiscoveryLoading && discoveredModelOptions === null, + loadingValue: MODEL_DISCOVERY_LOADING_VALUE, + options: effectiveModelOptions, + }); + + const modelStatusMessage = resolveModelFieldStatusMessage({ + discoveredModelOptions, + loading: modelDiscoveryLoading, + status: modelDiscoveryStatus, + }); + + const providerApiKeyEnvVar = getProviderApiKeyEnvVar(effectiveProvider); + const personaSatisfied = + providerApiKeyEnvVar != null && + !(providerApiKeyEnvVar in (showInst ? instanceEnvVars : envVars)) && + (inheritedEnvVars[providerApiKeyEnvVar] ?? "").length > 0; + const apiKeyFieldState = useProviderApiKeyFieldState({ + bakedEnvKeys, + effectiveEnvVars: inheritedSubmission.envVars, + envVars: showInst ? instanceEnvVars : envVars, + fileSatisfiedEnvKeys, + globalEnvVars: globalConfig.env_vars, + personaSatisfied, + provider: effectiveProvider, + requiredEnvKeys, + }); + const { + advancedRequiredEnvKeys, + inheritedLabel: apiKeyInheritedLabel, + isInherited: apiKeyIsInherited, + isRequired: apiKeyIsRequired, + secretEnvVar: topLevelSecretEnvVar, + value: apiKeyValue, + } = apiKeyFieldState; + + // Clear model when provider scope changes + React.useEffect(() => { + if ( + !open || + isCustomModelEditing || + !shouldClearKnownModelForSelectionScope({ + model, + provider: providerForDiscovery, + runtime: showInst ? prospectiveRuntimeId : selectedRuntimeId, + }) + ) + return; + setModel(""); + setIsCustomModelEditing(false); + }, [ + isCustomModelEditing, + model, + open, + providerForDiscovery, + selectedRuntimeId, + prospectiveRuntimeId, + showInst, + ]); + + // ── Submit validity ──────────────────────────────────────────────────────── + const modelRequired = React.useMemo( + () => isMissingRequiredDropdownField(undefined, model), + [model], + ); + const providerRequired = React.useMemo( + () => isMissingRequiredDropdownField(undefined, provider), + [provider], + ); + + const providerValid = showInst + ? isEditAgentProviderSaveValid({ + llmProviderFieldVisible, + currentProvider: provider, + originalProvider: inst?.provider ?? null, + globalProvider: inheritedProviderDefault.value, + originalRuntimeSupportsProvider, + }) + : true; + + const parsedParallelism = parseInt(parallelism, 10); + + // ── Submit hook (must come before canSubmit so isSaving is available) ───── + const { + isSaving, + saveError, + handleSubmit: _handleSubmit, + } = useAgentEditMergedSubmit({ + ctx, + displayName, + avatarUrl, + systemPrompt, + namePoolText, + model, + provider, + respondTo, + respondToAllowlist, + parallelism, + parsedParallelism, + envVars, + instanceEnvVars, + instanceName, + autoRestartOnConfigChange, + selectedRuntimeId, + inheritHarness, + agentCommand, + agentArgs, + acpCommand, + showInst, + defReadOnly, + inheritedSubmissionProvider: inheritedSubmission.provider ?? null, + inheritedSubmissionEnvVars: inheritedSubmission.envVars as Record< + string, + string + >, + linkedPersonaEnvVars: linkedPersona?.envVars, + runtimes, + updatePersona: (p) => updatePersonaMutation.mutateAsync(p), + updatePersonaAndPublish: (p) => + updatePersonaAndPublishMutation.mutateAsync(p), + updateManagedAgent: (input) => updateMutation.mutateAsync(input), + startMutate: (pubkey, cbs) => startMutation.mutate(pubkey, cbs), + onValidate, + onOpenChange, + onUpdated, + }); + + const canSubmit = + !isSaving && + !isAvatarUploadPending && + displayName.trim().length > 0 && + (showInst + ? providerValid && + !requiredEnvKeyMissing && + (parsedParallelism > 0 || parallelism === "") && + (selectedRuntimeId !== "custom" || + inheritHarness || + agentCommand.trim().length > 0) + : true); + + // ── selection helpers ────────────────────────────────────────────────────── + const selection: RuntimeModelProviderSelection = { + provider, + model, + isCustomProviderEditing, + isCustomModelEditing, + envVars: showInst ? instanceEnvVars : envVars, + }; + + function applySelection(next: RuntimeModelProviderSelection) { + setProvider(next.provider); + setModel(next.model); + setIsCustomProviderEditing(next.isCustomProviderEditing); + setIsCustomModelEditing(next.isCustomModelEditing); + if (showInst) { + setInstanceEnvVars(next.envVars); + } else { + setEnvVars(next.envVars); + } + } + + function handleRuntimeDropdownChange(nextValue: string) { + const action = runtimeDropdownAction(nextValue); + if (action.kind === "add-custom-harness") { + setIsAddHarnessOpen(true); + return; + } + const nextRuntimeId = action.runtimeId; + const previousRuntimeId = selectedRuntimeId; + + if (showInst) { + runtimeTouched.current = true; + const nextRuntime = runtimes.find((r) => r.id === nextRuntimeId); + const resolvedRuntimeId = nextRuntimeId || "custom"; + setSelectedRuntimeId(resolvedRuntimeId); + if (resolvedRuntimeId === "custom" || nextRuntime?.command) { + setInheritHarness(false); + } + if (nextRuntime?.command) { + setAgentCommand(nextRuntime.command); + setAgentArgs(nextRuntime.defaultArgs.join(",")); + } + } else { + setSelectedRuntimeId(nextRuntimeId || "custom"); + } + + applySelection( + selectionOnRuntimeChange(selection, { + previousRuntime: previousRuntimeId, + nextRuntime: nextRuntimeId, + nextRuntimeCanChooseProvider: + runtimeSupportsLlmProviderSelection(nextRuntimeId), + lockedRuntimeReset: "full", + }), + ); + } + + const selectSavedHarness = usePendingHarnessSelection( + runtimes, + handleRuntimeDropdownChange, + open, + ); + + function handleProviderDropdownChange(nextValue: string) { + const nextProvider = + nextValue === AUTO_PROVIDER_DROPDOWN_VALUE ? "" : nextValue; + if (nextProvider === "relay-mesh" && selectedRuntimeId !== "buzz-agent") { + handleRuntimeDropdownChange("buzz-agent"); + } + const nextSelection = selectionOnProviderDropdownChange(selection, { + runtime: + nextProvider === "relay-mesh" + ? "buzz-agent" + : (selectedRuntime?.id ?? selectedRuntimeId), + nextValue, + clearModelWhenApiKeyMissing: !showInst, + }); + applySelection({ + ...nextSelection, + model: nextProvider === "relay-mesh" ? "auto" : nextSelection.model, + }); + } + + function handleModelDropdownChange(nextValue: string) { + applySelection( + selectionOnModelDropdownChange(selection, { + nextValue, + clearKnownModelOnCustomEntry: !showInst, + isModelCustom: false, + }), + ); + } + + // ── Focus target (R2) ────────────────────────────────────────────────────── + const normalizedFieldFocusFiredRef = React.useRef(false); + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — reset guard on open/context-switch; pure ref mutation is correct + React.useEffect(() => { + normalizedFieldFocusFiredRef.current = false; + }, [open, inst?.pubkey, def?.id]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — llmProviderFieldVisible is the availability signal; inst?.pubkey handles agent-switch + React.useEffect(() => { + if (!open || !initialFocus) return; + if (initialFocus.type !== "normalized_field") return; + if (normalizedFieldFocusFiredRef.current) return; + const targetId = + initialFocus.field === "provider" + ? "edit-agent-llm-provider" + : "edit-agent-model"; + const el = document.getElementById(targetId); + if (!(el instanceof HTMLElement)) return; + normalizedFieldFocusFiredRef.current = true; + const id = requestAnimationFrame(() => { + el.scrollIntoView({ block: "nearest" }); + el.focus(); + }); + return () => cancelAnimationFrame(id); + }, [open, initialFocus, inst?.pubkey, llmProviderFieldVisible]); + + // ── Render ───────────────────────────────────────────────────────────────── + function handleSubmit() { + void _handleSubmit(canSubmit); + } + + const previewLabel = displayName.trim() || "Agent name"; + const previewAvatarUrl = avatarUrl.trim() || null; + + const dialogTitle = inst + ? `Edit ${inst.name}` + : def + ? `Edit ${def.displayName}` + : "Edit agent"; + + return ( + { + if (!isSaving) onOpenChange(next); + }} + open={open} + > + + + + + } + > +
+ {/* ── Identity column: avatar + preview ────────────────────── */} +
+ setAvatarUrl("")} + onUploadPendingChange={setIsAvatarUploadPending} + onSelectAvatar={setAvatarUrl} + /> +
+ + {/* ── Main form column ──────────────────────────────────────── */} +
+ {/* Team-managed notice */} + {defReadOnly && showDef ? ( +

+ This agent is managed by a team. Its configuration cannot be + edited here. +

+ ) : null} + + {/* Built-in notice */} + {def?.isBuiltIn && !defReadOnly ? ( +

+ Built-in agent — fully editable. +

+ ) : null} + + {/* ── D-section: Identity + Behavior + Runtime ──────────── */} + {showDef ? ( + + ) : null} + + {/* ── I-section: Instance + Runtime (when instance present) ── */} + {showInst && inst ? ( + { + setInstanceEnvVars((prev) => ({ ...prev, [key]: value })); + }} + modelSelectValue={modelSelectValue} + modelDropdownOptions={modelDropdownOptions} + onModelChange={handleModelDropdownChange} + modelDiscoveryLoading={modelDiscoveryLoading} + showCustomModelInput={showCustomModelInput} + model={model} + onModelTextChange={setModel} + modelStatusMessage={modelStatusMessage} + modelRequired={modelRequired} + inheritedSubmission={inheritedSubmission} + inheritedModelDefault={inheritedModelDefault} + inheritedProviderDefault={inheritedProviderDefault} + aiDefaultsOpen={aiDefaultsOpen} + onAiDefaultsOpenChange={setAiDefaultsOpen} + aiDefaultsTriggerRef={aiDefaultsTriggerRef} + showAdvancedFields={showAdvancedFields} + onShowAdvancedFieldsChange={setShowAdvancedFields} + acpCommand={acpCommand} + agentArgs={agentArgs} + autoRestartOnConfigChange={autoRestartOnConfigChange} + instanceEnvVars={instanceEnvVars} + fileSatisfiedEnvKeys={fileSatisfiedEnvKeys} + advancedRequiredEnvKeys={advancedRequiredEnvKeys} + initialFocus={initialFocus} + inheritedEnvVarsForAdvanced={inheritedEnvVarsForAdvanced} + linkedPersona={linkedPersona} + prospectiveRuntimeId={prospectiveRuntimeId} + prospectiveRuntime={prospectiveRuntime} + parallelism={parallelism} + onAcpCommandChange={setAcpCommand} + onAgentArgsChange={setAgentArgs} + onAutoRestartChange={setAutoRestartOnConfigChange} + onEnvVarsChange={setInstanceEnvVars} + onInheritHarnessChange={setInheritHarness} + onParallelismChange={setParallelism} + systemPrompt={systemPrompt} + onSystemPromptChange={setSystemPrompt} + /> + ) : null} + + {/* Save error */} + {saveError ? ( +

{saveError.message}

+ ) : null} +
+
+
+
+ ); +} + +// ── Local helpers ───────────────────────────────────────────────────────────── + +function getDefaultLlmModelLabel(model: string): string { + return model ? `Default (${model})` : "Default model"; +} diff --git a/desktop/src/features/agents/ui/AgentEditMergedDialogDSection.tsx b/desktop/src/features/agents/ui/AgentEditMergedDialogDSection.tsx new file mode 100644 index 0000000000..43b443db09 --- /dev/null +++ b/desktop/src/features/agents/ui/AgentEditMergedDialogDSection.tsx @@ -0,0 +1,242 @@ +/** + * AgentEditMergedDialogDSection.tsx — Definition-field section for the merged edit surface. + * + * Renders agent name, system prompt, and (for definition-only contexts) + * runtime, LLM provider, and model. Team-managed fields render read-only. + * + * Extracted from AgentEditMergedDialog to satisfy the desktop file-size gate. + */ + +import { cn } from "@/shared/lib/cn"; +import { Input } from "@/shared/ui/input"; +import { Textarea } from "@/shared/ui/textarea"; + +import { + PERSONA_FIELD_CONTROL_CLASS, + PERSONA_FIELD_SHELL_CLASS, + PERSONA_LABEL_OPTIONAL_CLASS, + type PersonaDropdownOption, +} from "./agentConfigOptions"; +import { AgentHarnessField } from "./AgentHarnessField"; +import { PersonaDropdownField } from "./PersonaDropdownField"; + +// ── Props ───────────────────────────────────────────────────────────────────── + +export type AgentEditMergedDSectionProps = { + /** When true definition fields are team-managed and rendered read-only. */ + defReadOnly: boolean; + isSaving: boolean; + // Identity + displayName: string; + onDisplayNameChange: (value: string) => void; + // Behavior + systemPrompt: string; + onSystemPromptChange: (value: string) => void; + // Runtime (definition-only — hidden when showInst) + runtimeCatalogStatus: "loading" | "error" | "ready"; + runtimeDropdownValue: string; + defRuntimeDropdownOptions: PersonaDropdownOption[]; + defBlankLabel: string; + onRuntimeChange: (value: string) => void; + // LLM provider (definition-only — hidden when showInst) + llmProviderFieldVisible: boolean; + providerSelectValue: string; + providerDropdownOptions: PersonaDropdownOption[]; + onProviderChange: (value: string) => void; + isCustomProviderEditing: boolean; + provider: string; + onProviderTextChange: (value: string) => void; + // Model (definition-only — hidden when showInst) + modelSelectValue: string; + modelDropdownOptions: PersonaDropdownOption[]; + onModelChange: (value: string) => void; + modelDiscoveryLoading: boolean; + showCustomModelInput: boolean; + model: string; + onModelTextChange: (value: string) => void; + modelStatusMessage: string | null; +}; + +// ── Component ───────────────────────────────────────────────────────────────── + +export function AgentEditMergedDSection({ + defReadOnly, + isSaving, + displayName, + onDisplayNameChange, + systemPrompt, + onSystemPromptChange, + runtimeCatalogStatus, + runtimeDropdownValue, + defRuntimeDropdownOptions, + defBlankLabel, + onRuntimeChange, + llmProviderFieldVisible, + providerSelectValue, + providerDropdownOptions, + onProviderChange, + isCustomProviderEditing, + provider, + onProviderTextChange, + modelSelectValue, + modelDropdownOptions, + onModelChange, + modelDiscoveryLoading, + showCustomModelInput, + model, + onModelTextChange, + modelStatusMessage, +}: AgentEditMergedDSectionProps) { + return ( + <> + {/* Agent name */} +
+ +
+ onDisplayNameChange(e.target.value)} + placeholder="Agent name" + value={displayName} + /> +
+
+ + {/* System prompt */} +
+ +
+