Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
97 changes: 97 additions & 0 deletions codex-rs/tui/src/app_server_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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.
///
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -276,6 +329,7 @@ impl AppServerSession {
feedback_audience,
has_chatgpt_account,
available_models,
collaboration_modes,
})
}

Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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");
Expand Down
14 changes: 13 additions & 1 deletion codex-rs/tui/src/chatwidget/tests/helpers.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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() -> (
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/tui/src/chatwidget/tests/slash_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 3 additions & 3 deletions codex-rs/tui/src/collaboration_modes.rs
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
16 changes: 14 additions & 2 deletions codex-rs/tui/src/model_catalog.rs
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading