Skip to content
Merged
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
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive"] }
codex-ansi-escape = { workspace = true }
codex-app-server-client = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-backend-client = { workspace = true }
Expand Down
215 changes: 211 additions & 4 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,18 @@ use crate::tui::TuiEvent;
use crate::update_action::UpdateAction;
use crate::version::CODEX_CLI_VERSION;
use codex_ansi_escape::ansi_escape_line;
use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
use codex_app_server_client::InProcessAppServerClient;
use codex_app_server_client::InProcessClientStartArgs;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConfigLayerSource;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::PluginListParams;
use codex_app_server_protocol::PluginListResponse;
use codex_app_server_protocol::PluginReadParams;
use codex_app_server_protocol::PluginReadResponse;
use codex_app_server_protocol::RequestId;
use codex_arg0::Arg0DispatchPaths;
use codex_core::AuthManager;
use codex_core::CodexAuth;
use codex_core::ThreadManager;
Expand All @@ -50,7 +61,9 @@ use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::types::ApprovalsReviewer;
use codex_core::config::types::ModelAvailabilityNuxConfig;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::ConfigLayerStackOrdering;
use codex_core::config_loader::LoaderOverrides;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_core::models_manager::manager::RefreshStrategy;
use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
Expand Down Expand Up @@ -112,6 +125,7 @@ use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::mpsc::unbounded_channel;
use tokio::task::JoinHandle;
use toml::Value as TomlValue;
use uuid::Uuid;

mod agent_navigation;
mod pending_interactive_replay;
Expand Down Expand Up @@ -233,6 +247,114 @@ fn emit_skill_load_warnings(app_event_tx: &AppEventSender, errors: &[SkillErrorI
}
}

fn config_warning_notifications(config: &Config) -> Vec<ConfigWarningNotification> {
config
.startup_warnings
.iter()
.map(|warning| ConfigWarningNotification {
summary: warning.clone(),
details: None,
path: None,
range: None,
})
.collect()
}

async fn start_plugin_request_client(
arg0_paths: Arg0DispatchPaths,
config: Config,
cli_kv_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
) -> Result<InProcessAppServerClient> {
InProcessAppServerClient::start(InProcessClientStartArgs {
arg0_paths,
config_warnings: config_warning_notifications(&config),
config: Arc::new(config),
cli_overrides: cli_kv_overrides,
loader_overrides,
cloud_requirements,
feedback,
session_source: SessionSource::Cli,
enable_codex_api_key_env: false,
client_name: "codex-tui".to_string(),
client_version: env!("CARGO_PKG_VERSION").to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
})
.await
.wrap_err("failed to start embedded app server for plugin request")
}

async fn request_plugins_list(
arg0_paths: Arg0DispatchPaths,
config: Config,
cli_kv_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
cwd: PathBuf,
) -> Result<PluginListResponse> {
let client = start_plugin_request_client(
arg0_paths,
config,
cli_kv_overrides,
loader_overrides,
cloud_requirements,
feedback,
)
.await?;
let request_handle = client.request_handle();
let cwd = AbsolutePathBuf::try_from(cwd).wrap_err("plugin list cwd must be absolute")?;
let request_id = RequestId::String(format!("plugin-list-{}", Uuid::new_v4()));
let response = request_handle
.request_typed(ClientRequest::PluginList {
request_id,
params: PluginListParams {
cwds: Some(vec![cwd]),
force_remote_sync: false,
},
})
.await
.wrap_err("plugin/list failed in legacy TUI");
if let Err(err) = client.shutdown().await {
tracing::warn!(%err, "failed to shut down embedded app server after plugin/list");
}
response
}

async fn request_plugin_detail(
arg0_paths: Arg0DispatchPaths,
config: Config,
cli_kv_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
params: PluginReadParams,
) -> Result<PluginReadResponse> {
let client = start_plugin_request_client(
arg0_paths,
config,
cli_kv_overrides,
loader_overrides,
cloud_requirements,
feedback,
)
.await?;
let request_handle = client.request_handle();
let request_id = RequestId::String(format!("plugin-read-{}", Uuid::new_v4()));
let response = request_handle
.request_typed(ClientRequest::PluginRead { request_id, params })
.await
.wrap_err("plugin/read failed in legacy TUI");
if let Err(err) = client.shutdown().await {
tracing::warn!(%err, "failed to shut down embedded app server after plugin/read");
}
response
}

fn emit_project_config_warnings(app_event_tx: &AppEventSender, config: &Config) {
let mut disabled_folders = Vec::new();

Expand Down Expand Up @@ -706,6 +828,9 @@ pub(crate) struct App {
pub(crate) config: Config,
pub(crate) active_profile: Option<String>,
cli_kv_overrides: Vec<(String, TomlValue)>,
arg0_paths: Arg0DispatchPaths,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
harness_overrides: ConfigOverrides,
runtime_approval_policy_override: Option<AskForApproval>,
runtime_sandbox_policy_override: Option<SandboxPolicy>,
Expand Down Expand Up @@ -1184,6 +1309,62 @@ impl App {
.add_info_message(format!("Opened {url} in your browser."), /*hint*/ None);
}

fn fetch_plugins_list(&mut self, cwd: PathBuf) {
let config = self.config.clone();
let arg0_paths = self.arg0_paths.clone();
let cli_kv_overrides = self.cli_kv_overrides.clone();
let loader_overrides = self.loader_overrides.clone();
let cloud_requirements = self.cloud_requirements.clone();
let feedback = self.feedback.clone();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let cwd_for_event = cwd.clone();
let result = request_plugins_list(
arg0_paths,
config,
cli_kv_overrides,
loader_overrides,
cloud_requirements,
feedback,
cwd,
)
.await
.map_err(|err| format!("Failed to load plugins: {err}"));
app_event_tx.send(AppEvent::PluginsLoaded {
cwd: cwd_for_event,
result,
});
});
}

