mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Refine hook visibility modes
This commit is contained in:
@@ -1943,7 +1943,8 @@
|
||||
"HookVisibilityHint": {
|
||||
"enum": [
|
||||
"default",
|
||||
"hidden"
|
||||
"quiet",
|
||||
"verbose"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+2
-1
@@ -9832,7 +9832,8 @@
|
||||
"HookVisibilityHint": {
|
||||
"enum": [
|
||||
"default",
|
||||
"hidden"
|
||||
"quiet",
|
||||
"verbose"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+2
-1
@@ -6332,7 +6332,8 @@
|
||||
"HookVisibilityHint": {
|
||||
"enum": [
|
||||
"default",
|
||||
"hidden"
|
||||
"quiet",
|
||||
"verbose"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+2
-1
@@ -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
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1026,7 +1026,8 @@
|
||||
"HookVisibilityHint": {
|
||||
"enum": [
|
||||
"default",
|
||||
"hidden"
|
||||
"quiet",
|
||||
"verbose"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
}],
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -1523,7 +1523,8 @@ pub enum HookRunStatus {
|
||||
pub enum HookVisibilityHint {
|
||||
#[default]
|
||||
Default,
|
||||
Hidden,
|
||||
Quiet,
|
||||
Verbose,
|
||||
}
|
||||
|
||||
impl HookVisibilityHint {
|
||||
|
||||
@@ -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
|
||||
|
||||
-2
@@ -5,5 +5,3 @@ expression: "format!(\"started hidden:\\n{started_hidden_snapshot}\\nhistory:\\n
|
||||
started hidden:
|
||||
|
||||
history:
|
||||
• SessionStart hook (completed)
|
||||
hook context: session context
|
||||
-16
@@ -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
|
||||
+25
@@ -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
|
||||
-1
@@ -4,4 +4,3 @@ expression: combined
|
||||
---
|
||||
• PostToolUse hook (completed)
|
||||
warning: Heads up from the hook
|
||||
hook context: Remember the startup checklist.
|
||||
|
||||
-1
@@ -4,4 +4,3 @@ expression: combined
|
||||
---
|
||||
• PreToolUse hook (completed)
|
||||
warning: Heads up from the hook
|
||||
hook context: Remember the startup checklist.
|
||||
|
||||
-1
@@ -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}")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user