mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Expose prompt hooks to clients
This commit is contained in:
+46
@@ -8609,6 +8609,32 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"continueOnBlock": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"statusMessage": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timeoutSec": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"prompt"
|
||||
@@ -8618,6 +8644,8 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"continueOnBlock",
|
||||
"prompt",
|
||||
"type"
|
||||
],
|
||||
"title": "PromptConfiguredHookHandler",
|
||||
@@ -10836,6 +10864,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"continueOnBlock": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"currentHash": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10864,12 +10898,24 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v2/HookSource"
|
||||
},
|
||||
|
||||
+46
@@ -4849,6 +4849,32 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"continueOnBlock": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"statusMessage": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timeoutSec": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"prompt"
|
||||
@@ -4858,6 +4884,8 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"continueOnBlock",
|
||||
"prompt",
|
||||
"type"
|
||||
],
|
||||
"title": "PromptConfiguredHookHandler",
|
||||
@@ -7187,6 +7215,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"continueOnBlock": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"currentHash": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7215,12 +7249,24 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookSource"
|
||||
},
|
||||
|
||||
+28
@@ -231,6 +231,32 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"continueOnBlock": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"statusMessage": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timeoutSec": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"prompt"
|
||||
@@ -240,6 +266,8 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"continueOnBlock",
|
||||
"prompt",
|
||||
"type"
|
||||
],
|
||||
"title": "PromptConfiguredHookHandler",
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"continueOnBlock": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"currentHash": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -79,12 +85,24 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookSource"
|
||||
},
|
||||
|
||||
+1
-1
@@ -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 ConfiguredHookHandler = { "type": "command", command: string, commandWindows: string | null, timeoutSec: bigint | null, async: boolean, statusMessage: string | null, } | { "type": "prompt", } | { "type": "agent", };
|
||||
export type ConfiguredHookHandler = { "type": "command", command: string, commandWindows: string | null, timeoutSec: bigint | null, async: boolean, statusMessage: string | null, } | { "type": "prompt", prompt: string, model: string | null, timeoutSec: bigint | null, statusMessage: string | null, continueOnBlock: boolean, } | { "type": "agent", };
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { HookHandlerType } from "./HookHandlerType";
|
||||
import type { HookSource } from "./HookSource";
|
||||
import type { HookTrustStatus } from "./HookTrustStatus";
|
||||
|
||||
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, enabled: boolean, isManaged: boolean, currentHash: string, trustStatus: HookTrustStatus, };
|
||||
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, prompt: string | null, model: string | null, continueOnBlock: boolean | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, enabled: boolean, isManaged: boolean, currentHash: string, trustStatus: HookTrustStatus, };
|
||||
|
||||
@@ -484,7 +484,19 @@ pub enum ConfiguredHookHandler {
|
||||
},
|
||||
#[serde(rename = "prompt")]
|
||||
#[ts(rename = "prompt")]
|
||||
Prompt {},
|
||||
Prompt {
|
||||
prompt: String,
|
||||
model: Option<String>,
|
||||
#[serde(rename = "timeoutSec")]
|
||||
#[ts(rename = "timeoutSec")]
|
||||
timeout_sec: Option<u64>,
|
||||
#[serde(rename = "statusMessage")]
|
||||
#[ts(rename = "statusMessage")]
|
||||
status_message: Option<String>,
|
||||
#[serde(rename = "continueOnBlock")]
|
||||
#[ts(rename = "continueOnBlock")]
|
||||
continue_on_block: bool,
|
||||
},
|
||||
#[serde(rename = "agent")]
|
||||
#[ts(rename = "agent")]
|
||||
Agent {},
|
||||
|
||||
@@ -514,6 +514,9 @@ pub struct HookMetadata {
|
||||
pub handler_type: HookHandlerType,
|
||||
pub matcher: Option<String>,
|
||||
pub command: Option<String>,
|
||||
pub prompt: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub continue_on_block: Option<bool>,
|
||||
pub timeout_sec: u64,
|
||||
pub status_message: Option<String>,
|
||||
pub source_path: AbsolutePathBuf,
|
||||
|
||||
@@ -1724,6 +1724,8 @@ Hooks are returned even when disabled so clients can render and re-enable them.
|
||||
|
||||
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.
|
||||
|
||||
Command hooks expose their definition in `command`, while prompt hooks expose it in `prompt`. The unused field is `null`.
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "hooks/list",
|
||||
@@ -1747,6 +1749,7 @@ For unmanaged hooks, `currentHash` and `trustStatus` describe whether the curren
|
||||
"isManaged": false,
|
||||
"matcher": "Bash",
|
||||
"command": "python3 /Users/me/hook.py",
|
||||
"prompt": null,
|
||||
"timeoutSec": 5,
|
||||
"statusMessage": "running hook",
|
||||
"sourcePath": "/Users/me/.codex/config.toml",
|
||||
|
||||
@@ -70,6 +70,9 @@ fn hooks_to_info(hooks: &[codex_hooks::HookListEntry]) -> Vec<HookMetadata> {
|
||||
handler_type: hook.handler_type.into(),
|
||||
matcher: hook.matcher.clone(),
|
||||
command: hook.command.clone(),
|
||||
prompt: hook.prompt.clone(),
|
||||
model: hook.model.clone(),
|
||||
continue_on_block: hook.continue_on_block,
|
||||
timeout_sec: hook.timeout_sec,
|
||||
status_message: hook.status_message.clone(),
|
||||
source_path: hook.source_path.clone(),
|
||||
|
||||
@@ -465,7 +465,19 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand
|
||||
r#async,
|
||||
status_message,
|
||||
},
|
||||
CoreHookHandlerConfig::Prompt { .. } => ConfiguredHookHandler::Prompt {},
|
||||
CoreHookHandlerConfig::Prompt {
|
||||
prompt,
|
||||
model,
|
||||
timeout_sec,
|
||||
status_message,
|
||||
continue_on_block,
|
||||
} => ConfiguredHookHandler::Prompt {
|
||||
prompt,
|
||||
model,
|
||||
timeout_sec,
|
||||
status_message,
|
||||
continue_on_block,
|
||||
},
|
||||
CoreHookHandlerConfig::Agent {} => ConfiguredHookHandler::Agent {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,34 @@ fn command_hook_hash(
|
||||
codex_config::version_for_toml(&value)
|
||||
}
|
||||
|
||||
fn prompt_hook_hash(
|
||||
event_name: &'static str,
|
||||
matcher: Option<&str>,
|
||||
prompt: &str,
|
||||
model: Option<&str>,
|
||||
timeout_sec: u64,
|
||||
status_message: Option<&str>,
|
||||
continue_on_block: bool,
|
||||
) -> String {
|
||||
let identity = NormalizedHookIdentity {
|
||||
event_name,
|
||||
group: codex_config::MatcherGroup {
|
||||
matcher: matcher.map(ToOwned::to_owned),
|
||||
hooks: vec![codex_config::HookHandlerConfig::Prompt {
|
||||
prompt: prompt.to_string(),
|
||||
model: model.map(ToOwned::to_owned),
|
||||
timeout_sec: Some(timeout_sec),
|
||||
status_message: status_message.map(ToOwned::to_owned),
|
||||
continue_on_block,
|
||||
}],
|
||||
},
|
||||
};
|
||||
let Ok(value) = codex_config::TomlValue::try_from(identity) else {
|
||||
unreachable!("normalized hook identity should serialize to TOML");
|
||||
};
|
||||
codex_config::version_for_toml(&value)
|
||||
}
|
||||
|
||||
fn write_user_hook_config(codex_home: &std::path::Path) -> Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
@@ -166,6 +194,9 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
|
||||
handler_type: HookHandlerType::Command,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("python3 /tmp/listed-hook.py".to_string()),
|
||||
prompt: None,
|
||||
model: None,
|
||||
continue_on_block: None,
|
||||
timeout_sec: 5,
|
||||
status_message: Some("running listed hook".to_string()),
|
||||
source_path: config_path,
|
||||
@@ -191,7 +222,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
async fn hooks_list_shows_discovered_plugin_hooks() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
write_plugin_hook_config(
|
||||
@@ -210,6 +241,20 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "prompt",
|
||||
"prompt": "Reject prompts that mention secrets: $ARGUMENTS",
|
||||
"model": "gpt-5-mini",
|
||||
"timeout": 30,
|
||||
"statusMessage": "checking prompt",
|
||||
"continueOnBlock": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
@@ -242,29 +287,62 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
data,
|
||||
vec![HooksListEntry {
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
hooks: vec![HookMetadata {
|
||||
key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(),
|
||||
event_name: HookEventName::PreToolUse,
|
||||
handler_type: HookHandlerType::Command,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("echo plugin hook".to_string()),
|
||||
timeout_sec: 7,
|
||||
status_message: Some("running plugin hook".to_string()),
|
||||
source_path: plugin_hooks_path,
|
||||
source: HookSource::Plugin,
|
||||
plugin_id: Some("demo@test".to_string()),
|
||||
display_order: 0,
|
||||
enabled: true,
|
||||
is_managed: false,
|
||||
current_hash: command_hook_hash(
|
||||
"pre_tool_use",
|
||||
Some("Bash"),
|
||||
"echo plugin hook",
|
||||
/*timeout_sec*/ 7,
|
||||
Some("running plugin hook"),
|
||||
),
|
||||
trust_status: HookTrustStatus::Untrusted,
|
||||
}],
|
||||
hooks: vec![
|
||||
HookMetadata {
|
||||
key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(),
|
||||
event_name: HookEventName::PreToolUse,
|
||||
handler_type: HookHandlerType::Command,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("echo plugin hook".to_string()),
|
||||
prompt: None,
|
||||
model: None,
|
||||
continue_on_block: None,
|
||||
timeout_sec: 7,
|
||||
status_message: Some("running plugin hook".to_string()),
|
||||
source_path: plugin_hooks_path.clone(),
|
||||
source: HookSource::Plugin,
|
||||
plugin_id: Some("demo@test".to_string()),
|
||||
display_order: 0,
|
||||
enabled: true,
|
||||
is_managed: false,
|
||||
current_hash: command_hook_hash(
|
||||
"pre_tool_use",
|
||||
Some("Bash"),
|
||||
"echo plugin hook",
|
||||
/*timeout_sec*/ 7,
|
||||
Some("running plugin hook"),
|
||||
),
|
||||
trust_status: HookTrustStatus::Untrusted,
|
||||
},
|
||||
HookMetadata {
|
||||
key: "demo@test:hooks/hooks.json:user_prompt_submit:0:0".to_string(),
|
||||
event_name: HookEventName::UserPromptSubmit,
|
||||
handler_type: HookHandlerType::Prompt,
|
||||
matcher: None,
|
||||
command: None,
|
||||
prompt: Some("Reject prompts that mention secrets: $ARGUMENTS".to_string()),
|
||||
model: Some("gpt-5-mini".to_string()),
|
||||
continue_on_block: Some(true),
|
||||
timeout_sec: 30,
|
||||
status_message: Some("checking prompt".to_string()),
|
||||
source_path: plugin_hooks_path,
|
||||
source: HookSource::Plugin,
|
||||
plugin_id: Some("demo@test".to_string()),
|
||||
display_order: 1,
|
||||
enabled: true,
|
||||
is_managed: false,
|
||||
current_hash: prompt_hook_hash(
|
||||
"user_prompt_submit",
|
||||
/*matcher*/ None,
|
||||
"Reject prompts that mention secrets: $ARGUMENTS",
|
||||
Some("gpt-5-mini"),
|
||||
/*timeout_sec*/ 30,
|
||||
Some("checking prompt"),
|
||||
/*continue_on_block*/ true,
|
||||
),
|
||||
trust_status: HookTrustStatus::Untrusted,
|
||||
},
|
||||
],
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
}]
|
||||
@@ -462,6 +540,9 @@ timeout = 5
|
||||
handler_type: HookHandlerType::Command,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("echo project hook".to_string()),
|
||||
prompt: None,
|
||||
model: None,
|
||||
continue_on_block: None,
|
||||
timeout_sec: 5,
|
||||
status_message: None,
|
||||
source_path: project_config_path,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::HookHandlerType;
|
||||
use codex_app_server_protocol::HookMetadata;
|
||||
use codex_app_server_protocol::HookSource;
|
||||
use codex_app_server_protocol::HookTrustStatus;
|
||||
@@ -38,7 +39,7 @@ use crate::style::accent_style;
|
||||
|
||||
const EVENT_COLUMN_WIDTH: usize = 22;
|
||||
const COUNT_COLUMN_WIDTH: usize = 12;
|
||||
const MAX_COMMAND_DETAIL_LINES: usize = 3;
|
||||
const MAX_HANDLER_DEFINITION_DETAIL_LINES: usize = 3;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum HooksBrowserPage {
|
||||
@@ -485,12 +486,27 @@ impl HooksBrowserView {
|
||||
width,
|
||||
/*max_lines*/ None,
|
||||
));
|
||||
let (definition_label, definition) = hook_definition(hook);
|
||||
lines.extend(detail_wrapped_lines(
|
||||
"Command",
|
||||
hook.command.as_deref().unwrap_or("-"),
|
||||
definition_label,
|
||||
definition,
|
||||
width,
|
||||
Some(MAX_COMMAND_DETAIL_LINES),
|
||||
Some(MAX_HANDLER_DEFINITION_DETAIL_LINES),
|
||||
));
|
||||
if hook.handler_type == HookHandlerType::Prompt {
|
||||
lines.push(detail_line(
|
||||
"Model",
|
||||
hook.model.as_deref().unwrap_or("default"),
|
||||
));
|
||||
lines.push(detail_line(
|
||||
"On block",
|
||||
if hook.continue_on_block.unwrap_or(false) {
|
||||
"continue"
|
||||
} else {
|
||||
"stop"
|
||||
},
|
||||
));
|
||||
}
|
||||
lines.push(detail_line("Timeout", &format!("{}s", hook.timeout_sec)));
|
||||
lines.push(detail_line("Trust", hook_trust_label(hook.trust_status)));
|
||||
lines
|
||||
@@ -727,6 +743,14 @@ fn hook_trust_label(status: HookTrustStatus) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_definition(hook: &HookMetadata) -> (&'static str, &str) {
|
||||
match hook.handler_type {
|
||||
HookHandlerType::Command => ("Command", hook.command.as_deref().unwrap_or("-")),
|
||||
HookHandlerType::Prompt => ("Prompt", hook.prompt.as_deref().unwrap_or("-")),
|
||||
HookHandlerType::Agent => ("Agent", "-"),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_label(event_name: HookEventName) -> &'static str {
|
||||
match event_name {
|
||||
HookEventName::PreToolUse => "PreToolUse",
|
||||
@@ -932,6 +956,9 @@ mod tests {
|
||||
is_managed,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some(command.to_string()),
|
||||
prompt: None,
|
||||
model: None,
|
||||
continue_on_block: None,
|
||||
timeout_sec: 30,
|
||||
status_message: None,
|
||||
source_path: test_path_buf("/tmp/hooks.json").abs(),
|
||||
@@ -1267,6 +1294,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_prompt_hook_definition() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let mut prompt_hook = hook(
|
||||
"path:prompt-hook",
|
||||
HookEventName::PostToolUse,
|
||||
HookSource::User,
|
||||
/*plugin_id*/ None,
|
||||
/*command*/ "",
|
||||
/*enabled*/ true,
|
||||
/*is_managed*/ false,
|
||||
/*display_order*/ 0,
|
||||
);
|
||||
prompt_hook.handler_type = HookHandlerType::Prompt;
|
||||
prompt_hook.command = None;
|
||||
prompt_hook.prompt = Some("Reject prompts that mention secrets: $ARGUMENTS".to_string());
|
||||
prompt_hook.model = Some("gpt-5.1".to_string());
|
||||
prompt_hook.continue_on_block = Some(true);
|
||||
let mut view = HooksBrowserView::new(
|
||||
vec![prompt_hook],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
AppEventSender::new(tx_raw),
|
||||
);
|
||||
view.state.selected_idx = view
|
||||
.event_rows()
|
||||
.iter()
|
||||
.position(|row| row.event_name == HookEventName::PostToolUse);
|
||||
view.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_snapshot!(
|
||||
"hooks_browser_prompt_hook_definition",
|
||||
render_lines(&view, /*width*/ 112)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_empty_handler_browser_message() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/hooks_browser_view.rs
|
||||
expression: "render_lines(&view, 112)"
|
||||
---
|
||||
|
||||
PostToolUse hooks
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
[x] Hook 1
|
||||
|
||||
Event PostToolUse
|
||||
Matcher Bash
|
||||
Source User config - /tmp/hooks.json
|
||||
Prompt Reject prompts that mention secrets: $ARGUMENTS
|
||||
Model gpt-5.1
|
||||
On block continue
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
Press space or enter to toggle; esc to go back
|
||||
@@ -309,6 +309,9 @@ mod tests {
|
||||
is_managed: false,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("/tmp/hook.sh".to_string()),
|
||||
prompt: None,
|
||||
model: None,
|
||||
continue_on_block: None,
|
||||
timeout_sec: 30,
|
||||
status_message: None,
|
||||
source_path: test_path_buf("/tmp/hooks.json").abs(),
|
||||
|
||||
Reference in New Issue
Block a user