Handle thread settings updates in TUI

This commit is contained in:
Eric Traut
2026-05-18 22:11:07 -07:00
parent 5e622e429e
commit 10a45e9b71
6 changed files with 73 additions and 21 deletions

View File

@@ -163,7 +163,9 @@ impl App {
session.model = notification.thread_settings.model.clone();
session.model_provider_id = notification.thread_settings.model_provider.clone();
session.service_tier = notification.thread_settings.service_tier.clone();
session.cwd = notification.thread_settings.cwd.clone();
session.set_cwd_retargeting_implicit_runtime_workspace_root(
notification.thread_settings.cwd.clone(),
);
session.approval_policy = notification.thread_settings.approval_policy;
session.approvals_reviewer = notification.thread_settings.approvals_reviewer.to_core();
session.permission_profile = notification
@@ -178,6 +180,7 @@ impl App {
.map(codex_protocol::models::ActivePermissionProfile::from);
session.reasoning_effort = notification.thread_settings.effort;
};
let server_notification = ServerNotification::ThreadSettingsUpdated(notification.clone());
if self.primary_thread_id == Some(thread_id)
&& let Some(session) = self.primary_session_configured.as_mut()
@@ -189,16 +192,15 @@ impl App {
if let Some(session) = store.session.as_mut() {
update_session(session);
}
store.push_notification(server_notification.clone());
}
if self.chat_widget.thread_id() != Some(thread_id) {
return;
}
self.chat_widget.handle_server_notification(
ServerNotification::ThreadSettingsUpdated(notification),
/*replay_kind*/ None,
);
self.chat_widget
.handle_server_notification(server_notification, /*replay_kind*/ None);
}
async fn handle_server_request_event(

View File

@@ -234,29 +234,33 @@ async fn thread_settings_updated_notification_reaches_active_thread_ui() -> Resu
}
#[tokio::test]
async fn thread_settings_updated_notification_for_hidden_thread_updates_cache_only() -> Result<()> {
async fn hidden_thread_settings_update_buffers_for_replay() -> Result<()> {
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
let active_thread_id = ThreadId::new();
let hidden_thread_id = ThreadId::new();
let active_cwd = test_path_buf("/tmp/active").abs();
let hidden_cwd = test_path_buf("/tmp/hidden").abs();
let hidden_next_cwd = test_path_buf("/tmp/hidden-next").abs();
let shared_root = test_path_buf("/tmp/shared").abs();
app.enqueue_primary_thread_session(
test_thread_session(active_thread_id, active_cwd.to_path_buf()),
Vec::new(),
)
.await?;
let mut hidden_session = test_thread_session(hidden_thread_id, hidden_cwd.to_path_buf());
hidden_session.runtime_workspace_roots = vec![hidden_cwd.clone(), shared_root.clone()];
app.thread_event_channels.insert(
hidden_thread_id,
ThreadEventChannel::new_with_session(
THREAD_EVENT_CHANNEL_CAPACITY,
test_thread_session(hidden_thread_id, hidden_cwd.to_path_buf()),
hidden_session,
Vec::new(),
),
);
app.handle_thread_settings_updated_notification(ThreadSettingsUpdatedNotification {
thread_id: hidden_thread_id.to_string(),
thread_settings: test_thread_settings("gpt-hidden", hidden_cwd),
thread_settings: test_thread_settings("gpt-hidden", hidden_next_cwd.clone()),
})
.await;
@@ -273,6 +277,21 @@ async fn thread_settings_updated_notification_for_hidden_thread_updates_cache_on
.map(|session| session.model.as_str()),
Some("gpt-hidden")
);
let hidden_session = hidden_store.session.as_ref().expect("hidden session");
assert_eq!(hidden_session.cwd, hidden_next_cwd);
assert_eq!(
hidden_session.runtime_workspace_roots,
vec![hidden_next_cwd.clone(), shared_root]
);
assert!(
hidden_store.buffer.iter().any(|event| matches!(
event,
ThreadBufferedEvent::Notification(ServerNotification::ThreadSettingsUpdated(
notification
)) if notification.thread_settings.model == "gpt-hidden"
)),
"hidden thread settings update should be buffered for replay"
);
Ok(())
}

View File

