Show plugin hooks in plugin details (#21447)

Supersedes the abandoned #19859, rebuilt on latest `main`.

# Why

PR #19705 adds discovery for hooks bundled with plugins, but `/plugins`
still only shows skills, apps, and MCP servers. This follow-up makes
bundled hooks visible in the same plugin detail view so users can
inspect the full plugin surface in one place.

We also need `PluginHookSummary` to populate Plugin Hooks in the app;
`hooks/list` is not enough there because plugin detail needs to show
hooks for disabled plugins too.

# What

- extend `plugin/read` with `PluginHookSummary` entries for bundled
hooks
- summarize plugin hooks while loading plugin details
- render a `Hooks` row in the `/plugins` detail popup

<img width="3456" height="848" alt="CleanShot 2026-04-27 at 11 45 34@2x"
src="https://github.com/user-attachments/assets/fe3a38d6-a260-4351-8513-fb04c93d725b"
/>
This commit is contained in:
Abhinav
2026-05-07 00:21:14 -07:00
committed by GitHub
parent 898f5bfeaa
commit 40e282849c
23 changed files with 436 additions and 25 deletions
+29
View File
@@ -1728,6 +1728,12 @@ impl ChatWidget {
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)),
@@ -2142,6 +2148,29 @@ fn plugin_app_summary(plugin: &PluginDetail) -> String {
}
}
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()
@@ -11,6 +11,7 @@ expression: popup
1. Back to plugins Return to the plugin list.
2. Install plugin Install this plugin now.
Skills design-review, extract-copy
Hooks PreToolUse (1), Stop (2)
Apps Figma, Slack
MCP Servers figma-mcp, docs-mcp
@@ -9,6 +9,7 @@ expression: popup
1. Back to plugins Return to the plugin list.
2. Uninstall plugin Remove this plugin now.
Skills design-review, extract-copy
Hooks PreToolUse (1), Stop (2)
Apps Figma, Slack
MCP Servers figma-mcp, docs-mcp
@@ -1505,6 +1505,7 @@ pub(super) fn plugins_test_detail(
summary: PluginSummary,
description: Option<&str>,
skills: &[&str],
hooks: &[(codex_app_server_protocol::HookEventName, usize)],
apps: &[(&str, bool)],
mcp_servers: &[&str],
) -> PluginDetail {
@@ -1526,6 +1527,18 @@ pub(super) fn plugins_test_detail(
enabled: true,
})
.collect(),
hooks: hooks
.iter()
.enumerate()
.flat_map(|(event_index, (event_name, handler_count))| {
(0..*handler_count).map(move |handler_index| {
codex_app_server_protocol::PluginHookSummary {
key: format!("plugin:{event_index}:{handler_index}"),
event_name: *event_name,
}
})
})
.collect(),
apps: apps
.iter()
.map(|(name, needs_auth)| AppSummary {
@@ -656,6 +656,10 @@ async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summa
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", true), ("Slack", false)],
&["figma-mcp", "docs-mcp"],
),
@@ -696,6 +700,10 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
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", true), ("Slack", false)],
&["figma-mcp", "docs-mcp"],
),