feat(frontend): agent playground config panel + agent run-lane wire fixes#4850
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Railway Preview Environment
Updated at 2026-06-25T22:42:15.858Z |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (3)
web/packages/agenta-playground/tests/unit/agentRequest.test.ts (1)
330-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDrop the
anycast 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.toolsAs per coding guidelines,
web/packages/agenta-*/**/*.{ts,tsx}: "noanytypes".Source: Coding guidelines
web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx (1)
329-335: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the new inline avatar styles with utility classes.
ItemAvatarintroducesstyle={{...}}in a newweb/**/*.tsxcomponent. 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 winMove the avatar styling out of the inline
styleobject.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 "Avoidreact-jss,styled-components, and inlinestyle={{...}}."Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3c081eb4-c030-45b8-bc62-c1dbbedb3740
⛔ Files ignored due to path filters (1)
web/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (34)
web/oss/src/components/AgentChatSlice/AgentChatPanel.tsxweb/oss/src/components/AgentChatSlice/assets/trace.tsweb/oss/src/components/AgentChatSlice/assets/transport.tsweb/oss/src/components/AgentChatSlice/components/AgentMessage.tsxweb/oss/src/components/Playground/Components/Menus/PlaygroundVariantHeaderMenu/index.tsxweb/oss/src/components/Playground/Playground.tsxweb/oss/src/components/PlaygroundRouter/index.tsxweb/oss/src/components/WorkflowRevisionDrawerWrapper/index.tsxweb/packages/agenta-entities/src/workflow/state/store.tsweb/packages/agenta-entity-ui/package.jsonweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/CodeEditor.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ConfigItemDrawer.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/HarnessSelectControl.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/JsonObjectEditor.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/McpServerFormView.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/McpServerItemControl.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SandboxPermissionControl.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillFormView.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillUploadZone.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ToolFormView.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ToolItemControl.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ToolSelectorPopover.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentConfigLayout.tsweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.tsweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/skillUpload.tsweb/packages/agenta-entity-ui/src/DrillInView/index.tsweb/packages/agenta-playground-ui/src/components/ExecutionHeader/index.tsxweb/packages/agenta-playground/src/state/execution/agentRequest.tsweb/packages/agenta-playground/tests/unit/agentRequest.test.tsweb/packages/agenta-ui/src/components/presentational/index.tsweb/packages/agenta-ui/src/components/presentational/section/ConfigAccordionSection.tsxweb/packages/agenta-ui/src/components/presentational/section/index.tsx
| // 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 |
There was a problem hiding this comment.
🎯 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
| // 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, |
There was a problem hiding this comment.
🗄️ 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", |
There was a problem hiding this comment.
🩺 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.
| "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" | |
| } |
| // 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], | ||
| ) |
There was a problem hiding this comment.
🗄️ 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])| // 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]) |
There was a problem hiding this comment.
🗄️ 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.
| // 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]) |
| 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 |
There was a problem hiding this comment.
🎯 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.
| 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") | |
| } |
| 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} |
There was a problem hiding this comment.
🗄️ 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.
| <input | ||
| ref={inputRef} | ||
| type="file" | ||
| multiple | ||
| accept=".zip,.skill,.md,text/markdown,text/plain" | ||
| className="hidden" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
cat -n web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillUploadZone.tsx | head -n 100Repository: 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"| /** 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 | ||
| } |
There was a problem hiding this comment.
🎯 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
| onKeyDown={(e) => { | ||
| if (!canToggle) return | ||
| if (e.key === "Enter" || e.key === " ") { | ||
| e.preventDefault() | ||
| toggle() | ||
| } |
There was a problem hiding this comment.
🎯 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
…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
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
Context
The agent playground on
big-agentsshipped 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 currentbig-agents(merged, 0 behind), and folds in big-agents' Pi permission-policy gating.What this adds
A sectioned (accordion) agent config panel:
New building blocks sit next to
AgentConfigControl:ConfigAccordionSection,ConfigItemDrawer, form views (ToolFormView,SkillFormView,McpServerFormView), editors (CodeEditor,MarkdownEditor,JsonObjectEditor),HarnessSelectControl,SandboxPermissionControl, andSkillUploadZone.Fixes in this branch
WorkflowRevisionDrawerWrapper)./inspectwas 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).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.
Message format header. The FE sends
x-ag-messages-format: vercelon the agent/messagesrequest (matchesVERCEL_MESSAGE_PROTOCOLinsdk/agents/adapters/vercel/routing.py). Without it the endpoint negotiates down to batch JSON anduseChatcannot render the stream.Custom tool shape. App-defined function tools are emitted as the typed client config, not the OpenAI shape.
Before (rejected by
coerce_tool_configwith "Unsupported tool configuration shape"):After:
Gateway slugs (
tools.provider.integration.action.connection) and already-typed tools pass through untouched. SeeagentRequest.tsand its unit tests.Model catalog. The picker reads
/inspectmeta.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.Connection / ModelRef. The auth cards build
Connection{mode, slug?}andModelRef{provider, model, params, connection}from the connections contract (agenta vs self_managed;slugonly for agenta).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
gateway_connectionstable 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.tool-output-errorfor the sametoolCallIdin the same turn, which overwrites the approval-request state inuseChat. Suppressing the terminal denial while parked is a backend/SDK change.Tests / notes
agentRequestunit tests cover the message-format header and the client-tool rewrite (22 passing).agenta-entity-ui: tsc clean, eslint clean, 126 unit tests passing.What to QA