Skip to content

fix(ui-web): one provider list per scope, as the models already are - #604

Merged
0xKT merged 1 commit into
refactor/ui_web_architecturefrom
fix/provider_rows_per_scope
Sep 21, 2026
Merged

0xKT merged 1 commit into
refactor/ui_web_architecturefrom
fix/provider_rows_per_scope

Conversation

@gloryfromca

Copy link
Copy Markdown
Member

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

  • 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.

  • Relevant tests pass locally
  • 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.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

`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
gloryfromca requested a review from 0xKT September 21, 2026 13:47

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 passed
  • npm run type-check --prefix ui-web: passed

@0xKT
0xKT merged commit cbaa057 into refactor/ui_web_architecture Sep 21, 2026
38 of 39 checks passed
@0xKT
0xKT deleted the fix/provider_rows_per_scope branch September 21, 2026 14:01
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants