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
@@ -25,6 +25,7 @@ use codex_config::McpServerConfig;
use codex_config::McpServerToolConfig;
use codex_config::types::McpServerTransportConfig;
use codex_login::CodexAuth;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::Product;
use codex_utils_absolute_path::test_support::PathBufExt;
use pretty_assertions::assert_eq;
@@ -1933,13 +1934,48 @@ async fn read_plugin_for_config_installed_git_source_reads_from_cache_without_cl
&cached_plugin_root.join(".mcp.json"),
r#"{"mcpServers":{"toolkit":{"command":"toolkit-mcp"}}}"#,
);
write_file(
&cached_plugin_root.join("hooks/hooks.json"),
r#"{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo startup"
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "echo first"
},
{
"type": "command",
"command": "echo second"
}
]
}
]
}
}"#,
);
write_file(
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
plugin_hooks = true
[plugins."toolkit@debug"]
enabled = true
[hooks.state."toolkit@debug:hooks/hooks.json:pre_tool_use:0:0"]
enabled = false
"#,
);
@@ -1978,6 +2014,23 @@ enabled = true
outcome.plugin.apps,
vec![AppConnectorId("connector_calendar".to_string())]
);
assert_eq!(
outcome.plugin.hooks,
vec![
PluginHookSummary {
key: "toolkit@debug:hooks/hooks.json:pre_tool_use:0:0".to_string(),
event_name: HookEventName::PreToolUse,
},
PluginHookSummary {
key: "toolkit@debug:hooks/hooks.json:pre_tool_use:0:1".to_string(),
event_name: HookEventName::PreToolUse,
},
PluginHookSummary {
key: "toolkit@debug:hooks/hooks.json:session_start:0:0".to_string(),
event_name: HookEventName::SessionStart,
},
]
);
assert_eq!(outcome.plugin.mcp_server_names, vec!["toolkit".to_string()]);
assert!(
!tmp.path()