diff --git a/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx b/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx index ecf7d33699..e3838d366c 100644 --- a/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx +++ b/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx @@ -181,6 +181,53 @@ describe("SdkSectionPage", () => { ).toBeInTheDocument(); }); + it("renders each field once when the schema has duplicate sections for a key", async () => { + // The combined AgentSettings schema emits an "llm" section for each agent + // variant ("openhands" and "acp") with identical field keys. Only the first + // is used; without de-duplication every field would render twice (and React + // would warn about duplicate keys). + const llmSection = { + key: "llm", + label: "LLM", + fields: [ + { + key: "llm.api_version", + label: "API Version", + section: "llm", + section_label: "LLM", + value_type: "string" as const, + default: null, + choices: [], + depends_on: [], + prominence: "minor" as const, + secret: false, + required: false, + }, + ], + }; + const schema: NonNullable = { + model_name: "AgentSettings", + sections: [llmSection, { ...llmSection }], + }; + + vi.spyOn(SettingsService, "getSettings").mockResolvedValue( + buildSettings({ + agent_settings_schema: schema, + agent_settings: {}, + }), + ); + + renderSdkSectionPage({ + settingsSources: [ + { settingsSource: "agent_settings", sectionKeys: ["llm"] }, + ], + getInitialView: () => "all", + }); + + const fields = await screen.findAllByTestId("sdk-settings-llm.api_version"); + expect(fields).toHaveLength(1); + }); + it("preserves the selected view when parent rerenders with the same settings", async () => { const schema: NonNullable = { model_name: "AgentSettings", diff --git a/src/components/features/settings/sdk-settings/sdk-section-page.tsx b/src/components/features/settings/sdk-settings/sdk-section-page.tsx index 4d4f755b35..76e41f954e 100644 --- a/src/components/features/settings/sdk-settings/sdk-section-page.tsx +++ b/src/components/features/settings/sdk-settings/sdk-section-page.tsx @@ -299,9 +299,19 @@ export function SdkSectionPage({ return { ...src, filteredSchema: null }; } const sectionSet = new Set(src.sectionKeys); + // The agent schema can carry more than one section per key — e.g. the + // combined AgentSettings schema emits an "llm" section for both the + // "openhands" and "acp" variants. Only the first (openhands) is used, + // so keep the first section per key; otherwise every field renders + // twice and React sees duplicate section keys. + const seenKeys = new Set(); const filteredSchema: SettingsSchema = { ...schema, - sections: schema.sections.filter((s) => sectionSet.has(s.key)), + sections: schema.sections.filter((s) => { + if (!sectionSet.has(s.key) || seenKeys.has(s.key)) return false; + seenKeys.add(s.key); + return true; + }), }; return { ...src, filteredSchema }; }),