@@ -50,6 +50,7 @@ impl ThreadEventStore {
ThreadBufferedEvent::Request(_)
| ThreadBufferedEvent::Notification(ServerNotification::HookStarted(_))
| ThreadBufferedEvent::Notification(ServerNotification::HookCompleted(_))
| ThreadBufferedEvent::Notification(ServerNotification::ThreadSettingsUpdated(_))
| ThreadBufferedEvent::FeedbackSubmission(_)
)
}

View File

@@ -220,7 +220,6 @@ impl ChatWidget {
| ServerNotification::AccountRateLimitsUpdated(_)
| ServerNotification::ThreadStarted(_)
| ServerNotification::ThreadStatusChanged(_)
| ServerNotification::ThreadSettingsUpdated(_)
| ServerNotification::ThreadArchived(_)
| ServerNotification::ThreadUnarchived(_)
| ServerNotification::RawResponseItemCompleted(_)
@@ -249,8 +248,17 @@ impl ChatWidget {
&mut self,
thread_settings: codex_app_server_protocol::ThreadSettings,
) {
let previous_cwd = std::mem::replace(&mut self.config.cwd, thread_settings.cwd.clone());
self.current_cwd = Some(thread_settings.cwd.to_path_buf());
self.config.cwd = thread_settings.cwd;
if crate::session_state::retarget_implicit_workspace_root(
&mut self.config.workspace_roots,
previous_cwd,
thread_settings.cwd.clone(),
) {
self.config
.permissions
.set_workspace_roots(self.config.workspace_roots.clone());
}
self.config.model_reasoning_summary = thread_settings.summary;
self.config.personality = thread_settings.personality;
self.effective_service_tier = thread_settings.service_tier.clone();

View File

@@ -4,7 +4,14 @@ use pretty_assertions::assert_eq;
#[tokio::test]
async fn thread_settings_updated_notification_refreshes_active_ui_state_without_history() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let previous_cwd = test_path_buf("/tmp/thread-settings-previous").abs();
let cwd = test_path_buf("/tmp/thread-settings").abs();
let extra_root = test_path_buf("/tmp/thread-settings-extra-root").abs();
chat.config.cwd = previous_cwd.clone();
chat.config.workspace_roots = vec![previous_cwd, extra_root.clone()];
chat.config
.permissions
.set_workspace_roots(chat.config.workspace_roots.clone());
let permission_profile = PermissionProfile::workspace_write();
let collaboration_mode = CollaborationMode {
mode: ModeKind::Plan,
@@ -63,6 +70,12 @@ async fn thread_settings_updated_notification_refreshes_active_ui_state_without_
);
assert_eq!(chat.config_ref().personality, Some(Personality::Pragmatic));
assert_eq!(chat.current_collaboration_mode(), &collaboration_mode);
let expected_workspace_roots = vec![cwd, extra_root];
assert_eq!(chat.config_ref().workspace_roots, expected_workspace_roots);
assert_eq!(
chat.config_ref().permissions.user_visible_workspace_roots(),
expected_workspace_roots.as_slice()
);
assert!(drain_insert_history(&mut rx).is_empty());
}

View File

@@ -58,16 +58,25 @@ impl ThreadSessionState {
cwd: AbsolutePathBuf,
) {
let previous_cwd = std::mem::replace(&mut self.cwd, cwd.clone());
if !self.runtime_workspace_roots.contains(&previous_cwd) {
return;
}
let previous_roots = std::mem::take(&mut self.runtime_workspace_roots);
self.runtime_workspace_roots.push(cwd);
for root in previous_roots {
if root != previous_cwd && !self.runtime_workspace_roots.contains(&root) {
self.runtime_workspace_roots.push(root);
}
}
retarget_implicit_workspace_root(&mut self.runtime_workspace_roots, previous_cwd, cwd);
}
}
pub(crate) fn retarget_implicit_workspace_root(
workspace_roots: &mut Vec<AbsolutePathBuf>,
previous_cwd: AbsolutePathBuf,
cwd: AbsolutePathBuf,
) -> bool {
if !workspace_roots.contains(&previous_cwd) {
return false;
}
let previous_roots = std::mem::take(workspace_roots);
workspace_roots.push(cwd);
for root in previous_roots {
if root != previous_cwd && !workspace_roots.contains(&root) {
workspace_roots.push(root);
}
}
true
}