Codex Handbook
tui/src/chatwidget/plugins.rs 2377 lines
use std::path::Path;use std::path::PathBuf;use std::time::Duration;use std::time::Instant;use super::ChatWidget;use crate::app_event::AppEvent;use crate::app_event::PluginLocation;use crate::app_event::PluginRemoteSectionError;use crate::bottom_pane::ColumnWidthMode;use crate::bottom_pane::SelectionAction;use crate::bottom_pane::SelectionItem;use crate::bottom_pane::SelectionRowDisplay;use crate::bottom_pane::SelectionTab;use crate::bottom_pane::SelectionToggle;use crate::bottom_pane::SelectionViewParams;use crate::bottom_pane::custom_prompt_view::CustomPromptView;use crate::history_cell;use crate::key_hint;use crate::legacy_core::config::Config;use crate::motion::MotionMode;use crate::motion::shimmer_text;use crate::onboarding::mark_url_hyperlink;use crate::render::renderable::ColumnRenderable;use crate::render::renderable::Renderable;use crate::tui::FrameRequester;use codex_app_server_protocol::MarketplaceAddResponse;use codex_app_server_protocol::MarketplaceRemoveResponse;use codex_app_server_protocol::MarketplaceUpgradeResponse;use codex_app_server_protocol::PluginAvailability;use codex_app_server_protocol::PluginDetail;use codex_app_server_protocol::PluginInstallPolicy;use codex_app_server_protocol::PluginInstallResponse;use codex_app_server_protocol::PluginListResponse;use codex_app_server_protocol::PluginMarketplaceEntry;use codex_app_server_protocol::PluginReadResponse;use codex_app_server_protocol::PluginSource;use codex_app_server_protocol::PluginSummary;use codex_app_server_protocol::PluginUninstallResponse;use codex_core_plugins::is_openai_curated_marketplace_name;use codex_core_plugins::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME;use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;use codex_features::Feature;use crossterm::event::KeyCode;use crossterm::event::KeyEvent;use crossterm::event::KeyEventKind;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use ratatui::prelude::Widget;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::text::Span;use ratatui::widgets::Paragraph;use ratatui::widgets::WidgetRef;use ratatui::widgets::Wrap;use unicode_width::UnicodeWidthStr;const PLUGINS_SELECTION_VIEW_ID: &str = "plugins-selection";const ALL_PLUGINS_TAB_ID: &str = "all-plugins";const INSTALLED_PLUGINS_TAB_ID: &str = "installed-plugins";const MARKETPLACE_TAB_ID_PREFIX: &str = "marketplace:";const OPENAI_CURATED_TAB_ID: &str = "marketplace:openai-curated";const ADD_MARKETPLACE_TAB_ID: &str = "add-marketplace";const PLUGIN_ROW_PREFIX_WIDTH: usize = 6;const LOADING_ANIMATION_DELAY: Duration = Duration::from_secs(1);const LOADING_ANIMATION_INTERVAL: Duration = Duration::from_millis(100);#[derive(Debug, Clone, Default)]pub(super) struct PluginListFetchState {    pub(super) cache_cwd: Option<PathBuf>,    pub(super) in_flight_cwd: Option<PathBuf>,}#[derive(Debug, Clone)]pub(super) struct PluginInstallAuthFlowState {    plugin_display_name: String,    next_app_index: usize,}struct DelayedLoadingHeader {    started_at: Instant,    frame_requester: FrameRequester,    animations_enabled: bool,    loading_text: String,    note: Option<String>,}impl DelayedLoadingHeader {    fn new(        frame_requester: FrameRequester,        animations_enabled: bool,        loading_text: String,        note: Option<String>,    ) -> Self {        Self {            started_at: Instant::now(),            frame_requester,            animations_enabled,            loading_text,            note,        }    }}impl Renderable for DelayedLoadingHeader {    fn render(&self, area: Rect, buf: &mut Buffer) {        if area.is_empty() {            return;        }        let mut lines = Vec::with_capacity(3);        lines.push(Line::from("Plugins".bold()));        let now = Instant::now();        let elapsed = now.saturating_duration_since(self.started_at);        if elapsed < LOADING_ANIMATION_DELAY {            self.frame_requester                .schedule_frame_in(LOADING_ANIMATION_DELAY - elapsed);            lines.push(Line::from(self.loading_text.as_str().dim()));        } else if self.animations_enabled {            self.frame_requester                .schedule_frame_in(LOADING_ANIMATION_INTERVAL);            lines.push(Line::from(shimmer_text(                self.loading_text.as_str(),                MotionMode::Animated,            )));        } else {            lines.push(Line::from(self.loading_text.as_str().dim()));        }        if let Some(note) = &self.note {            lines.push(Line::from(note.as_str().dim()));        }        Paragraph::new(lines).render_ref(area, buf);    }    fn desired_height(&self, _width: u16) -> u16 {        2 + u16::from(self.note.is_some())    }}const APPS_HELP_ARTICLE_URL: &str = "https://help.openai.com/en/articles/11487775-apps-in-chatgpt";struct PluginDisclosureLine {    line: Line<'static>,}impl Renderable for PluginDisclosureLine {    fn render(&self, area: Rect, buf: &mut Buffer) {        Paragraph::new(self.line.clone())            .wrap(Wrap { trim: false })            .render(area, buf);        mark_url_hyperlink(buf, area, APPS_HELP_ARTICLE_URL);    }    fn desired_height(&self, width: u16) -> u16 {        Paragraph::new(self.line.clone())            .wrap(Wrap { trim: false })            .line_count(width)            .try_into()            .unwrap_or(u16::MAX)    }}#[derive(Debug, Clone, Default)]pub(super) enum PluginsCacheState {    #[default]    Uninitialized,    Loading,    Ready(PluginListResponse),    Failed(String),}impl ChatWidget {    pub(crate) fn add_plugins_output(&mut self) {        if !self.config.features.enabled(Feature::Plugins) {            self.add_info_message(                "Plugins are disabled.".to_string(),                Some("Enable the plugins feature to use /plugins.".to_string()),            );            return;        }        self.plugins_active_tab_id = Some(ALL_PLUGINS_TAB_ID.to_string());        self.prefetch_plugins();        match self.plugins_cache_for_current_cwd() {            PluginsCacheState::Ready(response) => {                self.open_plugins_popup(&response);            }            PluginsCacheState::Failed(err) => {                self.add_to_history(history_cell::new_error_event(err));            }            PluginsCacheState::Loading | PluginsCacheState::Uninitialized => {                self.open_plugins_loading_popup();            }        }        self.request_redraw();    }    pub(crate) fn on_plugins_loaded(        &mut self,        cwd: PathBuf,        result: Result<PluginListResponse, String>,    ) {        let request_was_in_flight =            self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path());        if request_was_in_flight {            self.plugins_fetch_state.in_flight_cwd = None;        }        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        let auth_flow_active = self.plugin_install_auth_flow.is_some();        let should_refresh_plugins_popup = !auth_flow_active            && (self                .bottom_pane                .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)                .is_some()                || self                    .bottom_pane                    .selected_index_for_active_view(PLUGINS_SELECTION_VIEW_ID)                    .is_some()                || !matches!(                    self.plugins_cache_for_current_cwd(),                    PluginsCacheState::Ready(_)                ));        match result {            Ok(response) => {                self.plugins_fetch_state.cache_cwd = Some(cwd);                self.plugin_remote_sections_loading = request_was_in_flight;                if request_was_in_flight {                    self.plugin_remote_sections_loaded = false;                }                self.plugin_remote_section_errors.clear();                let active_tab_id = self                    .plugins_active_tab_id                    .as_deref()                    .and_then(|tab_id| {                        marketplace_tab_id_matching_saved_id(tab_id, &response.marketplaces)                    })                    .or_else(|| self.plugins_active_tab_id.clone());                self.newly_installed_marketplace_tab_id = self                    .newly_installed_marketplace_tab_id                    .as_deref()                    .and_then(|tab_id| {                        marketplace_tab_id_matching_saved_id(tab_id, &response.marketplaces)                    });                self.plugins_active_tab_id = active_tab_id;                self.plugins_cache = PluginsCacheState::Ready(response.clone());                if should_refresh_plugins_popup {                    self.refresh_plugins_popup_if_open(&response);                }                self.newly_installed_marketplace_tab_id = None;            }            Err(err) => {                self.plugin_remote_sections_loading = false;                self.plugin_remote_sections_loaded = false;                if should_refresh_plugins_popup {                    self.plugins_fetch_state.cache_cwd = None;                    self.plugins_cache = PluginsCacheState::Failed(err.clone());                    let _ = self.bottom_pane.replace_selection_view_if_active(                        PLUGINS_SELECTION_VIEW_ID,                        self.plugins_error_popup_params(&err),                    );                }            }        }    }    pub(crate) fn on_plugin_remote_sections_loaded(        &mut self,        cwd: PathBuf,        marketplaces: Vec<PluginMarketplaceEntry>,        section_errors: Vec<PluginRemoteSectionError>,    ) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        let should_refresh_plugins_popup = self            .bottom_pane            .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)            .is_some();        self.plugin_remote_sections_loading = false;        self.plugin_remote_sections_loaded = true;        let refreshed_response = match &mut self.plugins_cache {            PluginsCacheState::Ready(response)                if self.plugins_fetch_state.cache_cwd.as_deref() == Some(cwd.as_path()) =>            {                merge_remote_marketplaces(response, marketplaces);                self.plugin_remote_section_errors = section_errors;                Some(response.clone())            }            _ => {                self.plugin_remote_section_errors = section_errors;                None            }        };        if let Some(response) = refreshed_response            && should_refresh_plugins_popup        {            self.refresh_plugins_popup_if_open(&response);        }    }    fn prefetch_plugins(&mut self) {        let cwd = self.config.cwd.to_path_buf();        if self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path()) {            return;        }        self.on_plugins_list_fetch_started(cwd.clone());        self.app_event_tx.send(AppEvent::FetchPluginsList { cwd });    }    pub(crate) fn on_plugins_list_fetch_started(&mut self, cwd: PathBuf) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        self.plugins_fetch_state.in_flight_cwd = Some(cwd.clone());        if self.plugins_fetch_state.cache_cwd.as_deref() != Some(cwd.as_path()) {            self.plugins_cache = PluginsCacheState::Loading;        }    }    fn plugins_cache_for_current_cwd(&self) -> PluginsCacheState {        if self.plugins_fetch_state.cache_cwd.as_deref() == Some(self.config.cwd.as_path()) {            self.plugins_cache.clone()        } else {            PluginsCacheState::Uninitialized        }    }    fn open_plugins_loading_popup(&mut self) {        if !self.bottom_pane.replace_selection_view_if_active(            PLUGINS_SELECTION_VIEW_ID,            self.plugins_loading_popup_params(),        ) {            self.bottom_pane                .show_selection_view(self.plugins_loading_popup_params());        }    }    fn open_plugins_popup(&mut self, response: &PluginListResponse) {        self.plugins_active_tab_id = Some(ALL_PLUGINS_TAB_ID.to_string());        self.bottom_pane            .show_selection_view(self.plugins_popup_params(                response,                self.plugins_active_tab_id.clone(),                /*initial_selected_idx*/ None,            ));    }    pub(crate) fn open_marketplace_add_prompt(&mut self) {        self.plugins_active_tab_id = Some(ADD_MARKETPLACE_TAB_ID.to_string());        let tx = self.app_event_tx.clone();        let cwd = self.config.cwd.to_path_buf();        let view = CustomPromptView::new(            "Add marketplace".to_string(),            "owner/repo, git URL, or local marketplace path".to_string(),            String::new(),            Some("Examples: owner/repo, git URL, ./marketplace".to_string()),            Box::new(move |source: String| {                let source = source.trim().to_string();                if source.is_empty() {                    return;                }                tx.send(AppEvent::OpenMarketplaceAddLoading {                    source: source.clone(),                });                tx.send(AppEvent::FetchMarketplaceAdd {                    cwd: cwd.clone(),                    source,                });            }),        );        self.bottom_pane.show_view(Box::new(view));    }    pub(crate) fn open_marketplace_add_loading_popup(&mut self, _source: &str) {        self.plugins_active_tab_id = Some(ADD_MARKETPLACE_TAB_ID.to_string());        let params = self.marketplace_add_loading_popup_params();        if !self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)        {            self.bottom_pane                .show_selection_view(self.marketplace_add_loading_popup_params());        }    }    pub(crate) fn open_marketplace_upgrade_loading_popup(        &mut self,        marketplace_name: Option<&str>,    ) {        self.plugins_active_tab_id = self            .bottom_pane            .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)            .map(str::to_string)            .or_else(|| self.plugins_active_tab_id.clone());        let params = self.marketplace_upgrade_loading_popup_params(marketplace_name);        if !self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)        {            self.bottom_pane.show_selection_view(                self.marketplace_upgrade_loading_popup_params(marketplace_name),            );        }    }    pub(crate) fn open_marketplace_remove_confirmation(        &mut self,        marketplace_name: String,        marketplace_display_name: String,    ) {        self.plugins_active_tab_id = self            .bottom_pane            .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)            .map(str::to_string)            .or_else(|| self.plugins_active_tab_id.clone());        let PluginsCacheState::Ready(plugins_response) = self.plugins_cache_for_current_cwd()        else {            return;        };        let params = self.marketplace_remove_confirmation_popup_params(            &plugins_response,            marketplace_name.clone(),            marketplace_display_name.clone(),        );        if !self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)        {            self.bottom_pane.show_selection_view(                self.marketplace_remove_confirmation_popup_params(                    &plugins_response,                    marketplace_name,                    marketplace_display_name,                ),            );        }    }    pub(crate) fn open_marketplace_remove_loading_popup(&mut self, marketplace_display_name: &str) {        let params = self.marketplace_remove_loading_popup_params(marketplace_display_name);        if !self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)        {            self.bottom_pane.show_selection_view(                self.marketplace_remove_loading_popup_params(marketplace_display_name),            );        }    }    pub(crate) fn open_plugin_detail_loading_popup(&mut self, plugin_display_name: &str) {        self.plugins_active_tab_id = self            .bottom_pane            .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)            .map(str::to_string)            .or_else(|| self.plugins_active_tab_id.clone());        let params = self.plugin_detail_loading_popup_params(plugin_display_name);        let _ = self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params);    }    pub(crate) fn open_plugin_install_loading_popup(&mut self, plugin_display_name: &str) {        let params = self.plugin_install_loading_popup_params(plugin_display_name);        let _ = self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params);    }    pub(crate) fn open_plugin_uninstall_loading_popup(&mut self, plugin_display_name: &str) {        let params = self.plugin_uninstall_loading_popup_params(plugin_display_name);        let _ = self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params);    }    pub(crate) fn on_plugin_detail_loaded(        &mut self,        cwd: PathBuf,        result: Result<PluginReadResponse, String>,    ) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        let plugins_response = match self.plugins_cache_for_current_cwd() {            PluginsCacheState::Ready(response) => Some(response),            _ => None,        };        match result {            Ok(response) => {                if let Some(plugins_response) = plugins_response {                    let _ = self.bottom_pane.replace_selection_view_if_active(                        PLUGINS_SELECTION_VIEW_ID,                        self.plugin_detail_popup_params(&plugins_response, &response.plugin),                    );                }            }            Err(err) => {                let _ = self.bottom_pane.replace_selection_view_if_active(                    PLUGINS_SELECTION_VIEW_ID,                    self.plugin_detail_error_popup_params(&err, plugins_response.as_ref()),                );            }        }    }    pub(crate) fn on_plugin_install_loaded(        &mut self,        cwd: PathBuf,        _location: PluginLocation,        _plugin_name: String,        plugin_display_name: String,        result: Result<PluginInstallResponse, String>,    ) -> bool {        if self.config.cwd.as_path() != cwd.as_path() {            return true;        }        match result {            Ok(response) => {                self.plugin_install_apps_needing_auth = response.apps_needing_auth;                self.plugin_install_auth_flow = None;                if self.plugin_install_apps_needing_auth.is_empty() {                    self.add_info_message(                        format!("Installed {plugin_display_name} plugin."),                        Some("No additional app authentication is required.".to_string()),                    );                    true                } else {                    let app_names = self                        .plugin_install_apps_needing_auth                        .iter()                        .map(|app| app.name.as_str())                        .collect::<Vec<_>>()                        .join(", ");                    self.add_info_message(                        format!("Installed {plugin_display_name} plugin."),                        Some(format!(                            "{} app(s) still need authentication: {app_names}",                            self.plugin_install_apps_needing_auth.len()                        )),                    );                    self.plugin_install_auth_flow = Some(PluginInstallAuthFlowState {                        plugin_display_name,                        next_app_index: 0,                    });                    self.open_plugin_install_auth_popup();                    false                }            }            Err(err) => {                self.plugin_install_apps_needing_auth.clear();                self.plugin_install_auth_flow = None;                let plugins_response = match self.plugins_cache_for_current_cwd() {                    PluginsCacheState::Ready(response) => Some(response),                    _ => None,                };                let _ = self.bottom_pane.replace_selection_view_if_active(                    PLUGINS_SELECTION_VIEW_ID,                    self.plugin_detail_error_popup_params(&err, plugins_response.as_ref()),                );                true            }        }    }    pub(crate) fn on_marketplace_add_loaded(        &mut self,        cwd: PathBuf,        _source: String,        result: Result<MarketplaceAddResponse, String>,    ) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        match result {            Ok(response) => {                let marketplace_tab_id = marketplace_tab_id_from_path(&response.installed_root);                self.plugins_active_tab_id = Some(marketplace_tab_id.clone());                self.newly_installed_marketplace_tab_id =                    (!response.already_added).then_some(marketplace_tab_id);                let message = if response.already_added {                    format!(                        "Marketplace {} is already added.",                        response.marketplace_name                    )                } else {                    format!("Added marketplace {}.", response.marketplace_name)                };                self.add_info_message(                    message,                    Some(format!(                        "Marketplace root: {}",                        response.installed_root.as_path().display()                    )),                );            }            Err(_) => {                self.plugins_active_tab_id = Some(ADD_MARKETPLACE_TAB_ID.to_string());                let params = self.marketplace_add_error_popup_params();                if !self                    .bottom_pane                    .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)                {                    self.bottom_pane                        .show_selection_view(self.marketplace_add_error_popup_params());                }            }        }    }    pub(crate) fn on_marketplace_remove_loaded(        &mut self,        cwd: PathBuf,        marketplace_name: String,        marketplace_display_name: String,        result: Result<MarketplaceRemoveResponse, String>,    ) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        match result {            Ok(response) => {                self.plugins_active_tab_id = Some(ALL_PLUGINS_TAB_ID.to_string());                self.add_info_message(                    format!("Removed marketplace {marketplace_display_name}."),                    Some(match response.installed_root {                        Some(installed_root) => {                            format!("Marketplace root: {}", installed_root.as_path().display())                        }                        None => format!(                            "Removed marketplace config for {}.",                            response.marketplace_name                        ),                    }),                );            }            Err(_) => {                let params = self.marketplace_remove_error_popup_params(                    &marketplace_name,                    &marketplace_display_name,                );                if !self                    .bottom_pane                    .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)                {                    self.bottom_pane.show_selection_view(                        self.marketplace_remove_error_popup_params(                            &marketplace_name,                            &marketplace_display_name,                        ),                    );                }            }        }    }    pub(crate) fn on_marketplace_upgrade_loaded(        &mut self,        cwd: PathBuf,        result: Result<MarketplaceUpgradeResponse, String>,    ) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        match result {            Ok(response) => {                if response.upgraded_roots.len() == 1 {                    self.plugins_active_tab_id =                        Some(marketplace_tab_id_from_path(&response.upgraded_roots[0]));                }                let selected_count = response.selected_marketplaces.len();                let upgraded_count = response.upgraded_roots.len();                let error_count = response.errors.len();                if selected_count == 0 {                    self.add_info_message(                        "No configured Git marketplaces to upgrade.".to_string(),                        Some("Only configured Git marketplaces can be upgraded.".to_string()),                    );                    return;                }                if upgraded_count == 0 && error_count == 0 {                    let message = if selected_count == 1 {                        format!(                            "Marketplace {} is already up to date.",                            response.selected_marketplaces[0]                        )                    } else {                        format!(                            "Checked {selected_count} marketplaces; all are already up to date."                        )                    };                    self.add_info_message(                        message,                        Some(format!(                            "Checked: {}",                            response.selected_marketplaces.join(", ")                        )),                    );                    return;                }                if upgraded_count > 0 {                    let noun = if upgraded_count == 1 {                        "marketplace"                    } else {                        "marketplaces"                    };                    self.add_info_message(                        format!("Upgraded {upgraded_count} {noun}."),                        Some(format!(                            "Updated roots: {}",                            response                                .upgraded_roots                                .iter()                                .map(|root| root.as_path().display().to_string())                                .collect::<Vec<_>>()                                .join(", ")                        )),                    );                }                if error_count > 0 {                    let noun = if error_count == 1 {                        "marketplace"                    } else {                        "marketplaces"                    };                    self.add_error_message(format!(                        "Failed to upgrade {error_count} {noun}: {}",                        response                            .errors                            .iter()                            .map(|err| format!("{}: {}", err.marketplace_name, err.message))                            .collect::<Vec<_>>()                            .join("; ")                    ));                }            }            Err(err) => {                self.add_error_message(err);            }        }    }    pub(crate) fn handle_plugins_popup_key_event(&mut self, key_event: KeyEvent) -> bool {        let remove_marketplace = key_hint::ctrl(KeyCode::Char('r')).is_press(key_event);        let upgrade_marketplace = key_hint::ctrl(KeyCode::Char('u')).is_press(key_event);        if !remove_marketplace && !upgrade_marketplace {            return false;        }        let Some(active_tab_id) = self            .bottom_pane            .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)        else {            return false;        };        let PluginsCacheState::Ready(plugins_response) = self.plugins_cache_for_current_cwd()        else {            return false;        };        let Some(marketplace) = plugins_response.marketplaces.iter().find(|marketplace| {            marketplace_tab_id(marketplace) == active_tab_id                && marketplace_is_user_configured(&self.config, &marketplace.name)        }) else {            return false;        };        if remove_marketplace {            self.open_marketplace_remove_confirmation(                marketplace.name.clone(),                marketplace_display_name(marketplace),            );            return true;        }        if marketplace.path.is_none()            || !marketplace_is_user_configured_git(&self.config, &marketplace.name)        {            return false;        }        if key_event.kind != KeyEventKind::Press {            return true;        }        let cwd = self.config.cwd.to_path_buf();        let marketplace_name = Some(marketplace.name.clone());        self.open_marketplace_upgrade_loading_popup(marketplace_name.as_deref());        self.app_event_tx            .send(AppEvent::OpenMarketplaceUpgradeLoading {                marketplace_name: marketplace_name.clone(),            });        self.app_event_tx.send(AppEvent::FetchMarketplaceUpgrade {            cwd,            marketplace_name,        });        true    }    pub(crate) fn on_plugin_enabled_set(        &mut self,        cwd: PathBuf,        plugin_id: String,        enabled: bool,        result: Result<(), String>,    ) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        if let Err(err) = result {            self.add_error_message(format!(                "Failed to update plugin config for {plugin_id}: {err}"            ));            if let PluginsCacheState::Ready(response) = self.plugins_cache_for_current_cwd() {                self.refresh_plugins_popup_if_open(&response);            }            return;        }        let refreshed_response = match &mut self.plugins_cache {            PluginsCacheState::Ready(response)                if self.plugins_fetch_state.cache_cwd.as_deref() == Some(cwd.as_path()) =>            {                for plugin in response                    .marketplaces                    .iter_mut()                    .flat_map(|marketplace| marketplace.plugins.iter_mut())                    .filter(|plugin| plugin.id == plugin_id)                {                    plugin.enabled = enabled;                }                Some(response.clone())            }            _ => None,        };        if let Some(response) = refreshed_response {            self.refresh_plugins_popup_if_open(&response);        }    }    pub(crate) fn on_plugin_uninstall_loaded(        &mut self,        cwd: PathBuf,        plugin_display_name: String,        result: Result<PluginUninstallResponse, String>,    ) {        if self.config.cwd.as_path() != cwd.as_path() {            return;        }        match result {            Ok(_response) => {                self.plugin_install_apps_needing_auth.clear();                self.plugin_install_auth_flow = None;                self.add_info_message(                    format!("Uninstalled {plugin_display_name} plugin."),                    Some("Bundled apps remain installed.".to_string()),                );            }            Err(err) => {                let plugins_response = match self.plugins_cache_for_current_cwd() {                    PluginsCacheState::Ready(response) => Some(response),                    _ => None,                };                let _ = self.bottom_pane.replace_selection_view_if_active(                    PLUGINS_SELECTION_VIEW_ID,                    self.plugin_detail_error_popup_params(&err, plugins_response.as_ref()),                );            }        }    }    pub(crate) fn advance_plugin_install_auth_flow(&mut self) {        let should_finish = {            let Some(flow) = self.plugin_install_auth_flow.as_mut() else {                return;            };            flow.next_app_index += 1;            flow.next_app_index >= self.plugin_install_apps_needing_auth.len()        };        if should_finish {            self.finish_plugin_install_auth_flow(/*abandoned*/ false);            return;        }        self.open_plugin_install_auth_popup();    }    pub(crate) fn abandon_plugin_install_auth_flow(&mut self) {        self.finish_plugin_install_auth_flow(/*abandoned*/ true);    }    fn open_plugin_install_auth_popup(&mut self) {        let Some(params) = self.plugin_install_auth_popup_params() else {            self.finish_plugin_install_auth_flow(/*abandoned*/ false);            return;        };        if !self            .bottom_pane            .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)            && let Some(params) = self.plugin_install_auth_popup_params()        {            self.bottom_pane.show_selection_view(params);        }    }    fn plugin_install_auth_popup_params(&self) -> Option<SelectionViewParams> {        let flow = self.plugin_install_auth_flow.as_ref()?;        let app = self            .plugin_install_apps_needing_auth            .get(flow.next_app_index)?;        let total = self.plugin_install_apps_needing_auth.len();        let current = flow.next_app_index + 1;        let is_installed = self.plugin_install_auth_app_is_installed(app.id.as_str());        let status_label = if is_installed {            "Already installed in this session."        } else {            "Install the required Apps in ChatGPT to continue:"        };        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from(            format!("{} plugin installed.", flow.plugin_display_name).bold(),        ));        header.push(Line::from(            format!("App setup {current}/{total}: {}", app.name).dim(),        ));        header.push(Line::from(status_label.dim()));        let mut items = Vec::new();        if let Some(install_url) = app.install_url.clone() {            let install_label = if is_installed {                "Manage on ChatGPT"            } else {                "Install on ChatGPT"            };            items.push(SelectionItem {                name: install_label.to_string(),                description: Some("Open the ChatGPT app management page".to_string()),                selected_description: Some("Open the app page in your browser.".to_string()),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::OpenUrlInBrowser {                        url: install_url.clone(),                    });                })],                ..Default::default()            });        } else {            items.push(SelectionItem {                name: "ChatGPT apps link unavailable".to_string(),                description: Some("This app did not provide an install/manage URL.".to_string()),                is_disabled: true,                ..Default::default()            });        }        if is_installed {            items.push(SelectionItem {                name: "Continue".to_string(),                description: Some("This app is already installed.".to_string()),                selected_description: Some("Advance to the next app.".to_string()),                actions: vec![Box::new(|tx| {                    tx.send(AppEvent::PluginInstallAuthAdvance {                        refresh_connectors: false,                    });                })],                ..Default::default()            });        } else {            items.push(SelectionItem {                name: "I've installed it".to_string(),                description: Some(                    "Trust your confirmation and continue to the next app.".to_string(),                ),                selected_description: Some(                    "Continue without waiting for refresh to complete.".to_string(),                ),                actions: vec![Box::new(|tx| {                    tx.send(AppEvent::PluginInstallAuthAdvance {                        refresh_connectors: true,                    });                })],                ..Default::default()            });        }        items.push(SelectionItem {            name: "Skip remaining app setup".to_string(),            description: Some("Stop this follow-up flow for this plugin.".to_string()),            selected_description: Some("Abandon remaining required app setup.".to_string()),            actions: vec![Box::new(|tx| {                tx.send(AppEvent::PluginInstallAuthAbandon);            })],            ..Default::default()        });        Some(SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            footer_hint: Some(plugin_detail_hint_line()),            items,            col_width_mode: ColumnWidthMode::AutoAllRows,            ..Default::default()        })    }    fn plugin_install_auth_app_is_installed(&self, app_id: &str) -> bool {        self.connectors_for_mentions().is_some_and(|connectors| {            connectors                .iter()                .any(|connector| connector.id == app_id && connector.is_accessible)        })    }    fn finish_plugin_install_auth_flow(&mut self, abandoned: bool) {        let Some(flow) = self.plugin_install_auth_flow.take() else {            return;        };        self.plugin_install_apps_needing_auth.clear();        if abandoned {            self.add_info_message(                format!(                    "Skipped remaining app setup for {} plugin.",                    flow.plugin_display_name                ),                Some("The plugin may not be usable until required apps are installed.".to_string()),            );        } else {            self.add_info_message(                format!(                    "Completed app setup flow for {} plugin.",                    flow.plugin_display_name                ),                Some("You can now continue managing plugins from /plugins.".to_string()),            );        }        let plugins_response = match self.plugins_cache_for_current_cwd() {            PluginsCacheState::Ready(response) => Some(response),            _ => None,        };        if let Some(plugins_response) = plugins_response {            let tab_id = self.plugins_active_tab_id.clone();            let _ = self.bottom_pane.replace_selection_view_if_active(                PLUGINS_SELECTION_VIEW_ID,                self.plugins_popup_params(                    &plugins_response,                    tab_id,                    /*initial_selected_idx*/ None,                ),            );        }    }    fn refresh_plugins_popup_if_open(&mut self, response: &PluginListResponse) {        let active_tab_id = self            .bottom_pane            .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)            .map(str::to_string)            .or_else(|| self.plugins_active_tab_id.clone());        let selected_idx = self            .bottom_pane            .selected_index_for_active_view(PLUGINS_SELECTION_VIEW_ID);        self.plugins_active_tab_id = active_tab_id.clone();        let _ = self.bottom_pane.replace_selection_view_if_active(            PLUGINS_SELECTION_VIEW_ID,            self.plugins_popup_params(response, active_tab_id, selected_idx),        );    }    fn plugins_loading_popup_params(&self) -> SelectionViewParams {        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(DelayedLoadingHeader::new(                self.frame_requester.clone(),                self.config.animations,                "Loading available plugins...".to_string(),                Some("This updates when the marketplace list is ready.".to_string()),            )),            items: vec![SelectionItem {                name: "Loading plugins...".to_string(),                description: Some("This updates when the marketplace list is ready.".to_string()),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn marketplace_add_loading_popup_params(&self) -> SelectionViewParams {        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(DelayedLoadingHeader::new(                self.frame_requester.clone(),                self.config.animations,                "Adding marketplace...".to_string(),                /*note*/ None,            )),            items: vec![SelectionItem {                name: "Adding marketplace...".to_string(),                description: Some(                    "This updates when marketplace installation completes.".to_string(),                ),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn marketplace_remove_confirmation_popup_params(        &self,        plugins_response: &PluginListResponse,        marketplace_name: String,        marketplace_display_name: String,    ) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from(            format!("Remove {marketplace_display_name} marketplace?").dim(),        ));        header.push(Line::from(            "This removes the configured marketplace from Codex.".dim(),        ));        let cwd_for_remove = self.config.cwd.to_path_buf();        let cwd_for_cancel = self.config.cwd.to_path_buf();        let cwd_for_on_cancel = self.config.cwd.to_path_buf();        let plugins_response_for_cancel = plugins_response.clone();        let plugins_response_for_on_cancel = plugins_response.clone();        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            footer_hint: Some(Line::from(vec![                Span::from(key_hint::plain(KeyCode::Enter)),                " select".dim(),                " · ".into(),                "esc close".dim(),            ])),            items: vec![                SelectionItem {                    name: "Remove marketplace".to_string(),                    description: Some(                        "Remove this marketplace from the available plugin list.".to_string(),                    ),                    selected_description: Some(                        "Remove this marketplace from the available plugin list.".to_string(),                    ),                    actions: vec![Box::new(move |tx| {                        tx.send(AppEvent::OpenMarketplaceRemoveLoading {                            marketplace_display_name: marketplace_display_name.clone(),                        });                        tx.send(AppEvent::FetchMarketplaceRemove {                            cwd: cwd_for_remove.clone(),                            marketplace_name: marketplace_name.clone(),                            marketplace_display_name: marketplace_display_name.clone(),                        });                    })],                    ..Default::default()                },                SelectionItem {                    name: "Back to plugins".to_string(),                    description: Some("Keep this marketplace installed.".to_string()),                    selected_description: Some("Keep this marketplace installed.".to_string()),                    actions: vec![Box::new(move |tx| {                        tx.send(AppEvent::PluginsLoaded {                            cwd: cwd_for_cancel.clone(),                            result: Ok(plugins_response_for_cancel.clone()),                        });                    })],                    ..Default::default()                },            ],            on_cancel: Some(Box::new(move |tx| {                tx.send(AppEvent::PluginsLoaded {                    cwd: cwd_for_on_cancel.clone(),                    result: Ok(plugins_response_for_on_cancel.clone()),                });            })),            ..Default::default()        }    }    fn marketplace_remove_loading_popup_params(        &self,        marketplace_display_name: &str,    ) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from(            format!("Removing {marketplace_display_name}...").dim(),        ));        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            items: vec![SelectionItem {                name: "Removing marketplace...".to_string(),                description: Some("This updates when marketplace removal completes.".to_string()),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn marketplace_upgrade_loading_popup_params(        &self,        marketplace_name: Option<&str>,    ) -> SelectionViewParams {        let loading_text = marketplace_name            .map(|name| format!("Upgrading {name} marketplace..."))            .unwrap_or_else(|| "Upgrading marketplaces...".to_string());        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(DelayedLoadingHeader::new(                self.frame_requester.clone(),                self.config.animations,                loading_text.clone(),                /*note*/ None,            )),            items: vec![SelectionItem {                name: loading_text,                description: Some("This updates when marketplace upgrade completes.".to_string()),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn plugin_detail_loading_popup_params(&self, plugin_display_name: &str) -> SelectionViewParams {        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(DelayedLoadingHeader::new(                self.frame_requester.clone(),                self.config.animations,                format!("Loading details for {plugin_display_name}..."),                /*note*/ None,            )),            items: vec![SelectionItem {                name: "Loading plugin details...".to_string(),                description: Some("This updates when plugin details load.".to_string()),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn plugin_install_loading_popup_params(        &self,        plugin_display_name: &str,    ) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from(            format!("Installing {plugin_display_name}...").dim(),        ));        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            items: vec![SelectionItem {                name: "Installing plugin...".to_string(),                description: Some("This updates when plugin installation completes.".to_string()),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn plugin_uninstall_loading_popup_params(        &self,        plugin_display_name: &str,    ) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from(            format!("Uninstalling {plugin_display_name}...").dim(),        ));        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            items: vec![SelectionItem {                name: "Uninstalling plugin...".to_string(),                description: Some("This updates when the plugin removal completes.".to_string()),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn plugins_error_popup_params(&self, err: &str) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from("Failed to load plugins.".dim()));        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            items: vec![SelectionItem {                name: "Plugin marketplace unavailable".to_string(),                description: Some(err.to_string()),                is_disabled: true,                ..Default::default()            }],            ..Default::default()        }    }    fn marketplace_add_error_popup_params(&self) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from("Failed to add marketplace.".dim()));        let mut items = vec![            SelectionItem {                name: "Marketplace add failed".to_string(),                description: Some(                    "Failed to add marketplace from the provided source.".to_string(),                ),                is_disabled: true,                ..Default::default()            },            SelectionItem {                name: "Try again".to_string(),                description: Some("Enter a marketplace source.".to_string()),                selected_description: Some("Enter a marketplace source.".to_string()),                actions: vec![Box::new(|tx| {                    tx.send(AppEvent::OpenMarketplaceAddPrompt);                })],                ..Default::default()            },        ];        if let PluginsCacheState::Ready(plugins_response) = self.plugins_cache_for_current_cwd() {            let cwd = self.config.cwd.to_path_buf();            items.push(SelectionItem {                name: "Back to plugins".to_string(),                description: Some("Return to the plugin list.".to_string()),                selected_description: Some("Return to the plugin list.".to_string()),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::PluginsLoaded {                        cwd: cwd.clone(),                        result: Ok(plugins_response.clone()),                    });                })],                ..Default::default()            });        }        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            footer_hint: Some(plugin_detail_hint_line()),            items,            ..Default::default()        }    }    fn marketplace_remove_error_popup_params(        &self,        marketplace_name: &str,        marketplace_display_name: &str,    ) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from("Failed to remove marketplace.".dim()));        let marketplace_name = marketplace_name.to_string();        let marketplace_display_name = marketplace_display_name.to_string();        let mut items = vec![            SelectionItem {                name: "Marketplace removal failed".to_string(),                description: Some("Failed to remove the selected marketplace.".to_string()),                is_disabled: true,                ..Default::default()            },            SelectionItem {                name: "Try again".to_string(),                description: Some("Review the confirmation prompt again.".to_string()),                selected_description: Some("Review the confirmation prompt again.".to_string()),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::OpenMarketplaceRemoveConfirm {                        marketplace_name: marketplace_name.clone(),                        marketplace_display_name: marketplace_display_name.clone(),                    });                })],                ..Default::default()            },        ];        if let PluginsCacheState::Ready(plugins_response) = self.plugins_cache_for_current_cwd() {            let cwd = self.config.cwd.to_path_buf();            items.push(SelectionItem {                name: "Back to plugins".to_string(),                description: Some("Return to the plugin list.".to_string()),                selected_description: Some("Return to the plugin list.".to_string()),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::PluginsLoaded {                        cwd: cwd.clone(),                        result: Ok(plugins_response.clone()),                    });                })],                ..Default::default()            });        }        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            footer_hint: Some(plugin_detail_hint_line()),            items,            ..Default::default()        }    }    fn plugin_detail_error_popup_params(        &self,        err: &str,        plugins_response: Option<&PluginListResponse>,    ) -> SelectionViewParams {        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from("Failed to load plugin details.".dim()));        let mut items = vec![SelectionItem {            name: "Plugin detail unavailable".to_string(),            description: Some(err.to_string()),            is_disabled: true,            ..Default::default()        }];        if let Some(plugins_response) = plugins_response.cloned() {            let cwd = self.config.cwd.to_path_buf();            items.push(SelectionItem {                name: "Back to plugins".to_string(),                description: Some("Return to the plugin list.".to_string()),                selected_description: Some("Return to the plugin list.".to_string()),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::PluginsLoaded {                        cwd: cwd.clone(),                        result: Ok(plugins_response.clone()),                    });                })],                ..Default::default()            });        }        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            footer_hint: Some(plugin_detail_hint_line()),            items,            ..Default::default()        }    }    fn plugins_popup_params(        &self,        response: &PluginListResponse,        active_tab_id: Option<String>,        initial_selected_idx: Option<usize>,    ) -> SelectionViewParams {        let marketplaces: Vec<&PluginMarketplaceEntry> = response.marketplaces.iter().collect();        let total: usize = marketplaces            .iter()            .map(|marketplace| marketplace.plugins.len())            .sum();        let installed = marketplaces            .iter()            .flat_map(|marketplace| marketplace.plugins.iter())            .filter(|plugin| plugin.installed)            .count();        let all_entries = plugin_entries_for_marketplaces(marketplaces.iter().copied());        let name_column_width = all_entries            .iter()            .map(|(_, _, display_name)| {                PLUGIN_ROW_PREFIX_WIDTH + UnicodeWidthStr::width(display_name.as_str())            })            .chain([UnicodeWidthStr::width("Add marketplace")])            .max();        let installed_entries = all_entries            .iter()            .filter(|(_, plugin, _)| plugin.installed)            .cloned()            .collect();        let mut tabs = Vec::new();        let mut tab_footer_hints = Vec::new();        tabs.push(SelectionTab {            id: ALL_PLUGINS_TAB_ID.to_string(),            label: "All Plugins".to_string(),            header: plugins_header(                "Browse plugins from available marketplaces.".to_string(),                format!("Installed {installed} of {total} available plugins."),            ),            items: self.plugin_selection_items(                all_entries,                /*include_marketplace_names*/ true,                "No marketplace plugins available",                "No plugins are available in the discovered marketplaces.",            ),        });        tabs.push(SelectionTab {            id: INSTALLED_PLUGINS_TAB_ID.to_string(),            label: format!("Installed ({installed})"),            header: plugins_header(                "Installed plugins.".to_string(),                format!("Showing {installed} installed plugins."),            ),            items: self.plugin_selection_items(                installed_entries,                /*include_marketplace_names*/ true,                "No installed plugins",                "No installed plugins.",            ),        });        let curated_marketplace = marketplaces            .iter()            .find(|marketplace| is_openai_curated_marketplace_name(&marketplace.name))            .copied();        let curated_entries = curated_marketplace            .map(|marketplace| plugin_entries_for_marketplaces([marketplace]))            .unwrap_or_default();        let curated_total = curated_entries.len();        let curated_installed = curated_entries            .iter()            .filter(|(_, plugin, _)| plugin.installed)            .count();        tabs.push(SelectionTab {            id: OPENAI_CURATED_TAB_ID.to_string(),            label: "OpenAI Curated".to_string(),            header: plugins_header(                "OpenAI Curated marketplace.".to_string(),                format!("Installed {curated_installed} of {curated_total} OpenAI Curated plugins."),            ),            items: self.plugin_selection_items(                curated_entries,                /*include_marketplace_names*/ false,                "No OpenAI Curated plugins available",                "No OpenAI Curated plugins available.",            ),        });        let mut additional_marketplaces: Vec<&PluginMarketplaceEntry> = marketplaces            .iter()            .copied()            .filter(|marketplace| !is_openai_curated_marketplace_name(&marketplace.name))            .collect();        additional_marketplaces.sort_by(|left, right| {            marketplace_display_name(left)                .to_ascii_lowercase()                .cmp(&marketplace_display_name(right).to_ascii_lowercase())                .then_with(|| marketplace_display_name(left).cmp(&marketplace_display_name(right)))                .then_with(|| left.name.cmp(&right.name))        });        let labels = disambiguate_duplicate_tab_labels(            additional_marketplaces                .iter()                .map(|marketplace| marketplace_display_name(marketplace))                .collect(),        );        for (marketplace, label) in additional_marketplaces.into_iter().zip(labels) {            let entries = plugin_entries_for_marketplaces([marketplace]);            let marketplace_total = entries.len();            let marketplace_installed = entries                .iter()                .filter(|(_, plugin, _)| plugin.installed)                .count();            let tab_id = marketplace_tab_id(marketplace);            let can_remove_marketplace =                marketplace_is_user_configured(&self.config, &marketplace.name);            let can_upgrade_marketplace = marketplace.path.is_some()                && marketplace_is_user_configured_git(&self.config, &marketplace.name);            if can_remove_marketplace || can_upgrade_marketplace {                tab_footer_hints.push((                    tab_id.clone(),                    plugins_popup_hint_line(                        /*can_remove_marketplace*/ can_remove_marketplace,                        /*can_upgrade_marketplace*/ can_upgrade_marketplace,                    ),                ));            }            let header = if self.newly_installed_marketplace_tab_id.as_deref() == Some(&tab_id) {                plugins_header(                    format!("{label} installed successfully."),                    "Select the plugins you want to use and press Enter to install or view details."                        .to_string(),                )            } else {                plugins_header(                    format!("{label}."),                    format!(                        "Installed {marketplace_installed} of {marketplace_total} {label} plugins."                    ),                )            };            tabs.push(SelectionTab {                id: tab_id,                label: label.clone(),                header,                items: self.plugin_selection_items(                    entries,                    /*include_marketplace_names*/ false,                    "No plugins available in this marketplace",                    "No plugins available in this marketplace.",                ),            });        }        tabs.push(self.marketplace_add_tab());        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(()),            footer_hint: Some(plugins_popup_hint_line(                /*can_remove_marketplace*/ false, /*can_upgrade_marketplace*/ false,            )),            tab_footer_hints,            tabs,            initial_tab_id: active_tab_id,            is_searchable: true,            search_placeholder: Some("Type to search plugins".to_string()),            col_width_mode: ColumnWidthMode::AutoAllRows,            row_display: SelectionRowDisplay::SingleLine,            name_column_width,            initial_selected_idx,            ..Default::default()        }    }    fn marketplace_add_tab(&self) -> SelectionTab {        SelectionTab {            id: ADD_MARKETPLACE_TAB_ID.to_string(),            label: "Add Marketplace".to_string(),            header: plugins_header(                "Add a marketplace from a Git repo or local root.".to_string(),                "Enter a source to make its plugins available in this menu.".to_string(),            ),            items: vec![SelectionItem {                name: "Add marketplace".to_string(),                description: Some(                    "Enter owner/repo, a Git URL, or a local marketplace path.".to_string(),                ),                selected_description: Some(                    "Press Enter to enter a marketplace source.".to_string(),                ),                actions: vec![Box::new(|tx| {                    tx.send(AppEvent::OpenMarketplaceAddPrompt);                })],                ..Default::default()            }],        }    }    fn plugin_detail_popup_params(        &self,        plugins_response: &PluginListResponse,        plugin: &PluginDetail,    ) -> SelectionViewParams {        let marketplace_label = plugin.marketplace_name.clone();        let display_name = plugin_display_name(&plugin.summary);        let detail_status_label =            if plugin.summary.availability == PluginAvailability::DisabledByAdmin {                "Disabled by admin"            } else if plugin.summary.installed {                if plugin.summary.enabled {                    "Installed"                } else {                    "Disabled"                }            } else {                match plugin.summary.install_policy {                    PluginInstallPolicy::NotAvailable => "Not installable",                    PluginInstallPolicy::Available => "Can be installed",                    PluginInstallPolicy::InstalledByDefault => "Available by default",                }            };        let mut header = ColumnRenderable::new();        header.push(Line::from("Plugins".bold()));        header.push(Line::from(            format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(),        ));        if !plugin.summary.installed {            header.push(PluginDisclosureLine {                line: Line::from(vec![                    "Data shared with this app is subject to the app's ".into(),                    "terms of service".bold(),                    " and ".into(),                    "privacy policy".bold(),                    ". ".into(),                    "Learn more".cyan().underlined(),                    ".".into(),                ]),            });        }        if let Some(description) = plugin_detail_description(plugin) {            header.push(Line::from(description.dim()));        }        let cwd = self.config.cwd.to_path_buf();        let plugins_response = plugins_response.clone();        let mut items = vec![SelectionItem {            name: "Back to plugins".to_string(),            description: Some("Return to the plugin list.".to_string()),            selected_description: Some("Return to the plugin list.".to_string()),            actions: vec![Box::new(move |tx| {                tx.send(AppEvent::PluginsLoaded {                    cwd: cwd.clone(),                    result: Ok(plugins_response.clone()),                });            })],            ..Default::default()        }];        if plugin.summary.installed {            if let Some(plugin_id) = plugin_uninstall_id(&plugin.summary) {                let uninstall_cwd = self.config.cwd.to_path_buf();                let plugin_display_name = display_name;                items.push(SelectionItem {                    name: "Uninstall plugin".to_string(),                    description: Some("Remove this plugin now.".to_string()),                    selected_description: Some("Remove this plugin now.".to_string()),                    actions: vec![Box::new(move |tx| {                        tx.send(AppEvent::OpenPluginUninstallLoading {                            plugin_display_name: plugin_display_name.clone(),                        });                        tx.send(AppEvent::FetchPluginUninstall {                            cwd: uninstall_cwd.clone(),                            plugin_id: plugin_id.clone(),                            plugin_display_name: plugin_display_name.clone(),                        });                    })],                    ..Default::default()                });            } else {                items.push(SelectionItem {                    name: "Uninstall plugin".to_string(),                    description: Some(                        "This remote plugin did not provide an uninstall identity.".to_string(),                    ),                    is_disabled: true,                    ..Default::default()                });            }        } else if plugin.summary.availability == PluginAvailability::DisabledByAdmin {            items.push(SelectionItem {                name: "Install plugin".to_string(),                description: Some("This plugin is disabled by your workspace admin.".to_string()),                is_disabled: true,                ..Default::default()            });        } else if plugin.summary.install_policy == PluginInstallPolicy::NotAvailable {            items.push(SelectionItem {                name: "Install plugin".to_string(),                description: Some(                    "This plugin is not installable from this marketplace.".to_string(),                ),                is_disabled: true,                ..Default::default()            });        } else if let Some(location) = plugin_detail_location(plugin) {            let install_cwd = self.config.cwd.to_path_buf();            let plugin_name = plugin_request_name(&plugin.summary);            let plugin_display_name = display_name;            items.push(SelectionItem {                name: "Install plugin".to_string(),                description: Some("Install this plugin now.".to_string()),                selected_description: Some("Install this plugin now.".to_string()),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::OpenPluginInstallLoading {                        plugin_display_name: plugin_display_name.clone(),                    });                    tx.send(AppEvent::FetchPluginInstall {                        cwd: install_cwd.clone(),                        location: location.clone(),                        plugin_name: plugin_name.clone(),                        plugin_display_name: plugin_display_name.clone(),                    });                })],                ..Default::default()            });        } else {            items.push(SelectionItem {                name: "Install plugin".to_string(),                description: Some("This plugin did not provide an install location.".to_string()),                is_disabled: true,                ..Default::default()            });        }        items.push(SelectionItem {            name: "Skills".to_string(),            description: Some(plugin_skill_summary(plugin)),            is_disabled: true,            ..Default::default()        });        items.push(SelectionItem {            name: "Hooks".to_string(),            description: Some(plugin_hook_summary(plugin)),            is_disabled: true,            ..Default::default()        });        items.push(SelectionItem {            name: "Apps".to_string(),            description: Some(plugin_app_summary(plugin)),            is_disabled: true,            ..Default::default()        });        items.push(SelectionItem {            name: "MCP Servers".to_string(),            description: Some(plugin_mcp_summary(plugin)),            is_disabled: true,            ..Default::default()        });        SelectionViewParams {            view_id: Some(PLUGINS_SELECTION_VIEW_ID),            header: Box::new(header),            footer_hint: Some(plugin_detail_hint_line()),            items,            col_width_mode: ColumnWidthMode::AutoAllRows,            ..Default::default()        }    }    fn plugin_selection_items<'a>(        &self,        mut plugin_entries: Vec<(&'a PluginMarketplaceEntry, &'a PluginSummary, String)>,        include_marketplace_names: bool,        empty_name: &str,        empty_description: &str,    ) -> Vec<SelectionItem> {        sort_plugin_entries(&mut plugin_entries);        let status_label_width = plugin_entries            .iter()            .map(|(_, plugin, _)| plugin_status_label(plugin).chars().count())            .max()            .unwrap_or(0);        let mut items: Vec<SelectionItem> = Vec::new();        for (marketplace, plugin, display_name) in plugin_entries {            let marketplace_label = marketplace_display_name(marketplace);            let status_label = plugin_status_label(plugin);            let description = if include_marketplace_names {                plugin_brief_description(plugin, &marketplace_label, status_label_width)            } else {                plugin_brief_description_without_marketplace(plugin, status_label_width)            };            let plugin_detail_request = plugin_detail_request_for_entry(marketplace, plugin);            let can_view_details = plugin_detail_request.is_some();            let disabled_by_admin = plugin.availability == PluginAvailability::DisabledByAdmin;            let can_toggle_plugin = plugin.installed && !disabled_by_admin;            let selected_status_label = format!("{status_label:<status_label_width$}");            let selected_description = if can_toggle_plugin {                let toggle_action = if plugin.enabled { "disable" } else { "enable" };                if can_view_details {                    format!(                        "{selected_status_label}   Space to {toggle_action}; Enter view details."                    )                } else {                    format!("{selected_status_label}   Space to {toggle_action}.")                }            } else if plugin.installed && can_view_details {                format!("{selected_status_label}   Press Enter to view plugin details.")            } else if plugin.installed {                format!("{selected_status_label}   Plugin details are unavailable.")            } else if disabled_by_admin && can_view_details {                format!("{selected_status_label}   Press Enter to view plugin details.")            } else if can_view_details {                format!("{selected_status_label}   Press Enter to install or view plugin details.")            } else {                format!("{selected_status_label}   Remote plugin details are not available yet.")            };            let search_value = format!(                "{display_name} {} {} {}",                plugin.id, plugin.name, marketplace_label            );            let cwd = self.config.cwd.to_path_buf();            let plugin_display_name = display_name.clone();            let toggle_cwd = cwd.clone();            let toggle_plugin_id = plugin.id.clone();            let toggle = can_toggle_plugin.then(|| SelectionToggle {                is_on: plugin.enabled,                action: Box::new(move |enabled, tx| {                    tx.send(AppEvent::SetPluginEnabled {                        cwd: toggle_cwd.clone(),                        plugin_id: toggle_plugin_id.clone(),                        enabled,                    });                }),            });            let actions: Vec<SelectionAction> =                if let Some((location, plugin_name)) = plugin_detail_request {                    vec![Box::new(move |tx| {                        tx.send(AppEvent::OpenPluginDetailLoading {                            plugin_display_name: plugin_display_name.clone(),                        });                        let (marketplace_path, remote_marketplace_name) =                            location.clone().into_request_params();                        tx.send(AppEvent::FetchPluginDetail {                            cwd: cwd.clone(),                            params: codex_app_server_protocol::PluginReadParams {                                marketplace_path,                                remote_marketplace_name,                                plugin_name: plugin_name.clone(),                            },                        });                    })]                } else {                    Vec::new()                };            let is_disabled = !can_view_details && !plugin.installed;            let disabled_reason = is_disabled.then(|| "plugin details are unavailable".to_string());            items.push(SelectionItem {                name: display_name,                toggle,                toggle_placeholder: (!can_toggle_plugin).then_some("[-] "),                description: Some(description),                selected_description: Some(selected_description),                search_value: Some(search_value),                actions,                is_disabled,                disabled_reason,                ..Default::default()            });        }        if items.is_empty() {            items.push(SelectionItem {                name: empty_name.to_string(),                description: Some(empty_description.to_string()),                is_disabled: true,                ..Default::default()            });        }        items    }}fn plugins_popup_hint_line(    can_remove_marketplace: bool,    can_upgrade_marketplace: bool,) -> Line<'static> {    match (can_remove_marketplace, can_upgrade_marketplace) {        (true, true) => Line::from(            "ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close",        ),        (true, false) => {            Line::from("ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close")        }        (false, true) => {            Line::from("ctrl + u upgrade · space toggle · ←/→ tabs · enter details · esc close")        }        (false, false) => Line::from(            "space enable/disable · ←/→ select marketplace · enter view details · esc close",        ),    }}fn plugin_detail_hint_line() -> Line<'static> {    Line::from("Press esc to close.")}fn plugins_header(subtitle: String, count_line: String) -> Box<dyn Renderable> {    let mut header = ColumnRenderable::new();    header.push(Line::from("Plugins".bold()));    header.push(Line::from(subtitle.dim()));    header.push(Line::from(count_line.dim()));    Box::new(header)}fn plugin_entries_for_marketplaces<'a>(    marketplaces: impl IntoIterator<Item = &'a PluginMarketplaceEntry>,) -> Vec<(&'a PluginMarketplaceEntry, &'a PluginSummary, String)> {    marketplaces        .into_iter()        .flat_map(|marketplace| {            marketplace                .plugins                .iter()                .map(move |plugin| (marketplace, plugin, plugin_display_name(plugin)))        })        .collect()}fn sort_plugin_entries(entries: &mut [(&PluginMarketplaceEntry, &PluginSummary, String)]) {    entries.sort_by(|left, right| {        right            .1            .installed            .cmp(&left.1.installed)            .then_with(|| {                left.2                    .to_ascii_lowercase()                    .cmp(&right.2.to_ascii_lowercase())            })            .then_with(|| left.2.cmp(&right.2))            .then_with(|| left.1.name.cmp(&right.1.name))            .then_with(|| left.1.id.cmp(&right.1.id))    });}fn marketplace_tab_id(marketplace: &PluginMarketplaceEntry) -> String {    match marketplace.path.as_ref() {        Some(path) => marketplace_tab_id_from_path(path.as_path()),        None => format!("marketplace:{}", marketplace.name),    }}fn marketplace_tab_id_from_path(path: &Path) -> String {    format!("{MARKETPLACE_TAB_ID_PREFIX}{}", path.display())}fn marketplace_tab_id_matching_saved_id(    saved_tab_id: &str,    marketplaces: &[PluginMarketplaceEntry],) -> Option<String> {    if let Some(tab_id) = marketplaces.iter().find_map(|marketplace| {        let tab_id = marketplace_tab_id(marketplace);        (tab_id == saved_tab_id).then_some(tab_id)    }) {        return Some(tab_id);    }    let root = saved_tab_id.strip_prefix(MARKETPLACE_TAB_ID_PREFIX)?;    if root.is_empty() {        return None;    }    let root = Path::new(root);    marketplaces.iter().find_map(|marketplace| {        marketplace            .path            .as_ref()            .is_some_and(|path| path.as_path().starts_with(root))            .then(|| marketplace_tab_id(marketplace))    })}fn merge_remote_marketplaces(    response: &mut PluginListResponse,    remote_marketplaces: Vec<PluginMarketplaceEntry>,) {    let remote_names = remote_marketplaces        .iter()        .map(|marketplace| marketplace.name.clone())        .collect::<std::collections::HashSet<_>>();    response.marketplaces.retain(|marketplace| {        marketplace.path.is_some()            || !remote_marketplace_is_remote_section(marketplace)                && !remote_names.contains(marketplace.name.as_str())    });    response.marketplaces.extend(remote_marketplaces);}fn remote_marketplace_is_remote_section(marketplace: &PluginMarketplaceEntry) -> bool {    matches!(        marketplace.name.as_str(),        REMOTE_WORKSPACE_MARKETPLACE_NAME            | REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME            | REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME            | REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME    )}fn disambiguate_duplicate_tab_labels(labels: Vec<String>) -> Vec<String> {    let mut counts: Vec<(String, usize)> = Vec::new();    for label in &labels {        if let Some((_, count)) = counts.iter_mut().find(|(existing, _)| existing == label) {            *count += 1;        } else {            counts.push((label.clone(), 1));        }    }    let mut seen: Vec<(String, usize)> = Vec::new();    labels        .into_iter()        .map(|label| {            let total = counts                .iter()                .find(|(existing, _)| existing == &label)                .map(|(_, count)| *count)                .unwrap_or(1);            if total == 1 {                return label;            }            let current = if let Some((_, seen_count)) =                seen.iter_mut().find(|(existing, _)| existing == &label)            {                *seen_count += 1;                *seen_count            } else {                seen.push((label.clone(), 1));                1            };            format!("{label} ({current}/{total})")        })        .collect()}fn marketplace_display_name(marketplace: &PluginMarketplaceEntry) -> String {    marketplace        .interface        .as_ref()        .and_then(|interface| interface.display_name.as_deref())        .map(str::trim)        .filter(|display_name| !display_name.is_empty())        .map(str::to_string)        .unwrap_or_else(|| marketplace.name.clone())}fn marketplace_is_user_configured(config: &Config, marketplace_name: &str) -> bool {    let Some(user_config) = config.config_layer_stack.effective_user_config() else {        return false;    };    user_config        .get("marketplaces")        .and_then(toml::Value::as_table)        .is_some_and(|marketplaces| marketplaces.contains_key(marketplace_name))}fn marketplace_is_user_configured_git(config: &Config, marketplace_name: &str) -> bool {    config        .config_layer_stack        .get_active_user_layer()        .and_then(|user_layer| user_layer.config.get("marketplaces"))        .and_then(toml::Value::as_table)        .and_then(|marketplaces| marketplaces.get(marketplace_name))        .and_then(toml::Value::as_table)        .and_then(|marketplace| marketplace.get("source_type"))        .and_then(toml::Value::as_str)        .is_some_and(|source_type| source_type == "git")}fn plugin_display_name(plugin: &PluginSummary) -> String {    plugin        .interface        .as_ref()        .and_then(|interface| interface.display_name.as_deref())        .map(str::trim)        .filter(|display_name| !display_name.is_empty())        .map(str::to_string)        .unwrap_or_else(|| plugin.name.clone())}fn plugin_brief_description(    plugin: &PluginSummary,    marketplace_label: &str,    status_label_width: usize,) -> String {    let status_label = plugin_status_label(plugin);    let status_label = format!("{status_label:<status_label_width$}");    match plugin_description(plugin) {        Some(description) => format!("{status_label} · {marketplace_label} · {description}"),        None => format!("{status_label} · {marketplace_label}"),    }}fn plugin_brief_description_without_marketplace(    plugin: &PluginSummary,    status_label_width: usize,) -> String {    let status_label = plugin_status_label(plugin);    let status_label = format!("{status_label:<status_label_width$}");    match plugin_description(plugin) {        Some(description) => format!("{status_label} · {description}"),        None => status_label,    }}fn plugin_status_label(plugin: &PluginSummary) -> &'static str {    if plugin.availability == PluginAvailability::DisabledByAdmin {        return "Disabled by admin";    }    if plugin.installed {        if plugin.enabled {            "Installed"        } else {            "Disabled"        }    } else {        match plugin.install_policy {            PluginInstallPolicy::NotAvailable => "Not installable",            PluginInstallPolicy::Available => "Available",            PluginInstallPolicy::InstalledByDefault => "Available",        }    }}fn plugin_location_for_marketplace(    marketplace: &PluginMarketplaceEntry,    plugin: &PluginSummary,) -> Option<PluginLocation> {    if let Some(marketplace_path) = marketplace.path.clone() {        return Some(PluginLocation::Local { marketplace_path });    }    plugin_remote_identity(plugin).map(|_| PluginLocation::Remote {        marketplace_name: marketplace.name.clone(),    })}fn plugin_detail_location(plugin: &PluginDetail) -> Option<PluginLocation> {    if let Some(marketplace_path) = plugin.marketplace_path.clone() {        return Some(PluginLocation::Local { marketplace_path });    }    plugin_remote_identity(&plugin.summary).map(|_| PluginLocation::Remote {        marketplace_name: plugin.marketplace_name.clone(),    })}fn plugin_detail_request_for_entry(    marketplace: &PluginMarketplaceEntry,    plugin: &PluginSummary,) -> Option<(PluginLocation, String)> {    plugin_location_for_marketplace(marketplace, plugin)        .map(|location| (location, plugin_request_name(plugin)))}fn plugin_request_name(plugin: &PluginSummary) -> String {    if matches!(&plugin.source, PluginSource::Remote)        && let Some(remote_plugin_id) = plugin_remote_identity(plugin)    {        return remote_plugin_id;    }    plugin.name.clone()}fn plugin_remote_identity(plugin: &PluginSummary) -> Option<String> {    plugin        .share_context        .as_ref()        .map(|context| context.remote_plugin_id.clone())        .or_else(|| plugin.remote_plugin_id.clone())}fn plugin_uninstall_id(plugin: &PluginSummary) -> Option<String> {    if matches!(&plugin.source, PluginSource::Remote) {        return plugin_remote_identity(plugin);    }    Some(plugin.id.clone())}fn plugin_description(plugin: &PluginSummary) -> Option<String> {    plugin        .interface        .as_ref()        .and_then(|interface| {            interface                .short_description                .as_deref()                .or(interface.long_description.as_deref())        })        .map(str::trim)        .filter(|description| !description.is_empty())        .map(str::to_string)}fn plugin_detail_description(plugin: &PluginDetail) -> Option<String> {    plugin        .description        .as_deref()        .or_else(|| {            plugin                .summary                .interface                .as_ref()                .and_then(|interface| interface.long_description.as_deref())        })        .or_else(|| {            plugin                .summary                .interface                .as_ref()                .and_then(|interface| interface.short_description.as_deref())        })        .map(str::trim)        .filter(|description| !description.is_empty())        .map(str::to_string)}fn plugin_skill_summary(plugin: &PluginDetail) -> String {    if plugin.skills.is_empty() {        "No plugin skills.".to_string()    } else {        plugin            .skills            .iter()            .map(|skill| skill.name.as_str())            .collect::<Vec<_>>()            .join(", ")    }}fn plugin_app_summary(plugin: &PluginDetail) -> String {    if plugin.apps.is_empty() {        "No plugin apps.".to_string()    } else {        plugin            .apps            .iter()            .map(|app| app.name.as_str())            .collect::<Vec<_>>()            .join(", ")    }}fn plugin_hook_summary(plugin: &PluginDetail) -> String {    if plugin.hooks.is_empty() {        "No plugin hooks.".to_string()    } else {        let mut event_counts = Vec::<(codex_app_server_protocol::HookEventName, usize)>::new();        for hook in &plugin.hooks {            if let Some((_, handler_count)) = event_counts                .iter_mut()                .find(|(event_name, _)| *event_name == hook.event_name)            {                *handler_count += 1;            } else {                event_counts.push((hook.event_name, 1));            }        }        event_counts            .into_iter()            .map(|(event_name, handler_count)| format!("{event_name:?} ({handler_count})"))            .collect::<Vec<_>>()            .join(", ")    }}fn plugin_mcp_summary(plugin: &PluginDetail) -> String {    if plugin.mcp_servers.is_empty() {        "No plugin MCP servers.".to_string()    } else {        plugin.mcp_servers.join(", ")    }}