feat(speed-dial): :1..:9 model quick-switch slots#3075
Open
Wang-tianhao wants to merge 8 commits intotailcallhq:mainfrom
Open
feat(speed-dial): :1..:9 model quick-switch slots#3075Wang-tianhao wants to merge 8 commits intotailcallhq:mainfrom
Wang-tianhao wants to merge 8 commits intotailcallhq:mainfrom
Conversation
Introduce persistent speed-dial bindings that map single-digit slots
(1..=9) to provider/model pairs, with a new SetSpeedDialSlot config
operation threaded through infra, services, app, and API layers.
Why: the TUI and zsh plugin need a session-scoped way to switch models
with one keystroke (:1../:9, /1..:/9) without rewriting the global
default model. The bindings themselves must survive shell restarts so a
[speed_dial] table is added to ForgeConfig with serde + JsonSchema.
- forge_config: new SpeedDial(BTreeMap<String, SpeedDialEntry>) newtype
keyed by decimal slot string so TOML renders as [speed_dial.1]; adds
is_valid_speed_dial_slot helper and SpeedDialError for range checks.
- forge_domain: ConfigOperation::SetSpeedDialSlot { slot, config: Option }
with None semantics = clear.
- forge_infra: apply_config_op handles the new variant, including
dropping speed_dial back to None when the last slot is cleared.
- forge_services/forge_app: AppConfigService gains get_speed_dial();
MockServices in command_generator.rs grows a matching stub.
- forge_api: API trait exposes get_speed_dial() returning SpeedDial.
- forge.schema.json: regenerated by cargo test -p forge_config.
Ported from spec commit 614a293 on branch feat/model-speed-dial; reworked
to land cleanly on main (25291b5) which already integrates websearch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `config set speed-dial <N> <provider> <model>` (and `--clear`), `config get speed-dial [<N>]`, and `config get speed-dial-slot <N>` porcelain helpers to the Clap surface. Wire matching arms into the `handle_config_set`/`handle_config_get` UI dispatch so the new enum variants round-trip end-to-end. Why: the zsh plugin resolves slot bindings via `config get speed-dial-slot <N>` (TAB-separated provider/model), and CLI users need a scripting path that matches the TUI `:sd` flow. Slot validation (1..=9) lives in the UI handler so an invalid slot fails fast before any `SetSpeedDialSlot` op reaches the infra layer. Ported from 614a293; the UI dispatch site had to be folded into this commit rather than step 4 because Clap's exhaustive match on `ConfigSetField`/`ConfigGetField` refuses to compile with the new variants uncovered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the speed-dial AppCommand variants and the digit-slot parsing hook
that short-circuits the Clap parser before it rejects a digit-only
subcommand.
Why: Clap's Subcommand derive cannot accept digit-only command names
like `/1`, so slot activation is handled by a hand-rolled hook inside
ForgeCommandManager::parse() that runs after sentinel stripping but
before ClapCmd::try_parse_from. The menu command `/speed-dial` (alias
`/sd`) is Clap-driven; only the digit slots are special-cased. `/0`,
`/10`, `/1abc`, and `/12abc` fall through to AppCommand::Message so
existing chat inputs starting with slash+digit keep working.
- AppCommand::SpeedDial { slot, message } marked #[command(skip)] and
is_internal() because it's dispatched by the hook, not Clap.
- AppCommand::SpeedDialMenu uses #[command(name = "speed-dial", alias =
"sd")] so Clap handles it normally.
- default_commands() manually registers digit slots 1..=9 as
ForgeCommand entries so completion can surface them even though
AppCommand::iter() filters SpeedDial out as internal.
- is_reserved_command() gains "speed-dial", "sd", and "1".."9" so
agent commands cannot shadow the new namespace.
- Stub handle_speed_dial_activate/menu in ui.rs bail with a placeholder
to keep the exhaustiveness match on on_command() compiling; the real
handlers land in the next commit.
Ported with 8 parser tests from 614a293 verbatim, renamed
SlashCommand → AppCommand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the step-3 stubs with the real `handle_speed_dial_activate` and `handle_speed_dial_menu` implementations, completing the TUI surface. Why: activating a slot reuses the existing `activate_provider_with_model` plumbing so session-scoped model overrides behave the same as `/config-model` — including falling back to a model picker if the bound model is missing from the provider. A trailing prompt (`/1 explain this diff`) triggers the spinner and dispatches via `on_message`, mirroring the zsh one-shot flow. An unbound slot prints a hint and returns cleanly instead of failing. The menu variant (/speed-dial, alias /sd) prints the populated bindings; an empty table prints a short setup hint. Slot validation is defensive even though the parser filters 0/10/out-of-range upstream. Ported from 614a293. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface the feature end-to-end: :info gains a SPEED DIAL block listing populated slots, the zsh dispatcher widens its accept-line regex to match `:1`..`:9` (and `:sd`), two new action handlers drive the one-shot switch and fzf management UX, and README gets a walkthrough with the opus/sonnet/gpt-5.4 binding example. Why: without the dispatcher regex change, `:1` falls through to `zle accept-line` and becomes a literal shell command. The regex is widened to only accept a single digit `1..9` — `:10`, `:12abc`, and `:0` still reject so they behave like ordinary shell commands as before. The regex change was smoke-tested manually against the 8 cases from the plan file appendix. - info.rs: SPEED DIAL section is suppressed entirely when no slot is configured to keep `:info` compact for non-users. - dispatcher.zsh: regex widened; two new case arms (speed-dial|sd and [1-9]) before the catch-all. - actions/config.zsh: `_forge_action_speed_dial` resolves via the new porcelain helper `forge config get speed-dial-slot <N>` so shell code does not need to parse TOML. `_forge_action_speed_dial_manage` supports the 3 forms from the plan: bare (fzf over slots), `<N>` (model picker), `<N> --clear`. - plans/: port of the v1 plan (manual smoke-test appendix included). - README: new Model Speed Dial section + quick-ref entries for :1..:9 and :speed-dial. Ported from 614a293. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new config-operation variant so callers can express "revert to no session override" symmetrically to SetSessionConfig(mc). Used by the upcoming speed-dial temporary-override flow to restore a prior "no override" snapshot after a one-shot :N <prompt>. - forge_domain: new ClearSessionConfig variant on ConfigOperation. - forge_infra: apply_config_op sets fc.session = None for the new op. - forge_services: mock apply in app_config.rs mirrors the same. - forge_api: update_config's needs_agent_reload predicate also matches ClearSessionConfig so the active-agent cache invalidates on clear. - Two new round-trip tests in forge_infra (remove-existing + empty-noop). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bare :N stays sticky. :N <prompt> now snapshots the current session config, quietly switches to slot N, runs the one-shot, and restores the snapshot after the agent turn ends — even if the turn errors. Why: the old contract made :N <prompt> a sticky switch too, forcing users on slot 1 who wanted one quick opinion from slot 2 to type :2 Hello then :1 back. The back-and-forth is exactly the friction speed dial was meant to remove. - ui.rs: thread `quiet: bool` through activate_provider_with_model and finalize_provider_activation. Only gates the two "is now the default provider/model" banners on !quiet. All non-speed-dial call sites pass false; the new temp path passes true. - handle_speed_dial_activate: branch on message.is_some_and(non-empty). None → sticky path unchanged. Some(prompt) → snapshot get_session_config(), quiet switch, dim "↻ slot N (temporary)" line, run on_message, always restore via SetSessionConfig(prev) or ClearSessionConfig, dim "↻ restored" on success, propagate errors in order (turn first, then restore). - README: document sticky vs. temporary, update quick-ref row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The zsh plugin's `_forge_action_speed_dial` was setting
`_FORGE_SESSION_MODEL` and `_FORGE_SESSION_PROVIDER` unconditionally,
then — if a prompt was appended — running the turn. Nothing restored
the previous overrides afterwards, so `:3 hello` left the shell stuck
on slot 3 even though the README promised the borrow was temporary.
Split the handler into the two paths that match the forge-REPL
behaviour of `/N` vs `/N <prompt>`:
- Sticky path (no prompt): assign overrides, log success, done.
- Temporary path (prompt appended): snapshot the prior override pair,
apply the slot's binding, log `(temporary)`, dispatch the prompt,
and restore via zsh `{ ... } always { ... }` so the snapshot is put
back even when forge exits non-zero, returns early, or the turn is
interrupted. Empty previous values round-trip correctly because the
plugin treats empty `_FORGE_SESSION_*` as "use global config", so no
special-casing is needed.
No behaviour change for the sticky path, no Rust-side change — this is
purely aligning the shell handler with documented semantics.
Co-Authored-By: Claude Opus 4.7 (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
:1–:9to a model preset for one-keystroke switching/1–/9activate a slot;/speed-diallists/manages bindings;:N <prompt>runs a single prompt under slot N without changing session configspeed-dial get/set; newConfigOperation::ClearSessionConfigso slot activation can cleanly reset session model stateCommits
feat(speed-dial): domain/config/infra support—forge_config::SpeedDial, env wiring, persistencefeat(speed-dial): extend config CLI with speed-dial get/setfeat(speed-dial): parse /1../9 and /speed-dial in Clap parserfeat(speed-dial): wire UI handlers for activate/menu + configfeat(speed-dial): info block, zsh dispatcher, READMEfeat(config): add ConfigOperation::ClearSessionConfigfeat(speed-dial): temp override for :N <prompt>Test plan
cargo check --workspace(passes locally)cargo test -p forge_config speed_dialforge config speed-dial set 1 <model>; then/1switches;/speed-diallists;:1 helloruns once and reverts/1–/9correctly🤖 Generated with Claude Code