mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
771a4e74ac
commit
edc48e4612
@@ -211,6 +211,7 @@ mod thread_events;
|
||||
mod thread_goal_actions;
|
||||
mod thread_routing;
|
||||
mod thread_session_state;
|
||||
mod thread_settings;
|
||||
|
||||
use self::agent_navigation::AgentNavigationDirection;
|
||||
use self::agent_navigation::AgentNavigationState;
|
||||
|
||||
@@ -178,12 +178,46 @@ pub(super) fn server_notification_thread_target(
|
||||
mod tests {
|
||||
use super::ServerNotificationThreadTarget;
|
||||
use super::server_notification_thread_target;
|
||||
use crate::test_support::PathBufExt;
|
||||
use crate::test_support::test_path_buf;
|
||||
use codex_app_server_protocol::GuardianWarningNotification;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadSettings;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdatedNotification;
|
||||
use codex_app_server_protocol::WarningNotification;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn test_thread_settings() -> ThreadSettings {
|
||||
ThreadSettings {
|
||||
cwd: test_path_buf("/tmp/thread-settings").abs(),
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::Never,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
|
||||
sandbox_policy: codex_app_server_protocol::SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
},
|
||||
active_permission_profile: None,
|
||||
model: "gpt-5.4".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
effort: Some(ReasoningEffort::High),
|
||||
summary: None,
|
||||
collaboration_mode: CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model: "gpt-5.4".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffort::High),
|
||||
developer_instructions: None,
|
||||
},
|
||||
},
|
||||
personality: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warning_notifications_without_threads_are_global() {
|
||||
let notification = ServerNotification::Warning(WarningNotification {
|
||||
@@ -221,4 +255,18 @@ mod tests {
|
||||
|
||||
assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_settings_updated_notifications_route_to_threads() {
|
||||
let thread_id = ThreadId::new();
|
||||
let notification =
|
||||
ServerNotification::ThreadSettingsUpdated(ThreadSettingsUpdatedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
thread_settings: test_thread_settings(),
|
||||
});
|
||||
|
||||
let target = server_notification_thread_target(¬ification);
|
||||
|
||||
assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,6 +692,8 @@ mod tests {
|
||||
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()),
|
||||
|
||||
@@ -760,12 +760,18 @@ impl App {
|
||||
}
|
||||
AppEvent::UpdateReasoningEffort(effort) => {
|
||||
self.on_update_reasoning_effort(effort);
|
||||
self.sync_active_thread_reasoning_setting(app_server, effort)
|
||||
.await;
|
||||
}
|
||||
AppEvent::UpdateModel(model) => {
|
||||
self.chat_widget.set_model(&model);
|
||||
self.sync_active_thread_model_setting(app_server, model)
|
||||
.await;
|
||||
}
|
||||
AppEvent::UpdatePersonality(personality) => {
|
||||
self.on_update_personality(personality);
|
||||
self.sync_active_thread_personality_setting(app_server, personality)
|
||||
.await;
|
||||
}
|
||||
AppEvent::OpenRealtimeAudioDeviceSelection { kind } => {
|
||||
self.chat_widget.open_realtime_audio_device_selection(kind);
|
||||
@@ -1548,6 +1554,8 @@ impl App {
|
||||
AppEvent::UpdatePlanModeReasoningEffort(effort) => {
|
||||
self.config.plan_mode_reasoning_effort = effort;
|
||||
self.chat_widget.set_plan_mode_reasoning_effort(effort);
|
||||
self.sync_active_thread_plan_mode_reasoning_setting(app_server)
|
||||
.await;
|
||||
}
|
||||
AppEvent::PersistFullAccessWarningAcknowledged => {
|
||||
if let Err(err) = ConfigEditsBuilder::for_config(&self.config)
|
||||
|
||||
@@ -59,6 +59,8 @@ use codex_app_server_protocol::SessionSource;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadClosedNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadSettings;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification;
|
||||
@@ -77,8 +79,10 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -108,6 +112,29 @@ fn test_absolute_path(path: &str) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::try_from(PathBuf::from(path)).expect("absolute test path")
|
||||
}
|
||||
|
||||
async fn next_thread_settings_updated(
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
) -> ThreadSettingsUpdatedNotification {
|
||||
for _ in 0..20 {
|
||||
let event = time::timeout(
|
||||
std::time::Duration::from_secs(/*secs*/ 2),
|
||||
app_server.next_event(),
|
||||
)
|
||||
.await
|
||||
.expect("app-server should emit an event")
|
||||
.expect("app-server event stream should remain open");
|
||||
if let codex_app_server_client::AppServerEvent::ServerNotification(
|
||||
ServerNotification::ThreadSettingsUpdated(notification),
|
||||
) = event
|
||||
&& notification.thread_id == thread_id.to_string()
|
||||
{
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
panic!("expected ThreadSettingsUpdated for thread {thread_id}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_mcp_inventory_result_clears_committed_loading_cell() {
|
||||
let mut app = make_test_app().await;
|
||||
@@ -3721,6 +3748,8 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::High),
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
@@ -3972,6 +4001,8 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState
|
||||
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()),
|
||||
@@ -4548,6 +4579,8 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
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()),
|
||||
@@ -4612,6 +4645,8 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
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()),
|
||||
@@ -4705,6 +4740,8 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
|
||||
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()),
|
||||
@@ -5105,6 +5142,8 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
|
||||
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()),
|
||||
@@ -5206,6 +5245,294 @@ async fn interrupt_without_active_turn_is_treated_as_handled() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn override_turn_context_sends_thread_settings_update() {
|
||||
Box::pin(async {
|
||||
let mut app = make_test_app().await;
|
||||
let mut app_server =
|
||||
crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())
|
||||
.await
|
||||
.expect("embedded app server");
|
||||
let started = app_server
|
||||
.start_thread(app.chat_widget.config_ref())
|
||||
.await
|
||||
.expect("thread/start should succeed");
|
||||
let thread_id = started.session.thread_id;
|
||||
let initial_model = started.session.model.clone();
|
||||
let initial_effort = started.session.reasoning_effort;
|
||||
app.enqueue_primary_thread_session(started.session, started.turns)
|
||||
.await
|
||||
.expect("primary thread should be registered");
|
||||
let service_tier = ServiceTier::Fast.request_value().to_string();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Plan,
|
||||
settings: Settings {
|
||||
model: "gpt-5.4".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::High),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let op = AppCommand::override_turn_context(
|
||||
/*cwd*/ None,
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(ApprovalsReviewer::AutoReview),
|
||||
Some(ActivePermissionProfile::new(
|
||||
codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
)),
|
||||
/*windows_sandbox_level*/ None,
|
||||
Some("gpt-5.4".to_string()),
|
||||
Some(Some(ReasoningEffortConfig::High)),
|
||||
/*summary*/ None,
|
||||
Some(Some(service_tier.clone())),
|
||||
Some(collaboration_mode.clone()),
|
||||
Some(Personality::Pragmatic),
|
||||
);
|
||||
|
||||
let handled = app
|
||||
.try_submit_active_thread_op_via_app_server(&mut app_server, thread_id, &op)
|
||||
.await
|
||||
.expect("settings update submission should not fail");
|
||||
|
||||
assert_eq!(handled, true);
|
||||
assert_eq!(
|
||||
app.primary_session_configured
|
||||
.as_ref()
|
||||
.expect("primary session")
|
||||
.model,
|
||||
initial_model,
|
||||
"thread/settings/update response is only an ack; cached state changes on notification"
|
||||
);
|
||||
|
||||
let notification = next_thread_settings_updated(&mut app_server, thread_id).await;
|
||||
assert_eq!(notification.thread_settings.model, "gpt-5.4");
|
||||
assert_eq!(
|
||||
notification.thread_settings.effort,
|
||||
Some(ReasoningEffortConfig::High)
|
||||
);
|
||||
assert_eq!(
|
||||
notification.thread_settings.service_tier,
|
||||
Some(service_tier.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
notification.thread_settings.approval_policy,
|
||||
AskForApproval::OnRequest
|
||||
);
|
||||
assert_eq!(
|
||||
notification.thread_settings.approvals_reviewer.to_core(),
|
||||
ApprovalsReviewer::AutoReview
|
||||
);
|
||||
let notified_mode = ¬ification.thread_settings.collaboration_mode;
|
||||
assert_eq!(notified_mode.mode, collaboration_mode.mode);
|
||||
assert_eq!(
|
||||
notified_mode.settings.model,
|
||||
collaboration_mode.settings.model
|
||||
);
|
||||
assert_eq!(
|
||||
notified_mode.settings.reasoning_effort,
|
||||
collaboration_mode.settings.reasoning_effort
|
||||
);
|
||||
assert_eq!(
|
||||
notification.thread_settings.personality,
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
|
||||
app.handle_app_server_event(
|
||||
&app_server,
|
||||
codex_app_server_client::AppServerEvent::ServerNotification(
|
||||
ServerNotification::ThreadSettingsUpdated(notification),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let updated_session = app
|
||||
.primary_session_configured
|
||||
.as_ref()
|
||||
.expect("primary session should be updated from notification");
|
||||
assert_eq!(updated_session.model, initial_model);
|
||||
assert_eq!(updated_session.reasoning_effort, initial_effort);
|
||||
let updated_mode = updated_session
|
||||
.collaboration_mode
|
||||
.as_deref()
|
||||
.expect("collaboration mode should be cached");
|
||||
assert_eq!(updated_mode.mode, collaboration_mode.mode);
|
||||
assert_eq!(
|
||||
updated_mode.settings.model,
|
||||
collaboration_mode.settings.model
|
||||
);
|
||||
assert_eq!(
|
||||
updated_mode.settings.reasoning_effort,
|
||||
collaboration_mode.settings.reasoning_effort
|
||||
);
|
||||
assert_eq!(updated_session.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(updated_session.service_tier, Some(service_tier));
|
||||
assert_eq!(updated_session.approval_policy, AskForApproval::OnRequest);
|
||||
assert_eq!(
|
||||
updated_session.approvals_reviewer,
|
||||
ApprovalsReviewer::AutoReview
|
||||
);
|
||||
assert_eq!(
|
||||
updated_session
|
||||
.active_permission_profile
|
||||
.as_ref()
|
||||
.expect("active profile")
|
||||
.id,
|
||||
codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_setting_update_params_sync_model_and_default_reasoning() {
|
||||
let mut app = make_test_app().await;
|
||||
let thread_id = ThreadId::new();
|
||||
app.active_thread_id = Some(thread_id);
|
||||
|
||||
app.chat_widget.set_model("gpt-5.4");
|
||||
let params = app
|
||||
.active_thread_model_setting_update_params("gpt-5.4".to_string())
|
||||
.expect("active thread should produce update params");
|
||||
|
||||
assert_eq!(params.thread_id, thread_id.to_string());
|
||||
assert_eq!(params.model, Some("gpt-5.4".to_string()));
|
||||
assert_eq!(
|
||||
params
|
||||
.collaboration_mode
|
||||
.as_ref()
|
||||
.expect("collaboration mode should sync with model")
|
||||
.settings
|
||||
.model,
|
||||
"gpt-5.4"
|
||||
);
|
||||
|
||||
app.chat_widget
|
||||
.set_reasoning_effort(Some(ReasoningEffortConfig::Low));
|
||||
app.chat_widget
|
||||
.set_collaboration_mask(CollaborationModeMask {
|
||||
name: "Plan".to_string(),
|
||||
mode: Some(ModeKind::Plan),
|
||||
model: Some("gpt-plan".to_string()),
|
||||
reasoning_effort: Some(Some(ReasoningEffortConfig::Medium)),
|
||||
developer_instructions: None,
|
||||
});
|
||||
app.on_update_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||
|
||||
let params = app
|
||||
.active_thread_reasoning_setting_update_params(Some(ReasoningEffortConfig::High))
|
||||
.expect("active thread should produce update params");
|
||||
|
||||
assert_eq!(params.thread_id, thread_id.to_string());
|
||||
assert_eq!(params.effort, Some(ReasoningEffortConfig::High));
|
||||
let collaboration_mode = params
|
||||
.collaboration_mode
|
||||
.expect("collaboration mode should sync with reasoning");
|
||||
assert_eq!(collaboration_mode.mode, ModeKind::Default);
|
||||
assert_eq!(
|
||||
collaboration_mode.settings.reasoning_effort,
|
||||
Some(ReasoningEffortConfig::High)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inactive_thread_settings_notification_updates_cached_collaboration_mode() {
|
||||
let mut app = make_test_app().await;
|
||||
let primary_thread_id = ThreadId::new();
|
||||
let inactive_thread_id = ThreadId::new();
|
||||
let primary_session = test_thread_session(primary_thread_id, test_path_buf("/tmp/main"));
|
||||
let inactive_session = test_thread_session(inactive_thread_id, test_path_buf("/tmp/inactive"));
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Plan,
|
||||
settings: Settings {
|
||||
model: "gpt-plan".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::High),
|
||||
developer_instructions: Some("draft a plan first".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
app.primary_thread_id = Some(primary_thread_id);
|
||||
app.active_thread_id = Some(primary_thread_id);
|
||||
app.primary_session_configured = Some(primary_session.clone());
|
||||
app.thread_event_channels.insert(
|
||||
primary_thread_id,
|
||||
ThreadEventChannel::new_with_session(
|
||||
THREAD_EVENT_CHANNEL_CAPACITY,
|
||||
primary_session,
|
||||
Vec::new(),
|
||||
),
|
||||
);
|
||||
app.thread_event_channels.insert(
|
||||
inactive_thread_id,
|
||||
ThreadEventChannel::new_with_session(
|
||||
THREAD_EVENT_CHANNEL_CAPACITY,
|
||||
inactive_session,
|
||||
Vec::new(),
|
||||
),
|
||||
);
|
||||
|
||||
let notification = ThreadSettingsUpdatedNotification {
|
||||
thread_id: inactive_thread_id.to_string(),
|
||||
thread_settings: ThreadSettings {
|
||||
cwd: test_absolute_path("/tmp/thread-settings"),
|
||||
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: "gpt-plan".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
effort: collaboration_mode.settings.reasoning_effort,
|
||||
summary: None,
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
personality: Some(Personality::Pragmatic),
|
||||
},
|
||||
};
|
||||
app.enqueue_thread_notification(
|
||||
inactive_thread_id,
|
||||
ServerNotification::ThreadSettingsUpdated(notification),
|
||||
)
|
||||
.await
|
||||
.expect("settings notification should be cached");
|
||||
|
||||
let cached_session = app
|
||||
.thread_event_channels
|
||||
.get(&inactive_thread_id)
|
||||
.expect("inactive thread channel")
|
||||
.store
|
||||
.lock()
|
||||
.await
|
||||
.session
|
||||
.clone()
|
||||
.expect("inactive session should remain cached");
|
||||
assert_eq!(cached_session.model, "gpt-test");
|
||||
assert_eq!(cached_session.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(
|
||||
cached_session.collaboration_mode.as_deref(),
|
||||
Some(&collaboration_mode)
|
||||
);
|
||||
|
||||
app.chat_widget.handle_thread_session(cached_session);
|
||||
assert_eq!(
|
||||
app.chat_widget.active_collaboration_mode_kind(),
|
||||
ModeKind::Plan
|
||||
);
|
||||
assert_eq!(app.chat_widget.current_model(), "gpt-plan");
|
||||
assert_eq!(
|
||||
app.chat_widget.current_collaboration_mode().model(),
|
||||
"gpt-test"
|
||||
);
|
||||
assert_eq!(
|
||||
app.chat_widget.current_reasoning_effort(),
|
||||
Some(ReasoningEffortConfig::High)
|
||||
);
|
||||
assert_eq!(
|
||||
app.chat_widget.config_ref().personality,
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
let mut app = make_test_app().await;
|
||||
@@ -5227,6 +5554,8 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
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()),
|
||||
|
||||
@@ -355,6 +355,8 @@ mod tests {
|
||||
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()),
|
||||
|
||||
@@ -685,7 +685,11 @@ impl App {
|
||||
self.refresh_in_memory_config_from_disk().await?;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommand::OverrideTurnContext { .. } => Ok(true),
|
||||
AppCommand::OverrideTurnContext { .. } => {
|
||||
self.sync_override_turn_context_settings(app_server, thread_id, op)
|
||||
.await;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommand::ApproveGuardianDeniedAction { event } => {
|
||||
app_server
|
||||
.thread_approve_guardian_denied_action(thread_id, event)
|
||||
@@ -825,6 +829,17 @@ impl App {
|
||||
thread_id: ThreadId,
|
||||
notification: ServerNotification,
|
||||
) -> Result<()> {
|
||||
if matches!(notification, ServerNotification::ThreadSettingsUpdated(_))
|
||||
&& self.primary_thread_id.is_some()
|
||||
&& self.primary_thread_id != Some(thread_id)
|
||||
&& !self.thread_event_channels.contains_key(&thread_id)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if let ServerNotification::ThreadSettingsUpdated(notification) = ¬ification {
|
||||
self.apply_thread_settings_to_cached_session(thread_id, ¬ification.thread_settings)
|
||||
.await;
|
||||
}
|
||||
let inferred_session = self
|
||||
.infer_session_for_thread_notification(thread_id, ¬ification)
|
||||
.await;
|
||||
|
||||
@@ -78,10 +78,16 @@ impl App {
|
||||
) -> ThreadSessionState {
|
||||
let permission_profile = self.current_permission_profile();
|
||||
let active_permission_profile = self.current_active_permission_profile();
|
||||
let mut session = self
|
||||
.primary_session_configured
|
||||
.clone()
|
||||
.unwrap_or(ThreadSessionState {
|
||||
let mut session = if let Some(mut session) = self.primary_session_configured.clone() {
|
||||
if session.thread_id != thread_id {
|
||||
// `thread/read` does not include thread settings, so do not carry
|
||||
// thread-scoped state from the currently active session.
|
||||
session.collaboration_mode = None;
|
||||
session.personality = None;
|
||||
}
|
||||
session
|
||||
} else {
|
||||
ThreadSessionState {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -99,10 +105,13 @@ impl App {
|
||||
runtime_workspace_roots: self.config.workspace_roots.clone(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: self.chat_widget.current_reasoning_effort(),
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: thread.path.clone(),
|
||||
});
|
||||
}
|
||||
};
|
||||
session.thread_id = thread_id;
|
||||
session.thread_name = thread.name.clone();
|
||||
session.model_provider_id = thread.model_provider.clone();
|
||||
@@ -178,6 +187,8 @@ mod tests {
|
||||
runtime_workspace_roots: vec![cwd.abs()],
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
//! Thread settings sync between TUI-local state and app-server thread state.
|
||||
|
||||
use super::App;
|
||||
use crate::app_command::AppCommand;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::session_state::ThreadSessionState;
|
||||
use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer;
|
||||
use codex_app_server_protocol::ThreadSettings;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdateParams;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
|
||||
impl App {
|
||||
pub(super) async fn sync_active_thread_model_setting(
|
||||
&mut self,
|
||||
app_server: &mut AppServerSession,
|
||||
model: String,
|
||||
) {
|
||||
let Some(params) = self.active_thread_model_setting_update_params(model) else {
|
||||
return;
|
||||
};
|
||||
self.send_thread_settings_update(app_server, params).await;
|
||||
}
|
||||
|
||||
pub(super) fn active_thread_model_setting_update_params(
|
||||
&self,
|
||||
model: String,
|
||||
) -> Option<ThreadSettingsUpdateParams> {
|
||||
let thread_id = self.active_thread_id?;
|
||||
Some(ThreadSettingsUpdateParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
model: Some(model),
|
||||
collaboration_mode: Some(self.chat_widget.effective_collaboration_mode()),
|
||||
..ThreadSettingsUpdateParams::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn sync_active_thread_reasoning_setting(
|
||||
&mut self,
|
||||
app_server: &mut AppServerSession,
|
||||
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
) {
|
||||
let Some(params) = self.active_thread_reasoning_setting_update_params(effort) else {
|
||||
return;
|
||||
};
|
||||
self.send_thread_settings_update(app_server, params).await;
|
||||
}
|
||||
|
||||
pub(super) fn active_thread_reasoning_setting_update_params(
|
||||
&self,
|
||||
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
) -> Option<ThreadSettingsUpdateParams> {
|
||||
let thread_id = self.active_thread_id?;
|
||||
Some(ThreadSettingsUpdateParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
effort,
|
||||
collaboration_mode: Some(self.chat_widget.current_collaboration_mode().clone()),
|
||||
..ThreadSettingsUpdateParams::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn sync_active_thread_plan_mode_reasoning_setting(
|
||||
&mut self,
|
||||
app_server: &mut AppServerSession,
|
||||
) {
|
||||
let Some(thread_id) = self.active_thread_id else {
|
||||
return;
|
||||
};
|
||||
let params = ThreadSettingsUpdateParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
collaboration_mode: Some(self.chat_widget.effective_collaboration_mode()),
|
||||
..ThreadSettingsUpdateParams::default()
|
||||
};
|
||||
self.send_thread_settings_update(app_server, params).await;
|
||||
}
|
||||
|
||||
pub(super) async fn sync_active_thread_personality_setting(
|
||||
&mut self,
|
||||
app_server: &mut AppServerSession,
|
||||
personality: codex_protocol::config_types::Personality,
|
||||
) {
|
||||
let Some(thread_id) = self.active_thread_id else {
|
||||
return;
|
||||
};
|
||||
let params = ThreadSettingsUpdateParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
personality: Some(personality),
|
||||
..ThreadSettingsUpdateParams::default()
|
||||
};
|
||||
self.send_thread_settings_update(app_server, params).await;
|
||||
}
|
||||
|
||||
pub(super) async fn sync_override_turn_context_settings(
|
||||
&mut self,
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
op: &AppCommand,
|
||||
) {
|
||||
let AppCommand::OverrideTurnContext {
|
||||
cwd,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
active_permission_profile,
|
||||
windows_sandbox_level: _,
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
} = op
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let params = ThreadSettingsUpdateParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
cwd: cwd.clone(),
|
||||
approval_policy: *approval_policy,
|
||||
approvals_reviewer: approvals_reviewer.map(AppServerApprovalsReviewer::from),
|
||||
permissions: active_permission_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.id.clone()),
|
||||
model: model.clone(),
|
||||
effort: effort.unwrap_or_default(),
|
||||
summary: *summary,
|
||||
service_tier: service_tier.clone(),
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
personality: *personality,
|
||||
..ThreadSettingsUpdateParams::default()
|
||||
};
|
||||
self.send_thread_settings_update(app_server, params).await;
|
||||
}
|
||||
|
||||
pub(super) async fn apply_thread_settings_to_cached_session(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
settings: &ThreadSettings,
|
||||
) {
|
||||
if self.primary_thread_id == Some(thread_id)
|
||||
&& let Some(session) = self.primary_session_configured.as_mut()
|
||||
{
|
||||
apply_thread_settings_to_session(session, settings);
|
||||
}
|
||||
|
||||
if let Some(channel) = self.thread_event_channels.get(&thread_id) {
|
||||
let mut store = channel.store.lock().await;
|
||||
if let Some(session) = store.session.as_mut() {
|
||||
apply_thread_settings_to_session(session, settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_thread_settings_update(
|
||||
&mut self,
|
||||
app_server: &mut AppServerSession,
|
||||
params: ThreadSettingsUpdateParams,
|
||||
) {
|
||||
if !thread_settings_update_has_changes(¶ms) {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = app_server.thread_settings_update(params).await {
|
||||
tracing::warn!("failed to update app-server thread settings from TUI: {err}");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to update thread settings: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings: &ThreadSettings) {
|
||||
if settings.collaboration_mode.mode == ModeKind::Default {
|
||||
session.model = settings.model.clone();
|
||||
session.reasoning_effort = settings.effort;
|
||||
}
|
||||
session.model_provider_id = settings.model_provider.clone();
|
||||
session.service_tier = settings.service_tier.clone();
|
||||
session.approval_policy = settings.approval_policy;
|
||||
session.approvals_reviewer = settings.approvals_reviewer.to_core();
|
||||
session.permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&settings.sandbox_policy.to_core(),
|
||||
settings.cwd.as_path(),
|
||||
);
|
||||
session.active_permission_profile = settings.active_permission_profile.clone().map(Into::into);
|
||||
session.set_cwd_retargeting_implicit_runtime_workspace_root(settings.cwd.clone());
|
||||
session.personality = settings.personality;
|
||||
let mut collaboration_mode = settings.collaboration_mode.clone();
|
||||
collaboration_mode
|
||||
.settings
|
||||
.model
|
||||
.clone_from(&settings.model);
|
||||
collaboration_mode.settings.reasoning_effort = settings.effort;
|
||||
session.collaboration_mode = Some(Box::new(collaboration_mode));
|
||||
}
|
||||
|
||||
fn thread_settings_update_has_changes(params: &ThreadSettingsUpdateParams) -> bool {
|
||||
params.cwd.is_some()
|
||||
|| params.approval_policy.is_some()
|
||||
|| params.approvals_reviewer.is_some()
|
||||
|| params.sandbox_policy.is_some()
|
||||
|| params.permissions.is_some()
|
||||
|| params.model.is_some()
|
||||
|| params.service_tier.is_some()
|
||||
|| params.effort.is_some()
|
||||
|| params.summary.is_some()
|
||||
|| params.collaboration_mode.is_some()
|
||||
|| params.personality.is_some()
|
||||
}
|
||||
@@ -86,6 +86,8 @@ use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadSetNameResponse;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdateParams;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdateResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandResponse;
|
||||
use codex_app_server_protocol::ThreadSource;
|
||||
@@ -121,10 +123,20 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
const JSONRPC_INVALID_REQUEST: i64 = -32600;
|
||||
const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;
|
||||
const THREAD_SETTINGS_UPDATE_METHOD: &str = "thread/settings/update";
|
||||
|
||||
fn bootstrap_request_error(context: &'static str, err: TypedRequestError) -> color_eyre::Report {
|
||||
color_eyre::eyre::eyre!("{context}: {err}")
|
||||
}
|
||||
|
||||
fn is_thread_settings_update_unsupported(source: &JSONRPCErrorError) -> bool {
|
||||
source.code == JSONRPC_METHOD_NOT_FOUND
|
||||
|| (source.code == JSONRPC_INVALID_REQUEST
|
||||
&& source.message.contains(THREAD_SETTINGS_UPDATE_METHOD))
|
||||
}
|
||||
|
||||
/// Data collected during the TUI bootstrap phase that the main event loop
|
||||
/// needs to configure the UI, telemetry, and initial rate-limit prefetch.
|
||||
///
|
||||
@@ -151,6 +163,7 @@ pub(crate) struct AppServerSession {
|
||||
next_request_id: i64,
|
||||
remote_cwd_override: Option<PathBuf>,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
thread_settings_update_supported: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -191,6 +204,7 @@ impl AppServerSession {
|
||||
next_request_id: 1,
|
||||
remote_cwd_override: None,
|
||||
thread_params_mode,
|
||||
thread_settings_update_supported: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +542,39 @@ impl AppServerSession {
|
||||
.wrap_err("thread/metadata/update failed while syncing git branch")
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_settings_update(
|
||||
&mut self,
|
||||
params: ThreadSettingsUpdateParams,
|
||||
) -> Result<()> {
|
||||
if !self.thread_settings_update_supported {
|
||||
return Ok(());
|
||||
}
|
||||
let request_id = self.next_request_id();
|
||||
match self
|
||||
.client
|
||||
.request_typed::<ThreadSettingsUpdateResponse>(ClientRequest::ThreadSettingsUpdate {
|
||||
request_id,
|
||||
params,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(TypedRequestError::Server { source, .. })
|
||||
if is_thread_settings_update_unsupported(&source) =>
|
||||
{
|
||||
// Older remote app servers can reject this experimental method as
|
||||
// method-not-found, experimental-capability-gated, or an unknown
|
||||
// request variant. Treat those as a session-level capability
|
||||
// downgrade so local TUI setting changes stay best-effort instead
|
||||
// of showing an error every time the user changes model, effort,
|
||||
// personality, or mode.
|
||||
self.thread_settings_update_supported = false;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err).wrap_err("thread/settings/update failed in TUI"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_inject_items(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
@@ -1599,6 +1646,8 @@ async fn thread_session_state_from_thread_response(
|
||||
runtime_workspace_roots,
|
||||
instruction_source_paths,
|
||||
reasoning_effort,
|
||||
collaboration_mode: None,
|
||||
personality: config.personality,
|
||||
message_history: Some(MessageHistoryMetadata {
|
||||
log_id,
|
||||
entry_count,
|
||||
@@ -1700,6 +1749,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_settings_update_compat_detects_unsupported_errors() {
|
||||
let cases = [
|
||||
(JSONRPC_METHOD_NOT_FOUND, "method not found", true),
|
||||
(
|
||||
JSONRPC_INVALID_REQUEST,
|
||||
"thread/settings/update requires experimentalApi capability",
|
||||
true,
|
||||
),
|
||||
(
|
||||
JSONRPC_INVALID_REQUEST,
|
||||
"Invalid request: unknown variant `thread/settings/update`",
|
||||
true,
|
||||
),
|
||||
(JSONRPC_INVALID_REQUEST, "invalid thread id", false),
|
||||
];
|
||||
|
||||
for (code, message, expected) in cases {
|
||||
let source = JSONRPCErrorError {
|
||||
code,
|
||||
data: None,
|
||||
message: message.to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
is_thread_settings_update_unsupported(&source),
|
||||
expected,
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_params_include_cwd_for_embedded_sessions() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -115,6 +115,8 @@ use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_app_server_protocol::ThreadGoal as AppThreadGoal;
|
||||
use codex_app_server_protocol::ThreadGoalStatus as AppThreadGoalStatus;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadSettings;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_app_server_protocol::Turn;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(¬ification.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()),
|
||||
|
||||
@@ -451,6 +451,8 @@ fn session_configured_event(model: &str) -> ThreadSessionState {
|
||||
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()),
|
||||
|
||||
@@ -7,6 +7,8 @@ use std::path::PathBuf;
|
||||
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -47,6 +49,8 @@ pub(crate) struct ThreadSessionState {
|
||||
pub(crate) runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub(crate) instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
pub(crate) collaboration_mode: Option<Box<CollaborationMode>>,
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) message_history: Option<MessageHistoryMetadata>,
|
||||
pub(crate) network_proxy: Option<SessionNetworkProxyRuntime>,
|
||||
pub(crate) rollout_path: Option<PathBuf>,
|
||||
|
||||
Reference in New Issue
Block a user