From 12dd57df097627dedff86c836f81f015581bea36 Mon Sep 17 00:00:00 2001
From: pablopupo <145598901+pablopupo@users.noreply.github.com>
Date: Thu, 2 Jul 2026 08:42:00 -0400
Subject: [PATCH] fix: support allOf and oneOf in tool input forms
Tool schemas that compose properties with allOf rendered an empty form
because ToolsTab and DynamicJsonForm only looked at top level
properties. Merge allOf branches (properties, required, refs) before
rendering, and render oneOf variant unions with a selector while
keeping the existing const and title enum selects untouched.
Fixes #496
---
client/src/components/DynamicJsonForm.tsx | 168 +++++++++++--
client/src/components/ToolsTab.tsx | 65 +++--
.../__tests__/DynamicJsonForm.test.tsx | 225 ++++++++++++++++++
.../components/__tests__/ToolsTab.test.tsx | 111 +++++++++
.../src/utils/__tests__/schemaUtils.test.ts | 134 +++++++++++
client/src/utils/jsonUtils.ts | 1 +
client/src/utils/schemaUtils.ts | 56 +++++
7 files changed, 720 insertions(+), 40 deletions(-)
diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx
index 90249ab1c..84dd41872 100644
--- a/client/src/components/DynamicJsonForm.tsx
+++ b/client/src/components/DynamicJsonForm.tsx
@@ -2,6 +2,7 @@ import {
useState,
useEffect,
useCallback,
+ useMemo,
useRef,
forwardRef,
useImperativeHandle,
@@ -10,7 +11,11 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import JsonEditor from "./JsonEditor";
import { updateValueAtPath } from "@/utils/jsonUtils";
-import { generateDefaultValue } from "@/utils/schemaUtils";
+import {
+ generateDefaultValue,
+ mergeAllOf,
+ resolveRef,
+} from "@/utils/schemaUtils";
import type {
JsonValue,
JsonSchemaType,
@@ -55,6 +60,44 @@ const isSimpleObject = (schema: JsonSchemaType): boolean => {
return false;
};
+// A oneOf whose members are full schemas is a variant union rendered with a
+// selector. oneOf members carrying const are titled enum options and keep
+// their existing select rendering, as do schemas that already render on
+// their own (a type other than a property-less object).
+const getVariantOptions = (schema: JsonSchemaType): JsonSchemaType[] | null => {
+ if (!schema.oneOf || schema.oneOf.length === 0) return null;
+ if (schema.oneOf.some((opt) => "const" in opt)) return null;
+ if (schema.type && !(schema.type === "object" && !schema.properties)) {
+ return null;
+ }
+ return schema.oneOf as JsonSchemaType[];
+};
+
+// Picks the variant whose properties best match the keys already present in
+// the value, so an existing value does not silently render the first variant
+const inferVariantIndex = (
+ variants: JsonSchemaType[],
+ value: JsonValue,
+): number | undefined => {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ return undefined;
+ }
+ const keys = Object.keys(value);
+ if (keys.length === 0) return undefined;
+
+ let bestIdx: number | undefined;
+ let bestScore = 0;
+ variants.forEach((variant, idx) => {
+ const props = variant.properties ?? {};
+ const score = keys.filter((key) => key in props).length;
+ if (score > bestScore) {
+ bestScore = score;
+ bestIdx = idx;
+ }
+ });
+ return bestIdx;
+};
+
const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {
if ("default" in schema && schema.default !== undefined) {
return schema.default;
@@ -81,14 +124,20 @@ const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {
const DynamicJsonForm = forwardRef {propSchema.description}