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/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ use codex_core::config::ConfigOverrides;
use codex_core::config::NetworkProxyAuditMetadata;
use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::connectors::AccessibleConnectorsStatus;
use codex_core::exec::ExecCapturePolicy;
use codex_core::exec::ExecExpiration;
use codex_core::exec::ExecParams;
Expand Down
55 changes: 44 additions & 11 deletions codex-rs/app-server/src/request_processors/apps_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@ impl AppsRequestProcessor {
request_id: &ConnectionRequestId,
params: AppsListParams,
) -> Result<Option<AppsListResponse>, JSONRPCErrorError> {
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;

if let Some(thread_id) = params.thread_id.as_deref() {
let (_, thread) = self.load_thread(thread_id).await?;
let thread = if let Some(thread_id) = params.thread_id.as_deref() {
let (_, loaded_thread) = self.load_thread(thread_id).await?;
Some(loaded_thread)
} else {
None
};
let fallback_cwd = match thread.as_ref() {
Some(thread) => Some(thread.config_snapshot().await.cwd.to_path_buf()),
None => None,
};
let mut config = self.load_latest_config(fallback_cwd).await?;

if let Some(thread) = thread {
let _ = config
.features
.set_enabled(Feature::Apps, thread.enabled(Feature::Apps));
Expand Down Expand Up @@ -88,16 +96,39 @@ impl AppsRequestProcessor {
config: Config,
environment_manager: Arc<EnvironmentManager>,
) {
let retry_params = params.clone();
let retry_config = config.clone();
let retry_environment_manager = Arc::clone(&environment_manager);
let result = Self::apps_list_response(&outgoing, params, config, environment_manager).await;
outgoing.send_result(request_id, result).await;
let should_retry = result
.as_ref()
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
outgoing
.send_result(request_id, result.map(|(response, _)| response))
.await;

if should_retry && !retry_params.force_refetch {
let mut retry_params = retry_params;
retry_params.force_refetch = true;
if let Err(err) = Self::apps_list_response(
&outgoing,
retry_params,
retry_config,
retry_environment_manager,
)
.await
{
warn!("failed to refresh app list after codex-apps readiness retry: {err:?}");
}
}
}

async fn apps_list_response(
outgoing: &Arc<OutgoingMessageSender>,
params: AppsListParams,
config: Config,
environment_manager: Arc<EnvironmentManager>,
) -> Result<AppsListResponse, JSONRPCErrorError> {
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
let AppsListParams {
cursor,
limit,
Expand Down Expand Up @@ -130,7 +161,6 @@ impl AppsRequestProcessor {
&environment_manager,
)
.await
.map(|status| status.connectors)
.map_err(|err| format!("failed to load accessible apps: {err}"));
let _ = accessible_tx.send(AppListLoadResult::Accessible(result));
});
Expand All @@ -146,6 +176,7 @@ impl AppsRequestProcessor {
let app_list_deadline = tokio::time::Instant::now() + APP_LIST_LOAD_TIMEOUT;
let mut accessible_loaded = false;
let mut all_loaded = false;
let mut codex_apps_ready = true;
let mut last_notified_apps = None;

if accessible_connectors.is_some() || all_connectors.is_some() {
Expand Down Expand Up @@ -178,9 +209,10 @@ impl AppsRequestProcessor {
};

match result {
AppListLoadResult::Accessible(Ok(connectors)) => {
accessible_connectors = Some(connectors);
AppListLoadResult::Accessible(Ok(status)) => {
accessible_connectors = Some(status.connectors);
accessible_loaded = true;
codex_apps_ready = status.codex_apps_ready;
}
AppListLoadResult::Accessible(Err(err)) => {
return Err(internal_error(err));
Expand Down Expand Up @@ -222,7 +254,8 @@ impl AppsRequestProcessor {
}

if accessible_loaded && all_loaded {
return paginate_apps(merged.as_slice(), start, limit);
let response = paginate_apps(merged.as_slice(), start, limit)?;
return Ok((response, codex_apps_ready));
}
}
}
Expand Down Expand Up @@ -279,7 +312,7 @@ impl AppsRequestProcessor {
const APP_LIST_LOAD_TIMEOUT: Duration = Duration::from_secs(90);

enum AppListLoadResult {
Accessible(Result<Vec<AppInfo>, String>),
Accessible(Result<AccessibleConnectorsStatus, String>),
Directory(Result<Vec<AppInfo>, String>),
}

Expand Down
4 changes: 0 additions & 4 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,6 @@ impl App {
) -> crate::chatwidget::ChatWidgetInit {
crate::chatwidget::ChatWidgetInit {
config: cfg,
environment_manager: self.environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
workspace_command_runner: self.workspace_command_runner.clone(),
Expand Down Expand Up @@ -781,7 +780,6 @@ impl App {
.await;
let init = crate::chatwidget::ChatWidgetInit {
config: config.clone(),
environment_manager: environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: app_event_tx.clone(),
workspace_command_runner: Some(workspace_command_runner.clone()),
Expand Down Expand Up @@ -818,7 +816,6 @@ impl App {
})?;
let init = crate::chatwidget::ChatWidgetInit {
config: config.clone(),
environment_manager: environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: app_event_tx.clone(),
workspace_command_runner: Some(workspace_command_runner.clone()),
Expand Down Expand Up @@ -860,7 +857,6 @@ impl App {
})?;
let init = crate::chatwidget::ChatWidgetInit {
config: config.clone(),
environment_manager: environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: app_event_tx.clone(),
workspace_command_runner: Some(workspace_command_runner.clone()),
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/tui/src/app/app_server_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::app_server_event_targets::server_notification_thread_target;
use super::app_server_event_targets::server_request_thread_id;
use crate::app_command::AppCommand;
use crate::app_event::AppEvent;
use crate::app_event::ConnectorsSnapshot;
use crate::app_server_session::AppServerSession;
use crate::app_server_session::status_account_display_from_auth_mode;
use codex_app_server_client::AppServerEvent;
Expand Down Expand Up @@ -106,6 +107,15 @@ impl App {
self.fetch_plugins_list(app_server_client, cwd);
return;
}
ServerNotification::AppListUpdated(notification) => {
self.chat_widget.on_connectors_loaded(
Ok(ConnectorsSnapshot {
connectors: notification.data.clone(),
}),
/*is_final*/ false,
);
return;
}
_ => {}
}

Expand Down
47 changes: 47 additions & 0 deletions codex-rs/tui/src/app/background_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use super::plugin_mentions::fetch_plugin_mentions;
use super::*;
use crate::app_event::ConnectorsSnapshot;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::MarketplaceAddParams;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::MarketplaceRemoveParams;
Expand Down Expand Up @@ -92,6 +95,27 @@ impl App {
});
}

pub(super) fn fetch_connectors_list(
&mut self,
app_server: &AppServerSession,
force_refetch: bool,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
let thread_id = self
.current_displayed_thread_id()
.map(|thread_id| thread_id.to_string());
tokio::spawn(async move {
let result = fetch_connectors_list(request_handle, force_refetch, thread_id)
.await
.map_err(|err| err.to_string());
app_event_tx.send(AppEvent::ConnectorsLoaded {
result,
is_final: true,
});
});
}

pub(super) fn fetch_plugins_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
Expand Down Expand Up @@ -634,6 +658,29 @@ pub(super) async fn fetch_skills_list(
.wrap_err("skills/list failed in TUI")
}

pub(super) async fn fetch_connectors_list(
request_handle: AppServerRequestHandle,
force_refetch: bool,
thread_id: Option<String>,
) -> Result<ConnectorsSnapshot> {
let request_id = RequestId::String(format!("apps-list-{}", Uuid::new_v4()));
let response: AppsListResponse = request_handle
.request_typed(ClientRequest::AppsList {
Comment thread
etraut-openai marked this conversation as resolved.
request_id,
params: AppsListParams {
cursor: None,
limit: None,
thread_id,
force_refetch,
},
})
.await
.wrap_err("app/list failed in TUI")?;
Ok(ConnectorsSnapshot {
connectors: response.data,
})
}

pub(super) async fn fetch_plugins_list(
request_handle: AppServerRequestHandle,
cwd: PathBuf,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/config_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ mod tests {
use super::*;
use crate::app::test_support::app_enabled_in_effective_config;
use crate::app::test_support::make_test_app;
use crate::legacy_core::config::edit::ConfigEdit;
use crate::test_support::PathBufExt;
use codex_protocol::models::PermissionProfile;
use pretty_assertions::assert_eq;
Expand Down
66 changes: 30 additions & 36 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ impl App {
AppEvent::RefreshConnectors { force_refetch } => {
self.chat_widget.refresh_connectors(force_refetch);
}
AppEvent::FetchConnectorsList { force_refetch } => {
self.fetch_connectors_list(app_server, force_refetch);
}
AppEvent::PluginInstallAuthAdvance { refresh_connectors } => {
if refresh_connectors {
self.chat_widget.refresh_connectors(/*force_refetch*/ true);
Expand Down Expand Up @@ -1662,18 +1665,18 @@ impl App {
self.chat_widget.open_manage_skills_popup();
}
AppEvent::SetSkillEnabled { path, enabled } => {
let edits = [ConfigEdit::SetSkillConfig {
path: path.to_path_buf(),
match crate::config_update::write_skill_enabled(
app_server.request_handle(),
path.clone(),
enabled,
}];
match ConfigEditsBuilder::for_config(&self.config)
.with_edits(edits)
.apply()
.await
)
.await
{
Ok(()) => {
self.chat_widget.update_skill_enabled(path, enabled);
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
if !app_server.is_remote()
&& let Err(err) = self.refresh_in_memory_config_from_disk().await
{
tracing::warn!(
error = %err,
"failed to refresh config after skill toggle"
Expand All @@ -1691,44 +1694,35 @@ impl App {
AppEvent::SetAppEnabled { id, enabled } => {
let edits = if enabled {
vec![
ConfigEdit::ClearPath {
segments: vec!["apps".to_string(), id.clone(), "enabled".to_string()],
},
ConfigEdit::ClearPath {
segments: vec![
"apps".to_string(),
id.clone(),
"disabled_reason".to_string(),
],
},
crate::config_update::clear_config_value(
crate::config_update::app_scoped_key_path(&id, "enabled"),
),
crate::config_update::clear_config_value(
crate::config_update::app_scoped_key_path(&id, "disabled_reason"),
),
]
} else {
vec![
ConfigEdit::SetPath {
segments: vec!["apps".to_string(), id.clone(), "enabled".to_string()],
value: false.into(),
},
ConfigEdit::SetPath {
segments: vec![
"apps".to_string(),
id.clone(),
"disabled_reason".to_string(),
],
value: "user".into(),
},
crate::config_update::replace_config_value(
crate::config_update::app_scoped_key_path(&id, "enabled"),
serde_json::json!(false),
),
crate::config_update::replace_config_value(
crate::config_update::app_scoped_key_path(&id, "disabled_reason"),
serde_json::json!("user"),
),
]
};
match ConfigEditsBuilder::for_config(&self.config)
.with_edits(edits)
.apply()
match crate::config_update::write_config_batch(app_server.request_handle(), edits)
.await
{
Ok(()) => {
Ok(_) => {
self.chat_widget.update_connector_enabled(&id, enabled);
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
if !app_server.is_remote()
&& let Err(err) = self.refresh_in_memory_config_from_disk().await
{
tracing::warn!(error = %err, "failed to refresh config after app toggle");
}
self.chat_widget.submit_op(AppCommand::reload_user_config());
}
Err(err) => {
self.chat_widget.add_error_message(format!(
Expand Down
2 changes: 0 additions & 2 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_subm
let model = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
app.chat_widget = ChatWidget::new_with_app_event(ChatWidgetInit {
config,
environment_manager: app.environment_manager.clone(),
frame_requester: crate::tui::FrameRequester::test_dummy(),
app_event_tx: app.app_event_tx.clone(),
workspace_command_runner: None,
Expand Down Expand Up @@ -4842,7 +4841,6 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() {

let replacement = ChatWidget::new_with_app_event(ChatWidgetInit {
config: app.config.clone(),
environment_manager: app.environment_manager.clone(),
frame_requester: crate::tui::FrameRequester::test_dummy(),
app_event_tx: app.app_event_tx.clone(),
workspace_command_runner: None,
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ pub(crate) enum AppEvent {
force_refetch: bool,
},

/// Fetch app connector state from the app server after the widget accepts a refresh request.
FetchConnectorsList {
force_refetch: bool,
},

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