From f52e187048dd1da8ccc08f3bfc8a33e9abe6552f Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 4 May 2026 13:33:04 -0700 Subject: [PATCH 1/4] Use backend collaboration modes in TUI picker --- codex-rs/tui/src/app.rs | 5 ++- codex-rs/tui/src/app_server_session.rs | 32 ++++++++++++++++++++ codex-rs/tui/src/chatwidget/tests/helpers.rs | 14 ++++++++- codex-rs/tui/src/collaboration_modes.rs | 6 ++-- codex-rs/tui/src/model_catalog.rs | 16 ++++++++-- 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 66c5dfd1840d..e859c2493bbb 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -686,7 +686,10 @@ impl App { if let Some(updated_model) = config.model.clone() { model = updated_model; } - let model_catalog = Arc::new(ModelCatalog::new(available_models.clone())); + let model_catalog = Arc::new(ModelCatalog::new( + available_models.clone(), + bootstrap.collaboration_modes, + )); let feedback_audience = bootstrap.feedback_audience; let auth_mode = bootstrap.auth_mode; let has_chatgpt_account = bootstrap.has_chatgpt_account; diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 449da8e212bb..9930cb10fc28 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -20,6 +20,9 @@ use codex_app_server_protocol::Account; use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::CollaborationModeListParams; +use codex_app_server_protocol::CollaborationModeListResponse; +use codex_app_server_protocol::CollaborationModeMask as ApiCollaborationModeMask; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigWriteResponse; use codex_app_server_protocol::ExternalAgentConfigDetectParams; @@ -105,6 +108,7 @@ use codex_app_server_protocol::UserInput; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; use codex_protocol::approvals::GuardianAssessmentEvent; +use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfileModification; use codex_protocol::models::PermissionProfile; @@ -143,6 +147,7 @@ pub(crate) struct AppServerBootstrap { pub(crate) feedback_audience: FeedbackAudience, pub(crate) has_chatgpt_account: bool, pub(crate) available_models: Vec, + pub(crate) collaboration_modes: Vec, } pub(crate) struct AppServerSession { @@ -215,6 +220,22 @@ impl AppServerSession { .into_iter() .map(model_preset_from_api_model) .collect::>(); + let collaboration_modes_request_id = self.next_request_id(); + let collaboration_modes: CollaborationModeListResponse = self + .client + .request_typed(ClientRequest::CollaborationModeList { + request_id: collaboration_modes_request_id, + params: CollaborationModeListParams::default(), + }) + .await + .map_err(|err| { + bootstrap_request_error("collaborationMode/list failed during TUI bootstrap", err) + })?; + let collaboration_modes = collaboration_modes + .data + .into_iter() + .map(collaboration_mode_mask_from_api_mask) + .collect::>(); let default_model = config .model .clone() @@ -276,6 +297,7 @@ impl AppServerSession { feedback_audience, has_chatgpt_account, available_models, + collaboration_modes, }) } @@ -1058,6 +1080,16 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset { } } +fn collaboration_mode_mask_from_api_mask(mask: ApiCollaborationModeMask) -> CollaborationModeMask { + CollaborationModeMask { + name: mask.name, + mode: mask.mode, + model: mask.model, + reasoning_effort: mask.reasoning_effort, + developer_instructions: None, + } +} + fn approvals_reviewer_override_from_config( config: &Config, ) -> Option { diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 2631e506599f..a41d0d56f913 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -1,5 +1,6 @@ use super::*; use codex_app_server_protocol::PluginAvailability; +use codex_protocol::config_types::CollaborationModeMask; use pretty_assertions::assert_eq; pub(super) async fn test_config() -> Config { @@ -136,9 +137,20 @@ pub(super) fn test_session_telemetry(config: &Config, model: &str) -> SessionTel pub(super) fn test_model_catalog(_config: &Config) -> Arc { Arc::new(ModelCatalog::new( crate::legacy_core::test_support::all_model_presets().clone(), + test_collaboration_mode_presets(), )) } +fn test_collaboration_mode_presets() -> Vec { + crate::legacy_core::test_support::builtin_collaboration_mode_presets() + .into_iter() + .map(|mut preset| { + preset.developer_instructions = None; + preset + }) + .collect() +} + // --- Helpers for tests that need direct construction and event draining --- pub(super) async fn make_chatwidget_manual( model_override: Option<&str>, @@ -437,7 +449,7 @@ pub(crate) fn set_fast_mode_test_catalog(chat: &mut ChatWidget) { .map(Into::into) .collect(); - chat.model_catalog = Arc::new(ModelCatalog::new(models)); + chat.model_catalog = Arc::new(ModelCatalog::new(models, test_collaboration_mode_presets())); } pub(crate) async fn make_chatwidget_manual_with_sender() -> ( diff --git a/codex-rs/tui/src/collaboration_modes.rs b/codex-rs/tui/src/collaboration_modes.rs index e0881bc9cf17..dc4cd8e89ad4 100644 --- a/codex-rs/tui/src/collaboration_modes.rs +++ b/codex-rs/tui/src/collaboration_modes.rs @@ -1,11 +1,11 @@ -use codex_models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use crate::model_catalog::ModelCatalog; -fn filtered_presets(_model_catalog: &ModelCatalog) -> Vec { - builtin_collaboration_mode_presets() +fn filtered_presets(model_catalog: &ModelCatalog) -> Vec { + model_catalog + .list_collaboration_modes() .into_iter() .filter(|mask| mask.mode.is_some_and(ModeKind::is_tui_visible)) .collect() diff --git a/codex-rs/tui/src/model_catalog.rs b/codex-rs/tui/src/model_catalog.rs index 69c9ded5669b..681d458132cf 100644 --- a/codex-rs/tui/src/model_catalog.rs +++ b/codex-rs/tui/src/model_catalog.rs @@ -1,17 +1,29 @@ +use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::openai_models::ModelPreset; use std::convert::Infallible; #[derive(Debug, Clone)] pub(crate) struct ModelCatalog { models: Vec, + collaboration_modes: Vec, } impl ModelCatalog { - pub(crate) fn new(models: Vec) -> Self { - Self { models } + pub(crate) fn new( + models: Vec, + collaboration_modes: Vec, + ) -> Self { + Self { + models, + collaboration_modes, + } } pub(crate) fn try_list_models(&self) -> Result, Infallible> { Ok(self.models.clone()) } + + pub(crate) fn list_collaboration_modes(&self) -> Vec { + self.collaboration_modes.clone() + } } From bd73e5e16044b4c9cfe95a22ffdfc5ca926f3cd2 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 4 May 2026 14:44:15 -0700 Subject: [PATCH 2/4] Handle missing remote collaboration mode list --- codex-rs/tui/src/app_server_session.rs | 84 +++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 9930cb10fc28..2974d3f65b8c 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -105,6 +105,7 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInput; +use codex_models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; use codex_protocol::approvals::GuardianAssessmentEvent; @@ -124,10 +125,25 @@ use color_eyre::eyre::WrapErr; use std::collections::HashMap; use std::path::PathBuf; +const JSONRPC_INVALID_REQUEST_ERROR_CODE: i64 = -32600; +const JSONRPC_METHOD_NOT_FOUND_ERROR_CODE: i64 = -32601; + fn bootstrap_request_error(context: &'static str, err: TypedRequestError) -> color_eyre::Report { color_eyre::eyre::eyre!("{context}: {err}") } +fn is_missing_collaboration_mode_list_error(err: &TypedRequestError) -> bool { + match err { + TypedRequestError::Server { method, source } if method == "collaborationMode/list" => { + source.code == JSONRPC_METHOD_NOT_FOUND_ERROR_CODE + || (source.code == JSONRPC_INVALID_REQUEST_ERROR_CODE + && source.message.contains("collaborationMode/list") + && source.message.contains("unknown variant")) + } + TypedRequestError::Transport { .. } | TypedRequestError::Deserialize { .. } => false, + } +} + /// Data collected during the TUI bootstrap phase that the main event loop /// needs to configure the UI, telemetry, and initial rate-limit prefetch. /// @@ -221,21 +237,36 @@ impl AppServerSession { .map(model_preset_from_api_model) .collect::>(); let collaboration_modes_request_id = self.next_request_id(); - let collaboration_modes: CollaborationModeListResponse = self + let collaboration_modes_response: std::result::Result< + CollaborationModeListResponse, + TypedRequestError, + > = self .client .request_typed(ClientRequest::CollaborationModeList { request_id: collaboration_modes_request_id, params: CollaborationModeListParams::default(), }) - .await - .map_err(|err| { - bootstrap_request_error("collaborationMode/list failed during TUI bootstrap", err) - })?; - let collaboration_modes = collaboration_modes - .data - .into_iter() - .map(collaboration_mode_mask_from_api_mask) - .collect::>(); + .await; + let collaboration_modes = match collaboration_modes_response { + Ok(collaboration_modes) => collaboration_modes + .data + .into_iter() + .map(collaboration_mode_mask_from_api_mask) + .collect::>(), + Err(err) if self.is_remote() && is_missing_collaboration_mode_list_error(&err) => { + tracing::debug!( + %err, + "remote app-server does not support collaborationMode/list; using built-in collaboration modes" + ); + builtin_collaboration_mode_presets() + } + Err(err) => { + return Err(bootstrap_request_error( + "collaborationMode/list failed during TUI bootstrap", + err, + )); + } + }; let default_model = config .model .clone() @@ -1552,6 +1583,39 @@ mod tests { .expect("config should build") } + fn collaboration_mode_list_server_error(code: i64, message: &str) -> TypedRequestError { + TypedRequestError::Server { + method: "collaborationMode/list".to_string(), + source: JSONRPCErrorError { + code, + message: message.to_string(), + data: None, + }, + } + } + + #[test] + fn detects_missing_collaboration_mode_list_errors() { + assert!(is_missing_collaboration_mode_list_error( + &collaboration_mode_list_server_error( + JSONRPC_METHOD_NOT_FOUND_ERROR_CODE, + "Method not found" + ) + )); + assert!(is_missing_collaboration_mode_list_error( + &collaboration_mode_list_server_error( + JSONRPC_INVALID_REQUEST_ERROR_CODE, + "Invalid request: unknown variant `collaborationMode/list`" + ) + )); + assert!(!is_missing_collaboration_mode_list_error( + &collaboration_mode_list_server_error( + JSONRPC_INVALID_REQUEST_ERROR_CODE, + "Experimental API `collaborationMode/list` is not enabled" + ) + )); + } + #[tokio::test] async fn thread_start_params_include_cwd_for_embedded_sessions() { let temp_dir = tempfile::tempdir().expect("tempdir"); From 4a7184353c8526aaed40457c8ab35a3cccf52292 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 4 May 2026 14:51:10 -0700 Subject: [PATCH 3/4] Fix collaboration mode error classifier --- codex-rs/tui/src/app_server_session.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 2974d3f65b8c..8e5462f64118 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -134,11 +134,12 @@ fn bootstrap_request_error(context: &'static str, err: TypedRequestError) -> col fn is_missing_collaboration_mode_list_error(err: &TypedRequestError) -> bool { match err { - TypedRequestError::Server { method, source } if method == "collaborationMode/list" => { - source.code == JSONRPC_METHOD_NOT_FOUND_ERROR_CODE - || (source.code == JSONRPC_INVALID_REQUEST_ERROR_CODE - && source.message.contains("collaborationMode/list") - && source.message.contains("unknown variant")) + TypedRequestError::Server { method, source } => { + method == "collaborationMode/list" + && (source.code == JSONRPC_METHOD_NOT_FOUND_ERROR_CODE + || (source.code == JSONRPC_INVALID_REQUEST_ERROR_CODE + && source.message.contains("collaborationMode/list") + && source.message.contains("unknown variant"))) } TypedRequestError::Transport { .. } | TypedRequestError::Deserialize { .. } => false, } From b801904c5e0ae7a74deca1394399d685c8c22724 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 11 May 2026 16:58:53 -0700 Subject: [PATCH 4/4] fix --- codex-rs/tui/src/chatwidget/tests/helpers.rs | 2 +- codex-rs/tui/src/chatwidget/tests/slash_commands.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 9a0eea61515f..5be04c3f2458 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -141,7 +141,7 @@ pub(super) fn test_model_catalog(_config: &Config) -> Arc { )) } -fn test_collaboration_mode_presets() -> Vec { +pub(super) fn test_collaboration_mode_presets() -> Vec { crate::legacy_core::test_support::builtin_collaboration_mode_presets() .into_iter() .map(|mut preset| { diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index a1bf28c807bf..3a255cfd69cf 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -68,7 +68,10 @@ async fn service_tier_commands_lowercase_catalog_names() { let mut preset = get_available_model(&chat, "gpt-5.4"); let expected_description = preset.service_tiers[0].description.clone(); preset.service_tiers[0].name = "Fast".to_string(); - chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![preset])); + chat.model_catalog = std::sync::Arc::new(ModelCatalog::new( + vec![preset], + test_collaboration_mode_presets(), + )); assert_eq!( chat.current_model_service_tier_commands(),