fix(tui): refuse provider switches while a turn is running (#2982 turn lock)

The #2982 turn lock (App::reject_setting_change_while_busy) refused mode,
thinking, model and permission changes during a turn. Most provider
switches skipped it. Only the cross-provider model picker checked
(apply.rs:939); AppAction::SwitchProvider (`/provider <id>`),
ProviderPickerApplied, the external-grant reuse, the custom provider, the
setup and login flows (Codewhale-owned, OrcaRouter) and local Ollama
adoption all called switch_provider directly. A mid-turn `/provider <id>`
then replaced the engine under the running turn. That turn kept running
on the retired engine, and after Ctrl+C or the stall watchdog the session
said "a cancelled turn is still settling" (design evidence V7 in the #6981
review).

switch_provider (provider_routes.rs) now checks the lock first. It refuses
with MessageId::SettingSubjectProvider, the existing localized "{setting}
is locked while a turn is running" sentence, and returns false. Every
caller already handles false. One choke point covers all callers, present
and future, instead of a check at each entry. The provider fallback and
auth-rollback respawns do not go through switch_provider and are unchanged.

Behaviour notes:
- A local Ollama adoption that would happen mid-turn shows the same
  status. It is not retried by itself; it happens on the next catalog
  adoption check. Before, it switched under the turn.
- The runtime API's provider-switch route is a separate server path and
  is unchanged.

Verification (source: 8867749f54 plus this diff)
- Real TUI in an isolated tmux socket (env -i, scratch HOME), with a local
  stub provider that holds each response for 25 s. During the turn
  ("working 3s ... Esc to interrupt"), `/provider openrouter` + Enter
  printed "Provider is locked while a turn is running — press Esc to
  interrupt first". The footer stayed "DeepSeek · probe-model" and the
  turn kept running.
- scripts/dev-cargo.sh clippy --workspace --all-targets --all-features
  --locked -- -D warnings -A clippy::uninlined_format_args
  -A clippy::too_many_arguments -A clippy::unnecessary_map_or: clean.
- scripts/dev-test.sh tui provider ollama onboarding setting_locked
  live_route turn_lock model_picker login:
  1127 tests run: 1127 passed, 14224 skipped.
- cargo fmt --check clean. sync-changelog.sh: mirrored.
- npm test and check:web were last run on 8867749f54 (101/19/420(413+7)/7,
  vitest 772/772, check:web exit 0). This diff touches one Rust function
  and CHANGELOG.

Not verified: a baseline run of the old binary in the same probe (the old
path is the design review's V7 evidence), Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-16 11:40:38 -07:00
1 parent 8867749f54
commit faaf61be64
3 files changed
+17

No files matched your search

+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Switching provider is now refused while a turn is running, as model,
thinking and permission changes already were. The refusal covers
`/provider <id>`, the provider picker, a custom provider, setup and login
flows, and adopting a local Ollama. The status line reads "Provider is
locked while a turn is running — press Esc to interrupt first". Before, a
mid-turn `/provider <id>` replaced the engine under the running turn
(Refs #2982).
- `/init` now asks the model to write `AGENTS.md` in the interface language
(for example Simplified Chinese when the TUI runs in zh-Hans), headings
included, keeping commands, file paths and code identifiers as they are.
+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Switching provider is now refused while a turn is running, as model,
thinking and permission changes already were. The refusal covers
`/provider <id>`, the provider picker, a custom provider, setup and login
flows, and adopting a local Ollama. The status line reads "Provider is
locked while a turn is running — press Esc to interrupt first". Before, a
mid-turn `/provider <id>` replaced the engine under the running turn
(Refs #2982).
- `/init` now asks the model to write `AGENTS.md` in the interface language
(for example Simplified Chinese when the TUI runs in zh-Hans), headings
included, keeping commands, file paths and code identifiers as they are.
+3
View File
@@ -626,6 +626,9 @@ pub(crate) async fn switch_provider(
identity: crate::config::ProviderIdentity,
model_override: Option<String>,
) -> bool {
if app.reject_setting_change_while_busy(MessageId::SettingSubjectProvider) {
return false;
}
if let Err(reason) = config.verify_provider_identity(&identity) {
app.push_status_toast(reason, StatusToastLevel::Error, Some(8_000));
return false;