Skip to content

Rework model selector with loadout-backed slots - #356

Draft
senxd wants to merge 11 commits into
zeronsh:mainfrom
senxd:rework-model-selector
Draft

senxd wants to merge 11 commits into
zeronsh:mainfrom
senxd:rework-model-selector

Conversation

@senxd

@senxd senxd commented Sep 14, 2026

Copy link
Copy Markdown

Summary

  • Rebuild the model picker on top of the five-slot loadout: slots carry harness, model, effort, provider model options, and a per-slot activation shortcut
  • Per-slot shortcut recording with physical-key conflict detection against the keymap, other loadouts, and reserved composer/app chords; Some("") disables a slot shortcut while None keeps the positional mod-shift-N default
  • Slot cards reorder via drag-and-drop or arrow keys, keeping model options and shortcuts attached to the card; provider catalog columns reorder the same way and persist a device-local provider_order
  • Slot menus split into frosted root + submenu cards (agent / model / effort / model options / speed / shortcut / remove) with full keyboard navigation, replacing the single glass slab over union bounds
  • Catalog requests are scoped to the effective device and workspace cwd, and reload on scope change; pending agent selections never persist an empty model
  • Claude harness passes model[1m] idempotently via a shared model_argument helper; picker rows show each row's resolved loadout shortcut badge
  • Split the loadout page into loadout/{bindings,cards,catalog,indicator,options}.rs and add extensive unit + gpui tests (shortcut normalization, reorder semantics, menu navigation, macOS shifted-digit aliases)

Screenshots

Redesigned the model selector (Reasoning bar, repositioned layout) Custom model loadouts
Redesigned model selector Custom model loadouts

Test plan

  • cargo check --workspace --all-targets
  • cargo test -p zeron-ui loadout (67 tests)
  • cargo fmt --check
  • Manual: record/clear/reorder slot shortcuts on macOS and verify Cmd+Shift+1-5 defaults still work
  • Manual: drag models from provider columns (incl. OpenRouter partition) into slots

Generated with Devin

senxd and others added 11 commits September 11, 2026 21:00
Co-authored-by: Cursor <cursoragent@cursor.com>
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
# Conflicts:
#	crates/ui/src/composer.rs
#	crates/ui/src/motion.rs
#	crates/ui/src/pickers.rs
#	crates/ui/src/settings.rs
#	crates/ui/src/settings/shortcuts.rs
#	crates/ui/src/shell.rs
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The reset button's job is better served by jumping to the Model Loadout
settings page, and the traits tray painted a second hairline under the
tab row's own bottom border. Also switches the loadouts tab glyph to a
star and lands the remaining upstream-merge adaptations (draft options
now live in defaults, test call sites updated).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@senxd

senxd commented Sep 14, 2026

Copy link
Copy Markdown
Author

Also this was not listed above but the Pi provider in the model selector respects the model filter that is set in the Pi config. It should accurately represent what /models looks like in the pi cli.

@wingleeio wingleeio 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.

Security audit

