Add hook visibility hints

This commit is contained in:
Abhinav Vedmala
2026-05-09 17:35:40 -04:00
parent 53468b97f6
commit 012d0aa0ab
38 changed files with 323 additions and 7 deletions
@@ -1873,6 +1873,14 @@
"string",
"null"
]
},
"visibilityHint": {
"allOf": [
{
"$ref": "#/definitions/HookVisibilityHint"
}
],
"default": "default"
}
},
"required": [
@@ -1932,6 +1940,13 @@
],
"type": "object"
},
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
],
"type": "string"
},
"ItemCompletedNotification": {
"properties": {
"completedAtMs": {
@@ -9751,6 +9751,14 @@
"string",
"null"
]
},
"visibilityHint": {
"allOf": [
{
"$ref": "#/definitions/v2/HookVisibilityHint"
}
],
"default": "default"
}
},
"required": [
@@ -9821,6 +9829,13 @@
],
"type": "string"
},
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
],
"type": "string"
},
"HooksListEntry": {
"properties": {
"cwd": {
@@ -6251,6 +6251,14 @@
"string",
"null"
]
},
"visibilityHint": {
"allOf": [
{
"$ref": "#/definitions/HookVisibilityHint"
}
],
"default": "default"
}
},
"required": [
@@ -6321,6 +6329,13 @@
],
"type": "string"
},
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
],
"type": "string"
},
"HooksListEntry": {
"properties": {
"cwd": {
@@ -132,6 +132,14 @@
"string",
"null"
]
},
"visibilityHint": {
"allOf": [
{
"$ref": "#/definitions/HookVisibilityHint"
}
],
"default": "default"
}
},
"required": [
@@ -169,6 +177,13 @@
"unknown"
],
"type": "string"
},
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
],
"type": "string"
}
},
"properties": {
@@ -132,6 +132,14 @@
"string",
"null"
]
},
"visibilityHint": {
"allOf": [
{
"$ref": "#/definitions/HookVisibilityHint"
}
],
"default": "default"
}
},
"required": [
@@ -169,6 +177,13 @@
"unknown"
],
"type": "string"
},
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
],
"type": "string"
}
},
"properties": {
@@ -9,5 +9,6 @@ import type { HookOutputEntry } from "./HookOutputEntry";
import type { HookRunStatus } from "./HookRunStatus";
import type { HookScope } from "./HookScope";
import type { HookSource } from "./HookSource";
import type { HookVisibilityHint } from "./HookVisibilityHint";
export type HookRunSummary = { id: string, eventName: HookEventName, handlerType: HookHandlerType, executionMode: HookExecutionMode, scope: HookScope, sourcePath: AbsolutePathBuf, source: HookSource, displayOrder: bigint, status: HookRunStatus, statusMessage: string | null, startedAt: bigint, completedAt: bigint | null, durationMs: bigint | null, entries: Array<HookOutputEntry>, };
export type HookRunSummary = { id: string, eventName: HookEventName, handlerType: HookHandlerType, executionMode: HookExecutionMode, scope: HookScope, sourcePath: AbsolutePathBuf, source: HookSource, displayOrder: bigint, status: HookRunStatus, statusMessage: string | null, visibilityHint: HookVisibilityHint, startedAt: bigint, completedAt: bigint | null, durationMs: bigint | null, entries: Array<HookOutputEntry>, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HookVisibilityHint = "default" | "hidden";
@@ -161,6 +161,7 @@ export type { HookScope } from "./HookScope";
export type { HookSource } from "./HookSource";
export type { HookStartedNotification } from "./HookStartedNotification";
export type { HookTrustStatus } from "./HookTrustStatus";
export type { HookVisibilityHint } from "./HookVisibilityHint";
export type { HooksListEntry } from "./HooksListEntry";
export type { HooksListParams } from "./HooksListParams";
export type { HooksListResponse } from "./HooksListResponse";
@@ -9,6 +9,7 @@ use codex_protocol::protocol::HookRunSummary as CoreHookRunSummary;
use codex_protocol::protocol::HookScope as CoreHookScope;
use codex_protocol::protocol::HookSource as CoreHookSource;
use codex_protocol::protocol::HookTrustStatus as CoreHookTrustStatus;
use codex_protocol::protocol::HookVisibilityHint as CoreHookVisibilityHint;
use codex_utils_absolute_path::AbsolutePathBuf;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -76,6 +77,14 @@ v2_enum_from_core!(
}
);
v2_enum_from_core!(
#[derive(Default)]
pub enum HookVisibilityHint from CoreHookVisibilityHint {
#[default]
Default, Hidden
}
);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -108,6 +117,8 @@ pub struct HookRunSummary {
pub display_order: i64,
pub status: HookRunStatus,
pub status_message: Option<String>,
#[serde(default)]
pub visibility_hint: HookVisibilityHint,
pub started_at: i64,
pub completed_at: Option<i64>,
pub duration_ms: Option<i64>,
@@ -127,6 +138,7 @@ impl From<CoreHookRunSummary> for HookRunSummary {
display_order: value.display_order,
status: value.status.into(),
status_message: value.status_message,
visibility_hint: value.visibility_hint.into(),
started_at: value.started_at,
completed_at: value.completed_at,
duration_ms: value.duration_ms,
+2
View File
@@ -1560,6 +1560,8 @@ Use `hooks/list` to fetch discovered hooks for one or more `cwds`. Each result i
Hooks are returned even when disabled so clients can render and re-enable them. User-controlled state lives under `hooks.state`. Managed hooks are non-configurable, and user entries for managed hook keys are ignored during loading.
Hook lifecycle notifications include each run's `visibilityHint`. Clients may use `"hidden"` to suppress routine background rendering while still surfacing runs that fail, block, stop, or otherwise produce user-relevant output.
For unmanaged hooks, `currentHash` and `trustStatus` describe whether the current definition is first-seen, approved, or changed since approval. Only trusted unmanaged hooks become runnable. Hook keys combine the source identity with a trailing event/group/handler selector that is currently positional.
```json
@@ -517,6 +517,7 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand
timeout_sec,
r#async,
status_message,
visibility_hint: _,
} => ConfiguredHookHandler::Command {
command,
timeout_sec,
@@ -56,6 +56,7 @@ fn command_hook_hash(
timeout_sec: Some(timeout_sec),
r#async: false,
status_message: status_message.map(ToOwned::to_owned),
visibility_hint: Default::default(),
}],
},
};
+3
View File
@@ -3,6 +3,7 @@ use std::path::Path;
use std::path::PathBuf;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookVisibilityHint;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -132,6 +133,8 @@ pub enum HookHandlerConfig {
r#async: bool,
#[serde(default, rename = "statusMessage")]
status_message: Option<String>,
#[serde(default, rename = "visibilityHint")]
visibility_hint: HookVisibilityHint,
},
#[serde(rename = "prompt")]
Prompt {},
+11 -6
View File
@@ -18,12 +18,13 @@ fn hooks_file_deserializes_existing_json_shape() {
{
"matcher": "^Bash$",
"hooks": [
{
"type": "command",
"command": "python3 /tmp/pre.py",
"timeout": 10,
"statusMessage": "checking"
}
{
"type": "command",
"command": "python3 /tmp/pre.py",
"timeout": 10,
"statusMessage": "checking",
"visibilityHint": "hidden"
}
]
}
]
@@ -43,6 +44,7 @@ fn hooks_file_deserializes_existing_json_shape() {
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: codex_protocol::protocol::HookVisibilityHint::Hidden,
}],
}],
..Default::default()
@@ -77,6 +79,7 @@ statusMessage = "checking"
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: Default::default(),
}],
}],
..Default::default()
@@ -113,6 +116,7 @@ command = "python3 /tmp/pre.py"
timeout_sec: None,
r#async: false,
status_message: None,
visibility_hint: Default::default(),
}],
}],
..Default::default()
@@ -157,6 +161,7 @@ command = "python3 /enterprise/place/pre.py"
timeout_sec: None,
r#async: false,
status_message: None,
visibility_hint: Default::default(),
}],
}],
..Default::default()
+15
View File
@@ -971,6 +971,14 @@
"command"
],
"type": "string"
},
"visibilityHint": {
"allOf": [
{
"$ref": "#/definitions/HookVisibilityHint"
}
],
"default": "default"
}
},
"required": [
@@ -1020,6 +1028,13 @@
},
"type": "object"
},
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
],
"type": "string"
},
"HooksToml": {
"properties": {
"PermissionRequest": {
@@ -1160,6 +1160,7 @@ async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: Default::default(),
}],
}],
..Default::default()
+1
View File
@@ -724,6 +724,7 @@ mod tests {
display_order: 0,
status,
status_message: None,
visibility_hint: Default::default(),
started_at: 10,
completed_at: Some(37),
duration_ms: Some(27),
+1
View File
@@ -1315,6 +1315,7 @@ async fn refresh_runtime_config_refreshes_hooks() -> anyhow::Result<()> {
timeout_sec: Some(600),
r#async: false,
status_message: None,
visibility_hint: Default::default(),
}],
},
};
+1
View File
@@ -68,6 +68,7 @@ mod tests {
timeout_sec: None,
r#async: false,
status_message: None,
visibility_hint: Default::default(),
},
],
}],
+7
View File
@@ -391,6 +391,7 @@ fn append_matcher_groups(
timeout_sec,
r#async,
status_message,
visibility_hint,
} => {
if r#async {
warnings.push(format!(
@@ -412,6 +413,7 @@ fn append_matcher_groups(
timeout_sec: Some(timeout_sec),
r#async,
status_message: status_message.clone(),
visibility_hint,
};
let current_hash =
command_hook_hash(event_name, matcher, &group, normalized_handler);
@@ -455,6 +457,7 @@ fn append_matcher_groups(
command,
timeout_sec,
status_message,
visibility_hint,
source_path: source.path.clone(),
source: source.source,
display_order: *display_order,
@@ -611,6 +614,7 @@ mod tests {
timeout_sec: None,
r#async: false,
status_message: None,
visibility_hint: Default::default(),
}],
}
}
@@ -642,6 +646,7 @@ mod tests {
command: "echo hello".to_string(),
timeout_sec: 600,
status_message: None,
visibility_hint: Default::default(),
source_path: source_path.clone(),
source: hook_source(),
display_order: 0,
@@ -677,6 +682,7 @@ mod tests {
command: "echo hello".to_string(),
timeout_sec: 600,
status_message: None,
visibility_hint: Default::default(),
source_path: source_path.clone(),
source: hook_source(),
display_order: 0,
@@ -756,6 +762,7 @@ mod tests {
timeout_sec: None,
r#async: false,
status_message: None,
visibility_hint: Default::default(),
}],
}],
..Default::default()
+3
View File
@@ -75,6 +75,7 @@ pub(crate) fn running_summary(handler: &ConfiguredHandler) -> HookRunSummary {
display_order: handler.display_order,
status: HookRunStatus::Running,
status_message: handler.status_message.clone(),
visibility_hint: handler.visibility_hint,
started_at: chrono::Utc::now().timestamp(),
completed_at: None,
duration_ms: None,
@@ -121,6 +122,7 @@ pub(crate) fn completed_summary(
display_order: handler.display_order,
status,
status_message: handler.status_message.clone(),
visibility_hint: handler.visibility_hint,
started_at: run_result.started_at,
completed_at: Some(run_result.completed_at),
duration_ms: Some(run_result.duration_ms),
@@ -168,6 +170,7 @@ mod tests {
source: HookSource::User,
display_order,
env: std::collections::HashMap::new(),
visibility_hint: Default::default(),
}
}
+1
View File
@@ -46,6 +46,7 @@ pub(crate) struct ConfiguredHandler {
pub command: String,
pub timeout_sec: u64,
pub status_message: Option<String>,
pub visibility_hint: codex_protocol::protocol::HookVisibilityHint,
pub source_path: AbsolutePathBuf,
pub source: HookSource,
pub display_order: i64,
+73
View File
@@ -88,6 +88,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: Default::default(),
}],
}],
..Default::default()
@@ -169,6 +170,73 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
assert!(log_contents.contains("\"hook_event_name\": \"PreToolUse\""));
}
#[test]
fn preview_runs_preserve_configured_visibility_hint() {
let temp = tempdir().expect("create temp dir");
let managed_dir =
AbsolutePathBuf::try_from(temp.path().join("managed-hooks")).expect("absolute path");
fs::create_dir_all(managed_dir.as_path()).expect("create managed hooks dir");
let managed_hooks = managed_hooks_for_current_platform(
managed_dir,
HookEventsToml {
user_prompt_submit: vec![MatcherGroup {
matcher: None,
hooks: vec![HookHandlerConfig::Command {
command: "python3 /tmp/managed.py".to_string(),
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: codex_protocol::protocol::HookVisibilityHint::Hidden,
}],
}],
..Default::default()
},
);
let config_layer_stack = ConfigLayerStack::new(
Vec::new(),
ConfigRequirements {
managed_hooks: Some(ConstrainedWithSource::new(
Constrained::allow_any(managed_hooks.clone()),
Some(RequirementSource::CloudRequirements),
)),
..ConfigRequirements::default()
},
ConfigRequirementsToml {
hooks: Some(managed_hooks),
..ConfigRequirementsToml::default()
},
)
.expect("config layer stack");
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
CommandShell {
program: String::new(),
args: Vec::new(),
},
);
let preview = engine.preview_user_prompt_submit(
&crate::events::user_prompt_submit::UserPromptSubmitRequest {
session_id: ThreadId::new(),
turn_id: "turn-1".to_string(),
cwd: cwd(),
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
prompt: "hello".to_string(),
},
);
assert_eq!(preview.len(), 1);
assert_eq!(
preview[0].visibility_hint,
codex_protocol::protocol::HookVisibilityHint::Hidden
);
}
#[test]
fn unknown_requirement_source_hooks_stay_managed() {
let temp = tempdir().expect("create temp dir");
@@ -185,6 +253,7 @@ fn unknown_requirement_source_hooks_stay_managed() {
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: Default::default(),
}],
}],
..Default::default()
@@ -247,6 +316,7 @@ fn user_disablement_filters_non_managed_hooks_but_not_managed_hooks() {
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: Default::default(),
}],
}],
..Default::default()
@@ -466,6 +536,7 @@ fn requirements_managed_hooks_warn_when_managed_dir_is_missing() {
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: Default::default(),
}],
}],
..Default::default()
@@ -677,6 +748,7 @@ print(json.dumps({
timeout_sec: Some(10),
r#async: false,
status_message: None,
visibility_hint: Default::default(),
}],
}],
..Default::default()
@@ -785,6 +857,7 @@ fn plugin_hook_sources_expand_plugin_placeholders() {
timeout_sec: Some(5),
r#async: false,
status_message: None,
visibility_hint: Default::default(),
}],
}],
..Default::default()
+1
View File
@@ -591,6 +591,7 @@ mod tests {
source: codex_protocol::protocol::HookSource::User,
display_order: 0,
env: std::collections::HashMap::new(),
visibility_hint: Default::default(),
}
}
@@ -551,6 +551,7 @@ mod tests {
source: codex_protocol::protocol::HookSource::User,
display_order: 0,
env: std::collections::HashMap::new(),
visibility_hint: Default::default(),
}
}
@@ -613,6 +613,7 @@ mod tests {
source: codex_protocol::protocol::HookSource::User,
display_order: 0,
env: std::collections::HashMap::new(),
visibility_hint: Default::default(),
}
}
@@ -364,6 +364,7 @@ mod tests {
source: codex_protocol::protocol::HookSource::User,
display_order: 0,
env: std::collections::HashMap::new(),
visibility_hint: Default::default(),
}
}
+1
View File
@@ -531,6 +531,7 @@ mod tests {
source: codex_protocol::protocol::HookSource::User,
display_order: 0,
env: std::collections::HashMap::new(),
visibility_hint: Default::default(),
}
}
@@ -422,6 +422,7 @@ mod tests {
source: codex_protocol::protocol::HookSource::User,
display_order: 0,
env: std::collections::HashMap::new(),
visibility_hint: Default::default(),
}
}
+10
View File
@@ -1518,6 +1518,14 @@ pub enum HookRunStatus {
Stopped,
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum HookVisibilityHint {
#[default]
Default,
Hidden,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum HookOutputEntryKind {
@@ -1549,6 +1557,8 @@ pub struct HookRunSummary {
pub display_order: i64,
pub status: HookRunStatus,
pub status_message: Option<String>,
#[serde(default)]
pub visibility_hint: HookVisibilityHint,
#[ts(type = "number")]
pub started_at: i64,
#[ts(type = "number | null")]
+2
View File
@@ -413,6 +413,7 @@ mod tests {
display_order: 0,
status: AppServerHookRunStatus::Running,
status_message: Some("checking go-workflow input policy".to_string()),
visibility_hint: Default::default(),
started_at: 1,
completed_at: None,
duration_ms: None,
@@ -436,6 +437,7 @@ mod tests {
display_order: 0,
status: AppServerHookRunStatus::Stopped,
status_message: Some("checking go-workflow input policy".to_string()),
visibility_hint: Default::default(),
started_at: 1,
completed_at: Some(11),
duration_ms: Some(10),
+6
View File
@@ -4073,6 +4073,9 @@ impl ChatWidget {
}
fn on_hook_started(&mut self, run: codex_app_server_protocol::HookRunSummary) {
if history_cell::hook_run_is_hidden(&run) {
return;
}
self.flush_answer_stream_with_separator();
self.flush_completed_hook_output();
match self.active_hook_cell.as_mut() {
@@ -4092,6 +4095,9 @@ impl ChatWidget {
}
fn on_hook_completed(&mut self, completed: codex_app_server_protocol::HookRunSummary) {
if history_cell::hook_run_is_hidden(&completed) {
return;
}
let completed_existing_run = self
.active_hook_cell
.as_mut()
@@ -0,0 +1,13 @@
---
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: "format!(\"{quiet_snapshot}\\n\\nvisible history:\\n{rendered}\")"
---
hidden background hook
live hooks:
<empty>
history:
<empty>
visible history:
• PostToolUse hook (failed)
error: hook exited with code 7
@@ -1608,6 +1608,7 @@ pub(super) fn hook_run(
display_order: 0,
status,
status_message: Some(status_message.to_string()),
visibility_hint: Default::default(),
started_at: 1,
completed_at: matches!(
status,
@@ -2206,6 +2206,7 @@ async fn user_prompt_submit_app_server_hook_notifications_render_snapshot() {
display_order: 0,
status: AppServerHookRunStatus::Running,
status_message: Some("checking go-workflow input policy".to_string()),
visibility_hint: Default::default(),
started_at: 1,
completed_at: None,
duration_ms: None,
@@ -2229,6 +2230,7 @@ async fn user_prompt_submit_app_server_hook_notifications_render_snapshot() {
display_order: 0,
status: AppServerHookRunStatus::Stopped,
status_message: Some("checking go-workflow input policy".to_string()),
visibility_hint: Default::default(),
started_at: 1,
completed_at: Some(11),
duration_ms: Some(10),
@@ -2352,6 +2354,55 @@ async fn quiet_hook_linger_starts_when_delayed_redraw_reveals_hook() {
assert_eq!(active_hook_blob(&chat), "<empty>\n");
}
#[tokio::test]
async fn hidden_hooks_skip_background_rendering_but_keep_failures_visible() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let mut hidden_running = hook_started_run(
"post-tool-use:0:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
Some("injecting background context"),
);
hidden_running.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_started(&mut chat, hidden_running);
let mut hidden_context_only = hook_completed_run(
"post-tool-use:0:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
codex_app_server_protocol::HookRunStatus::Completed,
vec![codex_app_server_protocol::HookOutputEntry {
kind: codex_app_server_protocol::HookOutputEntryKind::Context,
text: "time is 12:34".to_string(),
}],
);
hidden_context_only.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_completed(&mut chat, hidden_context_only);
let quiet_snapshot = hook_live_and_history_snapshot(&chat, "hidden background hook", "");
assert!(drain_insert_history(&mut rx).is_empty());
let mut hidden_failed = hook_completed_run(
"post-tool-use:1:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
codex_app_server_protocol::HookRunStatus::Failed,
vec![codex_app_server_protocol::HookOutputEntry {
kind: codex_app_server_protocol::HookOutputEntryKind::Error,
text: "hook exited with code 7".to_string(),
}],
);
hidden_failed.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_completed(&mut chat, hidden_failed);
let rendered = drain_insert_history(&mut rx)
.iter()
.map(|lines| lines_to_single_string(lines))
.collect::<String>();
assert_chatwidget_snapshot!(
"hidden_hook_background_and_failure_snapshot",
format!("{quiet_snapshot}\n\nvisible history:\n{rendered}")
);
}
#[tokio::test]
async fn blocked_and_failed_hooks_render_feedback_and_errors() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -2826,6 +2877,7 @@ fn hook_run_summary(
display_order: 0,
status,
status_message: status_message.map(str::to_string),
visibility_hint: Default::default(),
started_at: 1,
completed_at: (status != codex_app_server_protocol::HookRunStatus::Running).then_some(2),
duration_ms: (status != codex_app_server_protocol::HookRunStatus::Running).then_some(1),
+1
View File
@@ -933,6 +933,7 @@ approval_policy = "never"
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: Default::default(),
}],
}],
..Default::default()
+1
View File
@@ -105,6 +105,7 @@ const RAW_TOOL_OUTPUT_WIDTH: usize = 10_000;
mod hook_cell;
pub(crate) use hook_cell::HookCell;
pub(crate) use hook_cell::hook_run_is_hidden;
pub(crate) use hook_cell::new_active_hook_cell;
pub(crate) use hook_cell::new_completed_hook_cell;
@@ -22,6 +22,7 @@ use codex_app_server_protocol::HookOutputEntry;
use codex_app_server_protocol::HookOutputEntryKind;
use codex_app_server_protocol::HookRunStatus;
use codex_app_server_protocol::HookRunSummary;
use codex_app_server_protocol::HookVisibilityHint;
use ratatui::prelude::*;
use ratatui::style::Stylize;
use ratatui::widgets::Paragraph;
@@ -684,6 +685,19 @@ fn hook_run_is_quiet_success(run: &HookRunSummary) -> bool {
run.status == HookRunStatus::Completed && run.entries.is_empty()
}
/// Returns true when a hook marked as background has no user-relevant consequence to render.
pub(crate) fn hook_run_is_hidden(run: &HookRunSummary) -> bool {
run.visibility_hint == HookVisibilityHint::Hidden
&& match run.status {
HookRunStatus::Running => true,
HookRunStatus::Completed => run
.entries
.iter()
.all(|entry| entry.kind == HookOutputEntryKind::Context),
HookRunStatus::Blocked | HookRunStatus::Failed | HookRunStatus::Stopped => false,
}
}
fn hook_completed_bullet(status: HookRunStatus, entries: &[HookOutputEntry]) -> Span<'static> {
match status {
HookRunStatus::Completed => {
@@ -814,6 +828,7 @@ mod tests {
display_order: 0,
status: HookRunStatus::Running,
status_message: Some("checking output policy".to_string()),
visibility_hint: Default::default(),
started_at: 1,
completed_at: None,
duration_ms: None,