Compare commits

...

5 Commits

Author SHA1 Message Date
canvrno-oai
b801904c5e fix 2026-05-11 16:58:53 -07:00
canvrno-oai
3bee5f04f4 Merge branch 'main' into codex/backend-collab-mode-picker 2026-05-11 16:37:02 -07:00
canvrno-oai
4a7184353c Fix collaboration mode error classifier 2026-05-04 14:51:10 -07:00
canvrno-oai
bd73e5e160 Handle missing remote collaboration mode list 2026-05-04 14:44:15 -07:00
canvrno-oai
f52e187048 Use backend collaboration modes in TUI picker 2026-05-04 13:33:04 -07:00
6 changed files with 135 additions and 8 deletions

View File

@@ -709,7 +709,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;

View File

@@ -18,6 +18,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;
@@ -101,9 +104,11 @@ 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;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
@@ -120,10 +125,26 @@ 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 } => {
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.
///
@@ -143,6 +164,7 @@ pub(crate) struct AppServerBootstrap {
pub(crate) feedback_audience: FeedbackAudience,
pub(crate) has_chatgpt_account: bool,
pub(crate) available_models: Vec<ModelPreset>,
pub(crate) collaboration_modes: Vec<CollaborationModeMask>,
}
pub(crate) struct AppServerSession {
@@ -215,6 +237,37 @@ impl AppServerSession {
.into_iter()
.map(model_preset_from_api_model)
.collect::<Vec<_>>();
let collaboration_modes_request_id = self.next_request_id();
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;
let collaboration_modes = match collaboration_modes_response {
Ok(collaboration_modes) => collaboration_modes
.data
.into_iter()
.map(collaboration_mode_mask_from_api_mask)
.collect::<Vec<_>>(),
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()
@@ -276,6 +329,7 @@ impl AppServerSession {
feedback_audience,
has_chatgpt_account,
available_models,
collaboration_modes,
})
}
@@ -1067,6 +1121,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<codex_app_server_protocol::ApprovalsReviewer> {
@@ -1581,6 +1645,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");

View File

@@ -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<ModelCatalog> {
Arc::new(ModelCatalog::new(
crate::legacy_core::test_support::all_model_presets().clone(),
test_collaboration_mode_presets(),
))
}
pub(super) fn test_collaboration_mode_presets() -> Vec<CollaborationModeMask> {
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>,
@@ -303,7 +315,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() -> (

View File

@@ -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(),

View File

@@ -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<CollaborationModeMask> {
builtin_collaboration_mode_presets()
fn filtered_presets(model_catalog: &ModelCatalog) -> Vec<CollaborationModeMask> {
model_catalog
.list_collaboration_modes()
.into_iter()
.filter(|mask| mask.mode.is_some_and(ModeKind::is_tui_visible))
.collect()

View File

@@ -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<ModelPreset>,
collaboration_modes: Vec<CollaborationModeMask>,
}
impl ModelCatalog {
pub(crate) fn new(models: Vec<ModelPreset>) -> Self {
Self { models }
pub(crate) fn new(
models: Vec<ModelPreset>,
collaboration_modes: Vec<CollaborationModeMask>,
) -> Self {
Self {
models,
collaboration_modes,
}
}
pub(crate) fn try_list_models(&self) -> Result<Vec<ModelPreset>, Infallible> {
Ok(self.models.clone())
}
pub(crate) fn list_collaboration_modes(&self) -> Vec<CollaborationModeMask> {
self.collaboration_modes.clone()
}
}