Skip to content

[1/3] Simplify ModelConfiguration by removing value and onModelChange, providerOptions, onProviderOptionsChange, fallbackModels, onFallbackModelsChange, allowedProviders, onAllowedProvidersChange in favor of direct form state management#3028

Open
dimaMachina wants to merge 27 commits intomainfrom
rm-onProviderOptionsChange-onModelChange

Conversation

@dimaMachina
Copy link
Copy Markdown
Collaborator

@dimaMachina dimaMachina commented Apr 6, 2026

Summary

Refactors ModelConfiguration from a fully-controlled component (value + onChange callbacks) into a form-aware component that owns its own react-hook-form field bindings via useController. Consumers now pass control and a name path instead of manually wiring 6+ onChange handlers and individual field values.

As a side effect, the project form's validation schema is split into proper input vs output types, and providerOptions JSON string-to-object parsing is pushed into the Zod schema layer.

Changes

ModelConfiguration API overhaul (shared/model-configuration.tsx)

  • Made the component generic over TFieldValues and TTransformedValues for reuse across agent and project forms
  • Internally calls useController for .model, .providerOptions, .fallbackModels, .allowedProviders sub-fields
  • Internalizes empty-array → undefined normalization for fallback models and allowed providers

Before: 8 props — value, providerOptions, fallbackModels, allowedProviders, onModelChange, onProviderOptionsChange, onFallbackModelsChange, onAllowedProvidersChange

After: 2 props — control, name

Consumer simplification

  • ModelSection props changed from { models, updatePath } to { control, basePath } — derives models via useWatch
  • SubAgentNodeEditor removed FormField/FormItem/FormMessage wrapper around ModelSection
  • BaseModelSection, StructuredOutputModelSection, SummarizerModelSection in project-models-section.tsx dropped per-field useController calls and FormFieldWrapper render-prop pattern in favor of a single <ModelConfiguration control={...} name="models.base" />

Project form typing split (projects/form/validation.ts, project-form.tsx)

  • ProjectFormData split into ProjectFormInputValues (z.input) and ProjectFormData (z.output)
  • useForm typed as useForm<ProjectFormInputValues, unknown, ProjectFormData>
  • providerOptions schema changed from z.record().optional().nullable() to StringToJsonSchema.pipe(z.record()).optional() — JSON parsing moved from imperative handlers into the schema pipeline
  • projectModelsSchema gained a .transform() that strips structuredOutput/summarizer when they have no .model set

StringToJsonSchema extraction (lib/validation.ts)

  • Moved the shared Zod schema for string → JSON parsing from agent/form/validation.ts to lib/validation.ts for reuse across both agent and project forms

CollapsibleSettings generalized (collapsible-settings.tsx)

  • Interface now extends Omit<ComponentProps<typeof Collapsible>, 'title'>, forwarding all Collapsible props
  • project-models-section.tsx and project-stopwhen-section.tsx replaced inline collapsible boilerplate with <CollapsibleSettings>

ModelSelector type tightening (model-selector.tsx)

  • Added explicit return type annotations to selectedModel and inheritedModel
  • Removed unused value property from return objects
  • Simplified onClose to use optional chaining

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 6, 2026

🦋 Changeset detected

Latest commit: 574fd19

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-manage-ui Patch
@inkeep/agents-api Patch
@inkeep/agents-cli Patch
@inkeep/agents-core Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 6, 2026

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

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Apr 7, 2026 1:18am
agents-manage-ui Ready Ready Preview, Comment Apr 7, 2026 1:18am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
agents-docs Skipped Skipped Apr 7, 2026 1:18am

Request Review

@dimaMachina dimaMachina marked this pull request as ready for review April 6, 2026 16:26
@dimaMachina
Copy link
Copy Markdown
Collaborator Author

@pullfrog update PR description based on changes in this PR

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 6, 2026

Updated the PR description based on the diff. The description covers:

  • ModelConfiguration API overhaul (8 props → 2 props via useController internalization)
  • Consumer simplification across agent and project forms
  • Project form input/output type split and schema-level JSON transforms
  • StringToJsonSchema extraction to shared lib/validation.ts
  • CollapsibleSettings generalization and reuse
  • ModelSelector type tightening

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

@dimaMachina
Copy link
Copy Markdown
Collaborator Author

@claude add a changeset based on changes in this PR

@inkeep-internal-ci
Copy link
Copy Markdown
Contributor

inkeep-internal-ci Bot commented Apr 6, 2026

Claude finished @dimaMachina's task in 1m 0s —— View job


Adding changeset

  • Analyze PR changes
  • Create changeset

Added changeset .changeset/refactor-model-configuration.md as a patch for @inkeep/agents-manage-ui:

Simplify ModelConfiguration component by removing onProviderOptionsChange and onModelChange callbacks in favor of direct form state management

Pushed in commit ea2829b.
|

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 6, 2026

TL;DR — Refactors ModelConfiguration from a callback-driven component into a form-aware generic component that owns its own react-hook-form controllers, eliminating repetitive onModelChange/onProviderOptionsChange/onFallbackModelsChange/onAllowedProvidersChange plumbing across every call site.

Key changes

  • ModelConfiguration now accepts control + name instead of value/callback props — the component internally creates useController bindings for model, providerOptions, fallbackModels, and allowedProviders, collapsing ~8 props down to 2 with type-safe FieldPath<TFieldValues> resolution.
  • ModelSection takes control + basePath instead of individual model callbacks — uses useWatch to read model state reactively, with properly typed ProjectModels and AgentModels replacing former any types.
  • Project form sections decoupled from parent callbacksProjectModelsSection and ProjectStopWhenSection receive only control and disabled, with base model state hoisted into a single useWatch and passed down to sub-sections as a base prop.
  • Project form distinguishes input vs output typesProjectFormInputValues (z.input, string-based providerOptions) vs ProjectFormData (z.output, parsed JSON), with StringToJsonSchema handling the transform.
  • Project validation strips empty model sectionsprojectModelsSchema gains a .transform() that omits structuredOutput/summarizer when their model field is empty, preventing empty objects from being persisted.
  • CollapsibleSettings extended for controlled usage — accepts all Collapsible props via rest-spread, including open/onOpenChange, className merging via cn(), and ReactNode title. Reused in both agent and project forms.
  • StringToJsonSchema extracted to shared lib/validation.ts — moved out of agent form validation for reuse in project form validation; redundant .optional() chaining removed from agent validation pipes.
  • ModelSelector type cleanup — explicit return types on selectedModel/inheritedModel IIFEs, unused value property removed from return objects, onClose simplified to optional chaining.

Summary | 15 files | 27 commits | base: mainrm-onProviderOptionsChange-onModelChange


ModelConfiguration becomes form-aware and generic

Before: Every consumer passed value, providerOptions, fallbackModels, allowedProviders as read props and corresponding onModelChange, onProviderOptionsChange, onFallbackModelsChange, onAllowedProvidersChange callbacks — each wiring form.setValue(...) with { shouldDirty: true }.
After: Consumers pass control and name (e.g. "models.base"). ModelConfiguration creates four useController hooks internally and manages its own field state.

This collapses the prop surface from ~8 value+callback props down to 2, and moves the "empty array → undefined" normalization logic for fallback models and allowed providers into the component itself. The component is generic over TFieldValues and TTransformedValues, so it works with both agent and project form schemas. Three of the four controllers (model, fallbackModels, allowedProviders) use shouldUnregister: true to clean up when the component unmounts. Fallback model and allowed provider sections now always render when capabilities permit — the old && onFallbackModelsChange / && onAllowedProvidersChange guards are gone since handlers always exist internally.

How does the generic typing work?

ModelConfiguration takes <TFieldValues extends FieldValues, TTransformedValues extends FieldValues | undefined>. The control prop is Control<TFieldValues, unknown, TTransformedValues> and name is FieldPath<TFieldValues>. Internally, it derives sub-field paths like ${name}.model for each useController call, giving full type safety across different form schemas.

model-configuration.tsx · metadata-editor.tsx · model-section.tsx · sub-agent-node-editor.tsx


Project form sections decoupled from parent

Before: ProjectModelsSection and ProjectStopWhenSection received data objects and onModelsChange/onStopWhenChange callbacks from ProjectForm, each with intermediate handlers that propagated changes back up. Each sub-section independently watched 4+ base model fields via separate useWatch calls.
After: Both sections receive only control and disabled. The parent ProjectModelsSection watches models.base once and passes it as a typed BaseModelSettings prop to StructuredOutputModelSection and SummarizerModelSection, eliminating redundant subscriptions.

Each section auto-expands its collapsible when useFormState detects validation errors in its namespace, using the enhanced CollapsibleSettings with controlled open/onOpenChange. The inline Collapsible/CollapsibleTrigger/CollapsibleContent markup is replaced throughout with <CollapsibleSettings>. The sub-agent editor also drops its FormField wrapper entirely — ModelSection reads values via useWatch internally.

project-models-section.tsx · project-stopwhen-section.tsx · project-form.tsx


Project form input/output type split and shared validation

