Skip to content

refactor(frontend): split AgentTemplateControl into focused modules#4923

Merged
ardaerzin merged 6 commits into
big-agentsfrom
fe-refactor/split-agent-template-control
Jun 28, 2026
Merged

refactor(frontend): split AgentTemplateControl into focused modules#4923
ardaerzin merged 6 commits into
big-agentsfrom
fe-refactor/split-agent-template-control

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Context

AgentTemplateControl.tsx had grown to ~2,070 lines. It held the whole agent-template config panel in one file: the model/connection/harness/sandbox state machine, the tool/MCP/skill list logic, three near-identical item-drawer code paths, the section descriptors, and the render for all three layouts. That made it hard to read and hard to touch without scrolling past unrelated concerns.

This is a behavior-preserving refactor. No feature or UX change. The public AgentTemplateControl export is unchanged; only its internals moved into focused modules.

Changes

Split the orchestrator into a small set of co-located modules under a new agentTemplate/ folder. The orchestrator now just assembles the schema-gated sections array and renders it; everything else lives in a hook or a registry.

Before: one 2,070-line file.
After: a 580-line orchestrator + 8 focused modules.

Module Lines Responsibility
AgentTemplateControl.tsx 580 Orchestrator: assembles sections, renders accordion / tabs / cards + drawers
useModelHarness.tsx 815 Model/connection/harness/sandbox state + the Model & harness and Advanced section bodies
itemDescriptors.tsx 291 Tool/MCP/skill classifiers + describe functions
useAgentTools.ts 149 Tool-list add/remove (function / builtin / gateway / workflow-reference) + derived sets
ItemRow.tsx 145 Presentational rows (avatar, item row, instructions-file row)
itemKinds.tsx 137 ITEM_KINDS registry: the per-kind config that collapses 3 near-identical paths into 1
useConfigItemDrawer.ts 104 Shared draft-then-save state machine for all three list kinds
ConfigItemList.tsx 58 Shared list / empty-state body
agentTemplateUtils.ts 32 Pure helpers (enumLabel, countSummary, cloneItem)

The two real code-sharing wins: tools, MCP servers, and skills now run through one ITEM_KINDS registry and one useConfigItemDrawer state machine instead of three copies, and the Model & harness + Advanced sections share one useModelHarness hook (their state is coupled, so they belong together).

Tests / notes

  • @agenta/entity-ui types:check clean; eslint clean across the orchestrator and all agentTemplate/ files.
  • OSS tsc holds exactly at the pre-existing baseline (593 errors, unchanged) — no errors leaked into consumers.
  • Verified live on the playground against the dev server for this branch. No console errors. Exercised: all sections in the default accordion layout (correct summaries + the AGENTS.md preview row); the Model & harness section drawer (harness cards, model picker, capabilities panel, version-history skeleton); the tool selector popover; the MCP item create drawer (form renders, Create correctly disabled until valid); and both the Tabs and Cards layouts.

What to QA

  • Open an agent variant in the playground. The config panel shows the same sections as before: Model & harness, Instructions, Tools, MCP servers, Skills, Triggers, Advanced, each with its summary.
  • Open the Model & harness section. The drawer shows the harness cards, model picker, the right-hand capabilities panel, and version history.
  • Add a tool (custom function), an MCP server, and a skill. Each opens its drawer; Save is blocked until the draft is valid; the item appears in the list after Save and can be removed.
  • Switch layout from the panel's gear menu between Accordion, Tabs, and Cards. All three render the same sections.
  • Regression to watch: the workflow-reference tool flow (add a workflow as a tool) and the instructions-file editor drawer, since both moved.

…s (phase 1)

Pure, verbatim move out of the 2070-line AgentTemplateControl into a new
`agentTemplate/` folder — no logic change:

- agentTemplateUtils.ts — enumLabel, countSummary, cloneItem
- itemDescriptors.tsx — ItemDescriptor + describeTool/Mcp/Skill/Instruction and
  their classifiers (toolName, isFunctionTool, isStaticSkill, isEmbedRefSkill,
  mdPreview, …)
- ItemRow.tsx — ItemAvatar, ItemRow, InstructionsFileRow

Orchestrator re-imports them; dropped the now-dead phosphor/toolUtils imports.
2070 → 1628 lines. tsc + lint clean.
…ry (phase 2)

Tools, MCP servers and skills were three near-identical copies of: a list body, a
draft-then-save drawer, and the editing/commit/validate state. Collapse them onto
one code path, driven by a per-kind registry:

