mirror of
https://github.com/openai/codex.git
synced 2026-05-19 18:52:57 +00:00
tui: keep cleared Fast tier from reappearing after side-thread resume (#23121)
## Why After turning Fast mode off in the TUI, returning from a side thread could make `Fast` appear again in the main chat widget. The opt-out itself was still persisted; the display was being rebuilt from stale cached `ThreadSessionState` data, which made it look like Fast had been re-enabled. Fixes #23104. ## What changed - Keep the active thread's cached `service_tier` in sync whenever the user persists a service-tier selection. - Update both the primary-thread snapshot and the thread event store so restored TUI state reflects the current tier. - Add a focused regression test for clearing a cached Fast tier. ## Manual repro 1. Start a TUI session where `Fast` is enabled by default. 2. Run `/fast` and turn Fast mode off. Confirm `Fast` disappears from the chat widget display. 3. Re-enter thread navigation via either path: - Run `/side test`, then return to the main thread. - Run `/agent`, enter a child thread, then return to the main thread. 4. Before this fix, `Fast` reappears in the main chat widget display even though the opt-out was already persisted. 5. After this fix, `Fast` stays cleared. ## Verification - `cargo test -p codex-tui app::thread_session_state::tests::service_tier_sync_updates_active_cached_session -- --exact`
This commit is contained in:
@@ -1303,8 +1303,10 @@ impl App {
|
||||
}
|
||||
AppEvent::PersistServiceTierSelection { service_tier } => {
|
||||
self.refresh_status_line();
|
||||
let profile = self.active_profile.as_deref();
|
||||
self.config.service_tier = service_tier.clone();
|
||||
self.sync_active_thread_service_tier_to_cached_session()
|
||||
.await;
|
||||
let profile = self.active_profile.as_deref();
|
||||
let edits = crate::config_update::build_service_tier_selection_edits(
|
||||
profile,
|
||||
service_tier.as_deref(),
|
||||
|
||||
@@ -8,6 +8,30 @@ use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
|
||||
impl App {
|
||||
pub(super) async fn sync_active_thread_service_tier_to_cached_session(&mut self) {
|
||||
let Some(active_thread_id) = self.active_thread_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let service_tier = self.chat_widget.current_service_tier().map(str::to_string);
|
||||
let update_session = |session: &mut ThreadSessionState| {
|
||||
session.service_tier = service_tier.clone();
|
||||
};
|
||||
|
||||
if self.primary_thread_id == Some(active_thread_id)
|
||||
&& let Some(session) = self.primary_session_configured.as_mut()
|
||||
{
|
||||
update_session(session);
|
||||
}
|
||||
|
||||
if let Some(channel) = self.thread_event_channels.get(&active_thread_id) {
|
||||
let mut store = channel.store.lock().await;
|
||||
if let Some(session) = store.session.as_mut() {
|
||||
update_session(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn sync_active_thread_permission_settings_to_cached_session(&mut self) {
|
||||
let Some(active_thread_id) = self.active_thread_id else {
|
||||
return;
|
||||
@@ -125,6 +149,7 @@ mod tests {
|
||||
use crate::test_support::test_path_buf;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -316,6 +341,50 @@ mod tests {
|
||||
assert_eq!(store_session, Some(expected_session));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn service_tier_sync_updates_active_cached_session() {
|
||||
let mut app = make_test_app().await;
|
||||
let thread_id =
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000406").expect("valid thread");
|
||||
let session = ThreadSessionState {
|
||||
service_tier: Some(ServiceTier::Fast.request_value().to_string()),
|
||||
..test_thread_session(thread_id, test_path_buf("/tmp/main"))
|
||||
};
|
||||
|
||||
app.primary_thread_id = Some(thread_id);
|
||||
app.active_thread_id = Some(thread_id);
|
||||
app.primary_session_configured = Some(session.clone());
|
||||
app.thread_event_channels.insert(
|
||||
thread_id,
|
||||
ThreadEventChannel::new_with_session(/*capacity*/ 4, session.clone(), Vec::new()),
|
||||
);
|
||||
app.chat_widget.handle_thread_session(session);
|
||||
app.chat_widget.set_service_tier(/*service_tier*/ None);
|
||||
|
||||
app.sync_active_thread_service_tier_to_cached_session()
|
||||
.await;
|
||||
|
||||
let expected_session = ThreadSessionState {
|
||||
service_tier: None,
|
||||
..test_thread_session(thread_id, test_path_buf("/tmp/main"))
|
||||
};
|
||||
assert_eq!(
|
||||
app.primary_session_configured,
|
||||
Some(expected_session.clone())
|
||||
);
|
||||
|
||||
let store_session = app
|
||||
.thread_event_channels
|
||||
.get(&thread_id)
|
||||
.expect("thread channel")
|
||||
.store
|
||||
.lock()
|
||||
.await
|
||||
.session
|
||||
.clone();
|
||||
assert_eq!(store_session, Some(expected_session));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_read_fallback_uses_active_permission_settings() {
|
||||
let mut app = make_test_app().await;
|
||||
|
||||
Reference in New Issue
Block a user