mirror of
https://github.com/openai/codex.git
synced 2026-11-05 08:28:29 +00: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:
1 parent
f15df624a6
commit
a0d5fd772e
12 files changed
+362
-112
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user