Sync TUI thread settings through app server (#23507)

Builds on #23502.

## Why

#23502 adds the app-server `thread/settings/update` API and matching
`thread/settings/updated` notification. The TUI already lets users
change thread-scoped settings such as model, reasoning effort, service
tier, approvals, permissions, personality, and collaboration mode, but
those updates need to flow through the app server so embedded and
connected clients observe the same thread state.

This is a rework (simplification) of PR
https://github.com/openai/codex/pull/22510. It has the same
functionality, but the underlying `thread/settings/update` api is now
simpler in that it no longer returns the effective settings as a
response. Now, clients receive the effective settings only through the
`thread/settings/updated` notification.

## What Changed

This updates the TUI to send `thread/settings/update` whenever those
thread-scoped settings change and to treat the RPC response as the
authoritative acknowledgement. It also routes `thread/settings/updated`
notifications back into cached session state and the visible chat widget
so active and inactive threads stay in sync after app-server-originated
changes.

The implementation is kept to the TUI layer: settings conversion and
merge logic live under `codex-rs/tui/src/app/thread_settings.rs`, with
dispatch/routing hooks in the existing app and chat widget paths.

## Verification

I manually tested using `codex app-server --listen unix://` and then
launching two copies of the TUI that use the same local app server. I
then resumed the same thread on both and verified that changes like plan
mode, fast mode, model, reasoning effort, etc. are reflected "live" in
the second client when modified in the first and vice versa.
This commit is contained in:
Eric Traut authored and GitHub committed 2026-05-20 11:05:14 -07:00
1 parent 771a4e74ac
commit edc48e4612
25 files changed
+1086 -16

No files matched your search

+1 -1
View File
@@ -178,7 +178,7 @@ impl ChatWidget {
);
return;
}
self.set_collaboration_mask(collaboration_mode);
self.set_collaboration_mask_from_user_action(collaboration_mode);
let should_queue = self.is_plan_streaming_in_tui();
let user_message = UserMessage {
text,
+7
View File
@@ -51,6 +51,9 @@ impl ChatWidget {
ServerNotification::ThreadGoalCleared(notification) => {
self.on_thread_goal_cleared(notification.thread_id.as_str());
}
ServerNotification::ThreadSettingsUpdated(notification) => {
self.on_thread_settings_updated(notification);
}
ServerNotification::TurnStarted(notification) => {
self.turn_lifecycle.last_turn_id = Some(notification.turn.id);
self.last_non_retry_error = None;
@@ -247,6 +250,10 @@ impl ChatWidget {
notification: TurnCompletedNotification,
replay_kind: Option<ReplayKind>,
) {
// User-message dedupe only suppresses the app-server echo of a prompt
// this TUI already rendered locally. Once that turn ends, another
// client can submit the same text and it still needs its own user cell.
self.last_rendered_user_message_display = None;
match notification.turn.status {
TurnStatus::Completed => {
self.last_non_retry_error = None;
+20 -6
View File
@@ -73,18 +73,31 @@ impl ChatWidget {
}
}
self.config.approvals_reviewer = session.approvals_reviewer;
self.config.personality = session.personality;
self.status_line_project_root_name_cache = None;
let forked_from_id = session.forked_from_id;
let model_for_header = session.model.clone();
self.session_header.set_model(&model_for_header);
let default_model = session.model.clone();
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
Some(model_for_header.clone()),
Some(default_model.clone()),
Some(session.reasoning_effort),
/*developer_instructions*/ None,
);
if let Some(mask) = self.active_collaboration_mask.as_mut() {
mask.model = Some(model_for_header.clone());
mask.reasoning_effort = Some(session.reasoning_effort);
match session.collaboration_mode.as_deref() {
Some(collaboration_mode) => {
self.set_effective_collaboration_mode(collaboration_mode.clone());
}
None => {
self.active_collaboration_mask = Self::initial_collaboration_mask(
&self.config,
self.model_catalog.as_ref(),
Some(&default_model),
);
if let Some(mask) = self.active_collaboration_mask.as_mut() {
mask.reasoning_effort = Some(session.reasoning_effort);
}
self.update_collaboration_mode_indicator();
self.refresh_plan_mode_nudge();
}
}
self.refresh_model_display();
self.refresh_status_surfaces();
@@ -93,6 +106,7 @@ impl ChatWidget {
self.sync_plugins_command_enabled();
self.sync_goal_command_enabled();
self.refresh_plugin_mentions();
let model_for_header = self.current_model().to_string();
if display == SessionConfiguredDisplay::Normal {
let startup_tooltip_override = self.startup_tooltip_override.take();
let show_fast_status = self
+138 -2
View File
@@ -1,6 +1,7 @@
//! Runtime settings state and model/collaboration coordination for `ChatWidget`.
use super::*;
use crate::app_event::AppEvent;
impl ChatWidget {
/// Set the approval policy in the widget's config copy.
@@ -345,6 +346,24 @@ impl ChatWidget {
self.effective_reasoning_effort()
}
pub(crate) fn on_thread_settings_updated(
&mut self,
notification: ThreadSettingsUpdatedNotification,
) {
let Ok(thread_id) = ThreadId::from_string(&notification.thread_id) else {
tracing::warn!(
thread_id = notification.thread_id,
"ignoring app-server ThreadSettingsUpdated with invalid thread_id"
);
return;
};
if self.thread_id != Some(thread_id) {
return;
}
self.apply_thread_settings(notification.thread_settings);
}
#[cfg(test)]
pub(crate) fn active_collaboration_mode_kind(&self) -> ModeKind {
self.active_mode_kind()
@@ -430,7 +449,7 @@ impl ChatWidget {
.unwrap_or(current_effort)
}
pub(super) fn effective_collaboration_mode(&self) -> CollaborationMode {
pub(crate) fn effective_collaboration_mode(&self) -> CollaborationMode {
if !self.collaboration_modes_enabled() {
return self.current_collaboration_mode.clone();
}
@@ -462,6 +481,96 @@ impl ChatWidget {
self.refresh_status_line();
}
fn apply_thread_settings(&mut self, mut settings: ThreadSettings) {
let cwd_changed = self.config.cwd != settings.cwd;
self.apply_thread_settings_cwd(settings.cwd.clone());
self.config.model_provider_id = settings.model_provider.clone();
self.set_service_tier(settings.service_tier.clone());
self.set_approval_policy(settings.approval_policy);
self.set_approvals_reviewer(settings.approvals_reviewer.to_core());
self.config.personality = settings.personality;
let permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&settings.sandbox_policy.to_core(),
settings.cwd.as_path(),
);
let permission_snapshot = PermissionProfileSnapshot::from_session_snapshot(
permission_profile,
settings.active_permission_profile.take().map(Into::into),
);
if let Err(err) = self
.config
.permissions
.set_permission_profile_from_session_snapshot(permission_snapshot.clone())
{
tracing::warn!(%err, "failed to sync permissions from ThreadSettingsUpdated");
if let Err(replace_err) = self
.config
.permissions
.replace_permission_profile_from_session_snapshot(permission_snapshot)
{
tracing::error!(
%replace_err,
"failed to replace permissions from ThreadSettingsUpdated after constraint fallback"
);
}
}
settings.collaboration_mode.settings.model = settings.model;
settings.collaboration_mode.settings.reasoning_effort = settings.effort;
self.set_effective_collaboration_mode(settings.collaboration_mode);
self.refresh_status_surfaces();
self.sync_service_tier_commands();
self.sync_personality_command_enabled();
if cwd_changed {
self.refresh_skills_for_current_cwd(/*force_reload*/ true);
}
self.refresh_plugin_mentions();
self.request_redraw();
}
fn apply_thread_settings_cwd(&mut self, cwd: AbsolutePathBuf) {
let previous_cwd = std::mem::replace(&mut self.config.cwd, cwd.clone());
self.current_cwd = Some(cwd.to_path_buf());
self.status_line_project_root_name_cache = None;
if !self.config.workspace_roots.contains(&previous_cwd) {
return;
}
let previous_roots = std::mem::take(&mut self.config.workspace_roots);
self.config.workspace_roots.push(cwd);
for root in previous_roots {
if root != previous_cwd && !self.config.workspace_roots.contains(&root) {
self.config.workspace_roots.push(root);
}
}
self.config
.permissions
.set_workspace_roots(self.config.workspace_roots.clone());
}
pub(super) fn set_effective_collaboration_mode(&mut self, mode: CollaborationMode) {
let mode_kind = mode.mode;
let settings = mode.settings;
if mode_kind == ModeKind::Default {
self.current_collaboration_mode = CollaborationMode {
mode: ModeKind::Default,
settings: settings.clone(),
};
}
self.active_collaboration_mask = Some(CollaborationModeMask {
name: mode_kind.display_name().to_string(),
mode: Some(mode_kind),
model: Some(settings.model.clone()),
reasoning_effort: Some(settings.reasoning_effort),
developer_instructions: Some(settings.developer_instructions),
});
self.update_collaboration_mode_indicator();
self.refresh_plan_mode_nudge();
self.refresh_model_dependent_surfaces();
}
pub(super) fn model_display_name(&self) -> &str {
let model = self.current_model();
if model.is_empty() {
@@ -555,10 +664,15 @@ impl ChatWidget {
self.model_catalog.as_ref(),
self.active_collaboration_mask.as_ref(),
) {
self.set_collaboration_mask(next_mask);
self.set_collaboration_mask_from_user_action(next_mask);
}
}
pub(crate) fn set_collaboration_mask_from_user_action(&mut self, mask: CollaborationModeMask) {
self.set_collaboration_mask(mask);
self.submit_collaboration_mode_settings_update();
}
/// Update the active collaboration mask.
///
/// When collaboration modes are enabled and a preset is selected,
@@ -609,4 +723,26 @@ impl ChatWidget {
}
self.request_redraw();
}
fn submit_collaboration_mode_settings_update(&self) {
let Some(thread_id) = self.thread_id else {
return;
};
self.app_event_tx.send(AppEvent::SubmitThreadOp {
thread_id,
op: AppCommand::override_turn_context(
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*active_permission_profile*/ None,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
/*service_tier*/ None,
Some(self.effective_collaboration_mode()),
/*personality*/ None,
),
});
}
}
@@ -88,7 +88,7 @@ impl ChatWidget {
return false;
}
if let Some(mask) = collaboration_modes::plan_mask(self.model_catalog.as_ref()) {
self.set_collaboration_mask(mask);
self.set_collaboration_mask_from_user_action(mask);
true
} else {
self.add_info_message(
@@ -1,6 +1,65 @@
use super::*;
use pretty_assertions::assert_eq;
fn thread_settings_for_test(
model: &str,
thread_id: ThreadId,
) -> codex_app_server_protocol::ThreadSettingsUpdatedNotification {
codex_app_server_protocol::ThreadSettingsUpdatedNotification {
thread_id: thread_id.to_string(),
thread_settings: codex_app_server_protocol::ThreadSettings {
cwd: test_path_buf("/tmp/thread-settings").abs(),
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::AutoReview,
sandbox_policy: codex_app_server_protocol::SandboxPolicy::ReadOnly {
network_access: false,
},
active_permission_profile: Some(
codex_app_server_protocol::ActivePermissionProfile::read_only(),
),
model: model.to_string(),
model_provider: "openai".to_string(),
service_tier: Some(ServiceTier::Fast.request_value().to_string()),
effort: Some(ReasoningEffortConfig::High),
summary: None,
collaboration_mode: CollaborationMode {
mode: ModeKind::Plan,
settings: codex_protocol::config_types::Settings {
model: model.to_string(),
reasoning_effort: Some(ReasoningEffortConfig::High),
developer_instructions: None,
},
},
personality: Some(Personality::Pragmatic),
},
}
}
fn configured_thread_session(thread_id: ThreadId) -> crate::session_state::ThreadSessionState {
crate::session_state::ThreadSessionState {
thread_id,
forked_from_id: None,
fork_parent_title: None,
thread_name: None,
model: "gpt-5.3-codex".to_string(),
model_provider_id: "openai".to_string(),
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/tmp/thread-settings").abs(),
runtime_workspace_roots: vec![test_path_buf("/tmp/thread-settings").abs()],
instruction_source_paths: Vec::new(),
reasoning_effort: None,
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: None,
}
}
#[tokio::test]
async fn invalid_url_elicitation_is_declined() {
let (mut chat, _app_event_tx, mut rx, _op_rx) = make_chatwidget_manual_with_sender().await;
@@ -38,6 +97,98 @@ async fn invalid_url_elicitation_is_declined() {
);
}
#[tokio::test]
async fn thread_settings_updated_updates_visible_state_without_transcript() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
set_fast_mode_test_catalog(&mut chat);
let thread_id = ThreadId::new();
chat.handle_thread_session(configured_thread_session(thread_id));
let _ = drain_insert_history(&mut rx);
chat.handle_server_notification(
ServerNotification::ThreadSettingsUpdated(thread_settings_for_test("gpt-5.4", thread_id)),
/*replay_kind*/ None,
);
assert_eq!(chat.current_model(), "gpt-5.4");
assert_eq!(
chat.current_reasoning_effort(),
Some(ReasoningEffortConfig::High)
);
assert_eq!(
chat.current_service_tier(),
Some(ServiceTier::Fast.request_value())
);
assert_eq!(
chat.config_ref().permissions.approval_policy.value(),
AskForApproval::OnRequest.to_core()
);
assert_eq!(
chat.config_ref().approvals_reviewer,
ApprovalsReviewer::AutoReview
);
assert_eq!(
chat.config_ref()
.permissions
.active_permission_profile()
.expect("active profile")
.id,
codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY
);
assert_eq!(chat.config_ref().personality, Some(Personality::Pragmatic));
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan);
assert!(
drain_insert_history(&mut rx).is_empty(),
"ThreadSettingsUpdated should not render transcript history"
);
chat.handle_server_notification(
ServerNotification::ThreadSettingsUpdated(thread_settings_for_test(
"gpt-5.2",
ThreadId::new(),
)),
/*replay_kind*/ None,
);
assert_eq!(chat.current_model(), "gpt-5.4");
}
#[tokio::test]
async fn thread_settings_updated_preserves_default_settings_for_plan_mode() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
let thread_id = ThreadId::new();
let mut session = configured_thread_session(thread_id);
session.model = "gpt-default".to_string();
session.reasoning_effort = Some(ReasoningEffortConfig::Low);
chat.handle_thread_session(session);
let _ = drain_insert_history(&mut rx);
let default_mode = chat.current_collaboration_mode().clone();
chat.handle_server_notification(
ServerNotification::ThreadSettingsUpdated(thread_settings_for_test("gpt-plan", thread_id)),
/*replay_kind*/ None,
);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan);
assert_eq!(chat.current_model(), "gpt-plan");
assert_eq!(
chat.current_reasoning_effort(),
Some(ReasoningEffortConfig::High)
);
assert_eq!(chat.current_collaboration_mode(), &default_mode);
let default_mask = collaboration_modes::default_mask(chat.model_catalog.as_ref())
.expect("expected default collaboration mode");
chat.set_collaboration_mask(default_mask);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default);
assert_eq!(chat.current_model(), "gpt-default");
assert_eq!(
chat.current_reasoning_effort(),
Some(ReasoningEffortConfig::Low)
);
}
#[tokio::test]
async fn collab_spawn_end_shows_requested_model_and_effort() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -30,6 +30,8 @@ async fn submission_preserves_text_elements_and_local_images() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -135,6 +137,8 @@ async fn submission_includes_configured_active_permission_profile() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -188,6 +192,8 @@ async fn submission_omits_active_permission_profile_for_legacy_snapshot() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -231,6 +237,8 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -327,6 +335,8 @@ async fn enter_with_only_remote_images_submits_user_turn() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -392,6 +402,8 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -431,6 +443,8 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -470,6 +484,8 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -512,6 +528,8 @@ async fn submission_prefers_selected_duplicate_skill_path() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -976,6 +976,8 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -31,6 +31,8 @@ async fn resumed_initial_messages_render_history() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -102,6 +104,8 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -172,6 +176,8 @@ async fn replayed_user_message_preserves_remote_image_urls() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -272,6 +278,8 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
runtime_workspace_roots: vec![expected_cwd.clone()],
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: None,
@@ -343,6 +351,8 @@ async fn session_configured_preserves_profile_workspace_roots() {
runtime_workspace_roots: session_runtime_workspace_roots.clone(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: None,
@@ -388,6 +398,8 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: None,
@@ -427,6 +439,8 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -483,6 +497,8 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -755,6 +771,8 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: None,
@@ -801,6 +819,8 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: None,
@@ -584,6 +584,8 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(PathBuf::new()),
@@ -632,6 +634,8 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(PathBuf::new()),
@@ -1220,6 +1220,8 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
@@ -1407,6 +1409,8 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: None,
@@ -2493,6 +2493,8 @@ async fn session_configured_clears_goal_status_footer() {
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),