feat: add directional model and reasoning shortcuts - #1247
Conversation
Cycling was the only way to move through models and reasoning efforts, so reaching the previous entry meant wrapping all the way around. Add five commands with explicit direction: modelPicker.previousModel Mod+ArrowLeft modelPicker.nextModel Mod+ArrowRight modelPicker.decreaseReasoning Mod+ArrowDown modelPicker.increaseReasoning Mod+ArrowUp modelPicker.cycleProvider Command+Right Option (macOS) Distinguishing the provider chord needs the physical side of the Option key, which `key` reports as "Alt" for both; `normalizeAppShortcutInputKey` now falls back to `code` for AltRight. The two existing commands keep their chords and are relabelled to say what they do: "Next model" becomes "Cycle model", "Next reasoning level" becomes "Cycle reasoning effort". The repeated composer activation context is extracted to `composerWithoutModal`. Bundled SDK types and templates are regenerated for the new command ids. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The provider chord was Command+Right Option, which only macOS can produce, so Windows and Linux had no way to reach it. Move it to Alt+P, completing the Alt+M / Alt+P / Alt+T cycle family, and drop the right-Option handling that existed solely for the old chord — the shortcut recorder rejects modifier-only keys, so no user could bind it anyway. An Option chord that reaches the browser inserts a composed character, so the handler now claims the chord whenever it is in scope and no-ops when there is nowhere to rotate. It also owns the chord while the popover is open, matching the model and reasoning cycles. Group the cycle commands ahead of the directional ones in the picker, the default bindings, and Settings → Keyboard, and move the directional chords from prose into the shortcut table.
Three copies of the open-picker context literal, three copies of the cycle-chord ownership expression, and a hand-rolled reverse of `nextCycleValue` had accumulated across the model picker keybindings. Name the open-picker scope once as `pickerOpenOnly`, add `ownsCycleChord` beside the existing `ownsClosedNavigationChord`, and derive `previousCycleValue` from `nextCycleValue` over the reversed list so both directions share one policy for wrapping and absent values. No behavior change.
The expected-binding array spelled out all six shortcut fields six times and repeated both `when` shapes verbatim, so the Alt chord table was around ninety lines of near-identical literals. Hoist the two scopes, add an `altChord` builder, and fold the navigation `when` check into the tuple assertion beside it. Every field is still asserted; only the spelling is shorter. The scopes stay test-local literals rather than imports so the assertions cannot go circular.
|
🚨 SLOP COP 🚨 · I am the SlopCop. I started the review for PR #1247. I will check security, code quality, architecture, duplicate code, performance, and the end-to-end behavior. |
| // individual picker level; `editableFocus` would reject Tab-focused buttons | ||
| // before those handlers can identify their composer. They only act on a | ||
| // closed picker, so they intentionally have no `modelPickerOpen` copies below. | ||
| binding("modelPicker.previousModel", "ArrowLeft", { mod: true }, composerWithoutModal), |
There was a problem hiding this comment.
🚨 slopcop/review — These defaults replace native prompt editing keys.
Mod+ArrowLeft/Right moves to a line boundary on macOS. Control+ArrowLeft/Right moves by word on Windows and Linux. The app dispatch runs before the editor handles the key. When multiple models exist, these bindings change the model instead of moving the caret. The browser smoke test reproduced that model change. Please use non-editor defaults or leave these commands unbound by default. Add an editor test that protects native caret movement.
| ): T | null { | ||
| if (options.length < 2) return null; | ||
|
|
||
| const currentIndex = options.findIndex((option) => option.value === current); |
There was a problem hiding this comment.
🚨 slopcop/review — Direction uses provider list order instead of the canonical reasoning rank.
activeModel.supportedReasoningEfforts keeps the provider response order. This helper then uses the adjacent array item. A valid unsorted response can make Increase select a lower effort, or make Decrease select a higher effort. reasoningLevelValues defines the load-bearing rank. Please traverse supported values by that rank, or sort once at the boundary. Add a test with unsorted options.
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
ELI5: This change adds keyboard controls for providers, models, and reasoning levels. The controls work, but two direction rules need correction.
I found two issues:
- High: The new
Mod+Arrowdefaults replace standard prompt editing keys. They can change execution options when a user wants to move the caret. - Medium: Reasoning direction follows provider response order. An unsorted response can make Increase move down or make Decrease move up.
I left an inline comment for each issue.
The security review found no actionable issue. The performance review found no actionable issue.
The architecture scan found no duplicate model or provider cycle implementation. It found one duplicate policy: reasoning rank already exists in reasoningLevelValues.
Validation results:
- The app and server type checks passed.
- The 46 focused app tests passed.
- The five focused server tests passed.
- The browser check passed for model, provider, and reasoning controls.
- The browser check also reproduced the native caret-key conflict.
- A broad server run had 1,369 passes and ten unrelated timeout failures.
Summary
Cycling was the only way to move through models and reasoning efforts, so reaching the previous entry meant wrapping all the way around. This adds explicit directional commands, and a provider cycle that works on every platform.
Alt+MAlt+PAlt+TMod+←/Mod+→Mod+↓/Mod+↑The two existing chords keep their keys and are relabelled to say what they do: "Next model" becomes "Cycle model", "Next reasoning level" becomes "Cycle reasoning effort".
Models wrap at either end; reasoning effort clamps. The cycle chords also work while the picker popover is open, since the popover is itself modal and would otherwise block them.
macOS composes
Option+<letter>into another character, so the Alt chords match on the physical key. For the same reason an in-scope cycle chord always claims the key, even with nowhere to rotate — otherwise the composed character lands in the prompt.Commits
feat(app): add directional model and reasoning shortcuts— the five new commands and their bindingsfix: cycle providers with a cross-platform chord— replaces the macOS-onlyCommand+Right OptionwithAlt+Pand drops the right-Option handling it neededrefactor: dedupe model picker keybinding scopes— one named open-picker scope, one ownership helper, one cycle-direction policytest(server): compact app keybinding chord assertionsTest plan
turbo run typecheck test lintover@bb/app,@bb/server,@bb/domain: 505 test files pass, 0 lint errors🤖 Generated with Claude Code