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(