Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(site): align AI Settings model provider dropdowns - #27391

Merged
DanielleMaywood merged 8 commits into
mainfrom
fix/models-provider-dropdown-consistency
Jul 22, 2026
Merged

fix(site): align AI Settings model provider dropdowns#27391
DanielleMaywood merged 8 commits into
mainfrom
fix/models-provider-dropdown-consistency

Conversation

@DanielleMaywood

@DanielleMaywood DanielleMaywood commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

"Add model" dropdown before vs after

image image

Provider filter dropdown before vs after

image image

The provider dropdowns on the AI Settings Models page rendered options inconsistently and in an unclear order. The "Add model" and "All providers" selects now render provider options identically: the provider icon beside its label, sorted alphabetically by display label.

deriveProviderStates previously returned providers in an emergent order (configured providers by internal slug, then catalog providers, then orphans), which read as unordered. It now sorts by display label, so every consumer (Models list filter, Add model, Update model) is alphabetical. The "All providers" filter select also gained the same icon + label markup the Add model select already used.

Provider icons are now decorative-by-default. Radix computes each option's accessible name from the item text content, which previously included the icon's alt/aria-label and announced the provider name twice (e.g. "Anthropic Anthropic"). ProviderIcon exposed its accessible name by default, so correctness depended on every caller remembering to suppress it. The icon now renders alt=""/aria-hidden (lucide's fallback already does), since every caller renders the provider name as adjacent visible text. This closes the double-announcement class at the source across the Models page, Providers page, model selector, and compaction badge, rather than at each call site. The separate ChatModelAdminPanel/ProviderIcon (single decorative caller) got the same treatment.

Implementation notes
  • site/src/modules/aiModels/providerStates.ts: sort derived states by label; documented the ordering contract.
  • site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx: add icon + label markup to the filter select so it matches the Add model select.
  • site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx: make the icon decorative-by-default (alt=""/aria-hidden); removed the now-dead getProviderName helper. Removed the redundant per-call-site aria-hidden wrappers this replaces.
  • site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx: same decorative treatment (alt="").
  • Tests: rewrote the stale ordering test to assert alphabetical-by-label; added accessible-name assertions guarding the Add model menu and the model form provider select (the two sites that had the double-announcement); removed a getByAltText assertion that depended on the old icon alt.

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").
@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-07-22 09:59 UTC by @DanielleMaywood
Spend: $107.36 / $100.00

Review history
  • R1 (2026-07-21): 12 reviewers, 1 Nit, 4 Note, 1 P3, 1 P4, COMMENT. Review
  • R2 (2026-07-21): 12 reviewers, 2 Nit, 5 Note, 3 P3, 3 P4, COMMENT. Review
  • R3 (2026-07-22): 13 reviewers, 3 Nit, 8 Note, 3 P3, 4 P4, COMMENT. Review
  • R4 (2026-07-22): 14 reviewers, 5 Nit, 9 Note, 4 P3, 4 P4, COMMENT. Review

deep-review v0.9.0 | Round 4 | 48e9bb3..3951dfc

Last posted: Round 4, 22 findings (4 P3, 4 P4, 5 Nit, 9 Note), COMMENT. Review

Finding inventory

Finding inventory: PR #27391

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 Note Author accepted R2 (alphabetical default predictable; shared-module sort keeps 3 dropdowns consistent; UpdateModelPage unaffected) AddModelPage.tsx:52 Alphabetical sort changes Add-model default provider pre-selection (sort lives in shared data layer) R1 Netero, Pariston Yes
CRF-2 P3 Author fixed (4bb418c), verified R2 (Bisky mutation-tested both sites) ModelsPageView.tsx:87 a11y accessible-name fix only asserted on the filter Select, which never had the bug; the two sites that did are untested R1 Netero, Mafu-san P3, Bisky Yes
CRF-3 Nit Author fixed (4bb418c), verified R2 (Gon) providerStates.ts:192 Added comments restate the code they annotate (also stories.tsx:98); Leorio rates the same comments exemplary R1 Gon P2 Yes
CRF-4 Note Author contested; panel closed R2 (accept, cosmetic; corrected record: label can be non-ASCII via user-set display_name) providerStates.ts:193 localeCompare omits explicit locale; order depends on runtime ICU locale (safe for current ASCII labels) R1 Mafuuu Nit, Komugi Note Yes
CRF-5 Note Author fixed (4bb418c), verified R2 (Leorio, Gon, Netero) providerStates.ts:87 Exported deriveProviderStates gained a load-bearing ordering contract with no doc comment R1 Leorio Yes
CRF-6 P4 Author fixed (4bb418c), verified R2; test gap in CRF-9 ProvidersPageView.tsx:65 Same double-announce bug in the "Add provider" DropdownMenuItem, one page over, out of stated scope R1 Mafuuu Yes
CRF-7 Note Author fixed 3 siblings (4bb418c), verified R2; missed ModelRow in CRF-10, test gaps in CRF-9 ModelSelector.tsx:254 Lower-impact siblings of the double-announce class (ModelSelector heading, UserCompaction badge, ProviderRow) R1 Mafuuu, Pariston Yes
CRF-8 P3 Author contested; panel closed R3 (accept, design judgment: clear-filter row deliberately distinct) ModelsPageView.tsx:225 "All providers" filter row has no icon/spacer, so it sits flush-left while icon'd provider rows are pushed right; ragged first row contradicts the PR's "render identically" goal R2 Hisoka Yes
CRF-9 P4 Author fixed (e9f36b4) via decorative-by-default; all four wrappers deleted ProvidersPageView.tsx:65 Four new aria-hidden wraps (ProvidersPageView, ProviderRow, ModelSelector, UserCompaction) ship with no accessible-name assertion though play-stories exist; repeat of CRF-2 class on lower-impact sites R2 Mafu-san P3, Bisky Note, Netero Note, Leorio Note Yes
CRF-10 P4 Author fixed (e9f36b4) via decorative-by-default source fix ModelRow.tsx:41 ModelRow renders ProviderIcon unwrapped beside the model name (provider already in its own column); missed sibling of the class on the in-scope Models page R2 Mafu-san P3, Nami Note, Pariston Note, Meruem Note Yes
CRF-11 P3 Author fixed (e9f36b4): ProviderIcon decorative-by-default, 7 wrappers + getProviderName deleted, admin-panel variant too ModelsPageView.tsx:88 Structural: ProviderIcon exposes its name by default, so every caller must remember the aria-hidden wrap; make it decorative-by-default (like Avatar alt="") to eliminate the class and the 7 wraps R2 Meruem P3, Pariston Note Yes
CRF-12 Nit Author fixed (e9f36b4): wrapper deleted UserCompactionThresholdSettings.tsx:286 aria-hidden wrapper omits className="flex items-center" that the six sibling wrappers carry; can shift icon off vertical center in the Badge R2 Netero, Gon, Mafuuu Yes
CRF-13 Note Author contested; panel closed R3 (accept; record corrected: transitive coverage pins the sort at the data layer, not the sort-to-render wiring) ModelForm.stories.tsx:144 Rewritten AddHidesDisabledProviders story dropped the render-level option-order assertion for the headline Add/Update-model select (still covered transitively at the data layer) R2 Mafuuu, Hisoka, Mafu-san Yes
CRF-14 Nit Author fixed (3951dfc), verified R4 ProviderIcon.tsx:44 Two decorative ProviderIcon variants diverge: ProvidersPage image branch adds redundant aria-hidden (ExternalImage alt="" already hides), admin-panel uses bare alt="" with no explanatory comment R3 Gon, Leorio Yes
CRF-15 Note Author fixed (3951dfc) on ProvidersPage export, verified R4; admin sibling still undocumented (CRF-20) ProviderIcon.tsx:41 Exported ProviderIcon's load-bearing decorative a11y contract lives in a body comment, not a doc comment on the export; no opt-in name prop or mechanical guard R3 Leorio Yes
CRF-16 Note Author acknowledged; panel closed R4 (accept, visual-regression coverage confirmed via Lost Pixel CI job for the list-row icon) ModelsPageView.stories.tsx:72 Dropped getByAltText was not replaced; no Models-page story now verifies any provider icon actually renders (visual-regression coverage loss, not a11y) R3 Mafu-san Yes
CRF-17 P3 Author acknowledged R4; panel re-raised R4 (premise false: Azure/Bedrock model strings mislead, duplicate display_names indistinguishable; PR removed the provider from the badge accessible name present at base) ChatModelAdminPanel/ProviderIcon.tsx:35 admin-panel alt="" drops the provider from the compaction badge's accessible name; that site never had a double-announce, so the de-dup rationale does not apply R3 Pariston P3; Mafuuu/Hisoka/Mafu-san/Nami/Meruem/Kite accept low-stakes Yes
CRF-18 P4 Author fixed (3951dfc), verified R4; residual divergent fallback glyph (CRF-22) ChatModelAdminPanel/ProviderIcon.tsx:7 Two ProviderIcon components carry duplicated, already-drifted icon-source maps; same provider shows a real icon on one surface and a fallback glyph on the other (vercel, gemini). Pre-existing, surfaced because the PR touched both files R3 Zoro P4, Meruem Note Yes
CRF-19 Nit Open ProviderIcon.tsx:48 // alt="" removes the image from the accessibility tree. restates a standard idiom the export doc comment already covers; ExternalImage defaults alt="" R4 Gon P2 Yes
CRF-20 Nit Open ChatModelAdminPanel/ProviderIcon.tsx:26 admin ProviderIcon export is now decorative (alt="") but has no doc comment, while its sibling got the CRF-15 contract; the sibling instance the CRF-15 fix missed R4 Gon, Leorio, Mafuuu Yes
CRF-21 Note Open ModelsPageView.tsx:226 Dropdown provider-icon rendering is unguarded: name assertions are icon-agnostic (icon decorative) and Lost Pixel snapshots the closed dropdown, so dropping ProviderIcon from the SelectItem/menuitem markup ships undetected (list-row icon is covered) R4 Mafu-san, Bisky Yes
CRF-22 Note Open ChatModelAdminPanel/ProviderIcon.tsx:28 CRF-18 consolidated the icon-source map but left the unmapped-provider fallback glyph divergent (Building2Icon vs ServerIcon); openrouter/openai-compat are real configurable providers unmapped in getProviderIcon, so they render different glyphs across surfaces R4 Meruem Yes

Contested and acknowledged

CRF-1 (Note, AddModelPage.tsx:52) - Alphabetical sort changes Add-model default pre-selection

  • Finding: Sorting deriveProviderStates by label in the shared data module changes the Add-model default provider (providerStates.find(canManageProviderModels)) from the first configured provider to the alphabetically-first manageable one. Pariston's root cause: the sort is presentational but lives in the data layer.
  • Author accepted (R2): "Not changing. The alphabetical default is at least as predictable as the prior slug order, and keeping the sort in the shared module is what makes the three dropdowns consistent. UpdateModelPage is unaffected (selects by model id)." No ticket filed; acknowledged as acceptable behavior.

CRF-4 (Note, providerStates.ts:193) - localeCompare omits explicit locale

  • Finding: states.toSorted((a, b) => a.label.localeCompare(b.label)) omits an explicit locale, so ordering of accented/non-ASCII display names can differ across runtimes. Mafuuu proposed "en-US" to match a sibling sort; Komugi confirmed safe for current ASCII labels; Hisoka noted bare localeCompare is the dominant convention.
  • Author defense (R2): "Leaving bare localeCompare as-is. All current provider labels are ASCII (so ordering is identical across ICU locales), and bare calls are the dominant convention here (29 bare vs 3 pinned). Worth pinning en-US only if provider display names ever gain non-ASCII."
  • Status: contested.
  • Panel closure (R2, accept): Panel accepts leaving bare localeCompare. Consequence is cosmetic: the site is a client SPA, so localeCompare is deterministic within any one runtime; only two users on different browser locales could see non-ASCII-named providers in a different order (and the CRF-1-coupled default could differ for them). Bare localeCompare is the dominant convention (author counts 29 bare vs 3 pinned). Correction to the record: the author's "all current labels are ASCII" premise is only true for the formatProviderLabel fallbacks; label resolves to readOptionalString(display_name) ?? formatProviderLabel(...) (providerStates.ts:164), and display_name is a user-settable provider field (Hisoka, Pariston, Nami, Meruem). Non-ASCII names are possible today, but the impact stays cosmetic, so no change required.

CRF-8 (P3, ModelsPageView.tsx:225) - "All providers" row alignment

  • Finding: The icon-less "All providers" clear-filter row sits flush-left while the icon'd provider rows are pushed right, so the filter's first row is ragged, contradicting the PR's "render identically" goal.
  • Author defense (R3): Intentional. The row clears the filter rather than selecting a provider, so it is deliberately distinct; an icon-sized spacer would imply it is a provider, and the ~26px offset reads as a separator between the reset action and the options. Open to revisiting if design wants them aligned.
  • Status: contested.
  • Panel closure (R3, accept): Unanimous accept, including Hisoka (the original finder). Consequence is purely cosmetic (~24-26px offset on one non-selectable reset row, no functional or a11y impact; the row's accessible name is still "All providers"). Distinguishing a reset action from the options it resets is defensible, and the PR's "render identically" goal concerns the provider options, which now match. One aria-hidden icon-sized spacer aligns them if design wants that. No change required.

CRF-13 (Note, ModelForm.stories.tsx:144) - dropped render-level order assertion

  • Finding: The rewritten AddHidesDisabledProviders story replaced its rendered option-order assertion with presence + length checks, so no story now pins the rendered order of the Add/Update-model select the PR headlines as alphabetical.
  • Author defense (R3): Declines. Rendered order is pinned transitively: ModelFormProviderSelect maps deriveProviderStates output (filter/map preserve order) and the unit test asserts alphabetical-by-label; a render-level assertion would duplicate coverage without new signal.
  • Status: contested.
  • Panel closure (R3, accept with correction): Panel accepts leaving it (Bisky, Hisoka, Nami, Mafuuu, Pariston, Zoro, Meruem, Netero). Record correction (Mafu-san, Bisky): the transitive coverage pins the alphabetical sort only at the data layer; the sort-to-render wiring is untested, and the removed assertion pinned args-passthrough order (non-alphabetical fixtures), not the alphabetical property. So "duplicate coverage" is imprecise (a render assertion would catch a future in-view re-sort), but the gap is latent and low-signal, so no change required.

CRF-16 (Note, ModelsPageView.stories.tsx:72) - dropped icon-render assertion

  • Finding: The removed getByAltText was not replaced, so no Models-page story verifies a provider icon renders.
  • Author position (R4): Declines; icon rendering is covered by visual regression testing, so a story assertion would duplicate the signal.
  • Status: acknowledged.
  • Panel closure (R4, accept): Verified. The Lost Pixel pnpm pixel-storybook CI job (.github/workflows/ci.yaml:1036-1064, gated on PIXEL_KEY) snapshots the ModelsPageView Default story, which renders the list-row provider icon on mount, so a missing/wrong icon surfaces as a pixel diff (Bisky, Mafu-san, Meruem, Kite; Hisoka/Nami also cite Chromatic). The removed getByAltText could not survive the decorative change anyway. Coverage exists for the list-row icon; the dropdown-open icons are not snapshotted (tracked separately in CRF-21).

CRF-17 (P3, ChatModelAdminPanel/ProviderIcon.tsx:35) - provider dropped from compaction badge accessible name

  • Finding: Making the admin-panel icon decorative (alt="") removes the provider from the compaction badge's accessible name; that site never had a double-announce, so alt="" there drops the provider signal rather than de-duplicating it.
  • Author position (R4): Keeping it decorative intentionally; accepts the tradeoff because the model name self-identifies the provider (claude-/gpt-), the provider is still shown visually, and it is a low-stakes per-user settings table.
  • Status: re-raised (P3).
  • Panel re-raise (R4, Pariston P3): The author's self-identification premise is false in concrete cases: Azure models read as "gpt-4", Bedrock models as "anthropic.claude-..." (misleading about the actual provider), and display_name is a free-form admin field, so two configs with the same display_name on different providers produce two badges a screen-reader user cannot distinguish, which matters because thresholds are set per row. Relative to the PR base the admin icon rendered alt="${provider} logo", so the change removes provider information that AT users previously had. Six reviewers accept it as low-stakes (per-user settings table, provider visible to sighted users), so it is survivable, hence P3 not higher, but the acknowledgment rests on a disproven premise. Per no-agent-accepted-permanence, this needs a human decision: fix (aria-label with the provider, or render the provider as adjacent visible text as the shared ProviderIcon doc comment requires) or explicitly accept the gap on the correct "low-stakes" reasoning.

Round log

Round 4

Churn guard: PROCEED (3 addressed CRF-14/15/18, 2 acknowledged CRF-16/CRF-17, 0 silent). Author pushed 3951dfc: dropped the redundant aria-hidden (CRF-14), lifted the decorative contract to a doc comment on the ProvidersPage export (CRF-15), consolidated the two icon-source maps (CRF-18: admin imports shared getProviderIcon, which gained gemini; wrapper divs collapsed). Diff vs base +59/-80, 7 files.

Netero advisory (no findings) + 14 panel (bisky, ging-ts, ging-react, gon, hisoka, komugi, leorio, mafu-san, mafuuu, nami, pariston, meruem, zoro + kite wildcard). Panel verified CRF-14/15/18 complete; the consolidation is a strict superset (no provider regresses, admin gains copilot/vercel), confirmed by 6+ reviewers.

Dispositions: CRF-16 closed-accept (Lost Pixel job covers the list-row icon). CRF-17 RE-RAISED to P3 (Pariston; premise false, provider info removed vs base; panel split 6 accept vs P3). Needs a human decision, not a settled agent-accept.

New findings (2 Nit, 2 Note): CRF-19 Nit (redundant alt="" comment, Gon P2 downgraded), CRF-20 Nit (admin export missing the CRF-15 doc comment; Gon/Leorio/Mafuuu), CRF-21 Note (dropdown icon-render unguarded; Mafu-san/Bisky), CRF-22 Note (consolidation left divergent fallback glyphs; openrouter/openai-compat unmapped; Meruem). Body notes: recurring over-length commit subjects (Leorio), getProviderIcon normalization asymmetry (Pariston, latent), unrelated failing chat stories in the worktree (Nami). Highest severity P3, event=COMMENT.
Reviewed against 48e9bb3..3951dfc.

Round 3

Churn guard: PROCEED (4 addressed CRF-9/10/11/12, 2 contested CRF-8/CRF-13, 0 silent). Author pushed e9f36b4 implementing the CRF-11 structural fix: ProviderIcon (ProvidersPage variant) decorative-by-default (alt=""/aria-hidden, lucide fallback aria-hidden), getProviderName deleted, all 7 per-site wrappers removed, ChatModelAdminPanel/ProviderIcon given the same alt="". Diff vs base +49/-57, 7 files.

Netero advisory (no findings) + 13 panel (bisky, ging-ts, ging-react, gon, hisoka, komugi, leorio, mafu-san, mafuuu, nami, pariston, meruem + zoro wildcard). Whole panel independently traced both ProviderIcon variants' callers and confirmed none renders the icon as its sole accessible content; source fix verified complete (getProviderName gone, no dangling refs, tests bite at the two previously-buggy sites). CRF-9/10/11/12 confirmed resolved at source.

Contested closed: CRF-8 accepted unanimously (design judgment, incl. original finder Hisoka); CRF-13 accepted with a record correction.

New (1 P4, 1 Nit, 3 Notes): CRF-14 Nit (variants diverge), CRF-15 Note (decorative contract belongs in a doc comment on the export; no opt-in/guard), CRF-16 Note (dropped getByAltText not replaced), CRF-17 Note (admin-panel alt="" drops provider from compaction badge accessible name), CRF-18 P4 (pre-existing drifted duplicate icon maps). Body notes: commit subject 84>72 (Leorio), invariant untested at owner story (Meruem), no end-to-end sort-to-render story (Bisky). Highest severity P4, event=COMMENT.
Reviewed against 48e9bb3..e9f36b4.

Round 2

Churn guard: PROCEED (5 addressed, 1 acknowledged CRF-1, 1 contested CRF-4, 0 silent). Author pushed 4bb418c, diff grew to +72/-34 across 10 files: R1 fixes plus new a11y wraps in ProvidersPageView, ProviderRow, ModelSelector, UserCompactionThresholdSettings, and new exact accessible-name assertions in ModelsPageView/ModelForm stories. Panel re-reviews the delta.

Workspace outage mid-round hit ~8 of 13 reviewers; all were resumed in place after recovery and completed. Netero advisory + 12 panel (bisky, ging-ts, ging-react, gon, hisoka, komugi, leorio, mafu-san, mafuuu, nami, pariston + meruem wildcard).

Prior findings verified: CRF-2 fixed (Bisky mutation-tested both sites, guards bite), CRF-3/CRF-5 fixed, CRF-6/CRF-7 wraps applied (ProviderRow name preserved via AvatarData title, so no regression). CRF-4 contested -> panel accepts (cosmetic) with corrected record. CRF-1 acknowledged, no ticket (human call).

New findings: CRF-8 P3 (All-providers row misalignment, verified against SelectItem CSS), CRF-9 P4 (4 new wraps untested; Mafu-san P3 vs Bisky/Netero/Leorio Note, settled P4 tracking the P4/Note site severity), CRF-10 P4 (ModelRow missed sibling on in-scope page, verified; Mafu-san P3 vs 3 Notes, settled P4), CRF-11 P3 (decorative-by-default ProviderIcon structural alternative, preserved), CRF-12 Nit (UserCompaction wrapper missing flex class), CRF-13 Note (dropped order assertion). Stale PR description ("icons unchanged elsewhere"; "ProviderRow relies on name") raised as a body process note (Meruem, Pariston, Netero). Highest severity P3, event=COMMENT.
Reviewed against 48e9bb3..4bb418c.

Round 1

Netero first pass: no P0-P3, 2 Notes (CRF-1, CRF-2). Clean mechanical floor. Panel of 12 (bisky, ging-ts, ging-react, gon, hisoka, komugi, leorio, mafu-san, mafuuu, nami, pariston + knov wildcard).
Cross-check outcomes: CRF-2 upgraded Note->P3 (Mafu-san new evidence + Bisky mechanism proof: the only tested site never had the bug). Gon P2 comment-bloat downgraded to Nit (CRF-3) after Leorio, the writing specialist, rated the same comments exemplary; consequence is stylistic verbosity. localeCompare locale settled at Note (CRF-4): determinism specialist Komugi rated Note and safe; Hisoka showed bare localeCompare is the dominant convention (31 vs 3), weakening Mafuuu's "match the pattern" Nit rationale. Sibling class bugs preserved out-of-scope (CRF-6 P4 verified real, CRF-7 Note). Highest severity P3, so event=COMMENT.
Reviewed against 48e9bb3..f28acd5.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tight, well-scoped a11y and ordering fix. The double-announcement diagnosis is correct and verified by multiple reviewers: ProviderIcon renders an <img alt={name}>, so beside a text label Radix folded the provider name into each option's accessible name ("Anthropic Anthropic"), and wrapping the icon in aria-hidden is the right, minimal fix applied consistently to all three call sites. The rewritten unit test genuinely exercises the sort (its inputs are deliberately out of order), and the commit message and PR description are exemplary, quoting the actual symptom so the next reader knows what broke.

Severity count: 1 P3, 1 P4, 1 Nit, 4 Notes. No P0-P1, so this is a comment-level review.

Worth addressing before merge (P3): the only accessible-name assertion guards the filter Select, which is the site that never had the double-announcement bug. The two sites that did have it (the Add-model DropdownMenuItem and ModelFormProviderSelect) are fixed but untested, so a future removal of aria-hidden there regresses silently.

Process observations:

  • The panel split on the two added comments. Gon flagged them as restating the code (originally P2); Leorio, reviewing the same lines, called them exemplary and said "keep writing them like this." Both carry a genuine why, so this sits at Nit, surfaced for your call.
  • Sibling instances of the same double-announce class exist outside this PR's scope: the "Add provider" DropdownMenuItem on the Providers page is the identical bug in an identical widget (CRF-6), plus lower-impact spots (CRF-7). Whether the class fix stops at the Models-page boundary is a human decision.

Hisoka, on trying to break it: "I came to fight this diff. It held up. Silence is a rating."


site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPage.tsx:52

Note [CRF-1] Alphabetical sort also changes the Add-model default provider pre-selection. (Netero, Pariston)

selectedProviderState falls back to providerStates.find(canManageProviderModels) when no provider query param is set. Because deriveProviderStates now returns alphabetical-by-label order, the pre-selected provider changes from the first configured provider to the alphabetically-first manageable one. Pariston named the root cause: the sort lives in the shared data module (#/modules/aiModels/), not the view, so a presentational change leaks into selection logic. UpdateModelPage selects by model id and is unaffected. Alphabetical is at least as predictable as the old slug order, so this is a Note; if you want ordering to stay purely presentational, sort at the render site and pick the Add-model default explicitly.

🤖

site/src/modules/aiModels/providerStates.ts:87

Note [CRF-5] deriveProviderStates gained a load-bearing ordering contract with no doc comment. (Leorio)

The return order is now a real contract (alphabetical by label), and a caller already depends on it (CRF-1's .find(canManageProviderModels) default). The exported signature says only readonly ProviderState[] and the function has no doc comment. Leorio suggests one line above the declaration so callers reading the signature know the order is defined.

🤖

site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx:65

P4 [CRF-6] Same double-announce bug in the "Add provider" DropdownMenuItem, one page over. (Mafuuu)

The Providers page renders <ProviderIcon provider={entry.value} /><span>{entry.label}</span> in a DropdownMenuItem with no aria-hidden, the exact structure and bug this PR fixes in the Models page (confirmed in the current file). Same class, same widget type, adjacent page; the fix is the same one-line wrap. Out of this PR's stated scope, so this needs a human decision: fix it here, or file an issue so the class fix does not stop at the page boundary.

🤖

site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx:254

Note [CRF-7] Lower-impact siblings of the double-announce class. (Mafuuu, Pariston)

The same ProviderIcon + text-label pattern recurs in ModelSelector.tsx's CommandGroup heading, UserCompactionThresholdSettings.tsx:286 (icon in a badge beside the model name), and ProviderRow. Both reviewers rate these lower impact than dropdown options (a heading and a table cell are not single named widgets), and AIBridge's ProviderFilter is unaffected (no alt). Worth knowing when scoping a follow-up so the decorative-icon pattern gets one shared treatment rather than per-site patches.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx Outdated
Comment thread site/src/modules/aiModels/providerStates.ts Outdated
Comment thread site/src/modules/aiModels/providerStates.ts
…ccessible 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.

Copy link
Copy Markdown
Contributor Author

Addressing the remaining body-only items (all in 4bb418c unless noted):

CRF-1 (default pre-selection): Not changing. The alphabetical default is at least as predictable as the prior slug order, and keeping the sort in the shared module is what makes the three dropdowns consistent. UpdateModelPage is unaffected (selects by model id).

CRF-5 (ordering contract): Added a one-line doc comment above deriveProviderStates noting the alphabetical-by-label contract and that callers rely on it.

CRF-6 (Add provider menu): Fixed. Wrapped the DropdownMenuItem icon in aria-hidden in ProvidersPageView.tsx, same treatment as the Models page. No follow-up issue needed.

CRF-7 (lower-impact siblings): Verified all three were genuine (icon name redundantly announced beside a visible label) and fixed them: ModelSelector group heading, UserCompactionThresholdSettings badge, and ProviderRow avatar. One nuance: UserCompactionThresholdSettings uses a different ProviderIcon (ChatModelAdminPanel/ProviderIcon) with a self-styled root div, so its wrapper omits the redundant layout class.

61 story tests pass across the six affected story files; tsc and Biome clean.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2. Strong follow-through: all seven round-1 findings were addressed or consciously accepted, and the two fixes that mattered most were verified by experiment, not taken on faith. Bisky mutation-tested the CRF-2 guards (removing an aria-hidden wrap flips the option name to "OpenAI OpenAI"/"Anthropic Anthropic" and fails the story), and the panel confirmed the CRF-6/CRF-7 wraps preserve each element's accessible name (ProviderRow now takes its name from AvatarData title, not the icon). CRF-3 and CRF-5 are cleanly resolved.

CRF-4 (contested): the panel accepts leaving bare localeCompare, the impact is cosmetic and bare is the house convention. One correction for the record though: the "all provider labels are ASCII" premise does not fully hold. label resolves to a user-settable display_name, so an operator can enter non-ASCII names today. It stays cosmetic (client SPA, deterministic per runtime), so no change is needed, but the reasoning behind the decision was slightly off.

The round-2 diff generalized the a11y fix well beyond the reported scope, and that generalization surfaced new findings. Severity count for this round: 2 P3, 2 P4, 1 Nit, 1 Note.

The theme across the new findings is class-not-instance. The per-site aria-hidden approach already leaked: it wrapped four off-page siblings but missed ModelRow (CRF-10) on the very Models page this PR is named after, and four of the new wraps shipped with no regression test (CRF-9), the same untested-fix shape CRF-2 flagged. Meruem and Pariston both point at the root cause (CRF-11): ProviderIcon exposes its name by default, so correctness depends on every caller remembering the wrapper. Making it decorative-by-default (the pattern Avatar already uses with alt="") would delete all seven wrappers, make the ModelRow miss impossible, and stop the class from recurring. Worth considering as a follow-up shape rather than growing this PR.

One process note: the PR description is now stale relative to the delivered diff. It says the icons are "unchanged elsewhere" and that ProviderRow "relies on" the icon's name, both contradicted by this round's changes to ProviderRow, ModelSelector, and UserCompactionThresholdSettings (raised by Meruem, Pariston, and Netero). Worth a quick update so a future reader isn't misled about scope. CRF-1 remains accepted without a ticket; recording that the alphabetical-default tradeoff is a human's call to make explicit.

Hisoka, after trying to break it: "I came to fight the diff. It mostly held."


site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx:41

P4 [CRF-10] ModelRow's ProviderIcon is the one sibling of the class left unwrapped, on the page this PR is named after. (Mafu-san P3, Nami Note, Pariston Note, Meruem Note)

Confirmed in the current file: ModelRow renders <Avatar><ProviderIcon .../></Avatar> beside the model display name with no aria-hidden, and the provider already has its own column (providerLabel), so a screen reader announces the provider name redundantly on every row. It uses the same name-exposing ProviderIcon as ProviderRow, which the author wrapped this round. CRF-7 enumerated three siblings and the author fixed those three; this is the closest analog to ProviderRow and it was never in the list. Instance impact is minor verbosity (not the literal double-announce), so P4, but it is the missed in-scope member of the exact class this PR set out to close, and there is no follow-up round. Fix: wrap the icon in an aria-hidden span, matching ProviderRow.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
Comment thread site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx Outdated
Comment thread site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx Outdated
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.

Copy link
Copy Markdown
Contributor Author

Round-2 wrap-up (commit e9f36b4):

CRF-11 done (structural). ProviderIcon is now decorative-by-default, so the double-announcement class is closed at the source rather than per call site. This subsumes CRF-10 (ModelRow's wrapper is gone, not just wrapped), CRF-9 (no per-site wrappers left to guard), and CRF-12 (wrapper deleted). Verified via grep that no production caller relied on the icon's accessible name; all render an adjacent visible label or get the name via AvatarData title.

CRF-8, CRF-13: consciously not fixed (replies in their threads). The "All providers" row intentionally reads as a distinct clear-filter action, and rendered option order is already pinned by the deriveProviderStates unit test.

PR description updated to reflect the decorative-by-default source fix; the earlier "icons unchanged elsewhere"/"ProviderRow relies on the name" wording was stale.

74 story tests pass across the eight affected story files (including the round-1 accessible-name guards); tsc and Biome clean. Icons visually confirmed still rendering after the decorative change.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 3. This is the right fix, done at the right layer. The author took the CRF-11 structural route instead of another per-site patch: ProviderIcon is now decorative-by-default (alt=""/aria-hidden, lucide fallback already hidden), the getProviderName lookup table is deleted, all seven per-site wrappers are gone, and the admin-panel variant got the same treatment. The whole panel independently traced every caller of both ProviderIcon components and confirmed the one risk this change introduces (an icon left as its sole accessible content, now nameless) does not land anywhere: every caller renders the provider or model name as adjacent visible text, in its own column, or via AvatarData title. getProviderName is deleted with no dangling references, and the two exact-name story guards still bite at the sites that actually had the double-announce. CRF-9, CRF-10, and CRF-12 are resolved as consequences.

Both contested findings are accepted after panel review. CRF-8 (the ragged "All providers" row): accepted unanimously, including by Hisoka who raised it. The offset is cosmetic on a non-selectable reset row, and marking a clear-filter action as visually distinct from the options is a defensible choice; one aria-hidden spacer would align it if design ever wants that. CRF-13 (dropped render-level order assertion): accepted, with one correction to the record. The transitive coverage pins the alphabetical sort only at the data layer; the sort-to-render wiring is untested, and the removed assertion actually pinned args-passthrough order, not the alphabetical property. The residual gap is latent and low-signal, so no change is needed, but "duplicate coverage" undersells what a render assertion would catch.

New findings this round are all minor: 1 P4, 1 Nit, 3 Notes, none blocking. The through-line worth a look: the decorative default was applied to both ProviderIcon variants under one rationale ("callers render the name beside the icon") that fits the ProvidersPage variant cleanly but not the admin-panel one, whose single caller (the compaction badge) shows only the model name, so alt="" there drops the provider from the accessible name rather than de-duplicating it (CRF-17). It is acceptable (model names self-identify), but it is a real behavior change under a mismatched rationale. Relatedly, the new global contract ("the icon is nameless; callers must supply the name") rests on a comment inside the function body with no opt-in prop or mechanical guard (CRF-15), and the two variants now diverge on how they express the decorative intent (CRF-14). Touching both variants also surfaced a pre-existing bug: their icon-source maps have already drifted, so the same provider shows a real logo on one surface and a fallback glyph on the other (CRF-18, P4, human decision on whether to consolidate now).

Process notes: the intermediate commit subject is 84 chars (over 72), moot if this squash-merges; the decorative invariant is guarded only transitively by two consumer stories rather than at ProviderIcon's own story; and no story asserts the full deriveProviderStates-to-render order end to end. All optional.

Meruem, on the fix: "This is the class fix the panel recommended, not an instance patch."

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx Outdated
Comment thread site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx Outdated
Comment thread site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx
Comment thread site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx Outdated
…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.

Copy link
Copy Markdown
Contributor Author

Round-3 follow-up (commit 3951dfc):

Fixed: CRF-14 (dropped redundant aria-hidden, both variants now use bare alt=""), CRF-15 (decorative contract lifted to a doc comment on the export), CRF-18 (consolidated the two drifted icon-source maps into the shared getProviderIcon, which gained gemini; both surfaces now resolve the same providers).

Consciously not fixed: CRF-16 (icon rendering is covered by visual regression testing) and CRF-17 (compaction badge stays decorative; acknowledged as an intentional tradeoff, not the double-announce rationale).

All five threads have in-place replies. Story tests pass; tsc and Biome clean.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 4. The three actionable round-3 items are cleanly addressed: the redundant aria-hidden is gone so both variants express "decorative" the same way (CRF-14), the decorative contract now lives in a doc comment on the export (CRF-15), and the two provider-icon maps are consolidated into the shared getProviderIcon (CRF-18). The panel verified the consolidation independently and it is a strict superset of the deleted admin map, so no provider regresses to the fallback and the admin panel actually gains real copilot/vercel logos. The CRF-16 decline also checks out: the Lost Pixel CI job snapshots the Models Default story, so a broken list-row icon surfaces as a pixel diff.

Severity this round: 1 P3 (a re-raise), 2 Nits, 2 Notes. No P0-P1.

The one item that needs a human decision is CRF-17, which I'm re-raising to P3. The badge stays decorative on the acknowledgment that "the model name self-identifies the provider (claude-/gpt-)", but Pariston showed that premise does not hold: an Azure model reads as "gpt-4" and a Bedrock model as "anthropic.claude-..." in the accessible name (actively misleading about the provider), display_name is a free-form admin field, and two configs with the same display_name on different providers become two badges a screen-reader user cannot tell apart, which matters because the thresholds are set per row. Relative to the PR base, the admin badge previously announced the provider (alt=" logo") and this PR removes it. Six reviewers still rate it acceptable as a low-stakes, self-configured settings table with the provider visible to sighted users, so it is survivable, hence P3 not higher. But since an agent cannot accept a known a11y gap as permanent, this is a human call: keep it decorative and accept the gap on the correct low-stakes reasoning, or restore the provider (an aria-label carrying the provider, or adjacent visible provider text as the shared ProviderIcon doc comment already asks of callers).

The rest are minor and optional: two Nits (a redundant one-line comment, and the admin ProviderIcon export missing the same decorative doc comment its sibling just gained) and two Notes (the dropdown icons the PR headlines are not directly guarded, since the name assertions are icon-agnostic and Lost Pixel snapshots the closed dropdown; and the consolidation left the unmapped-provider fallback glyph divergent, so openrouter/openai-compat still render a building glyph on one surface and a server glyph on the other).

Process notes: the per-commit subjects keep running over 72 chars (84 in R2, 77 here), moot if this squash-merges; getProviderIcon is now called both with and without normalizeProvider, latent today since provider values are lowercase slugs; and three chat stories (AgentChatPage, AgentChatPageView, Tool) fail in the review worktree but are in edit/tracing paths this PR does not touch, worth a quick confirm they are unrelated.

Pariston, re-raising CRF-17: "the acknowledgment was justified by a premise the code does not support."

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx Outdated
Comment thread site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
Comment thread site/src/pages/AgentsPage/components/ChatModelAdminPanel/ProviderIcon.tsx Outdated
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.

Copy link
Copy Markdown
Contributor Author

Round-4 follow-up (commit 4e0b930):

CRF-17 — fixed (accessible name restored). You were right to re-raise this; my earlier "model name self-identifies the provider" premise does not hold. modelName is display_name || model, display_name is a free-form admin field, and even the raw model misleads about the configured provider (Azure gpt-4, Bedrock anthropic.claude-*), with duplicate display_name collisions on top. The badge now carries aria-label={${providerLabel} ${modelName}}, so it announces provider + model while the icon stays decorative. Per AccName, aria-label overrides content rather than concatenating, so the name is spoken once ("OpenAI GPT-4o"), not duplicated. Visual is unchanged, which suits a table already slated for redesign. Added a getByLabelText guard in the story. The per-row threshold input and reset button keep their existing model-only labels; happy to extend the provider into those as a follow-up if you want.

Nits — fixed. Added the decorative doc comment to the admin-panel ProviderIcon (matching its sibling), and removed the redundant alt="" comment from the shared one.

Notes — not changed. Dropdown icons remain guarded by the accessible-name assertions plus Lost Pixel snapshots (per CRF-16), and the unmapped-provider fallback glyph still diverges (building vs server for openrouter/openai-compat), cosmetic and out of scope here.

Compaction and icon story tests pass; tsc and Biome clean.

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.
@DanielleMaywood
DanielleMaywood marked this pull request as ready for review July 22, 2026 11:18

@ssncferreira ssncferreira left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

{iconPath ? (
<ExternalImage src={iconPath} alt="" className="size-3/5" />
) : (
<Building2Icon className="size-3/5 text-content-secondary" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if there is no provider icon, what are we showing? Do we have a test for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for context: This appears to only be used in the Coder Agents model compaction personal settings page.

If there is no ProviderIcon we're showing a building icon. Same fallback as used in the AISettingsPage.

When we eventually get round to redesigning the compaction page (it does not look good) we'll likely bin this entire component.

As for test coverage on this, it appears we do not have any coverage but I'm not entirely convinced we need it here.

@DanielleMaywood
DanielleMaywood merged commit ff71bb3 into main Jul 22, 2026
35 of 36 checks passed
@DanielleMaywood
DanielleMaywood deleted the fix/models-provider-dropdown-consistency branch July 22, 2026 13:13
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants