feat(settings): the model field is an open picker, sourced from the harness (#493) - #626
Merged
Conversation
…arness (#493) The last unchecked box on #493's layout checklist: "if possible, make model input a picker too (but model IDs change often so not sure if we can dynamically inspect?)". Both halves of that worry are right and they pull opposite ways, so the control is an <input list> over a <datalist> and not a <select>: a model ID this box has never heard of stays typeable, and a profile already naming one survives a Save with nothing touched - the same rule render_effort_options() keeps one control along. What CAN be inspected is the alias. Aliases are the stable half of the churn - `opus` and `sonnet` outlive the dated IDs behind them - so the list is read off the harness's own `--help` and never written down here. claude publishes its aliases in the `--model` option's own description; codex documents none, and gets an empty list rather than a guess we would ship stale. The second source is every MODEL already saved in a profile naming that harness: free, exactly what this box uses, and the only source a harness with a silent --help has at all. Only the `--model` option's own block is harvested. claude quotes an example in --fallback-model and modes in --sandbox, and offering a sandbox mode as a model is a suggestion that starts nothing. The binary comes from CONNECT_BINS via connect_flow(), so a harness the box has not installed yet is probed no more than its connection card is, and a probe that fails costs a suggestion rather than the field. One fork per harness per help-text change, cached on the binary's stamp. A saved profile's row names its harness, so the daemon renders that row's `list` itself; "New profile" preselects nothing, so settings.js follows that row's assistant picker. model_hint_lists() is the single answer to which datalists the page carries, so a row can never reference one that is not there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLLSUAnVUAgw7r4JUvbMYV
`--model` as the LAST option in a help page has no next-option line to stop at, so the block ran to the end and would quote whatever prose followed. A ceiling rather than a cleverer boundary: this is a suggestion list, and a dozen is already more than a picker wants to show. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLLSUAnVUAgw7r4JUvbMYV
… daemon's life Two findings from the review pass on this branch. settings.js synced the datalist only on `change`, which is not the only way a picker's value moves. form.reset() after a save restores the "Choose an assistant" prompt while leaving the attribute set, so reopening "Add profile" offered the previous assistant's aliases under a picker that no longer named it; and Firefox restores a <select>'s value across a plain reload without firing anything, so the new-profile row never got a list at all until the picker was touched. One walk over every profile row, called at init and after a post, covers both. harness_model_aliases() cached its negative results under the binary's own stamp - which never moves for an unchanged binary - so a probe that timed out once under load, or raced a lazy `nix profile add` still linking the CLI, pinned "this harness suggests nothing" for the life of the daemon while `claude --help` worked perfectly on the next call. A probe that RAN and named nothing (codex) is still a real answer and still cached forever; a probe that could not be made is now held only long enough not to re-fork on every render of a per-second live feed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLLSUAnVUAgw7r4JUvbMYV
10 tasks
|
Warning Review limit reachedNext included review available in 8 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
These pages paint one fixed dark palette and give every control its own background and colour, so nothing here looked like it needed this. But an authored colour never reaches the UI the browser draws for ITSELF - and #493's model field is the first thing on the page to open one. Its <datalist> popup arrived as near-black text on a dark ground, reported from a real browser against the preview of this branch. One declaration on :root fixes the popup and everything else in that class - scrollbars, autofill overlays, spinners, a <select>'s own dropdown list - on both pages, since settings.css is one shared STYLE. Declared rather than media-queried: this palette does not follow the reader's preference, so neither should the chrome beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLLSUAnVUAgw7r4JUvbMYV
…wn light" This reverts commit cd8a1ab.
… does The suggestions arrived as near-black text on a dark ground in a real browser. A <datalist> popup is drawn by the BROWSER, not by the page: no rule in settings.css reaches inside it, and declaring color-scheme: dark (tried, reverted in the commit before this) did not change what it drew. The box already had the answer. The "Starting folder" field on "Add session" (issue #131) is a combobox that owns its popup outright - a <ul class="ac"> the page styles like everything else - and it has looked right the whole time. This is the same control with a different source, so it uses it rather than re-litigating the question. The two differ in three places and nowhere else: where the entries come from (a fetch per directory level, versus a JSON blob the daemon already put on the page), what a keystroke filters (the fragment after the last "/", versus the whole value), and what picking one means (append "<name>/" and reveal the next level, versus fill the field and close). acKind() names which is which; the popup, the CSS, the keyboard handling and the mouse handling are shared untouched. It also deletes a moving part rather than adding one. There is no `list` attribute to keep in step with the assistant picker beside it, so the review's stale-datalist finding is gone by construction, along with syncModelLists() and the two call sites that existed to defend it: the row's assistant is read at the moment the popup opens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLLSUAnVUAgw7r4JUvbMYV
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
Closes the last unchecked box on #493's layout checklist: "if possible, make
modelinput a picker too (but model IDs change often so not sure if we can dynamically inspect?)". The other five went in with #571; I verified each againstmasterand checked them off in the issue before starting this.Both halves of that question are right, and they pull opposite ways. A closed
<select>would break a model ID this box has never heard of (untypeable) and a profile that already names one (a Save with nothing touched would erase it). So the control is free text over a popup — a picker that still accepts anything. That is the rulerender_effort_options()already keeps for a value its own list does not know, one control along.The popup is the page's own, not the browser's. This shipped as an
<input list>over a<datalist>first, and that was wrong for a reason no test could see: a<datalist>popup is drawn by the browser, so no rule insettings.cssreaches inside it, and on a real browser the suggestions arrived as near-black text on a dark ground. Declaringcolor-scheme: darkdid not change what it drew (tried, then reverted — the commits are on the branch as the record). The box already had the answer: the Starting folder field on "Add session" (issue #131) is a combobox that owns its popup outright — a<ul class="ac">the page styles like everything else — and has looked right the whole time. The model field is now the same control with a different source.The two differ in three places and nowhere else: where entries come from (a fetch per directory level, versus a JSON blob the daemon already put on the page), what a keystroke filters (the fragment after the last
/, versus the whole value), and what picking one means (append<name>/and reveal the next level, versus fill the field and close).acKind()names which is which; the popup, the CSS, and the keyboard and mouse handling are shared untouched.That also removed a moving part. There is no
listattribute to keep in step with the assistant picker beside it, so the stale-datalist review finding is gone by construction along withsyncModelLists()and the two call sites that existed to defend it — the row's assistant is read at the moment the popup opens.What can be inspected is the alias. Aliases are the stable half of the churn —
opusandsonnetoutlive the dated IDs behind them, which is why the harnesses publish them at all. So the list is read off the harness and never written down here; a box learns a new alias the day its harness updates, and there is no table for us to let rot.Two sources, neither of which can go stale on its own:
--help.claudenames them in the--modeloption's description (an alias for the latest model (e.g. 'fable', 'opus', or 'sonnet') or a model's full name (e.g. 'claude-fable-5')).codexdocuments none — its--modelsays only "Model the agent should use", andcodex completion bashfile-completes the value — so it contributes nothing rather than us guessing a list on its behalf and shipping it stale.MODELalready saved in a profile naming that harness. Free, exactly the set this box actually uses, and the only source a harness with a silent--helphas at all.Only the
--modeloption's own block is harvested.claudequotes an example in--fallback-modeland modes in--sandbox; offering a sandbox mode as a model is a suggestion that starts nothing.The binary comes from
CONNECT_BINSviaconnect_flow()— which is also where the owner's call on #493 put the harness list ("should match the list of harnesses in the connections UI") — so a harness the box has not installed yet is probed no more than its card is, and a probe that fails costs a suggestion rather than the field. One fork per harness per--helpchange, cached on the binary's(mtime, size); measured at 0.22s cold, ~0.2ms warm.A saved profile's row names its harness, so the daemon renders that row's
listitself; "New profile" preselects nothing (#493 removed the box default), sosettings.jsfollows that row's assistant picker.model_hint_lists()is the single answer to which datalists the page carries, so a row can never reference one that is not there.User-visible effect
The model field gains a dropdown of its assistant's aliases and of the models this box already uses. It is still free text: anything typeable before is typeable now, and no stored value can be erased by a Save that did not touch it. No new configuration, no new environment variable, nothing to migrate.
Security
No new surface. Suggestion values are
html.escape()d and, from the--helpsource, constrained to[A-Za-z0-9._-]before that. Nothing new is executed:--helpon a binary the connections panel already runsauth statuson, resolved through the sameos.access(X_OK)guard. No profile value is exposed that the panel did not already render —MODELwas already on the page in every row's edit form.That is a real headless render of the live page — which is itself a small argument for the change, since the browser's own popup could never appear in one. What the daemon puts on the page:
The panel with nothing open, showing the row layout the rest of #493's checklist asked for:
Test plan
python3 tests/test-profile-panel.py— 54 tests, 12 new, green.aarch64-linux(nix build --keep-goingovernix eval .#checks.aarch64-linux --apply builtins.attrNames), exit 0 —profile-panel,golden-snapshot,module-generated-up-to-date,backend-parity,one-spec-both-backendsincluded. Status captured without a pipe.nix run .#assemble+nix run .#update-goldencommitted.settings-daemon.py/.js/.cssare web-only payloads, sotests/native/expected/does not move.s->sonnet), switched the assistant and watched the list swap with no attribute in between (claudealiases ->gpt-5.1-codex-max), clicked a suggestion (fills the field, closes the popup), drove it from the keyboard (two ArrowDowns highlightopus, Enter accepts it and does not submit the form), and confirmed a saved row drives its own popup from its own assistant.claude/codex, then drove the live page in headless chromium over the DevTools protocol - not a curled copy - so every assertion below went through the page's own code: new pane starts with nolist; clicking "Add profile" and picking claude setspmodel-claude; picking codex setspmodel-codex; a harness whose datalist is absent removes the attribute; both saved rows carry their own harness's list from the server; and a real Save posts, collapses the editor, resets the form and leaveslist=(none)under the restored "Choose an assistant" prompt (finding 1 below), with the new row coming back assonnet=pmodel-claudeand the datalists refreshed inside the morphed#profiles-list.webhook.nix,connect.nix,settings-page.nix,sessions-web.nix) andtests/e2e/for assertions over the MODEL input's attribute run before insertingdata-model-input— none match it.Review
/code-review highover the whole branch. Two findings, both real, both fixed indfc3b2bwith a regression test each:change, which is not the only way a picker's value moves:form.reset()after a save restores the "Choose an assistant" prompt while leaving the attribute set, and Firefox restores a<select>'s value across a plain reload without firing anything. Fixed with a sync at init and after a post — and then made unreachable by the popup rewrite, which reads the assistant when the popup opens and keeps no attribute at all.harness_model_aliasescached negative results under the binary's own stamp, which never moves for an unchanged binary - so one timeout under load, or a race with a lazynix profile addstill linking the CLI, meant that harness offered nothing until the daemon restarted. A probe that ran and named nothing (codex) is still a real answer and still cached forever; a probe that could not be made is now held only long enough not to re-fork on every render of a per-second live feed.No security-review trigger: this touches no auth, secret, token, webhook routing, sudo rule or Caddy config. No AWS, IAM, networking or migration impact.
Notes
connect_flows()expands~at call time, anddaemon_with()restores the process environment once the import is done — so a profile-panel test that pinned no binary for a harness silently probed the real CLI on the machine running the suite.ProfileFixture.in_fixture()keeps the probe inside the fixture, and says why.Fixes #493
🤖 Generated with Claude Code
https://claude.ai/code/session_01NLLSUAnVUAgw7r4JUvbMYV