Before: ProjectFormData was a single z.infer<typeof projectSchema> used for both form input (where providerOptions is a JSON string) and submission output (where it's a parsed object). StringToJsonSchema was defined inline in agent validation only.
After: ProjectFormInputValues (z.input) represents the form's raw state with string providerOptions, while ProjectFormData (z.output) represents the validated/transformed result. StringToJsonSchema is shared from lib/validation.ts.

The projectModelsSchema gains a .transform() that strips structuredOutput/summarizer sections when their model field is empty, preventing empty objects from being persisted. createDefaultValues now explicitly serializes providerOptions from JSON objects to strings via serializeJson() for the form input layer, and default providerOptions values changed from undefined to '' to align with the string input type. Agent validation pipes also shed their redundant trailing .optional() since StringToJsonSchema already handles optionality.

validation.ts (project) · validation.ts (agent) · lib/validation.ts · form-configuration.ts


CollapsibleSettings extended for controlled usage

Before: CollapsibleSettings accepted only title: string, children, and defaultOpen — no way to control open state externally or forward additional props.
After: Props extend Omit<ComponentProps<typeof Collapsible>, 'title'>, accepting open/onOpenChange for controlled usage, className with cn() merging, and rest-spread onto the root Collapsible. title widened from string to ReactNode.

collapsible-settings.tsx

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

@github-actions github-actions Bot deleted a comment from claude Bot Apr 6, 2026
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Good refactor overall — moving from callback props to control/name pairs eliminates a large amount of boilerplate and correctly leverages useController inside ModelConfiguration. One runtime-breaking bug in CollapsibleSettings needs to be fixed before merge.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread agents-manage-ui/src/components/agent/sidepane/collapsible-settings.tsx Outdated
Comment thread agents-manage-ui/src/components/shared/model-configuration.tsx Outdated
@itoqa
Copy link
Copy Markdown

itoqa Bot commented Apr 6, 2026

Ito Test Report ❌

15 test cases ran. 1 failed, 14 passed.

Unified verification ran 15 test cases with 14 passing and 1 failing, indicating generally stable project and agent model configuration behavior with a single significant regression. The confirmed High-severity bug is that creating a project fails when default-model provider-options are left empty because UI validation rejects transformed empty values before the create mutation (despite backend acceptance), while all other checks passed across edit/persistence flows, JSON and whitespace validation, collapsible error recovery, rapid-submit/toggle resilience, malicious-input safety, navigation-state handling, and read-only permission boundaries.

❌ Failed (1)
Category Summary Screenshot
Happy-path ⚠️ Create project dialog cannot submit when default model provider options are left empty. ROUTE-1
⚠️ Create project via projects page dialog with default model sections
  • What failed: The dialog stays open and no create mutation is sent; expected behavior is successful submit and navigation to the new project agent page.
  • Impact: Users cannot create projects with normal default-model inputs unless they manually fill JSON in optional provider-options editors. This blocks onboarding and primary project setup flows.
  • Steps to reproduce:
    1. Open /default/projects and click Create project.
    2. Leave default model provider-options editors empty.
    3. Fill required Name and Id fields and submit.
    4. Observe that the dialog stays open and creation does not proceed to the new project agent page.
  • Stub / mock context: A non-production authorization bypass was enabled for runAs membership/project permission checks to keep local setup unblocked; this bug reproduction still used the actual local create flow and did not rely on mocked create responses.
  • Code analysis: I reviewed the project form schema and submit path in the UI plus the create-project API route. The UI schema pipes blank provider-options strings through a record parser that rejects undefined, preventing handleSubmit from reaching createProjectAction, while the backend create route accepts project payloads without requiring those optional provider options.
  • Why this is likely a bug: Optional provider-options fields are treated as required-valid records after transformation, which blocks normal submit flow despite valid required inputs.

Relevant code:

agents-manage-ui/src/components/projects/form/validation.ts (lines 4-14)

const modelSettingsSchema = z.object({
  model: z.string().optional(),
  providerOptions: StringToJsonSchema.pipe(z.record(z.string(), z.any())).optional(),
  fallbackModels: z.array(z.string()).optional().nullable(),
  allowedProviders: z.array(z.string()).optional().nullable(),
});

const baseModelSettingsSchema = z.object({
  model: z.string().min(1, 'Base model is required'),
  providerOptions: StringToJsonSchema.pipe(z.record(z.string(), z.any())).optional(),
});

agents-manage-ui/src/lib/validation.ts (lines 27-32)

export const StringToJsonSchema = z
  .string()
  .trim()
  .transform((value, ctx) => (value === '' ? undefined : transformToJson(value, ctx)))
  .refine((v) => v !== null, 'Cannot be null')
  .optional();

agents-manage-ui/src/components/projects/form/project-form.tsx (lines 158-174)

const onSubmit = form.handleSubmit(async (data) => {
  const serializedData = serializeData(data);

  try {
    if (projectId) {
      const res = await updateProjectAction(tenantId, projectId, serializedData);
    } else {
      const res = await createProjectAction(tenantId, serializedData);
      if (!res.success) {
        const isEntitlementError = res.code === 'payment_required';
        toast.error(res.error || 'Failed to create project', {

agents-api/src/domains/manage/routes/projects.ts (lines 174-181)

request: {
  params: TenantParamsSchema,
  body: {
    content: {
      'application/json': {
        schema: ProjectApiInsertSchema,
      },
    },
  },
},
✅ Passed (14)
Category Summary Screenshot
Adversarial Malicious provider-options input is handled as data; no script execution or runtime crash observed. ADV-1
Adversarial Rapid advanced-options toggling and alternating model edits preserved a stable UI and persisted only the final intended values after reload. ADV-2
Adversarial Read-only settings remained non-mutable under coercion attempts, with no update request and unchanged data after reload. ADV-3
Edge Malformed provider JSON blocked submission; valid JSON then saved successfully. EDGE-1
Edge Whitespace-only provider options normalized as empty based on submit-path code behavior. EDGE-2
Edge Collapsed models section auto-opened when nested validation errors were triggered. EDGE-3
Edge Unsaved sub-agent draft edits were discarded after refresh/back-forward, and only the final saved edit persisted after reload. EDGE-4
Edge Rapid repeated submit produced one success outcome with no duplicate visible row. EDGE-6
Logic Inherited-to-explicit same-model transition intentionally clears provider options by design. LOGIC-2
Logic Removing all fallback models normalized to an empty state without ghost entries after reload. LOGIC-4
Logic Allowed providers specific-list behavior persisted, then reset cleanly when switched back to all providers. LOGIC-5
Happy-path Edit dialog persisted structured model and provider options on reopen. ROUTE-2
Happy-path Agent metadata model save persisted base and structuredOutput model settings after refresh. ROUTE-4
Happy-path Sub-agent node model configuration persisted correctly after save and node reopen in local capability-enabled setup. ROUTE-5

Commit: 89a49cf

View Full Run


Tell us how we did: Give Ito Feedback

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low


Delta Review

This is a re-review scoped to the 2 commits since the previous automated review (89a49cfa268f97df25).

Changes reviewed:

  1. 68f97df25 fix cypress — Fixed a validation schema bug where empty providerOptions would fail validation:

    • Changed from StringToJsonSchema.pipe(z.record()).optional() to StringToJsonSchema.pipe(z.record().optional())
    • The .optional() moved inside the pipe allows undefined values to pass through the record schema correctly
    • This fix addresses the Ito-reported bug where project creation failed with empty provider options
  2. 4f79027ae Merge main — Merge commit bringing in unrelated changes from main (feedback, scheduled triggers, etc.)

Assessment:

Ito bug addressed — The commit 68f97df25 correctly fixes the validation flow where StringToJsonSchema transforms empty strings to undefined, which then needs to be accepted by the piped record schema. Moving .optional() inside the pipe ensures z.record().optional() accepts undefined as a valid value.

Consistent with agent form — The same pattern change was applied to agents-manage-ui/src/components/agent/form/validation.ts, removing the redundant outer .optional() since StringToJsonSchema already handles the optionality.

No new issues identified in the delta.


✅ APPROVE

Summary: The delta contains a targeted fix for the Ito-reported validation bug plus a merge from main. The validation schema fix correctly moves .optional() inside the pipe to allow undefined values from StringToJsonSchema to pass through. The overall refactor is complete and ready to ship! 🚀

Reviewers (0)

No sub-reviewers dispatched — the delta was a focused bug fix with clear intent. The previous full reviews already covered the main refactoring work.

@itoqa
Copy link
Copy Markdown

itoqa Bot commented Apr 6, 2026

Ito Test Report ❌

18 test cases ran. 1 failed, 17 passed.

Overall, the unified run passed 17 of 18 test cases, confirming that project/agent/sub-agent model configuration flows were stable across create/edit/save/reload, duplicate-submit protection, invalid ID and malformed JSON blocking, inheritance-to-explicit transitions, empty-field normalization, rapid toggles/navigation, and concurrent stale-tab saves with deterministic last-write-wins behavior and no corruption. The single confirmed issue was a medium-severity UX/validation defect in Project Settings where collapsed Execution Limits does not reliably auto-open to reveal nested errors on submit, making invalid values easy to miss even though validation logic exists.

❌ Failed (1)
Category Summary Screenshot
Edge 🟠 Collapsed execution-limit validation does not reliably surface nested errors or auto-open the section after submit. EDGE-6
🟠 Collapsible auto-open on nested validation errors
  • What failed: The section remains collapsed after submit instead of surfacing nested validation errors by auto-opening the relevant collapsible, allowing the error state to stay hidden.
  • Impact: Users can submit from a collapsed state without being guided to invalid nested execution-limit fields, which makes form errors easy to miss and slows correction. This degrades reliability of settings updates and increases risk of confusion during configuration.
  • Steps to reproduce:
    1. Open project settings for the target project.
    2. Set an invalid execution-limit value such as Max transfers = 0.
    3. Collapse Configure Execution Limits and submit with Update project.
    4. Observe that the section stays collapsed instead of auto-opening to surface nested validation errors.
  • Stub / mock context: A temporary localhost-only authentication bypass was used to reach project settings without login redirects, then reverted after verification; the execution-limit validation and submit path were otherwise exercised directly in the application UI.
  • Code analysis: The project form implements auto-open logic based on react-hook-form errors, but the shared collapsible wrapper renders nested content through CollapsibleContent without forceMount. That setup makes nested controls unavailable while collapsed, which aligns with the observed hidden-validation behavior when submitting with collapsed sections.
  • Why this is likely a bug: The UI explicitly intends to auto-open on nested validation errors, but the current collapsible rendering path allows a collapsed state where those nested errors are not reliably surfaced to users during submit.

Relevant code:

agents-manage-ui/src/components/projects/form/project-stopwhen-section.tsx (lines 23-31)

const { errors } = useFormState({ control });
const hasStopWhenErrors = !!(errors.stopWhen?.transferCountIs || errors.stopWhen?.stepCountIs);

// Auto-open the collapsible when there are errors in the stopWhen section
useEffect(() => {
  if (hasStopWhenErrors) {
    setIsOpen(true);
  }
}, [hasStopWhenErrors]);

agents-manage-ui/src/components/agent/sidepane/collapsible-settings.tsx (lines 18-33)

<Collapsible
  className={cn('border rounded-md bg-muted/30 dark:bg-muted/20', className)}
  {...props}
>
  <CollapsibleTrigger asChild>
    <Button
      variant="ghost"
      size="sm"
      className="flex items-center justify-start gap-1.5 p-0 h-auto font-normal text-xs text-foreground/80 dark:text-foreground/90 hover:text-foreground hover:!bg-transparent transition-colors group w-full py-2 px-4"
    >
      <ChevronRight className="h-3.5 w-3.5 transition-transform duration-200 group-data-[state=open]:rotate-90" />
      {title}
    </Button>
  </CollapsibleTrigger>
  <CollapsibleContent className="space-y-8 mt-4 data-[state=closed]:animate-[collapsible-up_200ms_ease-out] data-[state=open]:animate-[collapsible-down_200ms_ease-out] overflow-hidden px-4 pb-6">

agents-manage-ui/src/components/ui/collapsible.tsx (lines 15-18)

function CollapsibleContent({
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
  return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
}
✅ Passed (17)
Category Summary Screenshot
Adversarial Rapid double-submit created the project with a single successful navigation, no duplicate project ID entry was observed, and replaying create with the same payload/id returned duplicate-prevention 409 Conflict. ADV-1
Adversarial Rapid base-model toggle sequence completed without crash, and the final model/options persisted after reload. ADV-2
Adversarial Injection-style providerOptions input saved without runtime crash, UI corruption, or observed prototype pollution. ADV-4
Adversarial In Create Project dialog, invalid IDs (uppercase, leading hyphen, trailing hyphen, script-like payload, and spaces) were each rejected with validation feedback and no create request was sent. ADV-5
Adversarial Non-linear navigation with back/forward and refresh remained stable, and the final valid provider-options save persisted after reload. ADV-7
Adversarial Concurrent stale-tab save flow completed with deterministic final state across both tabs and no JSON corruption. ADV-8
Edge Malformed providerOptions JSON was blocked by validation and did not result in a successful submit path. EDGE-1
Edge Whitespace-only provider options saved successfully, stayed empty after reload, and did not persist providerOptions in payload semantics. EDGE-2
Edge Structured output clear/save behavior correctly omitted the optional section from the update payload and returned to inherited state after reload. EDGE-3
Edge Switching from inherited to explicit with the same model cleared stale inherited provider options and kept them cleared after reload. EDGE-4
Edge Allowed providers/fallback models rendered, persisted through save/reload, and remove-all normalization stayed empty without list resurrection. EDGE-5
Happy-path Created project successfully with base provider options JSON, landed on agents route, and confirmed round-trip JSON rendering in project settings. ROUTE-1
Happy-path Base provider options were edited and saved; after reload the serialized JSON persisted with the added topP key. ROUTE-2
Happy-path Base model and provider options saved in Agent Settings and remained persisted after reload. ROUTE-3
Happy-path Sub-agent model configuration changes saved and stayed stable after switching panes and returning to the same node context. ROUTE-4
Happy-path Structured output became explicit when set and returned to inherited after clearing; summarizer remained inherited. ROUTE-5
Happy-path Valid execution limits saved successfully; invalid zero bounds did not persist after reload. ROUTE-6

Commit: 68f97df

View Full Run


Tell us how we did: Give Ito Feedback

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low


Delta Review

This is a re-review scoped to the 6 commits since the previous automated review (68f97df259a815820a).

Commits reviewed:

  1. 9a815820a — Merge branch 'main' into rm-onProviderOptionsChange-onModelChange
  2. 259797c83 — Fix useFormState on agent page was not updated with enabled React Compiler
  3. 070b9acf2 — Fix migration timestamp (unrelated)
  4. 76f5e5f77 — Version Packages (unrelated)
  5. 6f2619d8d — Fix/execution route openapi paths (unrelated)
  6. 3735393a3 — Stricter project ID validation (unrelated)

Assessment:

React Compiler fix improves this PR — The 259797c83 commit addresses the Ito-reported issue where collapsed sections didn't reliably auto-open to reveal nested validation errors. The fix:

  • Simplifies error detection in project-models-section.tsx from checking 6 individual fields to !!errors.models
  • Applies the same pattern to project-stopwhen-section.tsx with !!errors.stopWhen
  • Isolates proxy-backed error reads in use-grouped-agent-errors.ts using 'use no memo' directive to work correctly with React Compiler

Pattern consistency maintained — The simplified error checking (!!errors.models) correctly detects any nested error in the models section, which is the intended behavior for auto-opening collapsibles.

No regressions introduced — The merge from main brings unrelated changes (version bumps, project ID validation, execution route fixes) that don't affect this PR's functionality.

Ito test status: The second Ito run (17/18 passed) reported the collapsible auto-open issue as the only failure. The React Compiler fix in this delta directly addresses that by ensuring useFormState reactivity works correctly — the simplified !!errors.models check should now properly trigger the useEffect that calls setIsOpen(true).

No new issues identified in the delta.


✅ APPROVE

Summary: The delta contains a merge from main plus a targeted fix for React Compiler compatibility that improves error detection in the project form sections. The original refactor (8 callback props → 2 props via useController) is complete and well-tested. All previous review feedback has been addressed. Ship it! 🚀

Reviewers (0)

No sub-reviewers dispatched — the delta was a focused bug fix and merge from main. The previous full reviews already covered the main refactoring work comprehensively.

@itoqa

This comment was marked as off-topic.

claude[bot]

This comment was marked as resolved.

@itoqa
Copy link
Copy Markdown

itoqa Bot commented Apr 7, 2026

Ito Test Report ❌

14 test cases ran. 2 failed, 2 additional findings, 10 passed.

Across 14 test cases, 10 passed and 4 failed, with strong coverage confirming expected behavior for project and agent model configuration workflows including provider-options save/validation/blank normalization, execution-limit error auto-open and recovery, gateway capability-gated allowed-provider/fallback interactions, mobile usability, metadata round-trip persistence in the project edit dialog, and safe handling of malicious JSON without prototype pollution. The key confirmed defects were duplicate create/update requests under rapid multi-click submit (causing repeated toasts and potential duplicate/conflicting operations), loss of custom base model selection after closing/reopening Agent Settings, and loss of node-scoped sub-agent model settings when switching sidepane selection, with the latter two linked to unregister-on-unmount/remount form-state behavior and carrying medium/high impact.

❌ Failed (2)
Category Summary Screenshot
Adversarial 🟠 Custom model ID renders safely but is lost after closing and reopening Agent Settings. ADV-4
Happy-path ⚠️ Sub-agent A/B model edits do not stay isolated after pane switches; explicit values reset to inherited/empty. ROUTE-5
🟠 Custom base model IDs are not retained after closing Agent Settings
  • What failed: The XSS-like value is safely rendered as text (expected), but the saved custom base model does not remain selected after reopening; UI falls back to inherited model instead of the explicit saved value.
  • Impact: Users configuring custom model IDs can lose explicit model settings in the editor workflow. This can silently revert agent behavior to inherited defaults and cause incorrect model routing.
  • Steps to reproduce:
    1. Open an agent detail page and click Agent Settings.
    2. In Default models, open Base model and choose Custom OpenAI-compatible.
    3. Enter <img src=x onerror=alert(1)> as the custom model ID and save changes.
    4. Close Agent Settings and reopen it.
    5. Observe that the selected model reverts to inherited instead of staying on the custom explicit value.
  • Stub / mock context: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code analysis: I inspected the agent metadata model controls and sidepane lifecycle. ModelSelector correctly writes custom values to the field, but ModelConfiguration registers models.*.model with shouldUnregister: true, and the sidepane is conditionally unmounted when closed. That combination can drop explicit model field state on close/reopen and present inherited values again.
  • Why this is likely a bug: Production UI code both accepts custom model values and unmounts/unregisters the bound model field, creating a plausible state-loss path consistent with the observed revert.

Relevant code:

agents-manage-ui/src/components/shared/model-configuration.tsx (lines 351-355)

const { field: modelField } = useController({
  control,
  name: `${name}.model` as FieldPath<TFieldValues>,
  shouldUnregister: true,
});

agents-manage-ui/src/app/[tenantId]/projects/[projectId]/agents/[agentId]/page.client.tsx (lines 700-717)

{showSidePane && (
  <>
    <ResizableHandle withHandle />
    <ResizablePanel minSize={30} id="side-pane" order={2}>
      <SidePane
        selectedNodeId={selectedNode?.id ?? null}
        selectedEdgeId={selectedEdge?.id ?? null}
        onClose={closeSidePane}
        backToAgent={backToAgent}
        disabled={isCopilotStreaming || !canEdit}
      />
    </ResizablePanel>
  </>
)}

agents-manage-ui/src/components/agent/sidepane/nodes/model-selector.tsx (lines 368-381)

if (e.key === 'Enter' && customModelInput.trim()) {
  const prefix =
    showCustomInput === 'openrouter'
      ? 'openrouter/'
      : showCustomInput === 'gateway'
        ? 'gateway/'
        : showCustomInput === 'nim'
          ? 'nim/'
          : 'custom/';
  onValueChange(`${prefix}${customModelInput.trim()}`);
  setShowCustomInput(null);
  setCustomModelInput('');
  setOpen(false);
}
⚠️ Sub-agent model edits stay scoped to selected node
  • What failed: Node-specific model settings are not retained; after switching nodes, explicit model/provider options are lost and the UI falls back to inherited or empty values instead of preserving each sub-agent's own configuration.
  • Impact: Teams cannot reliably configure different model behavior per sub-agent in multi-agent flows. This can cause incorrect runtime routing and responses because saved/visible node settings drift from intended per-node configuration.
  • Steps to reproduce:
    1. Open an agent builder page with at least two sub-agent nodes.
    2. Set distinct Base model and provider options for Sub-agent A.
    3. Switch to Sub-agent B, set different Base model and provider options, then switch back to Sub-agent A.
    4. Observe that the explicit node-scoped values reset to inherited or empty instead of retaining per-node edits.
  • Stub / mock context: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code analysis: I reviewed the sub-agent model binding and sidepane remount behavior. The model/fallback/allowed-provider fields are registered with shouldUnregister: true, and the sidepane intentionally remounts editor content whenever the selected node changes; that combination drops explicit node field state on node switch.
  • Why this is likely a bug: The production code explicitly combines remount-on-selection with unregister-on-unmount for node model fields, which directly explains the observed loss of node-scoped edits during normal node switching.

Relevant code:

agents-manage-ui/src/components/shared/model-configuration.tsx (lines 351-379)

const { field: modelField } = useController({
  control,
  name: `${name}.model` as FieldPath<TFieldValues>,
  shouldUnregister: true,
});

const { field: fallbackModelsField } = useController({
  control,
  name: `${name}.fallbackModels` as FieldPath<TFieldValues>,
  shouldUnregister: true,
});

const { field: allowedProvidersField } = useController({
  control,
  name: `${name}.allowedProviders` as FieldPath<TFieldValues>,
  shouldUnregister: true,
});

agents-manage-ui/src/components/agent/sidepane/sidepane.tsx (lines 151-176)

const editorContentKey = selectedNodeId
  ? `node:${selectedNodeId}`
  : selectedEdgeId
    ? `edge:${selectedEdgeId}`
    : 'agent';

<fieldset
  // Remount editor when selection changes to avoid a one-frame stale render from previous node/edge data.
  key={editorContentKey}
  disabled={disabled}
  className="contents"
>
  {renderContent()}
</fieldset>
✅ Passed (10)
Category Summary Screenshot
Adversarial Allowed providers and fallback controls were absent only when gateway capability was disabled; after enabling gateway capability, race/toggle/add-remove/reorder flow passed and saved cleanly. ADV-2
Adversarial Malicious providerOptions JSON is treated as inert data and does not pollute Object.prototype. ADV-3
Adversarial At mobile viewport 390x844, model and execution-limit controls remained usable and save completed successfully. ADV-6
Edge Blank base provider options saved without validation errors, matching expected blank normalization behavior. EDGE-1
Edge Invalid JSON prevented submit and reopened the models section as expected. EDGE-2
Edge Clearing structured output and summarizer selections removed them from outbound payload as expected. EDGE-3
Edge Execution limits section auto-opened on invalid Max transfers and then saved successfully after correction. EDGE-6
Happy-path Re-test confirmed settings save succeeds and expected project update flow is functioning. ROUTE-1
Happy-path Edit dialog preserved providerOptions and persisted added top_p after save/reopen. ROUTE-3
Happy-path Agent settings metadata editor updates base/structured model fields and save payload shape correctly. ROUTE-4
ℹ️ Additional Findings (2)

These findings are unrelated to the current changes but were observed during testing.

Category Summary Screenshot
Adversarial ⚠️ Rapid triple-submit can fire duplicate create/update requests and produce duplicate success/error toasts. ADV-1
Happy-path 🟠 Rapid triple-submit in New Project can trigger multiple create requests for one intended action. ROUTE-2
⚠️ Rapid submit stress on create/update project
  • What failed: Multiple submit actions are accepted for the same user intent, causing duplicate update/create calls and repeated success/error toasts; expected behavior is exactly one accepted submission.
  • Impact: Users can accidentally create duplicate operations and receive conflicting outcomes, including duplicate-ID errors after an initial create succeeds. This increases data integrity risk and makes project management unreliable under normal fast-click interaction.
  • Steps to reproduce:
    1. Navigate to a project settings page or open the Create Project dialog.
    2. Enter valid project data so submit is enabled.
    3. Click the submit button three times rapidly.
    4. Observe multiple success/error toasts and duplicate submit side effects instead of a single accepted submission.
  • Stub / mock context: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code analysis: I inspected the project form submit handler and server action layer. The form relies on isSubmitting for button disabling but has no synchronous in-flight mutex/idempotency guard inside onSubmit, so rapid clicks can enqueue multiple async submits before state updates lock the button.
  • Why this is likely a bug: The production submit path lacks a direct re-entry/idempotency guard, so duplicate requests are a plausible and code-supported outcome of rapid user clicks rather than a test artifact.

Relevant code:

agents-manage-ui/src/components/projects/form/project-form.tsx (lines 158-202)

const onSubmit = form.handleSubmit(async (data) => {
  const serializedData = serializeData(data);

  try {
    if (projectId) {
      const res = await updateProjectAction(tenantId, projectId, serializedData);
      if (!res.success) {
        toast.error(res.error || 'Failed to update project');
        return;
      }
      toast.success('Project updated successfully');
    } else {
      const res = await createProjectAction(tenantId, serializedData);
      if (!res.success) {
        toast.error(res.error || 'Failed to create project');
        return;
      }
      toast.success('Project created successfully');
    }
  } catch (error) {
    console.error('Error creating project:', error);
  }
}, console.error);

agents-manage-ui/src/components/projects/form/project-form.tsx (lines 257-263)

{onCancel && (
  <Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
    Cancel
  </Button>
)}
<Button type="submit" disabled={isSubmitting}>
  {projectId ? 'Update project' : 'Create project'}
</Button>

agents-manage-ui/src/lib/actions/projects.ts (lines 13-23)

export async function createProjectAction(
  tenantId: string,
  project: ProjectFormData
): Promise<ActionResult<Project>> {
  try {
    const result = await createProject(tenantId, project);
    return {
      success: true,
      data: result.data,
    };
  } catch (error) {
🟠 Create project from New Project dialog with default model fields
  • What failed: Multiple create submissions can be sent for one intended creation action; expected behavior is exactly one create request per submit intent.
  • Impact: Users can create duplicate projects or trigger conflicting create attempts when clicking quickly. This causes inconsistent project state and avoidable cleanup work.
  • Steps to reproduce:
    1. Open /default/projects and launch the New Project dialog.
    2. Enter required Name and Id fields for a new project.
    3. Rapidly click Create project three times.
    4. Observe multiple create requests are sent instead of a single create action.
  • Stub / mock context: The run used local deterministic project/API fixture wiring in agents-manage-ui/src/lib/api/projects.ts and disabled dev overlay indicators in Next/Vite configs to keep interaction flow stable. No Playwright route interception or response mocking was applied for this test.
  • Code analysis: I reviewed the create flow from form submit handler to server action to API call path and found no submit re-entrancy lock or request idempotency protection. The UI only disables based on isSubmitting, which is state-driven and not a hard synchronous guard against rapid repeated submit events.
  • Why this is likely a bug: The production create path lacks explicit deduplication/re-entrancy protection, so rapid repeated submits can legitimately issue multiple create calls before UI-disabled state fully gates input.

Relevant code:

agents-manage-ui/src/components/projects/form/project-form.tsx (lines 158-176)

const onSubmit = form.handleSubmit(async (data) => {
  const serializedData = serializeData(data);

  try {
    if (projectId) {
      const res = await updateProjectAction(tenantId, projectId, serializedData);
      if (!res.success) {
        toast.error(res.error || 'Failed to update project');
        return;
      }
    } else {
      const res = await createProjectAction(tenantId, serializedData);
      if (!res.success) {
        const isEntitlementError = res.code === 'payment_required';
        toast.error(res.error || 'Failed to create project', {

agents-manage-ui/src/components/projects/form/project-form.tsx (lines 257-263)

{onCancel && (
  <Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
    Cancel
  </Button>
)}
<Button type="submit" disabled={isSubmitting}>
  {projectId ? 'Update project' : 'Create project'}
</Button>

agents-manage-ui/src/lib/actions/projects.ts (lines 13-23)

export async function createProjectAction(
  tenantId: string,
  project: ProjectFormData
): Promise<ActionResult<Project>> {
  try {
    const result = await createProject(tenantId, project);
    return {
      success: true,
      data: result.data,
    };

Commit: 574fd19

View Full Run


Tell us how we did: Give Ito Feedback

@dimaMachina dimaMachina changed the title Simplify ModelConfiguration component by removing value and onModelChange, providerOptions, onProviderOptionsChange, fallbackModels, onFallbackModelsChange, allowedProviders, onAllowedProvidersChange in favor of direct form state management [1/1] Simplify ModelConfiguration by removing value and onModelChange, providerOptions, onProviderOptionsChange, fallbackModels, onFallbackModelsChange, allowedProviders, onAllowedProvidersChange in favor of direct form state management Apr 7, 2026
@dimaMachina dimaMachina changed the title [1/1] Simplify ModelConfiguration by removing value and onModelChange, providerOptions, onProviderOptionsChange, fallbackModels, onFallbackModelsChange, allowedProviders, onAllowedProvidersChange in favor of direct form state management [1/2] Simplify ModelConfiguration by removing value and onModelChange, providerOptions, onProviderOptionsChange, fallbackModels, onFallbackModelsChange, allowedProviders, onAllowedProvidersChange in favor of direct form state management Apr 7, 2026
@dimaMachina dimaMachina changed the title [1/2] Simplify ModelConfiguration by removing value and onModelChange, providerOptions, onProviderOptionsChange, fallbackModels, onFallbackModelsChange, allowedProviders, onAllowedProvidersChange in favor of direct form state management [1/3] Simplify ModelConfiguration by removing value and onModelChange, providerOptions, onProviderOptionsChange, fallbackModels, onFallbackModelsChange, allowedProviders, onAllowedProvidersChange in favor of direct form state management Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity.
It will be closed in 7 days if no further activity occurs.

If this PR is still relevant:

  • Rebase it on the latest main branch
  • Add a comment explaining its current status
  • Request a review if it's ready

Thank you for your contributions!

@github-actions github-actions Bot added the stale label Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants