Repository navigation
fix(ui-web): one provider list per scope, as the models already are - #604
Merged
Merged
Conversation
`is_current` is the difference between the two reads: the backend marks the conversation's provider when `model.options` names a session and `agents.defaults`' when it does not. Both answers landed in one array, so the default-scoped read -- the settings dialog's, repeated by every settings write and by the agents page -- overwrote the conversation's. What the reader saw: open a conversation running a Gemini model while the default is a DeepSeek one, open the settings dialog once, then click the model chip. The picker opened on DeepSeek's column with `gemini-3.5-flash` prepended and ticked inside it, DeepSeek's count raised by that phantom row, and the current dot on both vendors. The model was shown as belonging to a vendor whose key does not serve it. Split, the way the pair above it already is: `providersLive` is the visible conversation's answer and `defaultProvidersLive` is the configured default's. The picker reads the first through `modelSource`, the settings snapshot and the onboarding step read the second, and the agents page -- which loads the default-scoped list for itself -- reads the second too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gloryfromca
commented
Sep 21, 2026
gloryfromca
left a comment
Member
Author
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the full target diff and the surrounding provider consumers. The new default-scoped list is read by settings, onboarding, and Raven-hosted agent model choices, while conversation switching and the composer continue to read the session-scoped list through modelSource. The renamed export has no stale callers, and the added test covers the cross-scope overwrite without weakening existing assertions.
Covered the repository rules and Web UI context, the diff, callers, relevant history, backward compatibility, test changes, and architecture boundaries.
Verification:
npm test --prefix ui-web: 191 files, 2486 tests passednpm run type-check --prefix ui-web: passed
0xKT
approved these changes
Sep 21, 2026
gloryfromca
added a commit
that referenced
this pull request
Sep 21, 2026
…604) ## Summary The model picker showed a conversation's model under a vendor whose key does not serve it. `is_current` is the whole difference between the two `model.options` reads: the backend marks the conversation's provider when the request names a session, and `agents.defaults`' when it does not. Both answers landed in one module-level array, so the default-scoped read -- the settings dialog's, repeated by every settings write, and the agents page's -- overwrote the conversation's. What the reader saw: a conversation running a Gemini model while the default is a DeepSeek one, one visit to the settings dialog, then a click on the model chip. The picker opened on DeepSeek's column with `gemini-3.5-flash` prepended and ticked inside it, DeepSeek's count raised by that phantom row, and the current dot on both vendors. Split, the way the model pair one layer down already is (#596 separated `defaultModelLive` from the chip's own value and left this array shared). `providersLive` is the visible conversation's answer, read by the picker through `modelSource`; `defaultProvidersLive` is the configured default's, read by the settings snapshot, the onboarding model step, and the agents page, which loads that list for itself. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification Driven on a real host (`raven web` on an isolated `RAVEN_HOME` built from a real config, default `deepseek/deepseek-v4-pro`, the conversation switched to `gemini/gemini-3.5-flash`): | Picker opened after one visit to the settings dialog | Before | After | | --- | --- | --- | | Column it opens on | DeepSeek | Gemini | | Where the tick sits | on a prepended `gemini-3.5-flash` row inside DeepSeek | on Gemini 3.5 Flash, in Gemini | | DeepSeek's model count | 4 (three plus the phantom) | 3 | | Vendors carrying the current dot | DeepSeek and Gemini | Gemini | Without the settings visit the picker was correct in both columns, which is what makes the shared array the cause. Commands: - `npm test --prefix ui-web` -- 191 files, 2486 tests, all pass - `npm run lint --prefix ui-web` -- 0 errors (4 pre-existing warnings in CronPage/SubagentsPage, untouched here) - `npm run type-check --prefix ui-web` -- clean, and it is what found the third reader: the agents page imports this export under an alias, so grep missed it - `npm run --prefix ui-web build && python3 ui-web/build.py && node ui-web/scripts/check-page.mjs` -- boot-snapshot OK (235 nodes match golden), page OK - `npx commitlint --from github/refactor/ui_web_architecture --to HEAD` -- clean The new case, `a default-scoped read leaves the conversation rows alone`, was run against a module made to share one array again and fails there on the assertion it is named for. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk One module-level array becomes two, and every reader was enumerated and pointed at the scope it means. No server change, no stored state change, no change to what any read asks for. Rollback is reverting one commit. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A Co-authored-by: gloryfromca <23442919+gloryfromca@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The model picker showed a conversation's model under a vendor whose key does not
serve it.
is_currentis the whole difference between the twomodel.optionsreads: thebackend marks the conversation's provider when the request names a session, and
agents.defaults' when it does not. Both answers landed in one module-levelarray, so the default-scoped read -- the settings dialog's, repeated by every
settings write, and the agents page's -- overwrote the conversation's.
What the reader saw: a conversation running a Gemini model while the default is
a DeepSeek one, one visit to the settings dialog, then a click on the model
chip. The picker opened on DeepSeek's column with
gemini-3.5-flashprependedand ticked inside it, DeepSeek's count raised by that phantom row, and the
current dot on both vendors.
Split, the way the model pair one layer down already is (#596 separated
defaultModelLivefrom the chip's own value and left this array shared).providersLiveis the visible conversation's answer, read by the picker throughmodelSource;defaultProvidersLiveis the configured default's, read by thesettings snapshot, the onboarding model step, and the agents page, which loads
that list for itself.
Type
Verification
Driven on a real host (
raven webon an isolatedRAVEN_HOMEbuilt from a realconfig, default
deepseek/deepseek-v4-pro, the conversation switched togemini/gemini-3.5-flash):gemini-3.5-flashrow inside DeepSeekWithout the settings visit the picker was correct in both columns, which is what
makes the shared array the cause.
Commands:
npm test --prefix ui-web-- 191 files, 2486 tests, all passnpm run lint --prefix ui-web-- 0 errors (4 pre-existing warnings inCronPage/SubagentsPage, untouched here)
npm run type-check --prefix ui-web-- clean, and it is what found the thirdreader: the agents page imports this export under an alias, so grep missed it
npm run --prefix ui-web build && python3 ui-web/build.py && node ui-web/scripts/check-page.mjs-- boot-snapshot OK (235 nodes match golden),page OK
npx commitlint --from github/refactor/ui_web_architecture --to HEAD-- cleanThe new case,
a default-scoped read leaves the conversation rows alone, was runagainst a module made to share one array again and fails there on the assertion
it is named for.
Risk
One module-level array becomes two, and every reader was enumerated and pointed
at the scope it means. No server change, no stored state change, no change to
what any read asks for. Rollback is reverting one commit.
Related Issues
N/A