Refine hook visibility modes

This commit is contained in:
Abhinav Vedmala
2026-05-10 17:55:48 -04:00
parent e75419b47d
commit 50970e5ae0
22 changed files with 114 additions and 71 deletions
@@ -1943,7 +1943,8 @@
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
"quiet",
"verbose"
],
"type": "string"
},
@@ -9832,7 +9832,8 @@
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
"quiet",
"verbose"
],
"type": "string"
},
@@ -6332,7 +6332,8 @@
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
"quiet",
"verbose"
],
"type": "string"
},
@@ -181,7 +181,8 @@
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
"quiet",
"verbose"
],
"type": "string"
}
@@ -181,7 +181,8 @@
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
"quiet",
"verbose"
],
"type": "string"
}
@@ -2,4 +2,4 @@
// 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";
export type HookVisibilityHint = "default" | "quiet" | "verbose";
@@ -81,7 +81,7 @@ v2_enum_from_core!(
#[derive(Default)]
pub enum HookVisibilityHint from CoreHookVisibilityHint {
#[default]
Default, Hidden
Default, Quiet, Verbose
}
);
+1 -1
View File
@@ -1560,7 +1560,7 @@ 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.
Hook lifecycle notifications include each run's `visibilityHint`. Clients may use `"default"` for concise foreground rendering, `"quiet"` to suppress transient background rows, and `"verbose"` to preserve the full hook stream.
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.
+2 -2
View File
@@ -23,7 +23,7 @@ fn hooks_file_deserializes_existing_json_shape() {
"command": "python3 /tmp/pre.py",
"timeout": 10,
"statusMessage": "checking",
"visibilityHint": "hidden"
"visibilityHint": "quiet"
}
]
}
@@ -44,7 +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,
visibility_hint: codex_protocol::protocol::HookVisibilityHint::Quiet,
}],
}],
..Default::default()
+2 -1
View File
@@ -1026,7 +1026,8 @@
"HookVisibilityHint": {
"enum": [
"default",
"hidden"
"quiet",
"verbose"
],
"type": "string"
},
+3 -3
View File
@@ -186,7 +186,7 @@ fn preview_runs_preserve_configured_visibility_hint() {
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
visibility_hint: codex_protocol::protocol::HookVisibilityHint::Hidden,
visibility_hint: codex_protocol::protocol::HookVisibilityHint::Quiet,
}],
}],
..Default::default()
@@ -233,7 +233,7 @@ fn preview_runs_preserve_configured_visibility_hint() {
assert_eq!(preview.len(), 1);
assert_eq!(
preview[0].visibility_hint,
codex_protocol::protocol::HookVisibilityHint::Hidden
codex_protocol::protocol::HookVisibilityHint::Quiet
);
}
@@ -514,7 +514,7 @@ fn presentation_only_visibility_changes_preserve_existing_hook_trust() {
"hooks": [{
"type": "command",
"command": "python3 /tmp/user.py",
"visibilityHint": "hidden",
"visibilityHint": "quiet",
}],
}],
},
+2 -1
View File
@@ -1523,7 +1523,8 @@ pub enum HookRunStatus {
pub enum HookVisibilityHint {
#[default]
Default,
Hidden,
Quiet,
Verbose,
}
impl HookVisibilityHint {
+2 -2
View File
@@ -4073,7 +4073,7 @@ impl ChatWidget {
}
fn on_hook_started(&mut self, run: codex_app_server_protocol::HookRunSummary) {
if history_cell::hook_run_is_hidden(&run) {
if history_cell::hook_run_should_skip_render(&run) {
return;
}
self.flush_answer_stream_with_separator();
@@ -4095,7 +4095,7 @@ impl ChatWidget {
}
fn on_hook_completed(&mut self, completed: codex_app_server_protocol::HookRunSummary) {
if history_cell::hook_run_is_hidden(&completed) {
if history_cell::hook_run_should_skip_render(&completed) {
return;
}
let completed_existing_run = self
@@ -5,5 +5,3 @@ expression: "format!(\"started hidden:\\n{started_hidden_snapshot}\\nhistory:\\n
started hidden:
history:
• SessionStart hook (completed)
hook context: session context
@@ -1,16 +0,0 @@
---
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 (completed)
warning: background hook warning
• PostToolUse hook (failed)
error: hook exited with code 7
@@ -0,0 +1,25 @@
---
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: "format!(\"{default_running_snapshot}\\n\\n{default_completed_snapshot}\\n\\nvisible history:\\n{rendered}\")"
---
default running hook
live hooks:
• Running PostToolUse hook: injecting background context
history:
<empty>
default completed hook
live hooks:
<empty>
history:
<empty>
visible history:
• PostToolUse hook (completed)
warning: background hook warning
• PostToolUse hook (failed)
error: hook exited with code 7
• PostToolUse hook (completed)
hook context: verbose visible context
@@ -4,4 +4,3 @@ expression: combined
---
• PostToolUse hook (completed)
warning: Heads up from the hook
hook context: Remember the startup checklist.
@@ -4,4 +4,3 @@ expression: combined
---
• PreToolUse hook (completed)
warning: Heads up from the hook
hook context: Remember the startup checklist.
@@ -4,4 +4,3 @@ expression: combined
---
• SessionStart hook (completed)
warning: Heads up from the hook
hook context: Remember the startup checklist.
@@ -2355,18 +2355,20 @@ async fn quiet_hook_linger_starts_when_delayed_redraw_reveals_hook() {
}
#[tokio::test]
async fn hidden_hooks_skip_background_rendering_but_keep_failures_visible() {
async fn visibility_hints_balance_running_rows_with_context_suppression() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let mut hidden_running = hook_started_run(
let default_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);
handle_hook_started(&mut chat, default_running);
reveal_running_hooks(&mut chat);
let default_running_snapshot =
hook_live_and_history_snapshot(&chat, "default running hook", "");
let mut hidden_context_only = hook_completed_run(
let default_context_only = hook_completed_run(
"post-tool-use:0:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
codex_app_server_protocol::HookRunStatus::Completed,
@@ -2375,13 +2377,22 @@ async fn hidden_hooks_skip_background_rendering_but_keep_failures_visible() {
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);
handle_hook_completed(&mut chat, default_context_only);
let quiet_snapshot = hook_live_and_history_snapshot(&chat, "hidden background hook", "");
expire_quiet_hook_linger(&mut chat);
let default_completed_snapshot =
hook_live_and_history_snapshot(&chat, "default completed hook", "");
assert!(drain_insert_history(&mut rx).is_empty());
let mut hidden_warning = hook_completed_run(
let mut quiet_running = hook_started_run(
"post-tool-use:1:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
Some("quiet background context"),
);
quiet_running.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Quiet;
handle_hook_started(&mut chat, quiet_running);
let mut quiet_warning = hook_completed_run(
"post-tool-use:1:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
codex_app_server_protocol::HookRunStatus::Completed,
@@ -2396,10 +2407,10 @@ async fn hidden_hooks_skip_background_rendering_but_keep_failures_visible() {
},
],
);
hidden_warning.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_completed(&mut chat, hidden_warning);
quiet_warning.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Quiet;
handle_hook_completed(&mut chat, quiet_warning);
let mut hidden_failed = hook_completed_run(
let mut quiet_failed = hook_completed_run(
"post-tool-use:2:/tmp/hooks.json",
codex_app_server_protocol::HookEventName::PostToolUse,
codex_app_server_protocol::HookRunStatus::Failed,
@@ -2414,16 +2425,30 @@ async fn hidden_hooks_skip_background_rendering_but_keep_failures_visible() {
},
],
);
hidden_failed.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Hidden;
handle_hook_completed(&mut chat, hidden_failed);
quiet_failed.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Quiet;
handle_hook_completed(&mut chat, quiet_failed);
let mut verbose_completed = hook_completed_run(
"post-tool-use:3:/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: "verbose visible context".to_string(),
}],
);
verbose_completed.visibility_hint = codex_app_server_protocol::HookVisibilityHint::Verbose;
handle_hook_completed(&mut chat, verbose_completed);
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}")
"hook_visibility_hint_rendering_snapshot",
format!(
"{default_running_snapshot}\n\n{default_completed_snapshot}\n\nvisible history:\n{rendered}"
)
);
}
@@ -2811,7 +2836,7 @@ async fn hidden_active_hook_does_not_add_transcript_separator() {
}
#[tokio::test]
async fn hook_completed_before_reveal_renders_completed_without_running_flash() {
async fn default_hook_completed_before_reveal_omits_context_only_completion() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
handle_hook_started(
@@ -2842,7 +2867,7 @@ async fn hook_completed_before_reveal_renders_completed_without_running_flash()
.map(|lines| lines_to_single_string(lines))
.collect::<String>();
assert_chatwidget_snapshot!(
"hook_completed_before_reveal_renders_completed_without_running_flash_snapshot",
"default_hook_completed_before_reveal_omits_context_only_completion_snapshot",
format!("started hidden:\n{started_hidden_snapshot}\nhistory:\n{history}")
);
}
+1 -1
View File
@@ -105,7 +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::hook_run_should_skip_render;
pub(crate) use hook_cell::new_active_hook_cell;
pub(crate) use hook_cell::new_completed_hook_cell;
+21 -14
View File
@@ -217,6 +217,8 @@ impl HookCell {
let Some(index) = self.runs.iter().position(|existing| existing.id == run.id) else {
return false;
};
let mut run = run;
run.entries = visible_hook_output_entries(run.visibility_hint, run.entries);
if hook_run_is_quiet_success(&run) {
if !self.runs[index]
.state
@@ -231,16 +233,12 @@ impl HookCell {
status_message,
status,
entries,
visibility_hint,
..
} = run;
let existing = &mut self.runs[index];
existing.event_name = event_name;
existing.status_message = status_message;
existing.state = HookRunState::completed(
status,
visible_hook_output_entries(visibility_hint, entries),
);
existing.state = HookRunState::completed(status, entries);
true
}
@@ -248,6 +246,8 @@ impl HookCell {
///
/// This is used for replay/restoration paths where the final run summary is already known.
pub(crate) fn add_completed_run(&mut self, run: HookRunSummary) {
let mut run = run;
run.entries = visible_hook_output_entries(run.visibility_hint, run.entries);
if hook_run_is_quiet_success(&run) {
return;
}
@@ -257,17 +257,13 @@ impl HookCell {
status_message,
status,
entries,
visibility_hint,
..
} = run;
self.runs.push(HookRunCell {
id,
event_name,
status_message,
state: HookRunState::completed(
status,
visible_hook_output_entries(visibility_hint, entries),
),
state: HookRunState::completed(status, entries),
});
}
@@ -693,9 +689,9 @@ 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
/// Returns true when a `quiet` hook has no user-relevant consequence to render at all.
pub(crate) fn hook_run_should_skip_render(run: &HookRunSummary) -> bool {
run.visibility_hint == HookVisibilityHint::Quiet
&& match run.status {
HookRunStatus::Running => true,
HookRunStatus::Completed => run
@@ -716,11 +712,22 @@ fn visible_hook_output_entries(
.collect()
}
/// `default` is concise, `quiet` is concise without transient running rows, and `verbose` preserves
/// the complete hook stream.
///
/// `Context` is the presentation-only payload suppressed by the concise modes: it can remain
/// available to the model without adding transcript noise for the user, while warnings, feedback,
/// stops, and errors still surface normally.
fn hook_output_entry_is_visible(
visibility_hint: HookVisibilityHint,
kind: HookOutputEntryKind,
) -> bool {
visibility_hint != HookVisibilityHint::Hidden || kind != HookOutputEntryKind::Context
match visibility_hint {
HookVisibilityHint::Default | HookVisibilityHint::Quiet => {
kind != HookOutputEntryKind::Context
}
HookVisibilityHint::Verbose => true,
}
}
fn hook_completed_bullet(status: HookRunStatus, entries: &[HookOutputEntry]) -> Span<'static> {