refactor(agent-form): split EditFormBody into 5 semantic sections + add Cancel/dirty-state + always-visible Thinking Effort + binary OrgChart hierarchy

Previously the agent Edit modal was a 351-LOC wall of 17 stacked form
fields with no visual grouping, mixed header styles between the
skills heading and the field labels, a Save button that was always
enabled, no Cancel affordance, and a Thinking Effort field that
silently disappeared when the selected connector did not support it.

This commit restructures the edit surface into five named sections
— Identity, Runtime, Budget & scope, Behavior, Skills — each with a
consistent SectionHeader (11px uppercase muted, border-top divider).
Every section lives in its own ≤150-LOC file under
agent-form-fields/edit-sections/ so nothing stays god-sized:

  EditFormBody.tsx          199 LOC (orchestrator: isDirty, handlers)
  IdentitySection.tsx        74 LOC
  RuntimeSection.tsx        114 LOC
  ConstraintsSection.tsx     58 LOC
  SkillsSection.tsx          36 LOC
  EditActionBar.tsx          87 LOC
  SectionHeader.tsx          18 LOC

Behavioral changes:

- Save is disabled until the form is dirty (trimmed-string + numeric
  budget comparison against the persisted agent record). The board
  sees whether changes are captured without clicking Save to find out.
- Cancel button added, wired through AgentForm → AgentEditor →
  OrganizationSections, which clears selectedAgentId (same as the
  modal's close X — a proper bottom action avoids users reaching for
  the corner).
- Thinking Effort is always shown. When the connector provider does
  not accept the hint the Select is disabled with an inline note,
  so the option is discoverable instead of silently hidden.
- Budget shows a live BudgetBar + "Spent $X of $Y" line when the
  budget or spend is non-zero. The raw number input on its own left
  burn state invisible.
- Capabilities placeholder replaced with a concrete example
  ("writes React components, reviews PRs, ...") — the previous
  "What this agent does" was a circular prompt.
- OrgChart role bar went from three tiers (CEO 4px primary /
  manager 3px warning / IC 2px muted 30%) to binary (root 4px
  primary / everyone else 2px border). Orange-warning for managers
  read as an alert, not a rank; managerial position is already
  encoded in the tree's topology.

Also added i18n keys (zh + en):
  agent.form.sectionIdentity / sectionRuntime / sectionConstraints /
  sectionBehavior / effortNotSupported / budgetBurn; updated
  capabilitiesPlaceholder copy.

Covers J2 / J7 / J8 / S5 / S6 / S7 / S8.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 19:50:53 -04:00
1 parent 62d0964af3
commit 9981ee342b
13 files changed
+510 -261

No files matched your search

@@ -131,10 +131,15 @@ export function OrgChart({ agents, connectors, onSelectAgent }: { agents: AgentR
const isActive = node.agent.status === "active" || isRunning;
const isError = node.agent.status === "error";
const isPending = node.agent.status === "pending_approval";
const isCEO = node.children.length > 0 && !node.agent.reportsTo;
// Binary hierarchy: the root (no reportsTo) gets a bold primary bar;
// everyone else gets a hairline neutral bar. Managerial rank is
// already communicated by tree position — duplicating it via a
// color (previously "warning" orange, which read as an alert
// rather than a rank) was misleading.
const isRoot = !node.agent.reportsTo;
return (
<div key={node.agent.id} data-org-card role="button" tabIndex={0} aria-label={t("org.agentAria").replace("{name}", node.agent.name).replace("{title}", node.agent.title || node.agent.role).replace("{status}", node.agent.status)} className={`absolute cursor-pointer select-none rounded-lg border bg-card shadow-sm transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${isRunning ? "border-[color:var(--primary)] shadow-[0_0_12px_color-mix(in_oklch,var(--success)_15%,transparent)]" : isError ? "border-[color:var(--destructive)] border-opacity-50" : isPending ? "border-[color:var(--destructive)] border-dashed" : "border-border hover:border-border"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelectAgent?.(node.agent.id); } }}>
<div className={`rounded-t-[7px] ${isCEO ? "h-[4px] bg-primary" : node.children.length > 0 ? "h-[3px] bg-warning" : "h-[2px] bg-[color:var(--muted-foreground)] opacity-30"}`} />
<div className={`rounded-t-[7px] ${isRoot ? "h-[4px] bg-primary" : "h-[2px] bg-border"}`} />
<div className="flex items-center gap-3 px-4 py-3">
<div className="relative shrink-0">
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-success-muted text-success" : isError ? "bg-destructive-muted text-destructive" : "bg-muted text-muted-foreground"}`}>{initials}</div>