- itemKinds.tsx — ITEM_KINDS: field, describe, FormView, drawerTitle/width,
  editView, jsonOnly, isReadOnly, createSeed, draftInvalid per kind.
- useConfigItemDrawer.ts — the shared editing/draft/commit/remove/validate
  machine, writing to each kind's array via the registry.
- ConfigItemList.tsx — one list body (rows + empty state) for all three.
- The three ConfigItemDrawer blocks become one registry-driven render.

Behavior preserved exactly (each per-kind rule mapped 1:1). Orchestrator
1638 → 1423 lines. tsc + lint clean.
…Harness

Move the agent-template model/connection/harness/sandbox state and the
Model & harness + Advanced section JSX out of AgentTemplateControl into a
dedicated useModelHarness hook. The two sections share one coupled state
machine (the model/connection state feeds both), so they live together and
the hook returns the summaries + section bodies the orchestrator renders.

AgentTemplateControl drops from ~1.4k to 672 lines; behavior is unchanged.
Move the agent-template Tools add/remove logic (inline function tools,
builtin/gateway tools, async workflow-reference tools, and the derived
selected-name set + referenceable-workflow pool) out of AgentTemplateControl
into a useAgentTools hook. The orchestrator now just wires the handlers into
the picker and renders.

AgentTemplateControl drops to 580 lines (from ~2.1k before the split);
behavior is unchanged.
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jun 28, 2026
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 28, 2026 11:03pm

Request Review

@dosubot dosubot Bot added Frontend refactoring A code change that neither fixes a bug nor adds a feature labels Jun 28, 2026
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 64a140e8-71bb-4cff-8fc4-e5a5076d63fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

AgentTemplateControl.tsx is refactored by extracting ~1,500 lines of inline logic into dedicated modules: itemDescriptors.tsx (classifier functions), itemKinds.tsx (ITEM_KINDS registry), useConfigItemDrawer.ts (draft state machine), useAgentTools.ts (tool management), useModelHarness.tsx (model/harness/advanced UI), ConfigItemList.tsx, ItemRow.tsx, and agentTemplateUtils.ts.

Changes

Agent Template Control Refactor

Layer / File(s) Summary
Item descriptor interface and classifiers
agentTemplate/itemDescriptors.tsx, agentTemplate/agentTemplateUtils.ts
Introduces ItemDescriptor interface and all per-kind classifier/mapper functions (describeTool, describeMcp, describeSkill, describeInstruction, mdPreview, isStaticSkill, isEmbedRefSkill, etc.) plus shared utilities enumLabel, countSummary, cloneItem.
ITEM_KINDS registry
agentTemplate/itemKinds.tsx
Defines ItemKind union, ItemKindDef interface, and the ITEM_KINDS registry for tool/mcp/skill covering field mapping, drawer metadata, FormView, seed creation, and draft-invalid validation per kind.
ItemRow, ItemAvatar, InstructionsFileRow components
agentTemplate/ItemRow.tsx
Adds three presentational components: ItemAvatar (colored square avatar), ItemRow (keyboard-accessible config row with optional remove button), and InstructionsFileRow (clickable markdown file row with word count and clamped preview).
ConfigItemList component
agentTemplate/ConfigItemList.tsx
Adds ConfigItemList which uses ITEM_KINDS[kind] to render ItemRow entries, wires openEdit/removeItem/closeEditor, disables rows per isReadOnly, and shows an empty-state line with an emptyAdd node.
useConfigItemDrawer draft state machine
agentTemplate/useConfigItemDrawer.ts
Introduces useConfigItemDrawer with EditingState interface, draft state, jsonInvalid/draftInvalid flags, and openCreate/openEdit/closeEditor/commitDraft/removeItem callbacks for a draft-then-save pattern.
useAgentTools hook
agentTemplate/useAgentTools.ts
Introduces useAgentTools normalizing config.tools, handling custom-source routing to openCreate, async workflow-reference appending with schema resolution and duplicate guard, removal helpers, selectedToolNames, and referenceableWorkflows.
useModelHarness hook
agentTemplate/useModelHarness.tsx
Introduces useModelHarness deriving harness/runner/sandbox schema slices, computing capability-aware model/mode options, implementing writeModel, building harness cards and a compatibility panel, rendering Authentication/Execution/Permissions Advanced controls, and returning drawer/inline JSX bodies with layout widths and summary flags.
AgentTemplateControl wired to new hooks
SchemaControls/AgentTemplateControl.tsx
Replaces ~1,500 lines of inline logic by consuming useConfigItemDrawer, useAgentTools, useModelHarness, ConfigItemList, and ITEM_KINDS; section descriptors, list rendering, unified item drawer UI, and SectionDrawer bodies are all delegated to the new building blocks.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Agenta-AI/agenta#4877: Introduces workflow-as-tool reference additions that directly overlap with the new toolReferenceSlug, describeTool type:"reference" handling, and useAgentTools's handleAddWorkflowReference logic.
  • Agenta-AI/agenta#4881: Adds SectionDrawer and ConfigAccordionSection drawer-opener behavior that the main PR's refactored AgentTemplateControl.tsx directly reuses for model-harness and advanced section drawers.
  • Agenta-AI/agenta#4883: Modifies the same AgentTemplateControl.tsx top-level section/tabs rendering logic by adding useAgentTriggers and a Triggers section, overlapping with the main PR's restructured section array and orchestration layer.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactor: splitting AgentTemplateControl into focused modules.
