diff --git a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx index c62641e0..9fbb3d69 100644 --- a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx +++ b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx @@ -631,9 +631,12 @@ export function SchemaForm({ // reordered or rewritten — and a numeric index then points at a different // branch than the one whose values are held, showing SMS while submitting // email. Re-derived from the values, which is where the answer actually is. - const branchesKey = branches - .map((branch) => `${branch.label}:${branch.declaredFields.join(",")}`) - .join("|"); + // The whole resolved alternative, not just its label and field names: two + // branches can share both while pinning different discriminators or typing a + // field differently, and a reorder of those would otherwise go unnoticed. + const branchesKey = serializeJson( + branches.map((branch) => [branch.label, branch.schema]), + ); useValueChange(branchesKey, () => setBranchIndex(selectBranchIndex(branches, values) ?? 0), ); diff --git a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx index d7e2e5c1..fc34c4bb 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx @@ -867,6 +867,70 @@ describe("InspectorView", () => { expect(screen.getByDisplayValue("retention")).toBeInTheDocument(); }); + it("deep-link appArgs select their root-union branch and keep its defaults (#2123)", async () => { + const unionAppTool: Tool = { + name: "notify", + title: "Notify", + inputSchema: { + type: "object", + properties: { note: { type: "string" } }, + anyOf: [ + { + type: "object", + properties: { + kind: { type: "string", const: "email" }, + address: { type: "string" }, + retries: { type: "number", default: 1 }, + }, + required: ["kind", "address"], + }, + { + type: "object", + properties: { + kind: { type: "string", const: "sms" }, + phone: { type: "string" }, + retries: { type: "number", default: 3 }, + }, + required: ["kind", "phone"], + }, + ], + }, + _meta: { ui: { resourceUri: "ui://apps/notify" } }, + }; + renderWithMantine( + , + ); + // The picker opens on the branch the args describe… + expect(await screen.findByDisplayValue("555-0100")).toBeInTheDocument(); + // Twice over: the Variant picker names the branch, and the read-only + // discriminator carries the value that will be submitted. + expect(screen.getAllByDisplayValue("sms")).toHaveLength(2); + // …with THAT branch's default, not the first branch's `1`. + expect(screen.getByDisplayValue("3")).toBeInTheDocument(); + // …and nothing from the branch this call is not making. + expect(screen.queryByRole("textbox", { name: /address/i })).toBeNull(); + }); + it("ignores a deep-link openApp whose tool is not an app (no tab switch)", async () => { renderWithMantine(