From 50685b952c12ef6a8c7f7a6b6d5ddb8893686b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 17 Apr 2026 21:14:00 -0400 Subject: [PATCH] refactor(agent-form): add tab navigation inside Edit modal; pair short fields in 2 columns; delete dead hidden input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Edit modal was still a vertical scroll of 5 stacked sections (12+ form fields + an embedded Heartbeat card + action bar) even after C5 named the sections. A modal is a transient affordance — making the user scroll inside it was the wrong design. Same IA move we made for the Settings page one round ago: add tabs. Tabs inside the modal: Identity | Runtime | Budget & scope | Behavior Each tab swaps the pane. Tab label carries the section context, so every section body lost its SectionHeader wrapper — the nested "card-in-a-card" look around HeartbeatSection is gone; Behavior tab just renders the heartbeat controls directly. Field-level polish bundled in the same pass because it hit the same render paths: - Identity tab: Name + Role paired in a 2-column grid; Department + Reports To paired. Title spans full width because its values are often longer. The modal no longer grows tall just to fit two short inputs side by side. - Runtime tab: Status stands alone at the top (high-frequency control — pause/resume). Connector + Model paired; Thinking Effort + Workspace paired. Each paired row keeps its inline hint (connector issue / effort-not-supported) tied to the field it belongs to, instead of appearing as loose paragraphs between fields. - Budget tab: Budget input + BudgetBar + burn line cluster into one grouped column so typing a new cap updates the visual context in place. - Behavior tab: HeartbeatSection rendered at the tab root — no SectionHeader wrapper around it. The card's "Continuous Execution" title + Wake-now button now read as the primary content of the tab, not as something buried beneath a header. Also removed: - `` in RuntimeSection — dead code, zero semantic purpose, leftover from an earlier experiment. Caught during the audit. - `SectionHeader.tsx` — no more callers after sections stopped wrapping themselves in it. File deleted. Added i18n keys (zh + en): agent.form.tabsAria. Tab-label copy reuses the existing `agent.form.sectionIdentity`, `sectionRuntime`, `sectionConstraints`, `sectionBehavior` keys — same strings that the removed SectionHeader rendered, now carried by the tab labels. Covers EJ1 / EJ2 / EJ3 / EJ4 / EJ5 / ES1 / ES3 / ES4 from the second-round Edit-modal audit. --- .../agent-form-fields/EditFormBody.tsx | 111 ++++++++++++----- .../edit-sections/ConstraintsSection.tsx | 54 ++++----- .../edit-sections/IdentitySection.tsx | 91 +++++++------- .../edit-sections/RuntimeSection.tsx | 114 +++++++++--------- .../edit-sections/SectionHeader.tsx | 18 --- src/renderer/i18n/en.ts | 1 + src/renderer/i18n/zh.ts | 1 + 7 files changed, 213 insertions(+), 177 deletions(-) delete mode 100644 src/renderer/components/organization/agent-form/agent-form-fields/edit-sections/SectionHeader.tsx diff --git a/src/renderer/components/organization/agent-form/agent-form-fields/EditFormBody.tsx b/src/renderer/components/organization/agent-form/agent-form-fields/EditFormBody.tsx index 2d55c4b3..cedd63ff 100644 --- a/src/renderer/components/organization/agent-form/agent-form-fields/EditFormBody.tsx +++ b/src/renderer/components/organization/agent-form/agent-form-fields/EditFormBody.tsx @@ -13,7 +13,8 @@ import { IdentitySection } from "./edit-sections/IdentitySection"; import { RuntimeSection } from "./edit-sections/RuntimeSection"; import { ConstraintsSection } from "./edit-sections/ConstraintsSection"; import { EditActionBar } from "./edit-sections/EditActionBar"; -import { SectionHeader } from "./edit-sections/SectionHeader"; + +type EditTab = "identity" | "runtime" | "constraints" | "behavior"; interface EditFormBodyProps { agent: AgentRecord; @@ -31,16 +32,17 @@ interface EditFormBodyProps { } /** - * Orchestrator for the agent Edit modal. Owns the save / wake / delete - * side effects, the save-error latch, and the dirty-state derivation; - * renders through five section components so each concern (identity, - * runtime, constraints, behavior, skills) lives in a ≤200 LOC file. + * Agent Edit modal body. Tabs replace the previous single-scroll stack + * so a 12-field edit surface no longer forces the modal to grow tall + * and scroll internally. Same IA pattern as the Settings page. * - * "Dirty" is computed from the form state vs. the persisted agent record - * plus the persisted budget policy. We intentionally compare trimmed - * strings and numeric budget so that whitespace-only edits do not falsely - * enable Save. Root-agent guards against Delete / terminate live here - * and are threaded into the relevant sections. + * Tab state is local — the modal is transient. Default is "identity" + * because that is where users land for name/role/department edits, + * which are the most common touches after the runtime cluster. + * + * Dirty-state and save/wake/delete handlers stay in this orchestrator; + * each tab renders a pure-body section component that receives form + * state + computed flags via props. */ export function EditFormBody({ agent, @@ -57,6 +59,7 @@ export function EditFormBody({ }: EditFormBodyProps) { const t = useT(); const [saveError, setSaveError] = useState(null); + const [activeTab, setActiveTab] = useState("identity"); const pendingApproval = agent.status === "pending_approval"; const isRootAgent = !agent.reportsTo; @@ -83,6 +86,13 @@ export function EditFormBody({ ); }, [form, agent, pendingApproval]); + const tabs: Array<{ id: EditTab; label: string }> = [ + { id: "identity", label: t("agent.form.sectionIdentity") }, + { id: "runtime", label: t("agent.form.sectionRuntime") }, + { id: "constraints", label: t("agent.form.sectionConstraints") }, + { id: "behavior", label: t("agent.form.sectionBehavior") }, + ]; + async function persistAgentDraft() { const meta = { ...form.existingMeta }; if (form.thinkingEffort) meta.thinkingEffort = form.thinkingEffort; @@ -159,28 +169,65 @@ export function EditFormBody({ } return ( -
- - - - - void handleWakeNow()} - /> - +
+ + +
+ {activeTab === "identity" ? ( + + ) : null} + {activeTab === "runtime" ? ( + + ) : null} + {activeTab === "constraints" ? ( + + ) : null} + {activeTab === "behavior" ? ( + void handleWakeNow()} + /> + ) : null} +
+ 0 || agent.spentMonthlyUsd > 0; return ( - - - {showBurn ? ( -
- -

- {t("agent.form.budgetBurn") - .replace("{spent}", formatMoney(agent.spentMonthlyUsd)) - .replace("{budget}", formatMoney(budgetValue))} -

-
- ) : null} +
+
+ + {showBurn ? ( + <> + +

+ {t("agent.form.budgetBurn") + .replace("{spent}", formatMoney(agent.spentMonthlyUsd)) + .replace("{budget}", formatMoney(budgetValue))} +

+ + ) : null} +