Add presentation hint for quiet managed hooks

This commit is contained in:
Abhinav Vedmala
2026-05-13 12:14:03 +00:00
parent 27e67a8c2a
commit c2a65edb6e
37 changed files with 465 additions and 1 deletions
+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
@@ -2917,6 +2917,9 @@ impl ChatWidget {
}
fn on_hook_started(&mut self, run: codex_app_server_protocol::HookRunSummary) {
if history_cell::hook_run_should_skip_render(&run) {
return;
}
self.flush_answer_stream_with_separator();
self.flush_completed_hook_output();
match self.active_hook_cell.as_mut() {
@@ -2936,6 +2939,9 @@ impl ChatWidget {
}
fn on_hook_completed(&mut self, completed: codex_app_server_protocol::HookRunSummary) {
if history_cell::hook_run_should_skip_render(&completed) {
return;
}
let completed_existing_run = self
.active_hook_cell
.as_mut()
@@ -0,0 +1,22 @@
---
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: "format!(\"{routine_running_snapshot}\\n\\n{routine_completed_snapshot}\\n\\n{failed_snapshot}\")"
---
routine running
live hooks:
<empty>
history:
<empty>
routine completed
live hooks:
<empty>
history:
<empty>
failed
live hooks:
<empty>
history:
• PostToolUse hook (failed)
error: hook exited with code 7
@@ -1483,6 +1483,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,
@@ -2545,6 +2545,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,
@@ -2568,6 +2569,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),
@@ -2776,6 +2778,58 @@ async fn completed_hook_with_output_flushes_immediately() {
);
}
#[tokio::test]
async fn visibility_hidden_hooks_skip_routine_rows_but_render_failures_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let mut routine_started = hook_started_run(
"post-tool-use:0:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
Some("checking output policy"),
);
routine_started.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_started(&mut chat, routine_started);
reveal_running_hooks(&mut chat);
let routine_running_snapshot = hook_live_and_history_snapshot(&chat, "routine running", "");
let mut routine_completed = hook_completed_run(
"post-tool-use:0:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
codex_app_server_protocol::HookRunStatus::Completed,
Vec::new(),
);
routine_completed.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_completed(&mut chat, routine_completed);
let routine_history = drain_insert_history(&mut rx)
.iter()
.map(|lines| lines_to_single_string(lines))
.collect::<String>();
let routine_completed_snapshot =
hook_live_and_history_snapshot(&chat, "routine completed", &routine_history);
let mut failed_completed = 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(),
}],
);
failed_completed.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_completed(&mut chat, failed_completed);
let failed_history = drain_insert_history(&mut rx)
.iter()
.map(|lines| lines_to_single_string(lines))
.collect::<String>();
let failed_snapshot = hook_live_and_history_snapshot(&chat, "failed", &failed_history);
assert_chatwidget_snapshot!(
"visibility_hidden_hooks_skip_routine_rows_but_render_failures_snapshot",
format!("{routine_running_snapshot}\n\n{routine_completed_snapshot}\n\n{failed_snapshot}")
);
}
#[tokio::test]
async fn completed_hook_output_precedes_following_assistant_message() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -3165,6 +3219,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
@@ -952,6 +952,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
@@ -106,6 +106,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_should_skip_render;
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;
@@ -679,6 +680,16 @@ pub(crate) fn new_completed_hook_cell(run: HookRunSummary, animations_enabled: b
HookCell::new_completed(run, animations_enabled)
}
/// Returns true when a hook marked as hidden has no user-relevant consequence to render.
pub(crate) fn hook_run_should_skip_render(run: &HookRunSummary) -> bool {
run.visibility_hint == HookVisibilityHint::Hidden
&& match run.status {
HookRunStatus::Running => true,
HookRunStatus::Completed => run.entries.is_empty(),
HookRunStatus::Blocked | HookRunStatus::Failed | HookRunStatus::Stopped => false,
}
}
/// Returns true for hook completions that should be invisible in history.
fn hook_run_is_quiet_success(run: &HookRunSummary) -> bool {
run.status == HookRunStatus::Completed && run.entries.is_empty()
@@ -802,6 +813,37 @@ mod tests {
);
}
#[test]
fn hidden_hook_skips_routine_running_and_empty_completion_rows() {
let mut run = hook_run_summary("hook-1");
run.visibility_hint = HookVisibilityHint::Hidden;
assert!(hook_run_should_skip_render(&run));
run.status = HookRunStatus::Completed;
assert!(hook_run_should_skip_render(&run));
}
#[test]
fn hidden_hook_still_surfaces_user_relevant_outcomes() {
let mut run = hook_run_summary("hook-1");
run.visibility_hint = HookVisibilityHint::Hidden;
run.status = HookRunStatus::Failed;
assert!(!hook_run_should_skip_render(&run));
run.status = HookRunStatus::Blocked;
assert!(!hook_run_should_skip_render(&run));
run.status = HookRunStatus::Stopped;
assert!(!hook_run_should_skip_render(&run));
run.status = HookRunStatus::Completed;
run.entries = vec![HookOutputEntry {
kind: HookOutputEntryKind::Warning,
text: "Review this hook result".to_string(),
}];
assert!(!hook_run_should_skip_render(&run));
}
fn hook_run_summary(id: &str) -> HookRunSummary {
HookRunSummary {
id: id.to_string(),
@@ -814,6 +856,7 @@ mod tests {
display_order: 0,
status: HookRunStatus::Running,
status_message: Some("checking output policy".to_string()),
visibility_hint: HookVisibilityHint::Default,
started_at: 1,
completed_at: None,
duration_ms: None,