From f28acd534eebb6c5815f7965887fc9080c8ac817 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 21 Jul 2026 16:29:32 +0000 Subject: [PATCH 1/8] fix(site): align AI Settings model provider dropdowns Make the Add model and All providers dropdowns render provider options identically: provider icon beside the label, sorted alphabetically by display label via deriveProviderStates. Hide the decorative provider icons from the accessibility tree so each option's accessible name is just its label instead of the label repeated (was "Anthropic Anthropic"). --- .../modules/aiModels/providerStates.test.ts | 24 ++++++++++++------- site/src/modules/aiModels/providerStates.ts | 5 +++- .../ModelsPage/ModelsPageView.stories.tsx | 5 +++- .../ModelsPage/ModelsPageView.tsx | 11 +++++++-- .../components/ModelFormProviderSelect.tsx | 4 +++- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/site/src/modules/aiModels/providerStates.test.ts b/site/src/modules/aiModels/providerStates.test.ts index d0da0d1008d4d..c4c260ba229d4 100644 --- a/site/src/modules/aiModels/providerStates.test.ts +++ b/site/src/modules/aiModels/providerStates.test.ts @@ -27,30 +27,38 @@ const baseProviderState: ProviderState = { }; describe("deriveProviderStates", () => { - it("orders provider configs first, then catalog-only providers", () => { + it("orders providers alphabetically by display label", () => { const providerConfigs = [ { ...MockChatProviderConfig, - id: "prov-anthropic", - provider: "anthropic", - display_name: "Anthropic", + id: "prov-openai", + provider: "openai", + display_name: "OpenAI", }, ]; const catalog: TypesGen.ChatModelsResponse = { providers: [ - { ...MockChatModelProvider, provider: "anthropic" }, { ...MockChatModelProvider, provider: "google" }, + { ...MockChatModelProvider, provider: "anthropic" }, ], unsupported_providers: [], }; const states = deriveProviderStates([], providerConfigs, catalog); - expect(states.map((s) => s.provider)).toEqual(["anthropic", "google"]); - expect(states[0].key).toBe("prov-anthropic"); - expect(states[1].key).toBe("google"); + expect(states.map((s) => s.provider)).toEqual([ + "anthropic", + "google", + "openai", + ]); + expect(states.map((s) => s.label)).toEqual([ + "Anthropic", + "Google", + "OpenAI", + ]); expect(states[0].hasEffectiveAPIKey).toBe(true); expect(states[1].hasEffectiveAPIKey).toBe(true); + expect(states[2].hasEffectiveAPIKey).toBe(true); }); it("matches model configs to provider configs by ai_provider_id", () => { diff --git a/site/src/modules/aiModels/providerStates.ts b/site/src/modules/aiModels/providerStates.ts index ce06c9063fc12..ed0b8a99416c9 100644 --- a/site/src/modules/aiModels/providerStates.ts +++ b/site/src/modules/aiModels/providerStates.ts @@ -144,7 +144,7 @@ export const deriveProviderStates = ( } } - return orderedEntries.map(({ key, provider }) => { + const states = orderedEntries.map(({ key, provider }) => { const providerConfigEntry = providerConfigsByKey.get(key); const providerConfigSource = providerConfigEntry?.source; const providerConfig = isDatabaseProviderConfig( @@ -188,6 +188,9 @@ export const deriveProviderStates = ( baseURL: getProviderBaseURL(providerConfigEntry), }; }); + + // Providers surface in dropdowns ordered by their display label. + return states.toSorted((a, b) => a.label.localeCompare(b.label)); }; export const canManageProviderModels = ( diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx index 86e388dd2d6e6..4c301c1643bfe 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx @@ -94,7 +94,10 @@ export const FilterByProvider: Story = { name: /filter by provider/i, }); await userEvent.click(providerFilter); - const anthropicOption = await within(document.body).findByRole("option", { + const listbox = await within(document.body).findByRole("listbox"); + // The provider icon is decorative (aria-hidden), so the option's + // accessible name is just the label, matching the Add model select. + const anthropicOption = await within(listbox).findByRole("option", { name: "Anthropic", }); await userEvent.click(anthropicOption); diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx index 80426c630a29c..5473419f327c3 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx @@ -84,7 +84,9 @@ const AddModelDropdown: FC<{ ) } > - + + + {providerState.label} )) @@ -223,7 +225,12 @@ const ModelsPageView: FC = ({ All providers {providerStates.map((providerState) => ( - {providerState.label} + + + + + {providerState.label} + ))} diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx index a5375567dbea0..8dfb8aa000fe1 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx @@ -58,7 +58,9 @@ export const ModelFormProviderSelect: FC<{ {selectableProviderStates.map((ps) => ( - + + + {ps.label} From 4bb418c49c7a334d7958ae23f8ecfa3d948c6caa Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 21 Jul 2026 20:50:28 +0000 Subject: [PATCH 2/8] refactor(site): extend decorative provider-icon treatment and guard accessible names Address review feedback on the provider dropdown alignment: - Wrap provider icons in aria-hidden at the remaining sibling call sites (Add provider menu, model selector group heading, compaction badge, provider row avatar) so adjacent labels are not announced twice. - Add accessible-name assertions for the Add model menu and the model form provider select, the two sites that had the double-announcement. - Document the alphabetical ordering contract on deriveProviderStates. --- site/src/modules/aiModels/providerStates.ts | 3 ++- .../ModelsPage/ModelsPageView.stories.tsx | 9 +++++++-- .../ModelsPage/components/ModelForm.stories.tsx | 10 +++++----- .../ProvidersPage/ProvidersPageView.tsx | 4 +++- .../ProvidersPage/components/ProviderRow.tsx | 10 ++++++---- .../components/ChatElements/ModelSelector.tsx | 12 +++++++----- .../UserCompactionThresholdSettings.tsx | 15 +++++++++------ 7 files changed, 39 insertions(+), 24 deletions(-) diff --git a/site/src/modules/aiModels/providerStates.ts b/site/src/modules/aiModels/providerStates.ts index ed0b8a99416c9..c5c67840fc27a 100644 --- a/site/src/modules/aiModels/providerStates.ts +++ b/site/src/modules/aiModels/providerStates.ts @@ -84,6 +84,8 @@ type ProviderEntry = { provider: string; }; +// Returns provider states ordered alphabetically by display label. Callers +// rely on this order for dropdowns and for picking a default provider. export const deriveProviderStates = ( modelConfigs: readonly TypesGen.ChatModelConfig[], providerConfigs: TypesGen.ChatProviderConfig[] | null | undefined, @@ -189,7 +191,6 @@ export const deriveProviderStates = ( }; }); - // Providers surface in dropdowns ordered by their display label. return states.toSorted((a, b) => a.label.localeCompare(b.label)); }; diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx index 4c301c1643bfe..488cde71d104a 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx @@ -73,6 +73,13 @@ export const Default: Story = { await expect(canvas.getAllByText("Enabled").length).toBeGreaterThan(0); await expect(canvas.getByText("Default")).toBeInTheDocument(); await expect(canvas.getByText("Disabled")).toBeInTheDocument(); + + // The Add model menu lists each provider by exact accessible name; a + // regressed icon would turn a name into "Anthropic Anthropic". + await userEvent.click(canvas.getByRole("button", { name: /add model/i })); + const menu = await within(document.body).findByRole("menu"); + await within(menu).findByRole("menuitem", { name: "Anthropic" }); + await userEvent.keyboard("{Escape}"); }, }; @@ -95,8 +102,6 @@ export const FilterByProvider: Story = { }); await userEvent.click(providerFilter); const listbox = await within(document.body).findByRole("listbox"); - // The provider icon is decorative (aria-hidden), so the option's - // accessible name is just the label, matching the Add model select. const anthropicOption = await within(listbox).findByRole("option", { name: "Anthropic", }); diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx index 5ae23484ad44b..7edf8873da8c6 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx @@ -139,11 +139,11 @@ export const AddHidesDisabledProviders: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByRole("combobox", { name: /provider/i })); - // Option names include the provider icon alt text, so match loosely. - const optionNames = screen - .getAllByRole("option") - .map((option) => option.textContent?.trim()); - await expect(optionNames).toEqual(["OpenAI", "Anthropic"]); + // Exact accessible-name matches guard the aria-hidden icon fix: a + // regressed icon would turn an option's name into "OpenAI OpenAI". + await screen.findByRole("option", { name: "OpenAI" }); + await screen.findByRole("option", { name: "Anthropic" }); + await expect(screen.getAllByRole("option")).toHaveLength(2); await expect( screen.queryByRole("option", { name: /Secondary/ }), ).not.toBeInTheDocument(); diff --git a/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx index 10ac4c75ca736..5791a5412c985 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx @@ -62,7 +62,9 @@ const AddProviderDropdown: React.FC<{ align?: "start" | "end" }> = ({ ) } > - + + + {entry.label} ))} diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx index 88b5ecd0958d2..8e8b3d35560fc 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx @@ -35,10 +35,12 @@ export const ProviderRow: React.FC = ({ size="lg" className="flex shrink-0 items-center justify-center" > - + + + } /> diff --git a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx index 4b56f0774565b..d3a058e2812de 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx @@ -251,11 +251,13 @@ export const ModelSelector: FC = ({ key={providerKey} heading={ - + + + {providerLabel} } diff --git a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx index 2d4e5290f0b4e..ae42dc5aab32c 100644 --- a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx +++ b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx @@ -283,12 +283,15 @@ export const UserCompactionThresholdSettings: FC< - + + + {modelName} {rowError && ( From e9f36b4cca760cdb36dc2c331adc67cacea095b1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 21 Jul 2026 23:45:22 +0000 Subject: [PATCH 3/8] refactor(site): make provider icons decorative by default ProviderIcon exposed its accessible name by default, so suppressing the double-announcement depended on every caller wrapping it in aria-hidden. Render the icon decorative (alt="", aria-hidden) since every caller shows the provider name as adjacent visible text, closing the double-announcement class at the source. Apply the same treatment to the single-caller ChatModelAdminPanel ProviderIcon and drop the now-dead per-call-site wrappers. --- .../ModelsPage/ModelsPageView.stories.tsx | 4 +- .../ModelsPage/ModelsPageView.tsx | 8 +--- .../components/ModelFormProviderSelect.tsx | 4 +- .../ProvidersPage/ProvidersPageView.tsx | 4 +- .../ProvidersPage/components/ProviderIcon.tsx | 40 ++++--------------- .../ProvidersPage/components/ProviderRow.tsx | 10 ++--- .../components/ChatElements/ModelSelector.tsx | 12 +++--- .../ChatModelAdminPanel/ProviderIcon.tsx | 7 +--- .../UserCompactionThresholdSettings.tsx | 15 +++---- 9 files changed, 29 insertions(+), 75 deletions(-) diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx index 488cde71d104a..2ca7758306dbd 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx @@ -68,8 +68,8 @@ export const Default: Story = { canvas.getByText("Claude Sonnet 4.5 (Bedrock)"), ).toBeInTheDocument(); await expect(canvas.getByText("AWS Bedrock")).toBeInTheDocument(); - // The Bedrock model config should render the Bedrock provider icon. - await expect(canvas.getByAltText("AWS Bedrock")).toBeInTheDocument(); + // The provider icon is decorative (alt=""), so its name comes from the + // visible label asserted above rather than the image alt text. await expect(canvas.getAllByText("Enabled").length).toBeGreaterThan(0); await expect(canvas.getByText("Default")).toBeInTheDocument(); await expect(canvas.getByText("Disabled")).toBeInTheDocument(); diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx index 5473419f327c3..0653f0f624048 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx @@ -84,9 +84,7 @@ const AddModelDropdown: FC<{ ) } > - - - + {providerState.label} )) @@ -226,9 +224,7 @@ const ModelsPageView: FC = ({ {providerStates.map((providerState) => ( - - - + {providerState.label} diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx index 8dfb8aa000fe1..a5375567dbea0 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderSelect.tsx @@ -58,9 +58,7 @@ export const ModelFormProviderSelect: FC<{ {selectableProviderStates.map((ps) => ( - - - + {ps.label} diff --git a/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx index 5791a5412c985..10ac4c75ca736 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx @@ -62,9 +62,7 @@ const AddProviderDropdown: React.FC<{ align?: "start" | "end" }> = ({ ) } > - - - + {entry.label} ))} diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx index 743a8a21445e8..8df280e4ac8d6 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx @@ -28,45 +28,19 @@ export const getProviderIcon = (provider: string): string | undefined => { } }; -const getProviderName = (provider: string): string => { - switch (provider) { - case "openai": - return "OpenAI"; - case "anthropic": - return "Anthropic"; - case "bedrock": - return "AWS Bedrock"; - case "azure": - return "Azure OpenAI"; - case "copilot": - return "GitHub Copilot"; - case "google": - return "Google"; - case "openai-compat": - return "OpenAI-compatible"; - case "openrouter": - return "OpenRouter"; - case "vercel": - return "Vercel"; - default: - return provider || "Unknown provider"; - } -}; - export const ProviderIcon: React.FC = ({ provider, icon, className = "size-icon-sm", }) => { const iconSrc = icon || getProviderIcon(provider); - const name = getProviderName(provider); if (iconSrc === undefined) { - return ( - - ); + // Lucide icons are aria-hidden by default when given no a11y prop. + return ; } - return ; + // Decorative: callers render the provider name as visible text beside the + // icon, so exposing it here too would double-announce it. + return ( + + ); }; diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx index 8e8b3d35560fc..88b5ecd0958d2 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.tsx @@ -35,12 +35,10 @@ export const ProviderRow: React.FC = ({ size="lg" className="flex shrink-0 items-center justify-center" > - - - + } /> diff --git a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx index d3a058e2812de..4b56f0774565b 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx @@ -251,13 +251,11 @@ export const ModelSelector: FC = ({ key={providerKey} heading={ - - - + {providerLabel} } diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx index b42c214d9b760..d6ca0143dcee2 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx @@ -3,7 +3,6 @@ import type { FC } from "react"; import { ExternalImage } from "#/components/ExternalImage/ExternalImage"; import { normalizeProvider } from "#/modules/aiModels/helpers"; import { cn } from "#/utils/cn"; -import { formatProviderLabel } from "../../utils/modelOptions"; const providerIconMap: Record = { openai: "/icon/openai.svg", @@ -33,11 +32,7 @@ export const ProviderIcon: FC = ({ className, )} > - + ); } diff --git a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx index ae42dc5aab32c..2d4e5290f0b4e 100644 --- a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx +++ b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx @@ -283,15 +283,12 @@ export const UserCompactionThresholdSettings: FC< - - - + {modelName} {rowError && ( From 3951dfcfd253eae33e04c77d91f0c0aa57b21797 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 22 Jul 2026 09:53:13 +0000 Subject: [PATCH 4/8] refactor(site): consolidate provider icon source and tidy decorative contract - Drop the redundant aria-hidden from the shared ProviderIcon (alt="" already removes the image from the accessibility tree), matching the admin-panel variant. - Lift the decorative contract to a doc comment on the exported symbol. - Consolidate the two drifted icon-source maps: the admin-panel ProviderIcon now shares getProviderIcon, which gains gemini so both surfaces resolve the same providers. --- .../ProvidersPage/components/ProviderIcon.tsx | 14 ++++++--- .../ChatModelAdminPanel/ProviderIcon.tsx | 31 +++++-------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx index 8df280e4ac8d6..0aaec83218e1a 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx @@ -23,11 +23,18 @@ export const getProviderIcon = (provider: string): string | undefined => { return "/icon/google.svg"; case "vercel": return "/icon/vercel.svg"; + case "gemini": + return "/icon/gemini.svg"; default: return undefined; } }; +/** + * Renders a decorative provider logo with no accessible name. Callers MUST + * render the provider name as adjacent visible text; the icon alone does not + * convey it to assistive technology. + */ export const ProviderIcon: React.FC = ({ provider, icon, @@ -38,9 +45,6 @@ export const ProviderIcon: React.FC = ({ // Lucide icons are aria-hidden by default when given no a11y prop. return ; } - // Decorative: callers render the provider name as visible text beside the - // icon, so exposing it here too would double-announce it. - return ( - - ); + // alt="" removes the image from the accessibility tree. + return ; }; diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx index d6ca0143dcee2..75426ca40e29c 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx @@ -2,17 +2,9 @@ import { ServerIcon } from "lucide-react"; import type { FC } from "react"; import { ExternalImage } from "#/components/ExternalImage/ExternalImage"; import { normalizeProvider } from "#/modules/aiModels/helpers"; +import { getProviderIcon } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderIcon"; import { cn } from "#/utils/cn"; -const providerIconMap: Record = { - openai: "/icon/openai.svg", - anthropic: "/icon/anthropic.svg", - azure: "/icon/azure.svg", - bedrock: "/icon/aws.svg", - google: "/icon/google.svg", - gemini: "/icon/gemini.svg", -}; - interface ProviderIconProps { provider: string; className?: string; @@ -22,20 +14,7 @@ export const ProviderIcon: FC = ({ provider, className, }) => { - const normalized = normalizeProvider(provider); - const iconPath = providerIconMap[normalized]; - if (iconPath) { - return ( -
- -
- ); - } + const iconPath = getProviderIcon(normalizeProvider(provider)); return (
= ({ className, )} > - + {iconPath ? ( + + ) : ( + + )}
); }; From 4e0b93065fbf9b777df72098d8167826c952ab76 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 22 Jul 2026 10:37:31 +0000 Subject: [PATCH 5/8] fix(site): announce provider on compaction threshold badges The badge icon is decorative, which dropped the provider from the badge's accessible name. The model name does not reliably identify the configured provider (Azure gpt-4, Bedrock anthropic.claude-*, and duplicate display_name collisions), so give the badge an aria-label carrying provider + model. Also add the decorative doc comment to the admin-panel ProviderIcon and drop a redundant comment in the shared one. --- .../ProvidersPage/components/ProviderIcon.tsx | 1 - .../ChatModelAdminPanel/ProviderIcon.tsx | 5 +++++ ...UserCompactionThresholdSettings.stories.tsx | 6 ++++++ .../UserCompactionThresholdSettings.tsx | 18 +++++++++++------- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx index 0aaec83218e1a..0fc775cb1e77f 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx @@ -45,6 +45,5 @@ export const ProviderIcon: React.FC = ({ // Lucide icons are aria-hidden by default when given no a11y prop. return ; } - // alt="" removes the image from the accessibility tree. return ; }; diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx index 75426ca40e29c..45386b3ea7d07 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx @@ -10,6 +10,11 @@ interface ProviderIconProps { className?: string; } +/** + * Renders a decorative provider logo with no accessible name. Callers MUST + * render the provider name as adjacent visible text; the icon alone does not + * convey it to assistive technology. + */ export const ProviderIcon: FC = ({ provider, className, diff --git a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx index 5b1d2771a1dbd..0306265cca655 100644 --- a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx +++ b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx @@ -78,6 +78,12 @@ export const Default: Story = { expect(canvas.getByText("Claude Sonnet")).toBeInTheDocument(); expect(canvas.queryByText("GPT-3.5 (Disabled)")).not.toBeInTheDocument(); + // Each badge announces provider + model (the icon itself is decorative). + expect(canvas.getByLabelText("OpenAI GPT-4o")).toBeInTheDocument(); + expect( + canvas.getByLabelText("Anthropic Claude Sonnet"), + ).toBeInTheDocument(); + // No footer visible when nothing is dirty expect( canvas.queryByRole("button", { name: /Save/i }), diff --git a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx index 2d4e5290f0b4e..d099fb24aab22 100644 --- a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx +++ b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx @@ -24,6 +24,7 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; +import { formatProviderLabel } from "#/utils/aiProviders"; import { cn } from "#/utils/cn"; import { ProviderIcon } from "./ChatModelAdminPanel/ProviderIcon"; @@ -278,17 +279,20 @@ export const UserCompactionThresholdSettings: FC< draftValue === "100" && drafts[modelConfig.id] !== undefined; const rowError = rowErrors[modelConfig.id]; const modelName = modelConfig.display_name || modelConfig.model; + const provider = + providerTypeByID.get(modelConfig.ai_provider_id) ?? ""; + const providerLabel = formatProviderLabel(provider); return ( - - + + {modelName} {rowError && ( From d30afbabb0aee07b79d581e86ceffc7689eab84e Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 22 Jul 2026 10:48:11 +0000 Subject: [PATCH 6/8] refactor(site): unify provider icon fallback glyph Use Building2Icon for unmapped providers in the admin-panel ProviderIcon to match the shared component, closing the residual cross-surface inconsistency left after consolidating the icon-source maps. --- .../components/ChatModelAdminPanel/ProviderIcon.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx index 45386b3ea7d07..f584227f59828 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx @@ -1,4 +1,4 @@ -import { ServerIcon } from "lucide-react"; +import { Building2Icon } from "lucide-react"; import type { FC } from "react"; import { ExternalImage } from "#/components/ExternalImage/ExternalImage"; import { normalizeProvider } from "#/modules/aiModels/helpers"; @@ -30,7 +30,7 @@ export const ProviderIcon: FC = ({ {iconPath ? ( ) : ( - + )} ); From bcf75177709ec639e292daeaf64dcf14607ce583 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 22 Jul 2026 10:58:28 +0000 Subject: [PATCH 7/8] refactor(site): drop redundant comments from provider icon and states --- site/src/modules/aiModels/providerStates.ts | 3 +-- .../ProvidersPage/components/ProviderIcon.tsx | 6 ------ .../components/ChatModelAdminPanel/ProviderIcon.tsx | 5 ----- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/site/src/modules/aiModels/providerStates.ts b/site/src/modules/aiModels/providerStates.ts index c5c67840fc27a..c0f09ce1b810e 100644 --- a/site/src/modules/aiModels/providerStates.ts +++ b/site/src/modules/aiModels/providerStates.ts @@ -84,8 +84,7 @@ type ProviderEntry = { provider: string; }; -// Returns provider states ordered alphabetically by display label. Callers -// rely on this order for dropdowns and for picking a default provider. +// Returns provider states ordered alphabetically by display label. export const deriveProviderStates = ( modelConfigs: readonly TypesGen.ChatModelConfig[], providerConfigs: TypesGen.ChatProviderConfig[] | null | undefined, diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx index 0fc775cb1e77f..d87b3b6cb15c0 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx @@ -30,11 +30,6 @@ export const getProviderIcon = (provider: string): string | undefined => { } }; -/** - * Renders a decorative provider logo with no accessible name. Callers MUST - * render the provider name as adjacent visible text; the icon alone does not - * convey it to assistive technology. - */ export const ProviderIcon: React.FC = ({ provider, icon, @@ -42,7 +37,6 @@ export const ProviderIcon: React.FC = ({ }) => { const iconSrc = icon || getProviderIcon(provider); if (iconSrc === undefined) { - // Lucide icons are aria-hidden by default when given no a11y prop. return ; } return ; diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx index f584227f59828..dfe897b36103f 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx @@ -10,11 +10,6 @@ interface ProviderIconProps { className?: string; } -/** - * Renders a decorative provider logo with no accessible name. Callers MUST - * render the provider name as adjacent visible text; the icon alone does not - * convey it to assistive technology. - */ export const ProviderIcon: FC = ({ provider, className, From 5dd57364fd327d5ba005e67dc6237b72ca814a7d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 22 Jul 2026 11:17:24 +0000 Subject: [PATCH 8/8] fix(site): remove shadow from models provider filter trigger --- site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx index 0653f0f624048..2b24b010d6859 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx @@ -214,7 +214,7 @@ const ModelsPageView: FC = ({