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
3 changes: 1 addition & 2 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -746,13 +746,12 @@ impl MessageProcessor {
self.outgoing.send_error(connection_request_id, error).await;
return;
}
let connection_id = connection_request_id.connection_id;
if self.config.features.enabled(Feature::GeneralAnalytics)
&& let ClientRequest::TurnStart { request_id, .. }
| ClientRequest::TurnSteer { request_id, .. } = &codex_request
{
self.analytics_events_client.track_request(
connection_id.0,
connection_request_id.connection_id.0,
request_id.clone(),
codex_request.clone(),
);
Expand Down
193 changes: 193 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,18 @@ use codex_app_server_client::TypedRequestError;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
use codex_app_server_protocol::ConfigLayerSource;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::FeedbackUploadParams;
use codex_app_server_protocol::FeedbackUploadResponse;
use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::ListMcpServerStatusParams;
use codex_app_server_protocol::ListMcpServerStatusResponse;
use codex_app_server_protocol::MarketplaceAddParams;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::McpServerStatus;
use codex_app_server_protocol::McpServerStatusDetail;
use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstallResponse;
use codex_app_server_protocol::PluginListParams;
Expand Down Expand Up @@ -1044,6 +1049,9 @@ pub(crate) struct App {
primary_session_configured: Option<ThreadSessionState>,
pending_primary_events: VecDeque<ThreadBufferedEvent>,
pending_app_server_requests: PendingAppServerRequests,
// Serialize plugin enablement writes per plugin so stale completions cannot
// overwrite a newer toggle.
pending_plugin_enabled_writes: HashMap<(PathBuf, String), Option<bool>>,
}

#[derive(Default)]
Expand Down Expand Up @@ -2083,6 +2091,28 @@ impl App {
});
}

fn fetch_marketplace_add(
&mut self,
app_server: &AppServerSession,
cwd: PathBuf,
source: String,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let cwd_for_event = cwd.clone();
let source_for_event = source.clone();
let result = fetch_marketplace_add(request_handle, source)
.await
.map_err(|err| format!("Failed to add marketplace: {err}"));
app_event_tx.send(AppEvent::MarketplaceAddLoaded {
cwd: cwd_for_event,
source: source_for_event,
result,
});
});
}

fn fetch_plugin_detail(
&mut self,
app_server: &AppServerSession,
Expand Down Expand Up @@ -2150,6 +2180,48 @@ impl App {
});
}

fn set_plugin_enabled(
&mut self,
app_server: &AppServerSession,
cwd: PathBuf,
plugin_id: String,
enabled: bool,
) {
let key = (cwd.clone(), plugin_id.clone());
if let Some(queued_enabled) = self.pending_plugin_enabled_writes.get_mut(&key) {
*queued_enabled = Some(enabled);
return;
}

self.pending_plugin_enabled_writes.insert(key, None);
self.spawn_plugin_enabled_write(app_server, cwd, plugin_id, enabled);
}

fn spawn_plugin_enabled_write(
&mut self,
app_server: &AppServerSession,
cwd: PathBuf,
plugin_id: String,
enabled: bool,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let cwd_for_event = cwd.clone();
let plugin_id_for_event = plugin_id.clone();
let result = write_plugin_enabled(request_handle, plugin_id, enabled)
.await
.map(|_| ())
.map_err(|err| format!("Failed to update plugin config: {err}"));
app_event_tx.send(AppEvent::PluginEnabledSet {
cwd: cwd_for_event,
plugin_id: plugin_id_for_event,
enabled,
result,
});
Comment thread
canvrno-oai marked this conversation as resolved.
});
}