Went through the full diff (39 files, +7910/−500) looking for malicious code, unintended side effects, and accidental exploit surface. Nothing concerning found.

  • Dependencies: the only new dep is glob = "0.3" (workspace + zeron-harness). It resolves to the glob 0.3.4 package that was already in Cargo.lock transitively — same crates.io source and checksum, no new package block. Used only for enabledModels pattern matching in acp/mod.rs.
  • Process / network / filesystem: no new Command spawns, no new HTTP calls, no new sockets. All UI ↔ engine traffic goes through the existing engine.client().call(methods::LIST_HARNESSES | LIST_MODELS) path. The only new fs reads are read_pi_settings on $PI_CODING_AGENT_DIR|~/.pi/agent/settings.json and <cwd>/.pi/settings.json — read-only, JSON-parsed, failures logged via tracing::warn! and treated as "no patterns". Test code writes only into tempfile::tempdir().
  • fake-cursor-shim.sh: test fixture only; the change adds an effort parameter to the fake catalog and a scenario:reasoning branch that echoes canned frames. No commands executed from input.
  • ListModelsParams.cwd (new RPC input): client-controlled path forwarded to models_for_cwd. For Pi it becomes the agent's current_dir for the discovery probe and the location of the project .pi/settings.json. This is the same trust level as existing SetChatCwd / history RPCs that already take an unjailed cwd, and the chat itself already runs Pi in that cwd, so no new capability is exposed. Non-Pi harnesses ignore it (default trait impl).
  • Shortcut recording: bindings.rs::on_key_down only captures a keystroke while recording_slot.is_some(), stores the normalized combo string in settings, emits RecordingChanged, and stops. No key logging or persistence beyond the slot field.
  • Unicode: no bidi/zero-width/homoglyph characters in added lines (only ⌘ ⇧ → – — … in display strings/comments).
  • Formatting churn in unrelated files (schema.rs, uploads.rs, e2e.rs, normalize.rs, subagent.rs, browser/*.rs, examples): main currently fails cargo fmt --check on exactly those spans; this PR just brings them in line. Pure whitespace/line-wrapping, verified.

Verification (Linux, PR head 37ef3a45)

Check Result
cargo fmt --check clean
cargo check --workspace --all-targets pass
cargo test -p zeron-ui loadout 67 passed
cargo test -p zeron-ui pickers / shortcut / settings:: 28 / 42 / 140 passed
cargo test -p zeron-harness 209 passed (incl. new Pi scope, model_argument, cursor effort tests)
cargo test -p zeron-engine --test e2e 20 passed, 1 ignored
clippy 2 pre-existing deny errors in opencode/tests.rs (untouched by this PR), nothing new

Manual test under Xvfb (X11, real claude/codex/cursor-agent/devin CLIs on PATH, mock engine)

Composer model picker: pinned loadout rows with shortcut badges, harness rail, context-window + reasoning bar Picker config popover
picker config
Settings › Model Loadout after drag-and-dropping three models into slots (catalogs populated from live CLIs) Slot root menu
loadout menu
Shortcut submenu, recording Conflict detection (recorded Ctrl+Shift+2 on slot 1) Slot activated from picker row → composer chip updates
recording conflict activated

Worked as described: drag-and-drop into slots, checkmarks on catalog rows, slot menus (agent / model / effort / context window / fast / shortcut / remove), shortcut recording + conflict notice, clicking a loadout row in the picker, and Ctrl+N auto-applying slot 1 to a new session. No loadout/picker/toast warnings or panics in the app log.

Findings

1. Default loadout chords Ctrl+Shift+1–5 don't dispatch on Linux/X11 (bug)

Pressing Ctrl+Shift+1…5 in the chat view never fired ActivateLoadout — no model switch on a filled slot and no "slot is empty" toast on an empty one (control: Ctrl+N, Ctrl+, work fine).

Root cause: gpui_linux::keystroke_from_xkb reports Ctrl+Shift+1 as {control, key: "!", shift: false} (shift is intentionally dropped for symbols), and Linux uses DummyKeyboardMapper, so a ctrl-shift-1 binding never matches. The PR already has the fix — loadout_symbol_alias (mod-shift-1mod-!) — but it is only bound under #[cfg(target_os = "macos")] in apply_keymap (shell.rs:409). The gpui tests pass because simulate_keystrokes bypasses XKB translation. Suggest dropping the cfg gate (bind the alias on every platform; harmless where the shifted form is what arrives).

The conflict screenshot shows the same mechanism from the recorder side: the notice reads "Ctrl+@ is already assigned to loadout slot 2" — the physical-key comparison correctly resolved it, but the display should fold mod-@ back to Ctrl+Shift+2 (invert SHIFTED_DIGITS in normalize_loadout_shortcut or in display_combo).

2. heal_reserved_shortcuts can silently rewrite a user's custom app shortcut (behavior note)

Because the five default loadout combos are now "owned", any persisted keymap entry on mod-shift-1..5 is reset to its default on load, and if the default is taken it becomes "" (disabled) with no notice. fixed_loadout_shortcuts_heal_persisted_conflicts asserts exactly this. Probably acceptable, but worth a changelog/toast since it changes existing users' bindings on upgrade.

3. Pi discovery error path hides the cause (minor)

In models_for_cwd, Err(_) if patterns.is_some() => Ok(Vec::new()) returns an empty catalog when the probe fails and the user has enabledModels, so the column shows nothing rather than an error. Consider propagating the error (or at least tracing::warn!).

4. Per-cwd Pi probe is uncached

Pi now spawns a fresh discovery process on every LIST_MODELS (the models_cache short-circuit is skipped so project-local enabledModels can apply). The UI caches per scope so it's only on scope change, but worth keeping in mind.

Overall: safe to merge from a security standpoint; #1 is a functional regression on Linux I'd fix before merging.

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