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

View File

@@ -17,6 +17,7 @@ use axum::http::Uri;
use axum::http::header::AUTHORIZATION;
use axum::routing::get;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
@@ -778,6 +779,7 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::create_dir_all(plugin_root.join("hooks"))?;
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer"))?;
std::fs::create_dir_all(plugin_root.join("skills/chatgpt-only"))?;
std::fs::write(
@@ -881,12 +883,44 @@ description: Visible only for ChatGPT
"command": "demo-server"
}
}
}"#,
)?;
std::fs::write(
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"
}
]
}
]
}
}"#,
)?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"[features]
plugins = true
plugin_hooks = true
[[skills.config]]
name = "demo-plugin:thread-summarizer"
@@ -894,6 +928,9 @@ enabled = false
[plugins."demo-plugin@codex-curated"]
enabled = true
[hooks.state."demo-plugin@codex-curated:hooks/hooks.json:pre_tool_use:0:0"]
enabled = false
"#,
)?;
write_installed_plugin(&codex_home, "codex-curated", "demo-plugin")?;
@@ -980,6 +1017,23 @@ enabled = true
"Summarize email threads"
);
assert!(!response.plugin.skills[0].enabled);
assert_eq!(
response.plugin.hooks,
vec![
codex_app_server_protocol::PluginHookSummary {
key: "demo-plugin@codex-curated:hooks/hooks.json:pre_tool_use:0:0".to_string(),
event_name: HookEventName::PreToolUse,
},
codex_app_server_protocol::PluginHookSummary {
key: "demo-plugin@codex-curated:hooks/hooks.json:pre_tool_use:0:1".to_string(),
event_name: HookEventName::PreToolUse,
},
codex_app_server_protocol::PluginHookSummary {
key: "demo-plugin@codex-curated:hooks/hooks.json:session_start:0:0".to_string(),
event_name: HookEventName::SessionStart,
},
]
);
assert_eq!(response.plugin.apps.len(), 1);
assert_eq!(response.plugin.apps[0].id, "gmail");
assert_eq!(response.plugin.apps[0].name, "gmail");