fn fetch_plugin_detail(&mut self, cwd: PathBuf, params: PluginReadParams) {
let config = self.config.clone();
let arg0_paths = self.arg0_paths.clone();
let cli_kv_overrides = self.cli_kv_overrides.clone();
let loader_overrides = self.loader_overrides.clone();
let cloud_requirements = self.cloud_requirements.clone();
let feedback = self.feedback.clone();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let cwd_for_event = cwd.clone();
let result = request_plugin_detail(
arg0_paths,
config,
cli_kv_overrides,
loader_overrides,
cloud_requirements,
feedback,
params,
)
.await
.map_err(|err| format!("Failed to load plugin details: {err}"));
app_event_tx.send(AppEvent::PluginDetailLoaded {
cwd: cwd_for_event,
result,
});
});
}

fn clear_ui_header_lines_with_version(
&self,
width: u16,
Expand Down Expand Up @@ -2000,6 +2181,9 @@ impl App {
auth_manager: Arc<AuthManager>,
mut config: Config,
cli_kv_overrides: Vec<(String, TomlValue)>,
arg0_paths: Arg0DispatchPaths,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
harness_overrides: ConfigOverrides,
active_profile: Option<String>,
initial_prompt: Option<String>,
Expand Down Expand Up @@ -2029,10 +2213,6 @@ impl App {
.enabled(Feature::DefaultModeRequestUserInput),
},
));
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
thread_manager
.plugins_manager()
.maybe_start_curated_repo_sync_for_config(&config, auth_manager.clone());
let mut model = thread_manager
.get_models_manager()
.get_default_model(&config.model, RefreshStrategy::Offline)
Expand Down Expand Up @@ -2227,6 +2407,9 @@ impl App {
config,
active_profile,
cli_kv_overrides,
arg0_paths,
loader_overrides,
cloud_requirements,
harness_overrides,
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
Expand Down Expand Up @@ -2770,6 +2953,15 @@ impl App {
AppEvent::RefreshConnectors { force_refetch } => {
self.chat_widget.refresh_connectors(force_refetch);
}
AppEvent::FetchPluginsList { cwd } => {
self.fetch_plugins_list(cwd);
}
AppEvent::OpenPluginDetailLoading {
plugin_display_name,
} => {
self.chat_widget
.open_plugin_detail_loading_popup(&plugin_display_name);
}
AppEvent::StartFileSearch(query) => {
self.file_search.on_user_query(query);
}
Expand All @@ -2782,6 +2974,15 @@ impl App {
AppEvent::ConnectorsLoaded { result, is_final } => {
self.chat_widget.on_connectors_loaded(result, is_final);
}
AppEvent::PluginsLoaded { cwd, result } => {
self.chat_widget.on_plugins_loaded(cwd, result);
}
AppEvent::FetchPluginDetail { cwd, params } => {
self.fetch_plugin_detail(cwd, params);
}
AppEvent::PluginDetailLoaded { cwd, result } => {
self.chat_widget.on_plugin_detail_loaded(cwd, result);
}
AppEvent::UpdateReasoningEffort(effort) => {
self.on_update_reasoning_effort(effort);
self.refresh_status_surfaces();
Expand Down Expand Up @@ -6553,6 +6754,9 @@ guardian_approval = true
config,
active_profile: None,
cli_kv_overrides: Vec::new(),
arg0_paths: Arg0DispatchPaths::default(),
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
harness_overrides: ConfigOverrides::default(),
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
Expand Down Expand Up @@ -6614,6 +6818,9 @@ guardian_approval = true
config,
active_profile: None,
cli_kv_overrides: Vec::new(),
arg0_paths: Arg0DispatchPaths::default(),
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
harness_overrides: ConfigOverrides::default(),
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
Expand Down
31 changes: 31 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

use std::path::PathBuf;

use codex_app_server_protocol::PluginListResponse;
use codex_app_server_protocol::PluginReadParams;
use codex_app_server_protocol::PluginReadResponse;
use codex_chatgpt::connectors::AppInfo;
use codex_file_search::FileMatch;
use codex_protocol::ThreadId;
Expand Down Expand Up @@ -162,6 +165,34 @@ pub(crate) enum AppEvent {
force_refetch: bool,
},

/// Fetch plugin marketplace state for the provided working directory.
FetchPluginsList {
cwd: PathBuf,
},

/// Result of fetching plugin marketplace state.
PluginsLoaded {
cwd: PathBuf,
result: Result<PluginListResponse, String>,
},

/// Replace the plugins popup with a plugin-detail loading state.
OpenPluginDetailLoading {
plugin_display_name: String,
},

/// Fetch detail for a specific plugin from a marketplace.
FetchPluginDetail {
cwd: PathBuf,
params: PluginReadParams,
},

/// Result of fetching plugin detail.
PluginDetailLoaded {
cwd: PathBuf,
result: Result<PluginReadResponse, String>,
},

InsertHistoryCell(Box<dyn HistoryCell>),

/// Apply rollback semantics to local transcript cells.
Expand Down
Loading
Loading