diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index dd7f04d96a..e233bee8c0 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1943,7 +1943,8 @@ "HookVisibilityHint": { "enum": [ "default", - "hidden" + "quiet", + "verbose" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6f948a1457..c552ab6e8c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9832,7 +9832,8 @@ "HookVisibilityHint": { "enum": [ "default", - "hidden" + "quiet", + "verbose" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 3919383ab5..3274975501 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -6332,7 +6332,8 @@ "HookVisibilityHint": { "enum": [ "default", - "hidden" + "quiet", + "verbose" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json index 24f0d35e0d..96f5d5c7d8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json @@ -181,7 +181,8 @@ "HookVisibilityHint": { "enum": [ "default", - "hidden" + "quiet", + "verbose" ], "type": "string" } diff --git a/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json index 64b99592a2..76409034da 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json @@ -181,7 +181,8 @@ "HookVisibilityHint": { "enum": [ "default", - "hidden" + "quiet", + "verbose" ], "type": "string" } diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HookVisibilityHint.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HookVisibilityHint.ts index add4477a8e..a238b694de 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/HookVisibilityHint.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HookVisibilityHint.ts @@ -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"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs index 0080fd5388..1b1bf110f0 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs @@ -81,7 +81,7 @@ v2_enum_from_core!( #[derive(Default)] pub enum HookVisibilityHint from CoreHookVisibilityHint { #[default] - Default, Hidden + Default, Quiet, Verbose } ); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2a612c02e5..1246ffbed7 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -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. diff --git a/codex-rs/config/src/hooks_tests.rs b/codex-rs/config/src/hooks_tests.rs index ac2703147d..e8b438baf7 100644 --- a/codex-rs/config/src/hooks_tests.rs +++ b/codex-rs/config/src/hooks_tests.rs @@ -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() diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index a1c1211ea1..f5ca4ec7a7 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1026,7 +1026,8 @@ "HookVisibilityHint": { "enum": [ "default", - "hidden" + "quiet", + "verbose" ], "type": "string" }, diff --git a/codex-rs/hooks/src/engine/mod_tests.rs b/codex-rs/hooks/src/engine/mod_tests.rs index b8ed200356..6c610a5f8c 100644 --- a/codex-rs/hooks/src/engine/mod_tests.rs +++ b/codex-rs/hooks/src/engine/mod_tests.rs @@ -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", }], }], }, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 22b2099f6b..292dcb442e 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1523,7 +1523,8 @@ pub enum HookRunStatus { pub enum HookVisibilityHint { #[default] Default, - Hidden, + Quiet, + Verbose, } impl HookVisibilityHint { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index de4641339b..45944b2c82 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -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 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hook_completed_before_reveal_renders_completed_without_running_flash_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__default_hook_completed_before_reveal_omits_context_only_completion_snapshot.snap similarity index 73% rename from codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hook_completed_before_reveal_renders_completed_without_running_flash_snapshot.snap rename to codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__default_hook_completed_before_reveal_omits_context_only_completion_snapshot.snap index b38d4e260a..05d5f1fa7f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hook_completed_before_reveal_renders_completed_without_running_flash_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__default_hook_completed_before_reveal_omits_context_only_completion_snapshot.snap @@ -5,5 +5,3 @@ expression: "format!(\"started hidden:\\n{started_hidden_snapshot}\\nhistory:\\n started hidden: history: -• SessionStart hook (completed) - hook context: session context diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hidden_hook_background_and_failure_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hidden_hook_background_and_failure_snapshot.snap deleted file mode 100644 index aa44d32b8b..0000000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hidden_hook_background_and_failure_snapshot.snap +++ /dev/null @@ -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: - -history: - - -visible history: -• PostToolUse hook (completed) - warning: background hook warning - -• PostToolUse hook (failed) - error: hook exited with code 7 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hook_visibility_hint_rendering_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hook_visibility_hint_rendering_snapshot.snap new file mode 100644 index 0000000000..bb4be35f73 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hook_visibility_hint_rendering_snapshot.snap @@ -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: + + +default completed hook +live hooks: + +history: + + +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 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__post_tool_use_hook_events_render_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__post_tool_use_hook_events_render_snapshot.snap index b00225f1cd..a88c5e95fa 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__post_tool_use_hook_events_render_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__post_tool_use_hook_events_render_snapshot.snap @@ -4,4 +4,3 @@ expression: combined --- • PostToolUse hook (completed) warning: Heads up from the hook - hook context: Remember the startup checklist. diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pre_tool_use_hook_events_render_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pre_tool_use_hook_events_render_snapshot.snap index 311be2edf5..614b7a42c1 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pre_tool_use_hook_events_render_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pre_tool_use_hook_events_render_snapshot.snap @@ -4,4 +4,3 @@ expression: combined --- • PreToolUse hook (completed) warning: Heads up from the hook - hook context: Remember the startup checklist. diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__session_start_hook_events_render_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__session_start_hook_events_render_snapshot.snap index 3d08e82937..c480f2f5ec 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__session_start_hook_events_render_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__session_start_hook_events_render_snapshot.snap @@ -4,4 +4,3 @@ expression: combined --- • SessionStart hook (completed) warning: Heads up from the hook - hook context: Remember the startup checklist. diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 9332618eb5..01e0ae5a09 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -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::(); 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::(); 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}") ); } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 88d58e563b..1e2c03a3d3 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -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; diff --git a/codex-rs/tui/src/history_cell/hook_cell.rs b/codex-rs/tui/src/history_cell/hook_cell.rs index ddd8bb442f..5d7505387a 100644 --- a/codex-rs/tui/src/history_cell/hook_cell.rs +++ b/codex-rs/tui/src/history_cell/hook_cell.rs @@ -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> {