Description check ✅ Passed The description matches the refactor and its extracted modules, and is clearly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 72.00% which is sufficient. The required threshold is 60.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fe-refactor/split-agent-template-control

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ItemRow.tsx (1)

14-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the static avatar styles into Tailwind classes.

Only background is dynamic here; fontSize, fontWeight, and lineHeight should live in classes to match the frontend styling guideline. As per coding guidelines, "Prefer Tailwind utility classes over CSS-in-JS, inline style={{...}}, and separate CSS files; use CSS-in-JS only for complex Ant Design overrides, JS-calculated dynamic theme styles, or legacy components."

Source: Coding guidelines

web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx (1)

1-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim copied narrative comments to concise notes.

Several extracted comments exceed the project’s one-line comment preference. Keep only the surprising constraints inline and move background/tracking detail to docs. As per coding guidelines, “Keep in-code comments to one short line maximum unless a genuinely surprising constraint requires a brief exception.”

Also applies to: 178-186, 323-332, 650-651, 680-685

Source: Coding guidelines

web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx (1)

96-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim the new explanatory comment blocks.

This block and the nearby newly added multi-line prose comments should be reduced to a single short comment or moved to docs.

As per coding guidelines, “Keep in-code comments to one short line maximum unless a genuinely surprising constraint requires a brief exception.”

Suggested local cleanup
-    // The item-config drawer (tools / MCP servers / skills). Edits happen on a local `draft`; they
-    // only apply to the config on Save, so creating an item never pollutes the config until
-    // confirmed, and editing an existing item can be cancelled cleanly (Cancel/X discards the
-    // draft). The shared machine writes to each kind's array via the ITEM_KINDS registry.
+    // Shared draft-then-save drawer for tools, MCP servers, and skills.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6cebc83a-2316-4421-9e4c-a1a6a212a457

📥 Commits

Reviewing files that changed from the base of the PR and between ebc4ec1 and 29b411d.

📒 Files selected for processing (9)
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ConfigItemList.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ItemRow.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/agentTemplateUtils.ts
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/itemDescriptors.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/itemKinds.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useAgentTools.ts
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useConfigItemDrawer.ts
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx

