Repository navigation
fix(ui-web): stop the settings load repainting the conversation chip - #596
Merged
gloryfromca merged 1 commit intoSep 21, 2026
Merged
gloryfromca merged 1 commit into
gloryfromca merged 1 commit into
Conversation
The model chip shows the open conversation's model; agents.defaults holds what new conversations start on. `loadSettings` painted the chip from the defaults, so opening the settings dialog -- or any settings write, each of which reloads -- put the default back over a conversation that had switched. The turns kept running on the model the reader picked, but the chip said otherwise until the page was reloaded, so the switch read as lost. The same line moved the chip onto a new default even for a conversation holding its own model. The line is older than the split between the two values; the split updated `setDefaultPair` above it and left this behind. The chip belongs to `loadProviders`, which asks `model.options` for the visible conversation and runs on every path that changes which one that is -- including the default write, when the server says that conversation follows the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0xKT
approved these changes
Sep 21, 2026
6 of 12 tasks
0xKT
pushed a commit
that referenced
this pull request
Sep 21, 2026
## Summary Restoring a conversation from the archive page put it back everywhere except the screen. `loadSessions` replaced the rail's rows and never drew them, and the restore reaches the rail only through that function, so the row came back in the server's listing and on disk while the rail kept showing the list from before. Reloading the page was what appeared to restore it. Every other writer in this feature already draws after a replace -- `leave.ts` does it on both of its transitions, the session registry does it after its reconcile -- so the fix is the missing call in the one path that did not, rather than a draw at the restore call site. At boot the rail is still held, where a draw sets the skeleton and `releaseRail` paints the rows, so the other caller is unaffected. Found while driving the merged archive flow end to end on a real host, after #585, #589 and #596. It is the same shape as #596: server state correct, screen stale until a reload. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification On a real host (`raven web` on an isolated `RAVEN_HOME` built from a real config, driven through the browser), reading the archived flag from the last metadata record of the session transcript rather than from the page: | Step | Before | After | | --- | --- | --- | | Archive a conversation from the rail | leaves the rail, `archived: true` on disk | same | | Settings, Archive page | it is listed | same | | Restart the gateway | still archived, does not come back | same | | Restore it from the Archive page | `archived: false` on disk, **rail still does not show it** | rail shows it at once | | Reload the page | rail shows it | rail shows it | Commands: - `npm test --prefix ui-web` -- 189 files, 2470 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 - `npm run --prefix ui-web build && python3 ui-web/build.py` -- boot-snapshot OK (235 nodes match golden) - `npx commitlint --from github/refactor/ui_web_architecture --to HEAD` -- clean The new case, `a plain re-read draws the rows it just replaced`, was run against the unfixed module first 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 added call in a function with two callers; the other one runs while the rail is held, where the draw is a no-op beyond the skeleton it already sets. No server change, no stored state change. 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>
6 of 12 tasks
0xKT
pushed 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>
gloryfromca
added a commit
that referenced
this pull request
Sep 21, 2026
…596) ## Summary The composer's model chip showed a model the conversation was not running. `loadSettings` painted the chip from `agents.defaults.model`, which is what NEW conversations start on, not what the open one runs. Every settings load ran that paint: opening the dialog, and every settings write, each of which reloads. So a conversation that had switched model had the default put back over it, the switch read as lost, and a page reload was what appeared to "apply" it. The turns had been running on the picked model the whole time -- only the chip was wrong. The same line moved the chip onto a new default even for a conversation holding a model of its own. The line is older than the split between the two values. That split added `setDefaultPair` right above it, for the settings page's own default-model row, and left this one behind. Deleting it is the whole change: the chip belongs to `loadProviders`, which asks `model.options` for the visible conversation and runs on every path that changes which conversation that is -- including a default-scoped write, when the server answers that this conversation follows the default. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification Reproduced and re-verified on a real host (`raven web` on an isolated `RAVEN_HOME` built from a real config, driven through the browser), reading the model each turn actually ran from `<raven_home>/telemetry/usage-<date>.jsonl` rather than from the page: | Step | Chip before the fix | Chip after | Model the turn ran | | --- | --- | --- | --- | | First turn of a new conversation | v4-pro | v4-pro | v4-pro | | Pick v4-flash in the chip's picker | v4-flash | v4-flash | -- | | Next turn | v4-flash | v4-flash | v4-flash | | Open the settings dialog | **v4-pro** | v4-flash | -- | | Next turn | v4-pro | v4-flash | v4-flash | | Reload the page | v4-flash | v4-flash | -- | | Change the default while this conversation holds its own model | **follows the default** | stays on v4-flash | v4-flash | | Change the default for a conversation that holds none | follows | follows | the new default | The last row is the regression the deleted line could have taken with it: it goes through `applies_to_session` and `loadProviders`, not through this line. Commands: - `npm test --prefix ui-web` -- 189 files, 2469 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 - `npm run gen:check --prefix ui-web` -- generated.ts matches the contract - `npm run --prefix ui-web build && python3 ui-web/build.py` -- boot-snapshot OK (235 nodes match golden), then `check-page.mjs`, `check-css.mjs`, `check-class-namespace.mjs` all OK - `npx commitlint --from github/refactor/ui_web_architecture --to HEAD` -- clean The new case, `a settings load moves the default pair and leaves the conversation chip alone`, was run against the unfixed module first 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 User-visible and in the right direction: the chip stops contradicting the conversation. No behaviour changes on the server, no stored state changes, and nothing else reads what the deleted line wrote. 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: Claude Opus 5 (1M context) <noreply@anthropic.com>
gloryfromca
added a commit
that referenced
this pull request
Sep 21, 2026
## Summary Restoring a conversation from the archive page put it back everywhere except the screen. `loadSessions` replaced the rail's rows and never drew them, and the restore reaches the rail only through that function, so the row came back in the server's listing and on disk while the rail kept showing the list from before. Reloading the page was what appeared to restore it. Every other writer in this feature already draws after a replace -- `leave.ts` does it on both of its transitions, the session registry does it after its reconcile -- so the fix is the missing call in the one path that did not, rather than a draw at the restore call site. At boot the rail is still held, where a draw sets the skeleton and `releaseRail` paints the rows, so the other caller is unaffected. Found while driving the merged archive flow end to end on a real host, after #585, #589 and #596. It is the same shape as #596: server state correct, screen stale until a reload. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification On a real host (`raven web` on an isolated `RAVEN_HOME` built from a real config, driven through the browser), reading the archived flag from the last metadata record of the session transcript rather than from the page: | Step | Before | After | | --- | --- | --- | | Archive a conversation from the rail | leaves the rail, `archived: true` on disk | same | | Settings, Archive page | it is listed | same | | Restart the gateway | still archived, does not come back | same | | Restore it from the Archive page | `archived: false` on disk, **rail still does not show it** | rail shows it at once | | Reload the page | rail shows it | rail shows it | Commands: - `npm test --prefix ui-web` -- 189 files, 2470 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 - `npm run --prefix ui-web build && python3 ui-web/build.py` -- boot-snapshot OK (235 nodes match golden) - `npx commitlint --from github/refactor/ui_web_architecture --to HEAD` -- clean The new case, `a plain re-read draws the rows it just replaced`, was run against the unfixed module first 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 added call in a function with two callers; the other one runs while the rail is held, where the draw is a no-op beyond the skeleton it already sets. No server change, no stored state change. 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>
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 composer's model chip showed a model the conversation was not running.
loadSettingspainted the chip fromagents.defaults.model, which is what NEWconversations start on, not what the open one runs. Every settings load ran that
paint: opening the dialog, and every settings write, each of which reloads. So a
conversation that had switched model had the default put back over it, the switch
read as lost, and a page reload was what appeared to "apply" it. The turns had
been running on the picked model the whole time -- only the chip was wrong. The
same line moved the chip onto a new default even for a conversation holding a
model of its own.
The line is older than the split between the two values. That split added
setDefaultPairright above it, for the settings page's own default-model row,and left this one behind. Deleting it is the whole change: the chip belongs to
loadProviders, which asksmodel.optionsfor the visible conversation and runson every path that changes which conversation that is -- including a
default-scoped write, when the server answers that this conversation follows the
default.
Type
Verification
Reproduced and re-verified on a real host (
raven webon an isolatedRAVEN_HOMEbuilt from a real config, driven through the browser), reading themodel each turn actually ran from
<raven_home>/telemetry/usage-<date>.jsonlrather than from the page:
The last row is the regression the deleted line could have taken with it: it goes
through
applies_to_sessionandloadProviders, not through this line.Commands:
npm test --prefix ui-web-- 189 files, 2469 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-- cleannpm run gen:check --prefix ui-web-- generated.ts matches the contractnpm run --prefix ui-web build && python3 ui-web/build.py-- boot-snapshot OK(235 nodes match golden), then
check-page.mjs,check-css.mjs,check-class-namespace.mjsall OKnpx commitlint --from github/refactor/ui_web_architecture --to HEAD-- cleanThe new case,
a settings load moves the default pair and leaves the conversation chip alone, was run against the unfixed module first and failsthere on the assertion it is named for.
Risk
User-visible and in the right direction: the chip stops contradicting the
conversation. No behaviour changes on the server, no stored state changes, and
nothing else reads what the deleted line wrote. Rollback is reverting one commit.
Related Issues
N/A