mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Add hook visibility hints
This commit is contained in:
@@ -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": {
|
||||
|
||||
+15
@@ -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": {
|
||||
|
||||
+15
@@ -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,
|
||||
|
||||
@@ -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,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 {},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -68,6 +68,7 @@ mod tests {
|
||||
timeout_sec: None,
|
||||
r#async: false,
|
||||
status_message: None,
|
||||
visibility_hint: Default::default(),
|
||||
},
|
||||
],
|
||||
}],
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
+13
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user