Files
buzz/desktop/src/features/agents/ui/AgentEditMergedDialogDSection.tsx
Duncan 45d76bbb90 feat(desktop): Phase 1 R2 — merged agent edit surface (one form, one coordinator)
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 <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 14:20:00 -04:00

243 lines
7.8 KiB
TypeScript

/**
* 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 */}
<div className="space-y-1.5">
<label
className="text-sm font-medium text-foreground"
htmlFor="edit-agent-display-name"
>
Agent name
</label>
<div
className={cn(
"flex min-h-11 items-center px-3",
PERSONA_FIELD_SHELL_CLASS,
)}
>
<Input
autoCorrect="off"
className={cn(
"h-8 px-0 py-0 leading-6",
PERSONA_FIELD_CONTROL_CLASS,
)}
disabled={isSaving || defReadOnly}
id="edit-agent-display-name"
onChange={(e) => onDisplayNameChange(e.target.value)}
placeholder="Agent name"
value={displayName}
/>
</div>
</div>
{/* System prompt */}
<div className="space-y-1.5">
<label
className="text-sm font-medium text-foreground"
htmlFor="edit-agent-system-prompt"
>
Agent instructions
</label>
<div className={PERSONA_FIELD_SHELL_CLASS}>
<Textarea
className={cn(
"min-h-40 resize-y px-3 py-3 leading-5",
PERSONA_FIELD_CONTROL_CLASS,
)}
disabled={isSaving || defReadOnly}
id="edit-agent-system-prompt"
onChange={(e) => onSystemPromptChange(e.target.value)}
placeholder="Describe what this agent should do."
value={systemPrompt}
/>
</div>
</div>
{/* Runtime (harness) — D-field, shown for all definition contexts */}
<AgentHarnessField
disabled={isSaving || runtimeCatalogStatus === "loading" || defReadOnly}
onValueChange={onRuntimeChange}
options={defRuntimeDropdownOptions}
placeholder={defBlankLabel}
value={runtimeDropdownValue}
warning={null}
/>
{/* LLM provider — D-field */}
{llmProviderFieldVisible ? (
<div className="space-y-1.5">
<label
className="text-sm font-medium text-foreground"
htmlFor="edit-agent-llm-provider"
>
LLM provider
<span className={PERSONA_LABEL_OPTIONAL_CLASS}>Optional</span>
</label>
<PersonaDropdownField
disabled={isSaving || defReadOnly}
id="edit-agent-llm-provider"
onValueChange={onProviderChange}
options={providerDropdownOptions}
placeholder="Default (auto)"
value={providerSelectValue}
/>
{isCustomProviderEditing ? (
<div
className={cn(
"mt-2 flex min-h-11 items-center px-3",
PERSONA_FIELD_SHELL_CLASS,
)}
>
<Input
aria-label="Custom provider ID"
autoCorrect="off"
className={cn(
"h-8 px-0 py-0 leading-6",
PERSONA_FIELD_CONTROL_CLASS,
)}
disabled={isSaving || defReadOnly}
id="edit-agent-custom-provider"
onChange={(e) => onProviderTextChange(e.target.value)}
placeholder="Custom provider ID"
value={provider}
/>
</div>
) : null}
</div>
) : null}
{/* Model — D-field */}
<div className="space-y-1.5">
<label
className="text-sm font-medium text-foreground"
htmlFor="edit-agent-model"
>
Model
<span className={PERSONA_LABEL_OPTIONAL_CLASS}>Optional</span>
</label>
<PersonaDropdownField
disabled={isSaving || modelDiscoveryLoading || defReadOnly}
id="edit-agent-model"
onValueChange={onModelChange}
options={modelDropdownOptions}
placeholder="Default model"
value={modelSelectValue}
/>
{showCustomModelInput ? (
<div
className={cn(
"mt-2 flex min-h-11 items-center px-3",
PERSONA_FIELD_SHELL_CLASS,
)}
>
<Input
aria-label="Custom model ID"
autoCorrect="off"
className={cn(
"h-8 px-0 py-0 leading-6",
PERSONA_FIELD_CONTROL_CLASS,
)}
disabled={isSaving || defReadOnly}
id="edit-agent-custom-model"
onChange={(e) => onModelTextChange(e.target.value)}
placeholder="Custom model ID"
value={model}
/>
</div>
) : null}
{modelStatusMessage ? (
<p className="text-xs text-muted-foreground">{modelStatusMessage}</p>
) : null}
</div>
</>
);
}