mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
TUI Plugin Sharing 5 - polish remote plugin catalog rows (#26705)
This is the final plugin sharing PR in the 5-PR stack. It applies the remaining TUI polish for remote plugin catalog rows and tabs: admin-disabled plugins now read as blocked/view-only instead of looking toggleable, admin-installed/default-installed plugins count and sort like installed plugins, plugin search matches richer metadata, and an empty successful `Shared with me` section stays hidden. - Admin-disabled rows use a blocked marker, show `Disabled`, and keep Enter-only detail behavior without a toggle hint. - Admin-installed/default-installed plugins show as installed in counts, ordering, tabs, and detail copy. - Plugin search now matches descriptions and keywords in addition to existing row metadata. - Successful-empty `Shared with me` tabs are hidden, while loading, error, workspace-empty, and real shared-plugin states remain visible. - Updates coverage in `plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_name`, `plugins_popup_admin_disabled_installed_plugin_has_no_toggle_hint`, `plugins_popup_search_matches_plugin_descriptions`, and `plugins_popup_remote_section_fallback_states_snapshot`. - Updates snapshots `plugins_popup_curated_marketplace` and `plugins_popup_empty_shared_section_hidden`. <img width="2034" height="106" alt="image" src="https://github.com/user-attachments/assets/3f9a57e1-edd8-4e6c-b0b0-9f632a3c9529" /> <img width="2038" height="380" alt="image" src="https://github.com/user-attachments/assets/45a47491-3381-4846-a13d-496bc0051d42" />
This commit is contained in:
committed by
GitHub
Unverified
parent
f15df624a6
commit
a0d5fd772e
@@ -281,6 +281,63 @@ pub(crate) struct ListSelectionView {
|
||||
keymap: ListKeymap,
|
||||
}
|
||||
|
||||
const SELECTION_TOGGLE_ON_PREFIX: &str = "[*] ";
|
||||
const SELECTION_TOGGLE_OFF_PREFIX: &str = "[ ] ";
|
||||
pub(crate) const SELECTION_TOGGLE_UNAVAILABLE_PREFIX: &str = "[-] ";
|
||||
pub(crate) const SELECTION_TOGGLE_BLOCKED_PREFIX: &str = "[!] ";
|
||||
|
||||
fn selection_toggle_prefix(toggle: &SelectionToggle) -> &'static str {
|
||||
if toggle.is_on {
|
||||
SELECTION_TOGGLE_ON_PREFIX
|
||||
} else {
|
||||
SELECTION_TOGGLE_OFF_PREFIX
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_item_toggle_prefix(item: &SelectionItem) -> Option<&'static str> {
|
||||
item.toggle
|
||||
.as_ref()
|
||||
.map(selection_toggle_prefix)
|
||||
.or(item.toggle_placeholder)
|
||||
}
|
||||
|
||||
impl ListSelectionView {
|
||||
fn selected_item_has_toggle(&self) -> bool {
|
||||
self.selected_actual_idx()
|
||||
.and_then(|actual_idx| self.active_items().get(actual_idx))
|
||||
.is_some_and(|item| item.toggle.is_some() && Self::item_is_enabled(item))
|
||||
}
|
||||
|
||||
fn selected_item_has_toggle_placeholder(&self) -> bool {
|
||||
self.selected_actual_idx()
|
||||
.and_then(|actual_idx| self.active_items().get(actual_idx))
|
||||
.is_some_and(|item| {
|
||||
item.toggle.is_none()
|
||||
&& item.toggle_placeholder.is_some()
|
||||
&& Self::item_is_enabled(item)
|
||||
})
|
||||
}
|
||||
|
||||
fn toggle_selected(&mut self) {
|
||||
let Some(actual_idx) = self.selected_actual_idx() else {
|
||||
return;
|
||||
};
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let Some(item) = self.active_items_mut().get_mut(actual_idx) else {
|
||||
return;
|
||||
};
|
||||
if !Self::item_is_enabled(item) {
|
||||
return;
|
||||
}
|
||||
let Some(toggle) = item.toggle.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
toggle.is_on = !toggle.is_on;
|
||||
(toggle.action)(toggle.is_on, &app_event_tx);
|
||||
}
|
||||
}
|
||||
|
||||
impl ListSelectionView {
|
||||
/// Create a selection popup view with filtering, scrolling, and callbacks wired.
|
||||
///
|
||||
@@ -533,10 +590,8 @@ impl ListSelectionView {
|
||||
let wrap_prefix_width = UnicodeWidthStr::width(wrap_prefix.as_str());
|
||||
let mut name_prefix_spans = Vec::new();
|
||||
name_prefix_spans.push(wrap_prefix.into());
|
||||
if let Some(toggle) = &item.toggle {
|
||||
name_prefix_spans.push(if toggle.is_on { "[*] " } else { "[ ] " }.into());
|
||||
} else if let Some(placeholder) = item.toggle_placeholder {
|
||||
name_prefix_spans.push(placeholder.into());
|
||||
if let Some(toggle_prefix) = selection_item_toggle_prefix(item) {
|
||||
name_prefix_spans.push(toggle_prefix.into());
|
||||
}
|
||||
name_prefix_spans.extend(item.name_prefix_spans.clone());
|
||||
let description = is_selected
|
||||
@@ -611,22 +666,6 @@ impl ListSelectionView {
|
||||
item.disabled_reason.is_none() && !item.is_disabled
|
||||
}
|
||||
|
||||
fn selected_item_has_toggle(&self) -> bool {
|
||||
self.selected_actual_idx()
|
||||
.and_then(|actual_idx| self.active_items().get(actual_idx))
|
||||
.is_some_and(|item| item.toggle.is_some() && Self::item_is_enabled(item))
|
||||
}
|
||||
|
||||
fn selected_item_has_toggle_placeholder(&self) -> bool {
|
||||
self.selected_actual_idx()
|
||||
.and_then(|actual_idx| self.active_items().get(actual_idx))
|
||||
.is_some_and(|item| {
|
||||
item.toggle.is_none()
|
||||
&& item.toggle_placeholder.is_some()
|
||||
&& Self::item_is_enabled(item)
|
||||
})
|
||||
}
|
||||
|
||||
fn actual_idx_for_enabled_number(&self, number: usize) -> Option<usize> {
|
||||
if number == 0 {
|
||||
return None;
|
||||
@@ -640,25 +679,6 @@ impl ListSelectionView {
|
||||
.map(|(idx, _)| idx)
|
||||
}
|
||||
|
||||
fn toggle_selected(&mut self) {
|
||||
let Some(actual_idx) = self.selected_actual_idx() else {
|
||||
return;
|
||||
};
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let Some(item) = self.active_items_mut().get_mut(actual_idx) else {
|
||||
return;
|
||||
};
|
||||
if !Self::item_is_enabled(item) {
|
||||
return;
|
||||
}
|
||||
let Some(toggle) = item.toggle.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
toggle.is_on = !toggle.is_on;
|
||||
(toggle.action)(toggle.is_on, &app_event_tx);
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
let before = self.selected_actual_idx();
|
||||
let len = self.visible_len();
|
||||
|
||||
@@ -190,6 +190,8 @@ use crate::status_indicator_widget::StatusDetailsCapitalization;
|
||||
use crate::status_indicator_widget::StatusIndicatorWidget;
|
||||
pub(crate) use experimental_features_view::ExperimentalFeatureItem;
|
||||
pub(crate) use experimental_features_view::ExperimentalFeaturesView;
|
||||
pub(crate) use list_selection_view::SELECTION_TOGGLE_BLOCKED_PREFIX;
|
||||
pub(crate) use list_selection_view::SELECTION_TOGGLE_UNAVAILABLE_PREFIX;
|
||||
pub(crate) use list_selection_view::SelectionAction;
|
||||
pub(crate) use list_selection_view::SelectionItem;
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ use crate::app_event::AppEvent;
|
||||
use crate::app_event::PluginLocation;
|
||||
use crate::app_event::PluginRemoteSectionError;
|
||||
use crate::bottom_pane::ColumnWidthMode;
|
||||
use crate::bottom_pane::SELECTION_TOGGLE_BLOCKED_PREFIX;
|
||||
use crate::bottom_pane::SELECTION_TOGGLE_UNAVAILABLE_PREFIX;
|
||||
use crate::bottom_pane::SelectionAction;
|
||||
use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionRowDisplay;
|
||||
@@ -67,6 +69,8 @@ const PERSONAL_MARKETPLACE_RELATIVE_PATH: &str = ".agents/plugins/marketplace.js
|
||||
const REMOTE_LOADING_TAB_ID_PREFIX: &str = "remote-loading:";
|
||||
const REMOTE_EMPTY_TAB_ID_PREFIX: &str = "remote-empty:";
|
||||
const REMOTE_ERROR_TAB_ID_PREFIX: &str = "remote-error:";
|
||||
const OPENAI_CURATED_LOADING_DESCRIPTION: &str =
|
||||
"This updates when OpenAI Curated plugins finish loading.";
|
||||
const WORKSPACE_SECTION_TAB_ORDER: u8 = 0;
|
||||
const SHARED_WITH_ME_SECTION_TAB_ORDER: u8 = 1;
|
||||
const SHARED_WITH_ME_LINK_SECTION_TAB_ORDER: u8 = 2;
|
||||
@@ -78,6 +82,7 @@ struct PreferredLocalPluginSource {
|
||||
marketplace_path: AbsolutePathBuf,
|
||||
plugin_name: String,
|
||||
installed: bool,
|
||||
install_policy: PluginInstallPolicy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -153,7 +158,9 @@ struct RemoteMarketplaceSection {
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
loading_tab_id: &'static str,
|
||||
loading_item_description: &'static str,
|
||||
marketplace_names: &'static [&'static str],
|
||||
show_empty_tab: bool,
|
||||
empty_item_name: &'static str,
|
||||
empty_item_description: &'static str,
|
||||
tab_order: u8,
|
||||
@@ -164,7 +171,9 @@ const REMOTE_MARKETPLACE_SECTIONS: [RemoteMarketplaceSection; 2] = [
|
||||
id: "workspace",
|
||||
label: "Workspace",
|
||||
loading_tab_id: "workspace-loading",
|
||||
loading_item_description: "This updates when workspace plugins finish loading.",
|
||||
marketplace_names: &[REMOTE_WORKSPACE_MARKETPLACE_NAME],
|
||||
show_empty_tab: true,
|
||||
empty_item_name: "No workspace plugins available",
|
||||
empty_item_description: "No workspace directory plugins are available.",
|
||||
tab_order: WORKSPACE_SECTION_TAB_ORDER,
|
||||
@@ -173,11 +182,13 @@ const REMOTE_MARKETPLACE_SECTIONS: [RemoteMarketplaceSection; 2] = [
|
||||
id: "shared-with-me",
|
||||
label: "Shared with me",
|
||||
loading_tab_id: "shared-with-me-loading",
|
||||
loading_item_description: "This updates when shared plugins finish loading.",
|
||||
marketplace_names: &[
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME,
|
||||
],
|
||||
show_empty_tab: false,
|
||||
empty_item_name: "No shared plugins available",
|
||||
empty_item_description: "No plugins have been shared with you.",
|
||||
tab_order: SHARED_WITH_ME_SECTION_TAB_ORDER,
|
||||
@@ -200,10 +211,16 @@ impl RemoteMarketplaceSection {
|
||||
}
|
||||
|
||||
let tab = if remote_sections_loading {
|
||||
remote_section_loading_tab(self.loading_tab_id, self.label)
|
||||
remote_section_loading_tab(
|
||||
self.loading_tab_id,
|
||||
self.label,
|
||||
self.loading_item_description,
|
||||
)
|
||||
} else if remote_sections_loaded {
|
||||
if let Some(section_error) = plugin_remote_section_error(section_errors, self.id) {
|
||||
remote_section_error_tab(section_error)
|
||||
} else if !self.show_empty_tab {
|
||||
return None;
|
||||
} else {
|
||||
remote_section_empty_tab(
|
||||
self.id,
|
||||
@@ -799,7 +816,7 @@ impl ChatWidget {
|
||||
if curated_loading && !curated_has_entries {
|
||||
(
|
||||
"Loading OpenAI Curated plugins...",
|
||||
"This section updates when app-server returns it.",
|
||||
OPENAI_CURATED_LOADING_DESCRIPTION,
|
||||
)
|
||||
} else if let Some(section_error) = by_openai_section_error
|
||||
&& !curated_has_entries
|
||||
@@ -819,7 +836,10 @@ impl ChatWidget {
|
||||
curated_empty_description,
|
||||
);
|
||||
if curated_loading && curated_has_entries {
|
||||
curated_items.push(remote_section_loading_item("OpenAI Curated"));
|
||||
curated_items.push(remote_section_loading_item(
|
||||
"OpenAI Curated",
|
||||
OPENAI_CURATED_LOADING_DESCRIPTION,
|
||||
));
|
||||
}
|
||||
if let Some(section_error) = by_openai_section_error
|
||||
&& curated_has_entries
|
||||
@@ -1024,7 +1044,16 @@ impl ChatWidget {
|
||||
}];
|
||||
|
||||
if plugin.summary.installed {
|
||||
if let Some(plugin_id) = plugin_uninstall_id(&plugin.summary) {
|
||||
if plugin.summary.install_policy == PluginInstallPolicy::InstalledByDefault {
|
||||
items.push(SelectionItem {
|
||||
name: "Installed by admin".to_string(),
|
||||
description: Some(
|
||||
"This plugin is installed by your workspace admin.".to_string(),
|
||||
),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
});
|
||||
} else 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 {
|
||||
@@ -1164,7 +1193,9 @@ impl ChatWidget {
|
||||
plugin_detail_request_for_entry(marketplace, plugin, preferred_local_sources);
|
||||
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 can_toggle_plugin = plugin.installed
|
||||
&& plugin.install_policy != PluginInstallPolicy::InstalledByDefault
|
||||
&& !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" };
|
||||
@@ -1175,20 +1206,26 @@ impl ChatWidget {
|
||||
} else {
|
||||
format!("{selected_status_label} Space to {toggle_action}.")
|
||||
}
|
||||
} else if disabled_by_admin && can_view_details {
|
||||
format!("{selected_status_label} Press Enter to view plugin details.")
|
||||
} else if disabled_by_admin {
|
||||
format!("{selected_status_label} Plugin details are unavailable.")
|
||||
} 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
|
||||
"{display_name} {} {} {} {} {}",
|
||||
plugin.id,
|
||||
plugin.name,
|
||||
marketplace_label,
|
||||
plugin_description(plugin).unwrap_or_default(),
|
||||
plugin.keywords.join(" ")
|
||||
);
|
||||
let cwd = self.config.cwd.to_path_buf();
|
||||
let plugin_display_name = display_name.clone();
|
||||
@@ -1230,7 +1267,13 @@ impl ChatWidget {
|
||||
items.push(SelectionItem {
|
||||
name: display_name,
|
||||
toggle,
|
||||
toggle_placeholder: (!can_toggle_plugin).then_some("[-] "),
|
||||
toggle_placeholder: if plugin.availability == PluginAvailability::DisabledByAdmin {
|
||||
Some(SELECTION_TOGGLE_BLOCKED_PREFIX)
|
||||
} else if can_toggle_plugin {
|
||||
None
|
||||
} else {
|
||||
Some(SELECTION_TOGGLE_UNAVAILABLE_PREFIX)
|
||||
},
|
||||
description: Some(description),
|
||||
selected_description: Some(selected_description),
|
||||
search_value: Some(search_value),
|
||||
@@ -1317,6 +1360,14 @@ fn plugin_entry_preferred(
|
||||
return candidate.1.installed;
|
||||
}
|
||||
|
||||
let candidate_is_admin_managed =
|
||||
candidate.1.install_policy == PluginInstallPolicy::InstalledByDefault;
|
||||
let existing_is_admin_managed =
|
||||
existing.1.install_policy == PluginInstallPolicy::InstalledByDefault;
|
||||
if candidate_is_admin_managed != existing_is_admin_managed {
|
||||
return candidate_is_admin_managed;
|
||||
}
|
||||
|
||||
let candidate_is_local_share =
|
||||
candidate.1.share_context.is_some() && !matches!(&candidate.1.source, PluginSource::Remote);
|
||||
let existing_is_local_share =
|
||||
@@ -1350,6 +1401,7 @@ fn preferred_local_plugin_sources(
|
||||
marketplace_path: marketplace_path.clone(),
|
||||
plugin_name: plugin.name.clone(),
|
||||
installed: plugin.installed,
|
||||
install_policy: plugin.install_policy,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1360,6 +1412,13 @@ fn plugin_detail_status_label(plugin: &PluginSummary) -> &'static str {
|
||||
if plugin.availability == PluginAvailability::DisabledByAdmin {
|
||||
return "Disabled by admin";
|
||||
}
|
||||
if plugin.install_policy == PluginInstallPolicy::InstalledByDefault {
|
||||
return if plugin.installed {
|
||||
"Installed by admin"
|
||||
} else {
|
||||
"Enabled by Admin"
|
||||
};
|
||||
}
|
||||
if plugin.installed {
|
||||
if plugin.enabled {
|
||||
"Installed"
|
||||
@@ -1370,7 +1429,7 @@ fn plugin_detail_status_label(plugin: &PluginSummary) -> &'static str {
|
||||
match plugin.install_policy {
|
||||
PluginInstallPolicy::NotAvailable => "Not installable",
|
||||
PluginInstallPolicy::Available => "Can be installed",
|
||||
PluginInstallPolicy::InstalledByDefault => "Available by default",
|
||||
PluginInstallPolicy::InstalledByDefault => "Installed by admin",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1643,10 +1702,10 @@ fn is_personal_marketplace_path(marketplace_path: &AbsolutePathBuf) -> bool {
|
||||
.is_some_and(|personal_path| personal_path.as_path() == marketplace_path.as_path())
|
||||
}
|
||||
|
||||
fn remote_section_loading_item(label: &str) -> SelectionItem {
|
||||
fn remote_section_loading_item(label: &str, description: &str) -> SelectionItem {
|
||||
SelectionItem {
|
||||
name: format!("Loading {label} plugins..."),
|
||||
description: Some("This section updates when app-server returns it.".to_string()),
|
||||
description: Some(description.to_string()),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}
|
||||
@@ -1670,7 +1729,7 @@ fn plugin_remote_section_error<'a>(
|
||||
.find(|section_error| section_error.section_id == section_id)
|
||||
}
|
||||
|
||||
fn remote_section_loading_tab(id: &str, label: &str) -> SelectionTab {
|
||||
fn remote_section_loading_tab(id: &str, label: &str, item_description: &str) -> SelectionTab {
|
||||
SelectionTab {
|
||||
id: format!("{REMOTE_LOADING_TAB_ID_PREFIX}{id}"),
|
||||
label: label.to_string(),
|
||||
@@ -1678,7 +1737,7 @@ fn remote_section_loading_tab(id: &str, label: &str) -> SelectionTab {
|
||||
format!("Loading {label} plugins."),
|
||||
"Local plugin functionality is already available.".to_string(),
|
||||
),
|
||||
items: vec![remote_section_loading_item(label)],
|
||||
items: vec![remote_section_loading_item(label, item_description)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1816,7 +1875,10 @@ fn plugin_brief_description_without_marketplace(
|
||||
|
||||
fn plugin_status_label(plugin: &PluginSummary) -> &'static str {
|
||||
if plugin.availability == PluginAvailability::DisabledByAdmin {
|
||||
return "Disabled by admin";
|
||||
return "Disabled";
|
||||
}
|
||||
if !plugin.installed && plugin.install_policy == PluginInstallPolicy::InstalledByDefault {
|
||||
return "Admin assigned";
|
||||
}
|
||||
if plugin.installed {
|
||||
if plugin.enabled {
|
||||
@@ -1828,7 +1890,7 @@ fn plugin_status_label(plugin: &PluginSummary) -> &'static str {
|
||||
match plugin.install_policy {
|
||||
PluginInstallPolicy::NotAvailable => "Not installable",
|
||||
PluginInstallPolicy::Available => "Available",
|
||||
PluginInstallPolicy::InstalledByDefault => "Available by default",
|
||||
PluginInstallPolicy::InstalledByDefault => "Installed",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1863,6 +1925,7 @@ fn plugin_detail_request_for_entry(
|
||||
&& let Some(remote_plugin_id) = plugin_remote_identity(plugin)
|
||||
&& let Some(preferred_source) = preferred_local_sources.get(remote_plugin_id)
|
||||
&& preferred_source.installed == plugin.installed
|
||||
&& preferred_source.install_policy == plugin.install_policy
|
||||
{
|
||||
return Some((
|
||||
PluginLocation::Local {
|
||||
|
||||
+9
-9
@@ -3,16 +3,16 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: strip_osc8_for_snapshot(&popup)
|
||||
---
|
||||
Plugins
|
||||
Figma · Installed · ChatGPT Marketplace
|
||||
Figma · Installed by admin · ChatGPT Marketplace
|
||||
Turn Figma files into implementation context.
|
||||
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
2. Uninstall plugin Remove this plugin now.
|
||||
Source Local
|
||||
Auth Auth on install
|
||||
Skills design-review, extract-copy
|
||||
Hooks PreToolUse (1), Stop (2)
|
||||
Apps Figma, Slack
|
||||
MCP Servers figma-mcp, docs-mcp
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
Installed by admin This plugin is installed by your workspace admin.
|
||||
Source Local
|
||||
Auth Auth on install
|
||||
Skills design-review, extract-copy
|
||||
Hooks PreToolUse (1), Stop (2)
|
||||
Apps Figma, Slack
|
||||
MCP Servers figma-mcp, docs-mcp
|
||||
|
||||
Press esc to close.
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: popup
|
||||
---
|
||||
Plugins
|
||||
Browse plugins from available marketplaces.
|
||||
Installed 1 of 1 available plugins.
|
||||
|
||||
[All Plugins] Installed (1) OpenAI Curated Workspace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
› [!] Admin Blocked Disabled Press Enter to view plugin details.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
+5
-5
@@ -6,12 +6,12 @@ expression: popup
|
||||
Browse plugins from available marketplaces.
|
||||
Installed 1 of 4 available plugins.
|
||||
|
||||
[All Plugins] Installed (1) OpenAI Curated Workspace Shared with me Repo Marketplace Add Marketplace
|
||||
[All Plugins] Installed (1) OpenAI Curated Workspace Repo Marketplace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
|
||||
[-] Bravo Search Available · OpenAI Curated · Search docs and tickets.
|
||||
[-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins.
|
||||
[-] Starter Available by default · OpenAI Curated · Included by default.
|
||||
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
|
||||
[-] Bravo Search Available · OpenAI Curated · Search docs and tickets.
|
||||
[-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins.
|
||||
[-] Starter Admin assigned · OpenAI Curated · Included by default.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: loaded_popup
|
||||
---
|
||||
Plugins
|
||||
Workspace.
|
||||
This section loaded successfully.
|
||||
|
||||
All Plugins Installed (0) OpenAI Curated [Workspace] Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
› No workspace plugins available No workspace directory plugins are available.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 473
|
||||
expression: popup
|
||||
---
|
||||
Plugins
|
||||
Debug Marketplace installed successfully.
|
||||
Select the plugins you want to use and press Enter to install or view details.
|
||||
|
||||
All Plugins Installed (0) OpenAI Curated Workspace Shared with me [Debug Marketplace]
|
||||
Add Marketplace
|
||||
All Plugins Installed (0) OpenAI Curated Workspace [Debug Marketplace] Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
› [-] Debug Plugin Available Press Enter to install or view plugin details.
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 767
|
||||
assertion_line: 1684
|
||||
expression: popup
|
||||
---
|
||||
Plugins
|
||||
Browse plugins from available marketplaces.
|
||||
Installed 0 of 3 available plugins.
|
||||
|
||||
[All Plugins] Installed (0) OpenAI Curated Workspace Shared with me Add Marketplace
|
||||
[All Plugins] Installed (0) OpenAI Curated Workspace Add Marketplace
|
||||
|
||||
sla
|
||||
› [-] Slack Available Press Enter to install or view plugin details.
|
||||
|
||||
@@ -186,7 +186,7 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_
|
||||
Some("Starter"),
|
||||
Some("Included by default."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
/*enabled*/ false,
|
||||
PluginInstallPolicy::InstalledByDefault,
|
||||
),
|
||||
]),
|
||||
@@ -665,10 +665,9 @@ async fn plugin_detail_popup_snapshot_labels_personal_marketplace_as_local() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
|
||||
async fn plugin_detail_popup_distinguishes_admin_installed_from_enabled() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = plugins_test_summary(
|
||||
"plugin-figma",
|
||||
"figma",
|
||||
@@ -676,7 +675,7 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
|
||||
Some("Design handoff."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
PluginInstallPolicy::InstalledByDefault,
|
||||
);
|
||||
let response = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
summary.clone(),
|
||||
@@ -684,32 +683,54 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
|
||||
chat.add_plugins_output();
|
||||
let mut plugin = plugins_test_detail(
|
||||
summary,
|
||||
Some("Turn Figma files into implementation context."),
|
||||
&["design-review", "extract-copy"],
|
||||
&[
|
||||
(codex_app_server_protocol::HookEventName::PreToolUse, 1),
|
||||
(codex_app_server_protocol::HookEventName::Stop, 2),
|
||||
],
|
||||
&["Figma", "Slack"],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
);
|
||||
chat.on_plugin_detail_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(PluginReadResponse {
|
||||
plugin: plugins_test_detail(
|
||||
summary,
|
||||
Some("Turn Figma files into implementation context."),
|
||||
&["design-review", "extract-copy"],
|
||||
&[
|
||||
(codex_app_server_protocol::HookEventName::PreToolUse, 1),
|
||||
(codex_app_server_protocol::HookEventName::Stop, 2),
|
||||
],
|
||||
&["Figma", "Slack"],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
),
|
||||
plugin: plugin.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
!popup.contains("Data shared with this app is subject to the app's"),
|
||||
"expected installed plugin details to hide the disclosure line, got:\n{popup}"
|
||||
popup.contains("Installed by admin")
|
||||
&& !popup.contains("Uninstall plugin")
|
||||
&& !popup.contains("Data shared with this app is subject to the app's"),
|
||||
"expected admin-installed plugin details to block uninstall and hide the disclosure line, got:\n{popup}"
|
||||
);
|
||||
assert_chatwidget_snapshot!(
|
||||
"plugin_detail_popup_installed",
|
||||
strip_osc8_for_snapshot(&popup)
|
||||
);
|
||||
|
||||
plugin.summary.installed = false;
|
||||
plugin.summary.enabled = false;
|
||||
chat.on_plugin_detail_loaded(cwd.to_path_buf(), Ok(PluginReadResponse { plugin }));
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("Enabled by Admin")
|
||||
&& popup.contains("Install plugin")
|
||||
&& !popup.contains("Installed by admin"),
|
||||
"expected an unmaterialized default plugin to show its admin assignment and remain installable, got:\n{popup}"
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
popup
|
||||
.lines()
|
||||
.find(|line| line.contains("Figma ·"))
|
||||
.expect("expected plugin detail header")
|
||||
.trim(),
|
||||
@"Figma · Enabled by Admin · ChatGPT Marketplace"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -769,17 +790,20 @@ async fn plugins_popup_remote_row_opens_remote_detail() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_remote_install_uses_remote_location() {
|
||||
async fn plugin_detail_unmaterialized_default_uses_remote_install_path() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = plugins_test_remote_summary(
|
||||
"plugins~Plugin_linear",
|
||||
"linear",
|
||||
Some("Linear"),
|
||||
Some("Issue tracking."),
|
||||
/*installed*/ false,
|
||||
);
|
||||
let summary = PluginSummary {
|
||||
install_policy: PluginInstallPolicy::InstalledByDefault,
|
||||
..plugins_test_remote_summary(
|
||||
"plugins~Plugin_linear",
|
||||
"linear",
|
||||
Some("Linear"),
|
||||
Some("Issue tracking."),
|
||||
/*installed*/ false,
|
||||
)
|
||||
};
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(
|
||||
cwd.to_path_buf(),
|
||||
@@ -1075,9 +1099,12 @@ async fn plugins_popup_admin_disabled_installed_plugin_has_no_toggle_hint() {
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 120);
|
||||
assert_chatwidget_snapshot!("plugins_popup_admin_disabled_installed", popup);
|
||||
assert!(
|
||||
popup.contains("Disabled by admin")
|
||||
popup.contains("[!] Admin Blocked")
|
||||
&& popup.contains("Disabled")
|
||||
&& popup.contains("Press Enter to view plugin details.")
|
||||
&& !popup.contains("Disabled by admin")
|
||||
&& !popup.contains("Space to disable"),
|
||||
"expected admin-disabled installed row to omit toggle hint, got:\n{popup}"
|
||||
);
|
||||
@@ -1170,9 +1197,16 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() {
|
||||
let curated_loading_popup =
|
||||
select_tab_containing(&mut chat, "Loading OpenAI Curated plugins...");
|
||||
let workspace_loading_popup = select_tab_containing(&mut chat, "Loading Workspace plugins.");
|
||||
let shared_loading_popup = select_tab_containing(&mut chat, "Loading Shared with me plugins.");
|
||||
let _ = select_tab_containing(&mut chat, "Loading Workspace plugins.");
|
||||
|
||||
chat.on_plugin_remote_sections_loaded(cwd.to_path_buf(), Vec::new(), Vec::new());
|
||||
let shared_empty_popup = select_tab_containing(&mut chat, "Shared with me.");
|
||||
let loaded_popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert_chatwidget_snapshot!("plugins_popup_empty_shared_section_hidden", loaded_popup);
|
||||
assert!(
|
||||
!loaded_popup.contains("Shared with me"),
|
||||
"expected empty shared section to stay hidden, got:\n{loaded_popup}"
|
||||
);
|
||||
|
||||
chat.on_plugin_remote_sections_loaded(
|
||||
cwd.to_path_buf(),
|
||||
@@ -1203,20 +1237,20 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() {
|
||||
[
|
||||
remote_section_state(&curated_loading_popup),
|
||||
remote_section_state(&workspace_loading_popup),
|
||||
remote_section_state(&shared_empty_popup),
|
||||
remote_section_state(&shared_loading_popup),
|
||||
remote_section_state(&workspace_error_popup),
|
||||
remote_section_state(&remote_curated_empty_popup),
|
||||
]
|
||||
.join("\n\n"),
|
||||
@r###"
|
||||
OpenAI Curated marketplace.
|
||||
Loading OpenAI Curated plugins... This section updates when app-server returns it.
|
||||
Loading OpenAI Curated plugins... This updates when OpenAI Curated plugins finish loading.
|
||||
|
||||
Loading Workspace plugins.
|
||||
Loading Workspace plugins... This section updates when app-server returns it.
|
||||
Loading Workspace plugins... This updates when workspace plugins finish loading.
|
||||
|
||||
Shared with me.
|
||||
No shared plugins available No plugins have been shared with you.
|
||||
Loading Shared with me plugins.
|
||||
Loading Shared with me plugins... This updates when shared plugins finish loading.
|
||||
|
||||
Workspace unavailable.
|
||||
Workspace unavailable Sign in to ChatGPT to load workspace plugins.
|
||||
@@ -1228,13 +1262,13 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_installed_remote_row_keeps_remote_detail_when_local_share_is_uninstalled() {
|
||||
async fn plugins_popup_remote_detail_tracks_physical_and_policy_install_state() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let remote_plugin_id = "plugins~Plugin_docs";
|
||||
let remote_marketplace_name = "workspace-shared-with-me-private";
|
||||
let local_summary = PluginSummary {
|
||||
let mut local_summary = PluginSummary {
|
||||
share_context: Some(PluginShareContext {
|
||||
remote_plugin_id: remote_plugin_id.to_string(),
|
||||
remote_version: None,
|
||||
@@ -1251,13 +1285,13 @@ async fn plugins_popup_installed_remote_row_keeps_remote_detail_when_local_share
|
||||
Some("Local editable docs plugin."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
PluginInstallPolicy::InstalledByDefault,
|
||||
)
|
||||
};
|
||||
let popup = render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(vec![local_summary]),
|
||||
plugins_test_curated_marketplace(vec![local_summary.clone()]),
|
||||
PluginMarketplaceEntry {
|
||||
name: remote_marketplace_name.to_string(),
|
||||
path: None,
|
||||
@@ -1315,6 +1349,62 @@ async fn plugins_popup_installed_remote_row_keeps_remote_detail_when_local_share
|
||||
}
|
||||
other => panic!("expected FetchPluginDetail event, got {other:?}"),
|
||||
}
|
||||
|
||||
local_summary.install_policy = PluginInstallPolicy::Available;
|
||||
let mut remote_summary = plugins_test_remote_summary(
|
||||
remote_plugin_id,
|
||||
"docs",
|
||||
Some("Docs"),
|
||||
Some("Shared docs plugin."),
|
||||
/*installed*/ false,
|
||||
);
|
||||
remote_summary.install_policy = PluginInstallPolicy::InstalledByDefault;
|
||||
let popup = render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(vec![local_summary]),
|
||||
PluginMarketplaceEntry {
|
||||
name: remote_marketplace_name.to_string(),
|
||||
path: None,
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Shared with me".to_string()),
|
||||
}),
|
||||
plugins: vec![remote_summary],
|
||||
},
|
||||
]),
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Installed 0 of 1 available plugins.") && popup.contains("Admin assigned"),
|
||||
"expected the unmaterialized admin-assigned remote duplicate to win without counting as installed, got:\n{popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
let installed_popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
installed_popup.contains("Showing 0 installed plugins.")
|
||||
&& installed_popup.contains("No installed plugins")
|
||||
&& !installed_popup.contains("Docs"),
|
||||
"expected the unmaterialized admin-assigned plugin to stay out of the Installed tab, got:\n{installed_popup}"
|
||||
);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Left));
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert!(matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::OpenPluginDetailLoading { .. })
|
||||
));
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::FetchPluginDetail { params, .. }) => {
|
||||
assert_eq!(params.marketplace_path, None);
|
||||
assert_eq!(
|
||||
params.remote_marketplace_name,
|
||||
Some(remote_marketplace_name.to_string())
|
||||
);
|
||||
assert_eq!(params.plugin_name, remote_plugin_id);
|
||||
}
|
||||
other => panic!("expected FetchPluginDetail event, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1685,6 +1775,44 @@ async fn plugins_popup_search_filters_visible_rows_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_search_matches_plugin_descriptions() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
type_plugins_search_query(&mut chat, "document");
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("Drive") && !popup.contains("Calendar"),
|
||||
"expected plugin search to match descriptions, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_installed_tab_filters_rows_and_clears_search() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -223,7 +223,7 @@ where
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn with_screen_size_and_cursor_position(
|
||||
fn with_screen_size_and_cursor_position(
|
||||
backend: B,
|
||||
screen_size: Size,
|
||||
cursor_pos: Position,
|
||||
@@ -245,6 +245,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_screen_size_and_cursor_position_for_test(
|
||||
backend: B,
|
||||
screen_size: Size,
|
||||
cursor_pos: Position,
|
||||
) -> Self {
|
||||
Self::with_screen_size_and_cursor_position(backend, screen_size, cursor_pos)
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame<'_> {
|
||||
Frame {
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::custom_terminal::Terminal;
|
||||
|
||||
pub(crate) fn make_test_tui() -> io::Result<Tui> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::with_screen_size_and_cursor_position(
|
||||
let terminal = Terminal::with_screen_size_and_cursor_position_for_test(
|
||||
backend,
|
||||
Size {
|
||||
width: 80,
|
||||
|
||||
Reference in New Issue
Block a user