Comment on lines +37 to +46
<ItemRow
key={`${kind}-${index}`}
descriptor={def.describe(item)}
onEdit={() => openEdit(kind, index, item, def.editView(item))}
onRemove={() => {
removeItem(kind, index)
closeEditor()
}}
// Read-only items (static `__ag__*` skills) can't be removed and open disabled.
disabled={disabled || def.isReadOnly(item)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't wire openEdit into rows that are meant to be read-only.

ConfigItemList treats disabled || def.isReadOnly(item) as if it disables the row, but ItemRow still fires onEdit on click and keyboard activation. Right now static/read-only skills only lose the trash button; they can still open the drawer and be saved back through useConfigItemDrawer.

Suggested fix
                 {items.map((item, index) => (
+                    (() => {
+                        const rowDisabled = Boolean(disabled || def.isReadOnly(item))
+                        return (
                     <ItemRow
                         key={`${kind}-${index}`}
                         descriptor={def.describe(item)}
-                        onEdit={() => openEdit(kind, index, item, def.editView(item))}
+                        onEdit={() => {
+                            if (rowDisabled) return
+                            openEdit(kind, index, item, def.editView(item))
+                        }}
                         onRemove={() => {
                             removeItem(kind, index)
                             closeEditor()
                         }}
-                        // Read-only items (static `__ag__*` skills) can't be removed and open disabled.
-                        disabled={disabled || def.isReadOnly(item)}
+                        disabled={rowDisabled}
                     />
+                        )
+                    })()
                 ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ItemRow
key={`${kind}-${index}`}
descriptor={def.describe(item)}
onEdit={() => openEdit(kind, index, item, def.editView(item))}
onRemove={() => {
removeItem(kind, index)
closeEditor()
}}
// Read-only items (static `__ag__*` skills) can't be removed and open disabled.
disabled={disabled || def.isReadOnly(item)}
{items.map((item, index) => (
(() => {
const rowDisabled = Boolean(disabled || def.isReadOnly(item))
return (
<ItemRow
key={`${kind}-${index}`}
descriptor={def.describe(item)}
onEdit={() => {
if (rowDisabled) return
openEdit(kind, index, item, def.editView(item))
}}
onRemove={() => {
removeItem(kind, index)
closeEditor()
}}
disabled={rowDisabled}
/>
)
})()
))}

Comment on lines +82 to +89
editView: (item) => (isFunctionTool(item) ? "form" : "json"),
jsonOnly: (draft) => !isFunctionTool(draft),
isReadOnly: () => false,
createSeed: () => ({}),
draftInvalid: (draft) => {
const fn = draft.function as Record<string, unknown> | undefined
if (fn && typeof fn === "object") return !String(fn.name ?? "").trim()
return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fresh tool drafts are saveable as {}.

useConfigItemDrawer uses ITEM_KINDS[editing.kind].draftInvalid(draft) as the only save gate and then commits draft directly. With createSeed: () => ({}) and this validator, a brand-new tool is immediately valid unless it already has a function object, so the drawer can persist an empty {} into config.tools.

Suggested fix
     tool: {
         kind: "tool",
         field: "tools",
@@
-        createSeed: () => ({}),
+        createSeed: () => ({
+            type: "function",
+            function: {name: "", description: "", parameters: {type: "object", properties: {}}},
+        }),
         draftInvalid: (draft) => {
             const fn = draft.function as Record<string, unknown> | undefined
             if (fn && typeof fn === "object") return !String(fn.name ?? "").trim()
-            return false
+            return !String((draft as Record<string, unknown>).type ?? "").trim()
         },
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
editView: (item) => (isFunctionTool(item) ? "form" : "json"),
jsonOnly: (draft) => !isFunctionTool(draft),
isReadOnly: () => false,
createSeed: () => ({}),
draftInvalid: (draft) => {
const fn = draft.function as Record<string, unknown> | undefined
if (fn && typeof fn === "object") return !String(fn.name ?? "").trim()
return false
editView: (item) => (isFunctionTool(item) ? "form" : "json"),
jsonOnly: (draft) => !isFunctionTool(draft),
isReadOnly: () => false,
createSeed: () => ({
type: "function",
function: {name: "", description: "", parameters: {type: "object", properties: {}}},
}),
draftInvalid: (draft) => {
const fn = draft.function as Record<string, unknown> | undefined
if (fn && typeof fn === "object") return !String(fn.name ?? "").trim()
return !String((draft as Record<string, unknown>).type ?? "").trim()

Comment on lines +39 to +80
<div
role="button"
tabIndex={0}
onClick={onEdit}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
onEdit()
}
}}
className="group flex cursor-pointer items-center gap-2.5 rounded border border-solid border-[var(--ag-c-EAEFF5,#eaeff5)] px-3 py-2 transition-colors hover:border-[var(--ag-c-97A4B0,#97a4b0)]"
>
<ItemAvatar descriptor={descriptor} />
<div className="min-w-0 flex-1">
<div className="truncate font-mono text-xs font-medium">{descriptor.name}</div>
{descriptor.description ? (
<Typography.Text
type="secondary"
className="block truncate text-xs leading-tight"
>
{descriptor.description}
</Typography.Text>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1.5">
{descriptor.tags.map((tag) => (
<Tag key={tag} className="m-0 text-[11px]">
{tag}
</Tag>
))}
{onRemove && !disabled ? (
<button
type="button"
aria-label="Remove"
onClick={(e) => {
e.stopPropagation()
onRemove()
}}
className="flex cursor-pointer items-center border-0 bg-transparent p-0 text-[var(--ag-c-97A4B0,#97a4b0)] opacity-0 transition-opacity hover:text-[var(--ag-c-FF4D4F,#ff4d4f)] group-hover:opacity-100"
>
<Trash size={14} />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Disabled/read-only rows still open, and keyboarding the trash button can also fire edit.

ConfigItemList passes disabled specifically to prevent opening read-only rows, but this container still calls onEdit on click/Enter/Space unconditionally. Because the key handler is on the parent, pressing Enter/Space while the nested remove button has focus can also bubble up and open the editor.

Suggested fix
 export function ItemRow({
@@
 }) {
+    const handleOpen = () => {
+        if (!disabled) onEdit()
+    }
+
     return (
         <div
             role="button"
-            tabIndex={0}
-            onClick={onEdit}
+            tabIndex={disabled ? -1 : 0}
+            aria-disabled={disabled || undefined}
+            onClick={handleOpen}
             onKeyDown={(e) => {
+                if (e.currentTarget !== e.target || disabled) return
                 if (e.key === "Enter" || e.key === " ") {
                     e.preventDefault()
-                    onEdit()
+                    onEdit()
                 }
             }}
-            className="group flex cursor-pointer items-center gap-2.5 rounded border border-solid border-[var(--ag-c-EAEFF5,`#eaeff5`)] px-3 py-2 transition-colors hover:border-[var(--ag-c-97A4B0,`#97a4b0`)]"
+            className="group flex items-center gap-2.5 rounded border border-solid border-[var(--ag-c-EAEFF5,`#eaeff5`)] px-3 py-2 transition-colors hover:border-[var(--ag-c-97A4B0,`#97a4b0`)] data-[disabled=true]:cursor-default"
+            data-disabled={disabled || undefined}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
role="button"
tabIndex={0}
onClick={onEdit}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
onEdit()
}
}}
className="group flex cursor-pointer items-center gap-2.5 rounded border border-solid border-[var(--ag-c-EAEFF5,#eaeff5)] px-3 py-2 transition-colors hover:border-[var(--ag-c-97A4B0,#97a4b0)]"
>
<ItemAvatar descriptor={descriptor} />
<div className="min-w-0 flex-1">
<div className="truncate font-mono text-xs font-medium">{descriptor.name}</div>
{descriptor.description ? (
<Typography.Text
type="secondary"
className="block truncate text-xs leading-tight"
>
{descriptor.description}
</Typography.Text>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1.5">
{descriptor.tags.map((tag) => (
<Tag key={tag} className="m-0 text-[11px]">
{tag}
</Tag>
))}
{onRemove && !disabled ? (
<button
type="button"
aria-label="Remove"
onClick={(e) => {
e.stopPropagation()
onRemove()
}}
className="flex cursor-pointer items-center border-0 bg-transparent p-0 text-[var(--ag-c-97A4B0,#97a4b0)] opacity-0 transition-opacity hover:text-[var(--ag-c-FF4D4F,#ff4d4f)] group-hover:opacity-100"
>
<Trash size={14} />
</button>
const handleOpen = () => {
if (!disabled) onEdit()
}
<div
role="button"
tabIndex={disabled ? -1 : 0}
aria-disabled={disabled || undefined}
onClick={handleOpen}
onKeyDown={(e) => {
if (e.currentTarget !== e.target || disabled) return
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
onEdit()
}
}}
className="group flex items-center gap-2.5 rounded border border-solid border-[var(--ag-c-EAEFF5,`#eaeff5`)] px-3 py-2 transition-colors hover:border-[var(--ag-c-97A4B0,`#97a4b0`)] data-[disabled=true]:cursor-default"
data-disabled={disabled || undefined}
>

Comment on lines +153 to +172
const nextModelId = patch.modelId !== undefined ? patch.modelId : modelId
// When the model changes, derive the provider from the picked model; otherwise keep it.
let nextProvider: string | null
if (patch.provider !== undefined) {
nextProvider = patch.provider
} else if (patch.modelId !== undefined) {
nextProvider =
providerForModel(capabilities, harnessValue, nextModelId) ?? connection.provider
} else {
nextProvider = connection.provider
}
setAgentField(
"llm",
composeModelValue({
modelId: nextModelId,
provider: nextProvider,
mode: patch.mode !== undefined ? patch.mode : connection.mode,
slug: patch.slug !== undefined ? patch.slug : connection.slug,
existing: llm,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear stale named connections when the provider changes.

writeModel({modelId}) can derive a new provider while retaining the previous connection.slug. Since named connection options are filtered by provider, this can persist an old-provider credential slug that is no longer selectable for the new model/provider.

Proposed fix
             } else {
                 nextProvider = connection.provider
             }
+            const nextMode = patch.mode !== undefined ? patch.mode : connection.mode
+            const nextSlug =
+                patch.slug !== undefined
+                    ? patch.slug
+                    : nextMode !== "agenta" || nextProvider !== connection.provider
+                      ? null
+                      : connection.slug
             setAgentField(
                 "llm",
                 composeModelValue({
                     modelId: nextModelId,
                     provider: nextProvider,
-                    mode: patch.mode !== undefined ? patch.mode : connection.mode,
-                    slug: patch.slug !== undefined ? patch.slug : connection.slug,
+                    mode: nextMode,
+                    slug: nextSlug,
                     existing: llm,
                 }),
             )

Also applies to: 193-196

Comment on lines +495 to +499
view={drawerView}
onViewChange={setDrawerView}
onCancel={closeEditor}
onSave={commitDraft}
saveDisabled={draftInvalid || (drawerView === "json" && jsonInvalid)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset stale JSON validity when switching drawer views.

If the user makes JSON invalid, switches to Form, then switches back to JSON, the editor remounts from the valid draft but jsonInvalid remains true, leaving Save disabled until another JSON edit.

Proposed fix
-                              onViewChange={setDrawerView}
+                              onViewChange={(view) => {
+                                  setDrawerView(view)
+                                  setJsonInvalid(false)
+                              }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
view={drawerView}
onViewChange={setDrawerView}
onCancel={closeEditor}
onSave={commitDraft}
saveDisabled={draftInvalid || (drawerView === "json" && jsonInvalid)}
view={drawerView}
onViewChange={(view) => {
setDrawerView(view)
setJsonInvalid(false)
}}
onCancel={closeEditor}
onSave={commitDraft}
saveDisabled={draftInvalid || (drawerView === "json" && jsonInvalid)}

Address review nitpicks: move the item-avatar's static font styles into
Tailwind classes (only background stays inline/dynamic), and compress the
extracted modules' multi-line narrative comments to one-/two-line notes,
keeping the genuinely surprising constraints (deliberate model non-clearing,
vault-async slug, tracked harness-capabilities gap).

No behavior change.
Clarify in itemKinds that tool creation seeds from the picker, not the
registry's createSeed stub (defuses a false-positive review read of an
empty-tool save path).

Record two review items from PR #4923 as deferred against the upcoming
schema-driven config redesign: tighter per-kind tool draft validation (kind
discriminated union) and re-binding the named-connection slug on a provider
change (gated on first-class provider via ModelSpec). Both are correct for the
redesign, not the current code, so they live in the design doc's Deferred list.
@ardaerzin

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass. I dug into each against the current code and the agent-config design docs. Summary: all five describe pre-existing behavior (this PR is a verbatim extraction of AgentTemplateControl, no behavior change), and the four "Major" items are either inaccurate for the current code or correct only for the upcoming schema-driven redesign. Detail:

Read-only skills "saved back" (ConfigItemList, ItemRow): static __ag__* skills open a view-only drawer, but Save is blocked. AgentTemplateControl passes disabled={readOnly} to ConfigItemDrawer, whose Save button is disabled={disabled || saveDisabled}, and the form/JSON editors are disabled too. So they cannot be edited or persisted. Opening to view is the intended read-only UX. The ItemRow here is byte-identical to the original.

Empty {} tool (itemKinds): the only custom-create path is buildInlineFunctionTool (ToolSelectorPopover.tsx:186), which seeds a complete {type:"function", function:{name,…}}; draftInvalid then blocks an empty function name. tool.createSeed: () => ({}) is an unused stub (now annotated). No path persists {}. The stricter per-kind validation you describe is correct under the incoming kind-discriminated tool schema, which replaces this registry; tracked as deferred.

Stale connection slug on provider change (useModelHarness): intentional. The connection option list is vault-secret async, so clearing the slug eagerly would wipe a valid slug mid-load. This is documented in design.md (deliberate "keep, don't auto-remap") and in the inline comment. The proper fix is first-class provider via ModelSpec (model-auth redesign); tracked as deferred.

Stale jsonInvalid on view switch (AgentTemplateControl): real but minor and pre-existing; Save just stays disabled until the next JSON edit. Noted for a follow-up.

Nitpicks (avatar inline styles → Tailwind, comment length) are addressed in 66d3572. The two deferred items are recorded in docs/design/agent-config-section-drawers/design.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend refactoring A code change that neither fixes a bug nor adds a feature size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant