Skip to content

feat(frontend): agent playground config panel + agent run-lane wire fixes#4850

Merged
mmabrouk merged 10 commits into
big-agentsfrom
fe-feat/agent-config-panel-onbig
Jun 25, 2026
Merged

feat(frontend): agent playground config panel + agent run-lane wire fixes#4850
mmabrouk merged 10 commits into
big-agentsfrom
fe-feat/agent-config-panel-onbig

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Context

The agent playground on big-agents shipped with a placeholder config form. This branch restores and extends our agent config panel and fixes the agent run lane so a configured agent runs end to end. It is reconciled onto the current big-agents (merged, 0 behind), and folds in big-agents' Pi permission-policy gating.

What this adds

A sectioned (accordion) agent config panel:

  • Model & harness: harness picker plus an inspect-driven model catalog.
  • Authentication: agenta-managed vs self-managed, with a connection picker.
  • Instructions, Tools, MCP servers, Skills.
  • Advanced: sandbox, sandbox permissions, and (Claude-only) permission policy.

New building blocks sit next to AgentConfigControl: ConfigAccordionSection, ConfigItemDrawer, form views (ToolFormView, SkillFormView, McpServerFormView), editors (CodeEditor, MarkdownEditor, JsonObjectEditor), HarnessSelectControl, SandboxPermissionControl, and SkillUploadZone.

Fixes in this branch

  • Create/edit drawer rendered the agent chat blank pre-creation because the drawer never mounted the agent generation panel. It now does (WorkflowRevisionDrawerWrapper).
  • Model catalog for drafts: /inspect was gated on a server uri/url that an uncommitted draft does not have yet, so the picker showed "No data". It now falls back to the builtin agent uri for drafts (workflow/state/store.ts).
  • Execution header: the testset connector and Clear/Run-all are hidden for agent entities, which run from the panel composer and have no testcase rows.
  • Tool picker popover: a connection with many actions (Gmail has 61) grew the popover unbounded and changed its height per provider, shifting the panel. It now has a fixed height with each pane scrolling internally.

Agent run-lane contracts (for Mahmoud / the agent service)

These are the FE-to-BE touch points this branch depends on. Each matches the current backend; listing them so they stay aligned as the agent service evolves.

  1. Message format header. The FE sends x-ag-messages-format: vercel on the agent /messages request (matches VERCEL_MESSAGE_PROTOCOL in sdk/agents/adapters/vercel/routing.py). Without it the endpoint negotiates down to batch JSON and useChat cannot render the stream.

  2. Custom tool shape. App-defined function tools are emitted as the typed client config, not the OpenAI shape.

    Before (rejected by coerce_tool_config with "Unsupported tool configuration shape"):

    {type: "function", function: {name, description, parameters}, permission}
    

    After:

    {type: "client", name, description, input_schema, permission?, needs_approval?}
    

    Gateway slugs (tools.provider.integration.action.connection) and already-typed tools pass through untouched. See agentRequest.ts and its unit tests.

  3. Model catalog. The picker reads /inspect meta.harness_capabilities. For an uncommitted draft (no server uri/url) it falls back to the builtin agent uri so the catalog still resolves; if inspect returns no models it falls back to the schema enum.

  4. Connection / ModelRef. The auth cards build Connection{mode, slug?} and ModelRef{provider, model, params, connection} from the connections contract (agenta vs self_managed; slug only for agenta).

  5. Permission policy is Claude-only. The field is hidden for Pi harnesses (pi_core, pi_agenta), which never gate tool use. Folded in from big-agents during the merge.

Integration dependencies not in this PR

  • Third-party (gateway) tools depend on the gateway_connections table from [feat] Add triggers (connections, subscriptions, schedules) #4749. On a base without that rename, the connection endpoints return 500 (relation "tool_connections" does not exist) and the picker shows no connections. The FE is ready and lights up once [feat] Add triggers (connections, subscriptions, schedules) #4749 lands.
  • HITL approvals: the FE resume path is in place, but a gated tool that parks still emits a terminal tool-output-error for the same toolCallId in the same turn, which overwrites the approval-request state in useChat. Suppressing the terminal denial while parked is a backend/SDK change.

Tests / notes

  • agentRequest unit tests cover the message-format header and the client-tool rewrite (22 passing).
  • agenta-entity-ui: tsc clean, eslint clean, 126 unit tests passing.
  • Verified live on the agent playground: create from home, config edit and discard, JSON round-trip, chat send, gateway tool add, popover scroll.

What to QA

  • Create an agent app from home. The create drawer shows the agent chat, not a blank panel.
  • Switch harness and model from the inspect catalog. Pre-creation the model list is populated, not "No data".
  • Add a tool, a skill (with file upload), and an MCP server. Toggle JSON edit and confirm it round-trips with the form.
  • Open the tool picker with a connected integration that has many actions. The popover stays a fixed height and the list scrolls; switching providers does not shift the layout. (Needs [feat] Add triggers (connections, subscriptions, schedules) #4749 for connections to load.)
  • Permission policy field shows for Claude and is hidden for Pi.
  • Regression: the prompt (non-agent) playground is unaffected.

…ents

Rebuilds the contained agent config-panel branch on top of big-agents after the agent stack landed
(#4830 canonical /inspect + models, #4839 model picker, #4840 collapse run-selection into AgentConfig
+ harness_kwargs). The inspect-driven picker, connectionUtils, inspectMeta and EnumSelectControl we
had ported now live in big-agents verbatim, so they collapse out — this branch is only our genuine UX
delta on top:

- AgentConfigControl as schema-driven accordion sections (ConfigAccordionSection primitive)
- per-item drawers on the shared EnhancedDrawer (ConfigItemDrawer) with lazy content
- two-pane skill editor, MCP server form, tool form, harness select, markdown/code/JSON editors
- Authentication as radio cards with explainers; live JSON-edit sync
- chat panel: inline per-turn run errors + truncation, empty-turn collapse
- playground wiring (header menu view modes, height calc, router)

Coherent with the merged backend: harness/sandbox/permission_policy ride parameters.agent,
harness_kwargs bag, pi_core default — taken from big-agents (the earlier Option-A reverts are undone).

tsc 0 (entity-ui/agenta-ui/entities/playground; oss 593 baseline, 0 new), eslint clean,
unit tests pass (entity-ui 126, playground 122, entities 683).
The app create/edit drawer (WorkflowRevisionDrawerWrapper) was wired for chat
and completion but left agents half-functional:

- The drawer never injected AgentGenerationPanel, so the agent generation arm
  rendered null (a blank Chat pane) — users couldn't invoke an agent before
  creating it, unlike chat/completion. Inject AgentChatPanel into the drawer's
  playground providers, mirroring the full playground.

- The model picker showed "No data" pre-creation. Inspect (the canonical
  per-harness model catalog) only resolved its uri/serviceUrl from the server
  query, which is empty for an uncommitted draft, and was gated on has_url
  (false before deploy). A builtin agent's service URL is always derivable
  (the same one invocationUrl invokes), and harness_capabilities is published
  per service, not per revision. workflowInspectAtomFamily now falls back to the
  local entity's builtin uri/url for agent drafts and treats has_url as true for
  builtin agents, so the picker resolves from inspect before the first commit.
  Scoped to agents; non-agent drafts are unchanged.

- The chat header's testset connector drives testcase-row runs, which the agent
  lane (panel composer, no rows) doesn't use. Hide it for agents, consistent
  with the Clear / Run all controls already gated the same way.
… header

The agent chat lane posts AI-SDK (Vercel) UIMessages to the agent /messages
endpoint. Declare that format on the request so the backend can pick the right
adapter from the header instead of inferring it from the route.

- agentRequest.ts and AgentChatSlice/transport.ts now send
  'x-ag-messages-format: vercel', matching the backend's VERCEL_MESSAGE_PROTOCOL
  identity (the same value it already stamps on /messages responses; see
  sdk/agents/adapters/vercel/routing.py).
- Inert today (the route already assumes the Vercel format); this is the
  request-side declaration agreed in the FE<->BE message-interface sync.
- Unit test asserts the header is present.
A custom in-line function tool is stored in the shared (prompt-style) OpenAI shape
`{type:"function", function:{name,description,parameters}, permission}`. The agent
backend's `coerce_tool_config` only accepts typed configs (builtin/gateway/code/client),
so an agent run with a custom tool 500s: "Unsupported tool configuration shape".

A schema-only function tool IS a client tool (the model emits the call, the app executes
it), so the agent request builder now rewrites it to the `ClientToolConfig` shape
`{type:"client", name, description, input_schema, permission}` at the wire boundary.
Gateway-slug function tools (`tools.provider.integration.action.connection`) and
already-typed tools pass through unchanged — the backend coerces gateway slugs itself.

- normalizeAgentToolShape in agentRequest.ts, applied via pruneBlankEntries' tools branch
  (so it covers tools wherever they nest, e.g. parameters.agent.tools).
- Unit test: custom function tool -> client; gateway-slug + typed tools untouched.
…sts scroll

The third-party tool picker grew unbounded when a connection exposed many
actions (Gmail = 61) and its height changed per provider, shifting the
surrounding config panel. Give the popover a fixed height and let each pane
scroll internally (min-h-0 + overflow-y-auto), and drop the unreliable
ResizeObserver height-sync between the two panes.
…onfig-panel-onbig

# Conflicts:
#	web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. feature Frontend labels Jun 25, 2026
@vercel

vercel Bot commented Jun 25, 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 25, 2026 10:19pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

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: 307e6269-4ee9-4ab5-b791-8b512de4e147

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fe-feat/agent-config-panel-onbig

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.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-06-25T22:42:15.858Z

@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: 12

🧹 Nitpick comments (3)
web/packages/agenta-playground/tests/unit/agentRequest.test.ts (1)

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

Drop the any cast in this package test.

A narrow structural cast keeps the assertion readable without disabling type-checking for the rest of the expression.

♻️ Suggested change
-        const tools = (req!.requestBody.data as any).parameters.tools
+        const tools = (
+            req!.requestBody.data as {parameters: {tools: unknown[]}}
+        ).parameters.tools

As per coding guidelines, web/packages/agenta-*/**/*.{ts,tsx}: "no any types".

Source: Coding guidelines

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

329-335: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the new inline avatar styles with utility classes.

ItemAvatar introduces style={{...}} in a new web/**/*.tsx component. The typography can move directly into Tailwind classes, and the background can come from a small color-to-class/token map instead of raw inline styling.

Suggested direction
 interface ItemDescriptor {
-    color: string
+    colorClass: string
     icon?: React.ReactNode
 }

 <span
-    className="flex h-7 w-7 shrink-0 items-center justify-center rounded text-white"
-    style={{background: descriptor.color, fontSize: 10, fontWeight: 600, lineHeight: 1}}
+    className={cn(
+        "flex h-7 w-7 shrink-0 items-center justify-center rounded text-[10px] font-semibold leading-none text-white",
+        descriptor.colorClass,
+    )}
 >

As per coding guidelines, web/**/*.tsx: Always prefer Tailwind utility classes over CSS-in-JS or separate CSS files for styling.

Source: Coding guidelines

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

51-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the avatar styling out of the inline style object.

This component is under web/**/*.tsx, and the new avatar still relies on inline styles for sizing/background/font size. Please convert this to Tailwind/class-driven variants (or a small class map/CSS-variable wrapper) so it stays consistent with the frontend styling rules.

As per coding guidelines, web/**/*.tsx: "Always prefer Tailwind utility classes over CSS-in-JS or separate CSS files for styling" and "Avoid react-jss, styled-components, and inline style={{...}}."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c081eb4-c030-45b8-bc62-c1dbbedb3740

📥 Commits

Reviewing files that changed from the base of the PR and between 0c8226a and 9eea2a1.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • web/oss/src/components/AgentChatSlice/AgentChatPanel.tsx
  • web/oss/src/components/AgentChatSlice/assets/trace.ts
  • web/oss/src/components/AgentChatSlice/assets/transport.ts
  • web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx
  • web/oss/src/components/Playground/Components/Menus/PlaygroundVariantHeaderMenu/index.tsx
  • web/oss/src/components/Playground/Playground.tsx
  • web/oss/src/components/PlaygroundRouter/index.tsx
  • web/oss/src/components/WorkflowRevisionDrawerWrapper/index.tsx
  • web/packages/agenta-entities/src/workflow/state/store.ts
  • web/packages/agenta-entity-ui/package.json
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/CodeEditor.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ConfigItemDrawer.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/HarnessSelectControl.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/JsonObjectEditor.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/McpServerFormView.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/McpServerItemControl.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SandboxPermissionControl.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillFormView.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillUploadZone.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ToolFormView.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ToolItemControl.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ToolSelectorPopover.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentConfigLayout.ts
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/skillUpload.ts
  • web/packages/agenta-entity-ui/src/DrillInView/index.ts
  • web/packages/agenta-playground-ui/src/components/ExecutionHeader/index.tsx
  • web/packages/agenta-playground/src/state/execution/agentRequest.ts
  • web/packages/agenta-playground/tests/unit/agentRequest.test.ts
  • web/packages/agenta-ui/src/components/presentational/index.ts
  • web/packages/agenta-ui/src/components/presentational/section/ConfigAccordionSection.tsx
  • web/packages/agenta-ui/src/components/presentational/section/index.tsx

Comment on lines +179 to +182
// A failure can reach us two ways: recorded on the trace (backend), or stamped onto the turn
// FE-side from the useChat stream error (AgentChatPanel). Prefer whichever is present.
const runError = getMessageRunError(message)
const errorText = traceError || runError

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

Keep showing the failure when the stream dies after partial output.

AgentChatPanel now stamps runError onto any failing assistant turn, but this component only renders that error when noResponse is true. If the model emitted text/tool output before the stream failed, the top banner is gone and the turn now looks successful even though it terminated in error.

💡 Minimal fix
     const traceError = useAtomValue(traceDataSummaryAtomFamily(traceId ?? null)).error
     // A failure can reach us two ways: recorded on the trace (backend), or stamped onto the turn
     // FE-side from the useChat stream error (AgentChatPanel). Prefer whichever is present.
     const runError = getMessageRunError(message)
     const errorText = traceError || runError
+    const showError = !isStreaming && !!errorText
@@
-    const isError = noResponse && !!errorText
+    const isError = noResponse && showError
 
-    if (noResponse && !isError && !hasContent && precededByEmptyAssistant) return null
+    if (noResponse && !showError && !hasContent && precededByEmptyAssistant) return null
@@
-    const body = isError ? errorBody : defaultBody
+    const body =
+        showError && !isError ? (
+            <div className="flex min-w-0 max-w-full flex-col gap-2">
+                {defaultBody}
+                {errorBody}
+            </div>
+        ) : isError ? (
+            errorBody
+        ) : (
+            defaultBody
+        )

Also applies to: 214-219, 327-328

Comment on lines +186 to +188
// Agent entities render the agent-chat surface here too, so the create/edit drawer
// can invoke an agent the same way it invokes chat/completion.
AgentGenerationPanel: AgentChatPanel,

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 | 🏗️ Heavy lift

Scope the agent chat state to the drawer/entity.

AgentChatPanel reads shared session atoms (sessionsListAtom, activeSessionIdAtom, etc.), so mounting the same panel here makes the drawer reuse tabs/history from other agent surfaces. Opening a different agent in the create/edit drawer can therefore inherit another entity's conversation and send that stale context on the next run. Please isolate the agent-chat atoms per drawer/entity, or reset them when entityId changes before rendering this provider.

"@types/react-dom": "^19.0.4",
"ajv": "^8.18.0",
"fast-deep-equal": "^3.1.3",
"fflate": "0.4.8",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move fflate to dependencies.

web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/skillUpload.ts imports fflate from package source, so consumers need it at runtime. Leaving it in devDependencies will only work while the workspace hoists it; isolated installs/builds can fail with a missing-module error.

Suggested diff
     "dependencies": {
         "`@agenta/entities`": "workspace:../agenta-entities",
         "`@agenta/shared`": "workspace:../agenta-shared",
         "`@agenta/ui`": "workspace:../agenta-ui",
         "`@phosphor-icons/react`": "^2.1.10",
         "clsx": "^2.1.1",
+        "fflate": "0.4.8",
         "js-yaml": "^4.1.1",
         "lodash": "^4.17.23",
         "lucide-react": "^0.479.0",
         "uuid": "^11.1.1",
         "zod": "^4.3.6"
@@
-        "fflate": "0.4.8",
         "jsonrepair": "^3.13.3",
📝 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
"fflate": "0.4.8",
"dependencies": {
"`@agenta/entities`": "workspace:../agenta-entities",
"`@agenta/shared`": "workspace:../agenta-shared",
"`@agenta/ui`": "workspace:../agenta-ui",
"`@phosphor-icons/react`": "^2.1.10",
"clsx": "^2.1.1",
"fflate": "0.4.8",
"js-yaml": "^4.1.1",
"lodash": "^4.17.23",
"lucide-react": "^0.479.0",
"uuid": "^11.1.1",
"zod": "^4.3.6"
},
"devDependencies": {
"jsonrepair": "^3.13.3"
}

Comment on lines 534 to 548
// On harness switch, clear a model the new harness can't reach (rather than sending an
// unsupported model). Permissive when the new harness publishes no models.
useEffect(() => {
if (!harness || !modelId) return
if (!harnessAllowsModel(capabilities, harness, modelId)) {
if (!harnessValue || !modelId) return
if (!harnessAllowsModel(capabilities, harnessValue, modelId)) {
writeModel({modelId: null, provider: null})
}
// Only react to harness/capabilities changes, not every model edit.
}, [harness, capabilities])
}, [harnessValue, capabilities])

// Named connections selectable for the chosen provider under this harness (Agenta-managed).
const connectionOptions = useMemo(
() => namedConnectionOptions(vaultSecrets, capabilities, harness, connection.provider),
[vaultSecrets, capabilities, harness, connection.provider],
() => namedConnectionOptions(vaultSecrets, capabilities, harnessValue, connection.provider),
[vaultSecrets, capabilities, harnessValue, connection.provider],
)

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

Normalize stale connection settings after harness/provider changes.

This cleanup only clears the selected model. If the new harness/provider no longer supports the current connection.mode or named connection.slug, those stale values stay in config.model.connection even though modeOptions / connectionOptions no longer contain them. That leaves a persisted ModelRef outside the harness contract and can break the next run.

Suggested fix
     useEffect(() => {
         if (!harnessValue || !modelId) return
         if (!harnessAllowsModel(capabilities, harnessValue, modelId)) {
-            writeModel({modelId: null, provider: null})
+            writeModel({modelId: null, provider: null, slug: null})
         }
         // Only react to harness/capabilities changes, not every model edit.
     }, [harnessValue, capabilities])
+
+    useEffect(() => {
+        if (!modeOptions.includes(connection.mode)) {
+            writeModel({mode: modeOptions[0] ?? "agenta", slug: null})
+            return
+        }
+        if (
+            connection.mode === "agenta" &&
+            connection.slug &&
+            !connectionOptions.some((option) => option.value === connection.slug)
+        ) {
+            writeModel({slug: null})
+        }
+    }, [connection.mode, connection.slug, connectionOptions, modeOptions, writeModel])

Comment on lines +742 to +752
// Block Save until the draft has the minimum it needs to be a valid item (a name). `@ag.embed`
// skill references carry no name and are always valid (they round-trip as-is).
const draftInvalid = useMemo(() => {
if (!editing) return true
if (editing.kind === "mcp") return !String(draft.name ?? "").trim()
if (editing.kind === "skill")
return !isEmbedRefSkill(draft) && !String(draft.name ?? "").trim()
const fn = draft.function as Record<string, unknown> | undefined
if (fn && typeof fn === "object") return !String(fn.name ?? "").trim()
return false
}, [editing, draft])

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

Require a transport target before saving an MCP server.

draftInvalid only checks name, so the drawer will still save {transport: "stdio", command: ""} and {transport: "http", url: ""}. McpServerFormView makes command / url the actual launch target, so this persists MCP entries the runner cannot use.

Suggested fix
     const draftInvalid = useMemo(() => {
         if (!editing) return true
-        if (editing.kind === "mcp") return !String(draft.name ?? "").trim()
+        if (editing.kind === "mcp") {
+            const name = String(draft.name ?? "").trim()
+            const transport = draft.transport === "http" ? "http" : "stdio"
+            const target =
+                transport === "http"
+                    ? String(draft.url ?? "").trim()
+                    : String(draft.command ?? "").trim()
+            return !name || !target
+        }
         if (editing.kind === "skill")
             return !isEmbedRefSkill(draft) && !String(draft.name ?? "").trim()
         const fn = draft.function as Record<string, unknown> | 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
// Block Save until the draft has the minimum it needs to be a valid item (a name). `@ag.embed`
// skill references carry no name and are always valid (they round-trip as-is).
const draftInvalid = useMemo(() => {
if (!editing) return true
if (editing.kind === "mcp") return !String(draft.name ?? "").trim()
if (editing.kind === "skill")
return !isEmbedRefSkill(draft) && !String(draft.name ?? "").trim()
const fn = draft.function as Record<string, unknown> | undefined
if (fn && typeof fn === "object") return !String(fn.name ?? "").trim()
return false
}, [editing, draft])
// Block Save until the draft has the minimum it needs to be a valid item (a name). `@ag.embed`
// skill references carry no name and are always valid (they round-trip as-is).
const draftInvalid = useMemo(() => {
if (!editing) return true
if (editing.kind === "mcp") {
const name = String(draft.name ?? "").trim()
const transport = draft.transport === "http" ? "http" : "stdio"
const target =
transport === "http"
? String(draft.url ?? "").trim()
: String(draft.command ?? "").trim()
return !name || !target
}
if (editing.kind === "skill")
return !isEmbedRefSkill(draft) && !String(draft.name ?? "").trim()
const fn = draft.function as Record<string, unknown> | undefined
if (fn && typeof fn === "object") return !String(fn.name ?? "").trim()
return false
}, [editing, draft])

Comment on lines +166 to +171
const applyParsed = (parsed: ParsedSkill) => {
const next = {...skill}
if (parsed.name) next.name = parsed.name
if (parsed.description) next.description = parsed.description
if (parsed.body) next.body = parsed.body
if (parsed.files.length) next.files = parsed.files

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

Uploading a replacement skill doesn't clear removed content.

ParsedSkill.body and ParsedSkill.files are always present, but these truthy checks ignore valid empty results. Uploading a skill with no bundled files, or a SKILL.md whose body is intentionally empty, leaves the previous draft body/files attached and saves the wrong package.

Suggested diff
     const applyParsed = (parsed: ParsedSkill) => {
         const next = {...skill}
         if (parsed.name) next.name = parsed.name
         if (parsed.description) next.description = parsed.description
-        if (parsed.body) next.body = parsed.body
-        if (parsed.files.length) next.files = parsed.files
+        next.body = parsed.body
+        if (parsed.files.length) next.files = parsed.files
+        else delete next.files
         onChange(next)
         setSelected("skill")
     }
📝 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
const applyParsed = (parsed: ParsedSkill) => {
const next = {...skill}
if (parsed.name) next.name = parsed.name
if (parsed.description) next.description = parsed.description
if (parsed.body) next.body = parsed.body
if (parsed.files.length) next.files = parsed.files
const applyParsed = (parsed: ParsedSkill) => {
const next = {...skill}
if (parsed.name) next.name = parsed.name
if (parsed.description) next.description = parsed.description
next.body = parsed.body
if (parsed.files.length) next.files = parsed.files
else delete next.files
onChange(next)
setSelected("skill")
}

Comment on lines +66 to +74
const bundled: SkillFileEntry[] = files
.filter((f) => f !== skillFile)
.map((f) => {
const rel =
baseDir && f.path.startsWith(baseDir) ? f.path.slice(baseDir.length) : f.path
return {path: rel, content: strFromU8(f.bytes)}
})
.filter((f) => f.path)
return {name, description, body, files: bundled}

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 | 🏗️ Heavy lift

Uploaded skills lose bundled-file executable flags.

SkillFileEntry exposes executable, and the agent wire contract expects bundled files to preserve that bit, but buildSkillFromFiles() always emits {path, content}. Any imported skill that relied on +x round-tripping will come back as a non-executable package.

Comment on lines +80 to +85
<input
ref={inputRef}
type="file"
multiple
accept=".zip,.skill,.md,text/markdown,text/plain"
className="hidden"

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

🧩 Analysis chain

🏁 Script executed:

cat -n web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillUploadZone.tsx | head -n 100

Repository: Agenta-AI/agenta

Length of output: 4261


Add folder selection support to the file input.

The hidden file input is missing the directory (and webkitdirectory) attribute, preventing users from selecting a folder via the "Browse" button. This blocks the folder import flow despite the "Drag a skill folder..." text and the dropzone handler supporting it. Add webkitdirectory and directory attributes to the <input> element on line 80 to enable directory selection.

Code snippet
            <input
                ref={inputRef}
                type="file"
                multiple
                // Add directory attributes below to allow folder selection
                directory
                webkitdirectory
                accept=".zip,.skill,.md,text/markdown,text/plain"
                className="hidden"

Comment on lines +47 to +55
/** Canonical store is the top-level `permission`; fall back to a legacy `agenta_metadata.permission_mode`. */
function readPermission(
topLevel: unknown,
metadata: Record<string, unknown> | undefined,
): ToolPermission | undefined {
if (isPermission(topLevel)) return topLevel
if (isPermission(metadata?.permission_mode)) return metadata?.permission_mode
return undefined
}

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

Clearing permission doesn't work for legacy tools.

readPermission() falls back to agenta_metadata.permission_mode, but setPermission() never removes that legacy key. For an older tool, clicking “clear” deletes the top-level field and the next render resolves right back to the legacy value, so “inherit policy” is unreachable from this form.

Suggested fix
     const setPermission = (next: ToolPermission | null) => {
         const nextTool = {...tool}
+        const nextMetadata = {
+            ...((nextTool.agenta_metadata as Record<string, unknown> | undefined) ?? {}),
+        }
+        delete nextMetadata.permission_mode
+        if (Object.keys(nextMetadata).length > 0) nextTool.agenta_metadata = nextMetadata
+        else delete nextTool.agenta_metadata
+
         if (next) nextTool.permission = next
         else delete nextTool.permission
         onChange(nextTool)
     }

Also applies to: 74-79

Comment on lines +120 to +125
onKeyDown={(e) => {
if (!canToggle) return
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
toggle()
}

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

Prevent header hotkeys from firing when extra contains an interactive control.

extra only stops click propagation today. If it renders a switch/button, pressing Space or Enter inside that control will still bubble into the header onKeyDown and toggle the accordion as a side effect.

Suggested fix
-                    {extra ? <span onClick={(e) => e.stopPropagation()}>{extra}</span> : null}
+                    {extra ? (
+                        <span
+                            onClick={(e) => e.stopPropagation()}
+                            onKeyDown={(e) => e.stopPropagation()}
+                            onKeyUp={(e) => e.stopPropagation()}
+                        >
+                            {extra}
+                        </span>
+                    ) : null}

Also applies to: 151-151

- fflate moved to runtime dependencies (imported from package source)
- MCP server drawer requires a transport target (command/url), not just a name
- MCP JSON editor rejects non-object roots instead of silently collapsing them
- skill re-upload clears removed body/files instead of keeping stale content
- clearing a tool's permission also drops the legacy agenta_metadata.permission_mode
- accordion header ignores key events from interactive controls in `extra`
- raw-JSON drawers block Save while the JSON is invalid
- reset a connection mode the new harness no longer allows
- surface a run error even when the turn emitted partial output before failing
ardaerzin and others added 3 commits June 25, 2026 22:27
…n't share playground tabs

Addresses PR #4850 review: the agent-chat session state (history, open tabs, active tab) was
keyed only by app id, so the create/edit drawer — which mounts AgentChatPanel over the main
playground (same app) — inherited and overwrote the playground's tabs/history.

Key the session state by a scope key supplied via React context (AgentChatScopeProvider /
useChatScopeKey). The main playground keeps the app scope, so storage keys are unchanged and
existing sessions are preserved; the drawer uses a non-colliding drawer:<entityId> scope. The
scoped read atoms and writers become atomFamily(scopeKey); messages stay global (keyed by the
unique session id).
… conflicts)

Merge forward (not rebase) so Arda's resolved AgentConfigControl.tsx conflict
(9eea2a1) is not re-triggered. Resolved two AgentChatSlice files where our HITL
fix pass (F-026/F-033/F-036: removed the busy/disabled approval gating, the
agentShouldResumeAfterApproval resume predicate, the dev-overlay rejection guard)
overlapped his agent-chat rewrite. Kept OUR HITL behavior canonical and his
run-error/empty-turn UX:
- AgentChatPanel.tsx: dropped the now-dead lastAssistantMessageIsComplete...
  import (replaced by agentShouldResumeAfterApproval) and the unused Alert import.
- AgentMessage.tsx: auto-merged (busy/disabled gating removed; his RunErrorBody +
  precededByEmptyAssistant collapse kept).

Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc
…erface (R1)

The harness enum is pi_core / pi_agenta / claude, but HarnessSelectControl's
HARNESS_META was keyed pi / claude / agenta, so the two Pi variants missed the
map and fell back to derived labels ("Pi Core" / "Pi Agenta") with the grey
fallback avatar. Re-key HARNESS_META to the real enum values and prefer the
schema's oneOf {const,title} canonical display names (Pi / Pi (Agenta) /
Claude Code, from the backend HARNESS_IDENTITIES) for each value's label, with
metaFor as the fallback. Update the stale JSDoc.

Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc
@mmabrouk mmabrouk merged commit 4bbf659 into big-agents Jun 25, 2026
48 of 53 checks passed
mmabrouk added a commit that referenced this pull request Jun 25, 2026
Pull #4850 (agent config panel) + the agent fix pass forward into the
stream/batch channel branch. Conflicts were in the agent-lane barrels and
AgentChatPanel.tsx, all where #4861 and the fix pass each added an adjacent
agent export / changed the transport; resolved by keeping BOTH:
- playground barrels export agentChannelModeAtom (this PR) AND
  agentShouldResumeAfterApproval (fix pass).
- AgentChatPanel uses AgentChatTransport (this PR) with the fix pass's
  agentShouldResumeAfterApproval resume predicate and F-033 error handling.
Backend/wire shape stays canonical (our side); the channel switch is purely
the Accept header + a batch JSON->UIMessage replay.

Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Frontend size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants