diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 964bf99875..c150995f6d 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -372,6 +372,7 @@ fn sample_thread_resume_response_with_source( sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + collaboration_mode: None, multi_agent_mode: Default::default(), initial_turns_page: None, turns_backwards_cursor: None, diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index 4f4146b35c..c54a97687d 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -659,6 +659,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload { sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + collaboration_mode: None, multi_agent_mode: Default::default(), initial_turns_page: None, turns_backwards_cursor: None, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5eae6a1d42..fec88ff951 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -23344,6 +23344,17 @@ ], "description": "Reviewer currently used for approval requests on this thread." }, + "collaborationMode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/CollaborationMode" + }, + { + "type": "null" + } + ], + "description": "Effective collaboration mode. Absent when resuming from an older server." + }, "cwd": { "$ref": "#/definitions/v2/AbsolutePathBuf" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index a056709984..fcb6755496 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -21058,6 +21058,17 @@ ], "description": "Reviewer currently used for approval requests on this thread." }, + "collaborationMode": { + "anyOf": [ + { + "$ref": "#/definitions/CollaborationMode" + }, + { + "type": "null" + } + ], + "description": "Effective collaboration mode. Absent when resuming from an older server." + }, "cwd": { "$ref": "#/definitions/AbsolutePathBuf" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index ba6a006381..2019868698 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -324,6 +324,22 @@ ], "type": "string" }, + "CollaborationMode": { + "description": "Collaboration mode for a Codex session.", + "properties": { + "mode": { + "$ref": "#/definitions/ModeKind" + }, + "settings": { + "$ref": "#/definitions/Settings" + } + }, + "required": [ + "mode", + "settings" + ], + "type": "object" + }, "CommandAction": { "oneOf": [ { @@ -908,6 +924,14 @@ ], "type": "object" }, + "ModeKind": { + "description": "Initial collaboration mode to use when the TUI starts.", + "enum": [ + "plan", + "default" + ], + "type": "string" + }, "MultiAgentMode": { "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", "oneOf": [ @@ -1158,6 +1182,34 @@ } ] }, + "Settings": { + "description": "Settings for a collaboration mode.", + "properties": { + "developer_instructions": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "reasoning_effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "model" + ], + "type": "object" + }, "SubAgentActivityKind": { "enum": [ "started", @@ -2965,6 +3017,17 @@ ], "description": "Reviewer currently used for approval requests on this thread." }, + "collaborationMode": { + "anyOf": [ + { + "$ref": "#/definitions/CollaborationMode" + }, + { + "type": "null" + } + ], + "description": "Effective collaboration mode. Absent when resuming from an older server." + }, "cwd": { "$ref": "#/definitions/AbsolutePathBuf" }, diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 7c4f0b7f9c..7d613c2a4f 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index ecbb0b1669..6fd1937421 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index eaea856658..4bffeaa955 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -2,6 +2,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { CollaborationMode } from "../CollaborationMode"; import type { LegacyAppPathString } from "../LegacyAppPathString"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; @@ -23,6 +24,9 @@ approvalsReviewer: ApprovalsReviewer, /** * should prefer `activePermissionProfile` for profile provenance. */ sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, /** + * Effective collaboration mode. Absent when resuming from an older server. + */ +collaborationMode: CollaborationMode | null, /** * Opaque cursor for hydrating paginated turns backwards. * * Pass this as `cursor` to `thread/turns/list` with diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index c185747cab..496116f4e5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -327,6 +327,7 @@ fn thread_resume_response_round_trips_initial_turns_page() { sandbox: SandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + collaboration_mode: None, multi_agent_mode: Default::default(), initial_turns_page: Some(TurnsPage { data: Vec::new(), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index e369775ab3..162adb13bb 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -458,6 +458,8 @@ pub struct ThreadResumeResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, + /// Effective collaboration mode. Absent when resuming from an older server. + pub collaboration_mode: Option, /// @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. #[experimental("thread/resume.multiAgentMode")] #[serde(default)] diff --git a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs index 71fba6b25e..82c12ed9e5 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -732,6 +732,7 @@ pub(super) async fn handle_pending_thread_resume_request( active_permission_profile, workspace_roots, reasoning_effort, + collaboration_mode, originator, .. } = config_snapshot; @@ -755,6 +756,7 @@ pub(super) async fn handle_pending_thread_resume_request( sandbox, active_permission_profile, reasoning_effort, + collaboration_mode: Some(collaboration_mode), multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, initial_turns_page, turns_backwards_cursor, diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 4cd83dd37c..1f8b593841 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -4119,6 +4119,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, + collaboration_mode: Some(config_snapshot.collaboration_mode), multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, initial_turns_page, turns_backwards_cursor, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 52f24eb001..822d21c0ef 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -1448,11 +1448,11 @@ async fn thread_resume_preserves_acknowledged_model_effort_and_approvals_reviewe thread_id: thread_id.clone(), cwd: Some(persisted_cwd.clone()), collaboration_mode: Some(CollaborationMode { - mode: ModeKind::Default, + mode: ModeKind::Plan, settings: Settings { model: "gpt-5.2-codex".to_string(), reasoning_effort: None, - developer_instructions: None, + developer_instructions: Some("Persisted plan instructions".to_string()), }, }), ..Default::default() @@ -1481,21 +1481,105 @@ async fn thread_resume_preserves_acknowledged_model_effort_and_approvals_reviewe .await?; let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { - thread_id, + thread_id: thread_id.clone(), + model: Some("gpt-5.4".to_string()), ..Default::default() }) .await?; let ThreadResumeResponse { cwd, reasoning_effort, + collaboration_mode, .. } = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??; - assert_eq!(reasoning_effort, None); + assert_eq!(reasoning_effort, Some(ReasoningEffort::High)); assert_eq!(cwd.as_path(), persisted_cwd); + let (items, _, _) = RolloutRecorder::load_rollout_items(&rollout_path).await?; + let mode = items.iter().rev().find_map(|item| match item { + RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(event)) + if event + .thread_id + .is_some_and(|id| id.to_string() == thread_id) => + { + Some(event.thread_settings.collaboration_mode.clone()) + } + _ => None, + }); + assert_eq!(collaboration_mode, mode); + assert_eq!( + mode, + Some(CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: "gpt-5.4".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: Some("Persisted plan instructions".to_string()) + }, + }) + ); Ok(()) } +#[tokio::test] +async fn thread_resume_restores_collaboration_mode_from_legacy_turn_context() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + let fixture = setup_rollout_fixture(codex_home.path(), &server.uri()).await?; + mock_responses_config(&server.uri()) + .with_root_config("model_reasoning_effort = \"high\"") + .write(codex_home.path())?; + let saved_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: "gpt-5.2-codex".into(), + reasoning_effort: Some(ReasoningEffort::Low), + developer_instructions: Some("Legacy plan instructions".into()), + }, + }; + let context = serde_json::from_value(json!({ + "cwd": codex_home.path(), + "approval_policy": "never", + "sandbox_policy": {"type": "read-only"}, + "model": saved_mode.settings.model, + "effort": saved_mode.settings.reasoning_effort, + "summary": "auto", + "collaboration_mode": saved_mode, + }))?; + append_rollout_item_to_path( + &fixture.rollout_file_path, + &RolloutItem::TurnContext(context), + ) + .await?; + let (items, _, _) = RolloutRecorder::load_rollout_items(&fixture.rollout_file_path).await?; + assert!(!items.iter().any(|item| matches!( + item, + RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(_)) + ))); + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build_initialized() + .await?; + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: fixture.conversation_id, + model: Some("gpt-5.4".into()), + ..Default::default() + }) + .await?; + let response: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??; + assert_eq!( + response.collaboration_mode, + Some(saved_mode.with_updates( + Some("gpt-5.4".into()), + Some(Some(ReasoningEffort::High)), + /*developer_instructions*/ None, + )) + ); + Ok(()) +} + #[tokio::test] async fn cold_resume_reresolves_persisted_active_permission_profile() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index b35cc3905e..6cd5d41403 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -761,6 +761,34 @@ impl Session { }); // TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode // to avoid extracting these fields separately and constructing CollaborationMode here. + let persisted_collaboration_mode = match &conversation_history { + InitialHistory::Resumed(resumed) => resumed + .history + .iter() + .rev() + .find_map(|item| match item { + RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(event)) + if event.thread_id == Some(resumed.conversation_id) => + { + Some(&event.thread_settings.collaboration_mode) + } + _ => None, + }) + .or_else(|| { + resumed + .history + .iter() + .rev() + .find_map(|item| match item { + RolloutItem::TurnContext(context) => { + Some(context.collaboration_mode.as_ref()) + } + _ => None, + }) + .flatten() + }), + InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None, + }; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, settings: Settings { @@ -769,6 +797,13 @@ impl Session { developer_instructions: None, }, }; + let collaboration_mode = persisted_collaboration_mode.map_or(collaboration_mode, |saved| { + saved.with_updates( + Some(model.clone()), + Some(config.model_reasoning_effort.clone()), + /*developer_instructions*/ None, + ) + }); let fast_mode_enabled = config.features.enabled(Feature::FastMode); let initial_service_tier_warning = unsupported_service_tier_warning( config.service_tier.as_deref(), diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index f6a8fc3c05..8882e18a8b 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -951,13 +951,14 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { .await; let mut builder = test_codex(); - let initial = builder.build(&server).await?; + let initial = builder.build_with_auto_env(&server).await?; let collab_text = "resume instructions"; + let mode = collab_mode_for_model(ModeKind::Plan, "gpt-5.5", Some(collab_text)); core_test_support::submit_thread_settings( &initial.codex, ThreadSettingsOverrides { - collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))), + collaboration_mode: Some(mode), ..Default::default() }, ) @@ -973,6 +974,15 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let resumed = builder.restart(&server, &initial).await?; + assert_eq!( + resumed + .codex + .config_snapshot() + .await + .collaboration_mode + .mode, + ModeKind::Plan + ); resumed .codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { diff --git a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs index 560d652dbf..c9b4fbe5b3 100644 --- a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs @@ -51,11 +51,20 @@ async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() -> app.chat_widget.set_collaboration_mask( crate::collaboration_modes::plan_mask(app.model_catalog.as_ref()).unwrap(), ); - let expected_mode = app.chat_widget.effective_collaboration_mode().with_updates( + let cached_mode = app.chat_widget.effective_collaboration_mode().with_updates( Some("gpt-test".into()), Some(None), /*developer_instructions*/ None, ); + // A newer server can report a mode changed by another client while disconnected. + let server_mode = (!recovered_queue).then(|| CollaborationMode { + mode: ModeKind::Default, + settings: Settings { + developer_instructions: Some("Updated by another client".into()), + ..cached_mode.settings.clone() + }, + }); + let expected_mode = server_mode.clone().unwrap_or(cached_mode); let expected_submitted_mode = expected_mode.clone(); assert!(!app.model_catalog.collaboration_modes.is_empty()); if edit_offline { @@ -118,8 +127,12 @@ async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() -> let params = request.params.as_ref().unwrap(); assert_eq!(params["threadId"], id.to_string()); assert!(params["model"].is_null()); - Some(json!({"result": {"thread": thread, "model": "gpt-test", "modelProvider": "test-provider", "cwd": cwd, - "approvalPolicy": "never", "approvalsReviewer": "user", "sandbox": {"type": "dangerFullAccess"}, "reasoningEffort": null}})) + let mut result = json!({"thread": thread, "model": "gpt-test", "modelProvider": "test-provider", "cwd": cwd, + "approvalPolicy": "never", "approvalsReviewer": "user", "sandbox": {"type": "dangerFullAccess"}, "reasoningEffort": null}); + if let Some(mode) = &server_mode { + result["collaborationMode"] = json!(mode); + } + Some(json!({"result": result})) } "thread/read" => Some(json!({"result": {"thread": thread}})), "thread/list" | "thread/loaded/list" => Some(json!({"result": {"data": [], "nextCursor": null}})), diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 1e15836342..967649b46e 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -1814,6 +1814,10 @@ impl App { replay_filter::snapshot_has_pending_interactive_request(&snapshot); self.chat_widget .set_queue_autosend_suppressed(/*suppressed*/ true); + let has_resumed_collaboration_mode = snapshot + .session + .as_ref() + .is_some_and(|session| session.collaboration_mode.is_some()); if let Some(session) = snapshot.session { if session.reasoning_effort != Some(ReasoningEffortConfig::Ultra) { self.chat_widget @@ -1867,7 +1871,12 @@ impl App { .send(AppEvent::EndInitialHistoryReplayBuffer); } if recovered_input.is_some() { + let mode = has_resumed_collaboration_mode + .then(|| self.chat_widget.effective_collaboration_mode()); self.chat_widget.restore_reconnected_input(recovered_input); + if let Some(mode) = mode { + self.chat_widget.set_effective_collaboration_mode(mode); + } } self.restore_realtime_replay_state_after_replay( &replayed_final_items, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 29922e6213..cc576eb791 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -2342,7 +2342,7 @@ async fn thread_session_state_from_thread_resume_response( thread_params_mode, ) }; - thread_session_state_from_thread_response( + let mut session = thread_session_state_from_thread_response( &response.thread.id, response.thread.forked_from_id.clone(), response.thread.name.clone(), @@ -2361,7 +2361,9 @@ async fn thread_session_state_from_thread_resume_response( config.personality, local_settings, ) - .await + .await?; + session.collaboration_mode = response.collaboration_mode.clone().map(Box::new); + Ok(session) } async fn thread_session_state_from_thread_fork_response( @@ -3993,6 +3995,14 @@ mod tests { .into(), active_permission_profile: None, reasoning_effort: None, + collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { + mode: codex_protocol::config_types::ModeKind::Plan, + settings: codex_protocol::config_types::Settings { + model: "gpt-5.4".to_string(), + reasoning_effort: None, + developer_instructions: Some("Keep planning".to_string()), + }, + }), multi_agent_mode: Default::default(), initial_turns_page: None, turns_backwards_cursor: None, @@ -4021,6 +4031,30 @@ mod tests { assert_eq!(started.turns[0], response.thread.turns[0]); assert!(!started.blocks_direct_input); + // The first prompt after resume must preserve the server's restored mode. + let (mut chat, _sender, _events, mut commands) = + crate::chatwidget::tests::helpers::make_chatwidget_manual_with_sender().await; + chat.handle_thread_session(started.session); + insta::assert_snapshot!( + "resumed_plan_mode", + crate::chatwidget::tests::helpers::normalize_snapshot_paths( + crate::chatwidget::tests::helpers::render_bottom_popup(&chat, /*width*/ 40), + ) + ); + chat.handle_paste("Continue planning".to_string()); + chat.handle_key_event(crossterm::event::KeyCode::Enter.into()); + let submitted_mode = std::iter::from_fn(|| commands.try_recv().ok()).find_map(|command| { + if let crate::app_command::AppCommand::UserTurn { + collaboration_mode, .. + } = command + { + Some(collaboration_mode) + } else { + None + } + }); + assert_eq!(submitted_mode, Some(response.collaboration_mode.clone())); + let embedded_config = ConfigBuilder::default() .codex_home(temp_dir.path().join("embedded-codex-home")) .harness_overrides(ConfigOverrides { diff --git a/codex-rs/tui/src/chatwidget/reconnect.rs b/codex-rs/tui/src/chatwidget/reconnect.rs index 6e98446877..f6e954fbc3 100644 --- a/codex-rs/tui/src/chatwidget/reconnect.rs +++ b/codex-rs/tui/src/chatwidget/reconnect.rs @@ -52,7 +52,7 @@ impl ChatWidget { input.recovered_queue = true; } input.current_collaboration_mode = self.current_collaboration_mode.clone(); - // Resume supplies model/effort, but not the user's selected collaboration mode. + // Keep the local selection for older servers. Replay reapplies a supplied mode. if let Some(mask) = input.active_collaboration_mask.as_mut() { mask.model = Some(self.current_model().to_string()); mask.reasoning_effort = Some(self.effective_reasoning_effort()); diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index b087aa4f12..78dac65a9d 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -524,7 +524,7 @@ impl ChatWidget { .set_workspace_roots(self.config.workspace_roots.clone()); } - pub(super) fn set_effective_collaboration_mode(&mut self, mode: CollaborationMode) { + pub(crate) fn set_effective_collaboration_mode(&mut self, mode: CollaborationMode) { let mode_kind = mode.mode; let settings = mode.settings; if mode_kind == ModeKind::Default { diff --git a/codex-rs/tui/src/snapshots/codex_tui__app_server_session__tests__resumed_plan_mode.snap b/codex-rs/tui/src/snapshots/codex_tui__app_server_session__tests__resumed_plan_mode.snap new file mode 100644 index 0000000000..048a6f0f20 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__app_server_session__tests__resumed_plan_mode.snap @@ -0,0 +1,7 @@ +--- +source: tui/src/app_server_session.rs +expression: "crate::chatwidget::tests::helpers::render_bottom_popup(&chat, 40)" +--- +› Ask Codex to do anything + + gpt-5.4 default · /tmp/pr… Plan mode diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index ce48b6338e..c0ca73d30e 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -11849,6 +11849,13 @@ class ThreadResumeResponse(BaseModel): description="Reviewer currently used for approval requests on this thread.", ), ] + collaboration_mode: Annotated[ + CollaborationMode | None, + Field( + alias="collaborationMode", + description="Effective collaboration mode. Absent when resuming from an older server.", + ), + ] = None cwd: AbsolutePathBuf disabled_plugin_ids: Annotated[ list[str] | None,