From 4c789762e5b3e3ee5609be0bcc829df98674fe3b Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Sun, 8 Mar 2026 04:38:22 -0400 Subject: [PATCH 1/2] fix(tui/settings): defer 1M toggle until close Stage session fast-mode and context-mode changes while the model settings overlay is open, then apply them on close. This prevents the 1M context toggle from reconfiguring the session and compacting history while the user is still inside settings. Add regression tests covering deferred apply and no-op restores. --- .../src/bottom_pane/model_selection_view.rs | 150 +++++++++++++++++- .../tui/src/chatwidget/settings_overlay.rs | 6 + 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/code-rs/tui/src/bottom_pane/model_selection_view.rs b/code-rs/tui/src/bottom_pane/model_selection_view.rs index fff7b8dc68f..eab099cb411 100644 --- a/code-rs/tui/src/bottom_pane/model_selection_view.rs +++ b/code-rs/tui/src/bottom_pane/model_selection_view.rs @@ -144,6 +144,11 @@ pub(crate) struct ModelSelectionView { current_effort: ReasoningEffort, current_service_tier: Option, current_context_mode: Option, + baseline_service_tier: Option, + baseline_context_mode: Option, + defer_session_mode_toggles_until_close: bool, + staged_service_tier: Option>, + staged_context_mode: Option>, use_chat_model: bool, app_event_tx: AppEventSender, is_complete: bool, @@ -191,6 +196,11 @@ impl ModelSelectionView { current_effort, current_service_tier, current_context_mode, + baseline_service_tier: current_service_tier, + baseline_context_mode: current_context_mode, + defer_session_mode_toggles_until_close: false, + staged_service_tier: None, + staged_context_mode: None, use_chat_model, app_event_tx, is_complete: false, @@ -198,6 +208,35 @@ impl ModelSelectionView { } } + pub(crate) fn defer_session_mode_toggles_until_close(&mut self) { + self.defer_session_mode_toggles_until_close = true; + } + + pub(crate) fn flush_deferred_session_updates(&mut self) { + if let Some(service_tier) = self.staged_service_tier.take() { + if service_tier != self.baseline_service_tier { + let _ = self + .app_event_tx + .send(AppEvent::UpdateServiceTierSelection { service_tier }); + self.baseline_service_tier = service_tier; + } + } + + if let Some(context_mode) = self.staged_context_mode.take() { + if context_mode != self.baseline_context_mode { + let _ = self + .app_event_tx + .send(AppEvent::UpdateSessionContextModeSelection { context_mode }); + self.baseline_context_mode = context_mode; + } + } + } + + fn should_defer_session_mode_toggles(&self) -> bool { + self.defer_session_mode_toggles_until_close + && matches!(self.target, ModelSelectionTarget::Session) + } + pub(crate) fn update_presets(&mut self, presets: Vec) { let include_fast_mode = self.target.supports_fast_mode(); let include_context_mode = self.target.supports_fast_mode(); @@ -439,9 +478,13 @@ impl ModelSelectionView { Some(ServiceTier::Fast) }; self.current_service_tier = next_service_tier; - let _ = self.app_event_tx.send(AppEvent::UpdateServiceTierSelection { - service_tier: next_service_tier, - }); + if self.should_defer_session_mode_toggles() { + self.staged_service_tier = Some(next_service_tier); + } else { + let _ = self.app_event_tx.send(AppEvent::UpdateServiceTierSelection { + service_tier: next_service_tier, + }); + } return; } EntryKind::ContextMode => { @@ -451,9 +494,13 @@ impl ModelSelectionView { Some(ContextMode::Auto) => Some(ContextMode::Disabled), }; self.current_context_mode = next_context_mode; - let _ = self.app_event_tx.send(AppEvent::UpdateSessionContextModeSelection { - context_mode: next_context_mode, - }); + if self.should_defer_session_mode_toggles() { + self.staged_context_mode = Some(next_context_mode); + } else { + let _ = self.app_event_tx.send(AppEvent::UpdateSessionContextModeSelection { + context_mode: next_context_mode, + }); + } return; } EntryKind::FollowChat => { @@ -1387,6 +1434,97 @@ mod tests { assert!(!view.is_complete()); } + #[test] + fn deferred_context_mode_waits_until_close() { + let presets = vec![make_preset("gpt-5.4")]; + let (tx, rx) = mpsc::channel::(); + let mut view = ModelSelectionView::new( + presets, + "gpt-5.4".to_string(), + ReasoningEffort::Low, + None, + Some(ContextMode::Auto), + false, + ModelSelectionTarget::Session, + AppEventSender::new(tx), + ); + view.defer_session_mode_toggles_until_close(); + + let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert!(rx.try_recv().is_err(), "event should wait until close"); + + view.flush_deferred_session_updates(); + + let event = rx.try_recv().expect("context mode event after close"); + assert!(matches!( + event, + AppEvent::UpdateSessionContextModeSelection { + context_mode: Some(ContextMode::Disabled) + } + )); + assert!(!view.is_complete()); + } + + #[test] + fn deferred_fast_mode_waits_until_close() { + let presets = vec![make_preset("gpt-5.4")]; + let (tx, rx) = mpsc::channel::(); + let mut view = ModelSelectionView::new( + presets, + "gpt-5.4".to_string(), + ReasoningEffort::Low, + None, + None, + false, + ModelSelectionTarget::Session, + AppEventSender::new(tx), + ); + view.defer_session_mode_toggles_until_close(); + + let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert!(rx.try_recv().is_err(), "event should wait until close"); + + view.flush_deferred_session_updates(); + + let event = rx.try_recv().expect("service tier event after close"); + assert!(matches!( + event, + AppEvent::UpdateServiceTierSelection { + service_tier: Some(ServiceTier::Fast) + } + )); + assert!(!view.is_complete()); + } + + #[test] + fn deferred_context_mode_skips_event_when_restored_before_close() { + let presets = vec![make_preset("gpt-5.4")]; + let (tx, rx) = mpsc::channel::(); + let mut view = ModelSelectionView::new( + presets, + "gpt-5.4".to_string(), + ReasoningEffort::Low, + None, + Some(ContextMode::Auto), + false, + ModelSelectionTarget::Session, + AppEventSender::new(tx), + ); + view.defer_session_mode_toggles_until_close(); + + let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + view.flush_deferred_session_updates(); + + assert!(rx.try_recv().is_err(), "restoring the original mode should emit nothing"); + } + #[test] fn model_selection_shows_unavailable_context_hint_for_unsupported_model() { let presets = vec![make_preset("gpt-5.3-codex")]; diff --git a/code-rs/tui/src/chatwidget/settings_overlay.rs b/code-rs/tui/src/chatwidget/settings_overlay.rs index b4aea76d077..b662c390216 100644 --- a/code-rs/tui/src/chatwidget/settings_overlay.rs +++ b/code-rs/tui/src/chatwidget/settings_overlay.rs @@ -176,6 +176,8 @@ pub(crate) struct ModelSettingsContent { impl ModelSettingsContent { pub(crate) fn new(view: ModelSelectionView) -> Self { + let mut view = view; + view.defer_session_mode_toggles_until_close(); Self { view } } } @@ -192,6 +194,10 @@ impl SettingsContent for ModelSettingsContent { fn is_complete(&self) -> bool { self.view.is_complete() } + + fn on_close(&mut self) { + self.view.flush_deferred_session_updates(); + } } pub(crate) struct ThemeSettingsContent { From c6aeb65b62aad63721f45b794ec52fcb9a1d807d Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Sun, 8 Mar 2026 04:52:25 -0400 Subject: [PATCH 2/2] fix(tui/settings): flush deferred 1M changes on close Run `on_close` for all mounted settings sections so deferred model-setting updates still apply even if the user navigates away from Model before closing the overlay. Add a regression test covering section-switch then close. --- .../tui/src/chatwidget/settings_overlay.rs | 99 ++++++++++++++----- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/code-rs/tui/src/chatwidget/settings_overlay.rs b/code-rs/tui/src/chatwidget/settings_overlay.rs index b662c390216..df9977c8150 100644 --- a/code-rs/tui/src/chatwidget/settings_overlay.rs +++ b/code-rs/tui/src/chatwidget/settings_overlay.rs @@ -2448,33 +2448,78 @@ impl SettingsOverlayView { } pub(crate) fn notify_close(&mut self) { - match self.active_section() { - SettingsSection::Model => { - if let Some(content) = self.model_content.as_mut() { - content.on_close(); - } - } - SettingsSection::Theme => { - if let Some(content) = self.theme_content.as_mut() { - content.on_close(); - } - } - SettingsSection::Notifications => { - if let Some(content) = self.notifications_content.as_mut() { - content.on_close(); - } - } - SettingsSection::Mcp => { - if let Some(content) = self.mcp_content.as_mut() { - content.on_close(); - } - } - SettingsSection::Chrome => { - if let Some(content) = self.chrome_content.as_mut() { - content.on_close(); - } - } - _ => {} + if let Some(content) = self.model_content.as_mut() { + content.on_close(); + } + + if let Some(content) = self.theme_content.as_mut() { + content.on_close(); + } + + if let Some(content) = self.notifications_content.as_mut() { + content.on_close(); } + + if let Some(content) = self.mcp_content.as_mut() { + content.on_close(); + } + + if let Some(content) = self.chrome_content.as_mut() { + content.on_close(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use code_common::model_presets::builtin_model_presets; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + use std::sync::mpsc; + + #[test] + fn overlay_notify_close_flushes_deferred_model_updates_after_section_change() { + let presets = builtin_model_presets(None, false); + let current_model = presets + .first() + .expect("at least one builtin model preset") + .model + .clone(); + + let (tx, rx) = mpsc::channel::(); + let app_event_tx = crate::app_event_sender::AppEventSender::new(tx); + + let view = ModelSelectionView::new( + presets, + current_model, + ReasoningEffort::Low, + None, + None, + false, + crate::bottom_pane::ModelSelectionTarget::Session, + app_event_tx, + ); + + let mut content = ModelSettingsContent::new(view); + let _ = content.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert!(rx.try_recv().is_err(), "deferred update should wait until close"); + + let mut overlay = SettingsOverlayView::new(SettingsSection::Model); + overlay.set_model_content(content); + + overlay.set_mode_section(SettingsSection::Theme); + overlay.notify_close(); + + let event = rx + .try_recv() + .expect("closing overlay from non-model section should flush deferred model updates"); + + assert!(matches!( + event, + crate::app_event::AppEvent::UpdateServiceTierSelection { + service_tier: Some(code_core::config_types::ServiceTier::Fast), + } + )); } }