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
Unverified
parent 898f5bfeaa
commit 40e282849c
23 changed files with 436 additions and 25 deletions
+7 -22
View File
@@ -192,7 +192,10 @@ fn append_plugin_hook_sources(
display_order,
HookHandlerSource {
path: &source_path,
key_source: format!("{plugin_id}:{source_relative_path}"),
key_source: crate::declarations::plugin_hook_key_source(
plugin_id.as_str(),
source_relative_path.as_str(),
),
source: HookSource::Plugin,
is_managed: false,
hook_states,
@@ -416,13 +419,8 @@ fn append_matcher_groups(
command.replace(&format!("${{{key}}}"), value)
});
// TODO(abhinav): replace this positional suffix with a durable hook id.
let key = format!(
"{}:{}:{}:{}",
source.key_source,
hook_event_key_label(event_name),
group_index,
handler_index
);
let key =
crate::hook_key(&source.key_source, event_name, group_index, handler_index);
let state = source.hook_states.get(&key);
let enabled = hook_enabled(source.is_managed, state);
let trusted_hash = hook_trusted_hash(source.is_managed, state);
@@ -497,7 +495,7 @@ fn command_hook_hash(
group.matcher = matcher.map(ToOwned::to_owned);
group.hooks = vec![normalized_handler];
let identity = NormalizedHookIdentity {
event_name: hook_event_key_label(event_name),
event_name: crate::hook_event_key_label(event_name),
group,
};
let Ok(value) = TomlValue::try_from(identity) else {
@@ -506,19 +504,6 @@ fn command_hook_hash(
version_for_toml(&value)
}
fn hook_event_key_label(event_name: codex_protocol::protocol::HookEventName) -> &'static str {
match event_name {
codex_protocol::protocol::HookEventName::PreToolUse => "pre_tool_use",
codex_protocol::protocol::HookEventName::PermissionRequest => "permission_request",
codex_protocol::protocol::HookEventName::PostToolUse => "post_tool_use",
codex_protocol::protocol::HookEventName::PreCompact => "pre_compact",
codex_protocol::protocol::HookEventName::PostCompact => "post_compact",
codex_protocol::protocol::HookEventName::SessionStart => "session_start",
codex_protocol::protocol::HookEventName::UserPromptSubmit => "user_prompt_submit",
codex_protocol::protocol::HookEventName::Stop => "stop",
}
}
fn hook_trust_status(
is_managed: bool,
current_hash: &str,