fn refresh_plugin_mentions(&mut self) {
let config = self.config.clone();
let app_event_tx = self.app_event_tx.clone();
Expand Down Expand Up @@ -4012,6 +4084,7 @@ impl App {
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_plugin_enabled_writes: HashMap::new(),
};
if let Some(started) = initial_started_thread {
app.enqueue_primary_thread_session(started.session, started.turns)
Expand Down Expand Up @@ -4630,6 +4703,37 @@ impl App {
AppEvent::FetchPluginsList { cwd } => {
self.fetch_plugins_list(app_server, cwd);
}
AppEvent::OpenMarketplaceAddPrompt => {
self.chat_widget.open_marketplace_add_prompt();
}
AppEvent::OpenMarketplaceAddLoading { source } => {
self.chat_widget.open_marketplace_add_loading_popup(&source);
}
AppEvent::FetchMarketplaceAdd { cwd, source } => {
self.fetch_marketplace_add(app_server, cwd, source);
}
AppEvent::MarketplaceAddLoaded {
cwd,
source,
result,
} => {
let add_succeeded = result.is_ok();
if add_succeeded {
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(
error = %err,
"failed to refresh config after marketplace add"
);
}
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
}
self.chat_widget
.on_marketplace_add_loaded(cwd.clone(), source, result);
if add_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() {
self.fetch_plugins_list(app_server, cwd);
}
}
AppEvent::OpenPluginDetailLoading {
plugin_display_name,
} => {
Expand Down Expand Up @@ -4678,6 +4782,13 @@ impl App {
} => {
self.fetch_plugin_uninstall(app_server, cwd, plugin_id, plugin_display_name);
}
AppEvent::SetPluginEnabled {
cwd,
plugin_id,
enabled,
} => {
self.set_plugin_enabled(app_server, cwd, plugin_id, enabled);
}
AppEvent::PluginInstallLoaded {
cwd,
marketplace_path,
Expand Down Expand Up @@ -5231,6 +5342,47 @@ impl App {
self.fetch_plugins_list(app_server, cwd);
}
}
AppEvent::PluginEnabledSet {
cwd,
plugin_id,
enabled,
result,
} => {
let key = (cwd.clone(), plugin_id.clone());
let queued_enabled = self
.pending_plugin_enabled_writes
.get_mut(&key)
.and_then(Option::take);
let should_apply_result = if let Some(queued_enabled) = queued_enabled
&& (result.is_err() || queued_enabled != enabled)
{
self.spawn_plugin_enabled_write(
app_server,
cwd.clone(),
plugin_id.clone(),
queued_enabled,
);
false
} else {
true
};
if should_apply_result {
self.pending_plugin_enabled_writes.remove(&key);
let update_succeeded = result.is_ok();
if update_succeeded {
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(
error = %err,
"failed to refresh config after plugin toggle"
);
}
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
}
self.chat_widget
.on_plugin_enabled_set(cwd, plugin_id, enabled, result);
}
}
AppEvent::RefreshPluginMentions => {
self.refresh_plugin_mentions();
}
Expand Down Expand Up @@ -6429,6 +6581,24 @@ fn hide_cli_only_plugin_marketplaces(response: &mut PluginListResponse) {
.retain(|marketplace| !CLI_HIDDEN_PLUGIN_MARKETPLACES.contains(&marketplace.name.as_str()));
}

async fn fetch_marketplace_add(
request_handle: AppServerRequestHandle,
source: String,
) -> Result<MarketplaceAddResponse> {
let request_id = RequestId::String(format!("marketplace-add-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::MarketplaceAdd {
request_id,
params: MarketplaceAddParams {
source,
ref_name: None,
sparse_paths: None,
},
})
.await
.wrap_err("marketplace/add failed")
}

async fn fetch_plugin_detail(
request_handle: AppServerRequestHandle,
params: PluginReadParams,
Expand Down Expand Up @@ -6476,6 +6646,27 @@ async fn fetch_plugin_uninstall(
.wrap_err("plugin/uninstall failed in TUI")
}

async fn write_plugin_enabled(
request_handle: AppServerRequestHandle,
plugin_id: String,
enabled: bool,
) -> Result<ConfigWriteResponse> {
let request_id = RequestId::String(format!("plugin-enable-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::ConfigValueWrite {
request_id,
params: ConfigValueWriteParams {
key_path: format!("plugins.{plugin_id}"),
value: serde_json::json!({ "enabled": enabled }),
merge_strategy: MergeStrategy::Upsert,
file_path: None,
expected_version: None,
},
})
.await
.wrap_err("config/value/write failed while updating plugin enablement in TUI")
}

fn build_feedback_upload_params(
origin_thread_id: Option<ThreadId>,
rollout_path: Option<PathBuf>,
Expand Down Expand Up @@ -9726,6 +9917,7 @@ guardian_approval = true
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_plugin_enabled_writes: HashMap::new(),
}
}

Expand Down Expand Up @@ -9783,6 +9975,7 @@ guardian_approval = true
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_plugin_enabled_writes: HashMap::new(),
},
rx,
op_rx,
Expand Down
37 changes: 37 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use std::path::PathBuf;

use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::McpServerStatus;
use codex_app_server_protocol::PluginInstallResponse;
use codex_app_server_protocol::PluginListResponse;
Expand Down Expand Up @@ -212,6 +213,27 @@ pub(crate) enum AppEvent {
result: Result<PluginListResponse, String>,
},

/// Open the prompt for adding a marketplace source.
OpenMarketplaceAddPrompt,

/// Replace the plugins popup with a marketplace-add loading state.
OpenMarketplaceAddLoading {
source: String,
},

/// Add a marketplace from the provided source.
FetchMarketplaceAdd {
cwd: PathBuf,
source: String,
},

/// Result of adding a marketplace.
MarketplaceAddLoaded {
cwd: PathBuf,
source: String,
result: Result<MarketplaceAddResponse, String>,
},

/// Replace the plugins popup with a plugin-detail loading state.
OpenPluginDetailLoading {
plugin_display_name: String,
Expand Down Expand Up @@ -271,6 +293,21 @@ pub(crate) enum AppEvent {
result: Result<PluginUninstallResponse, String>,
},

/// Enable or disable an installed plugin.
SetPluginEnabled {
cwd: PathBuf,
plugin_id: String,
enabled: bool,
},

/// Result of enabling or disabling a plugin.
PluginEnabledSet {
cwd: PathBuf,
plugin_id: String,
enabled: bool,
result: Result<(), String>,
},

/// Refresh plugin mention bindings from the current config.
RefreshPluginMentions,

Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/bottom_pane/bottom_pane_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ pub(crate) trait BottomPaneView: Renderable {
None
}

/// Active tab id for tabbed list-based views.
fn active_tab_id(&self) -> Option<&str> {
None
}

/// Handle Ctrl-C while this view is active.
fn on_ctrl_c(&mut self) -> CancellationEvent {
CancellationEvent::NotHandled
Expand Down
Loading
Loading