Skip to main content

ai_agent/utils/hooks/
hook_events.rs

1// Source: ~/claudecode/openclaudecode/src/utils/hooks/hookEvents.ts
2#![allow(dead_code)]
3
4use std::sync::{Arc, Mutex};
5
6use tokio::time::{Duration, interval};
7
8/// Hook events that are always emitted regardless of the includeHookEvents option
9const ALWAYS_EMITTED_HOOK_EVENTS: [&str; 2] = ["SessionStart", "Setup"];
10
11const MAX_PENDING_EVENTS: usize = 100;
12
13/// Hook started event
14#[derive(Debug, Clone)]
15pub struct HookStartedEvent {
16    pub hook_id: String,
17    pub hook_name: String,
18    pub hook_event: String,
19}
20
21/// Hook progress event
22#[derive(Debug, Clone)]
23pub struct HookProgressEvent {
24    pub hook_id: String,
25    pub hook_name: String,
26    pub hook_event: String,
27    pub stdout: String,
28    pub stderr: String,
29    pub output: String,
30}
31
32/// Hook response event
33#[derive(Debug, Clone)]
34pub struct HookResponseEvent {
35    pub hook_id: String,
36    pub hook_name: String,
37    pub hook_event: String,
38    pub output: String,
39    pub stdout: String,
40    pub stderr: String,
41    pub exit_code: Option<i32>,
42    pub outcome: HookOutcome,
43}
44
45/// Hook execution event
46#[derive(Debug, Clone)]
47pub enum HookExecutionEvent {
48    Started(HookStartedEvent),
49    Progress(HookProgressEvent),
50    Response(HookResponseEvent),
51}
52
53/// Hook outcome
54#[derive(Debug, Clone)]
55pub enum HookOutcome {
56    Success,
57    Error,
58    Cancelled,
59}
60
61/// Hook event handler type
62pub type HookEventHandler = Box<dyn Fn(HookExecutionEvent) + Send + Sync>;
63
64/// Parameters for progress output
65pub struct ProgressOutput {
66    pub stdout: String,
67    pub stderr: String,
68    pub output: String,
69}
70
71/// Parameters for starting a progress interval
72pub struct StartHookProgressParams {
73    pub hook_id: String,
74    pub hook_name: String,
75    pub hook_event: String,
76    pub get_output: Arc<
77        dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = ProgressOutput> + Send>>
78            + Send
79            + Sync,
80    >,
81    pub interval_ms: Option<u64>,
82}
83
84/// Internal state for hook events
85struct HookEventState {
86    pending_events: Vec<HookExecutionEvent>,
87    event_handler: Option<HookEventHandler>,
88    all_hook_events_enabled: bool,
89}
90
91lazy_static::lazy_static! {
92    static ref HOOK_EVENT_STATE: Arc<Mutex<HookEventState>> = Arc::new(Mutex::new(
93        HookEventState {
94            pending_events: Vec::new(),
95            event_handler: None,
96            all_hook_events_enabled: false,
97        }
98    ));
99}
100
101/// Register a handler for hook execution events
102pub fn register_hook_event_handler(handler: Option<HookEventHandler>) {
103    let mut state = HOOK_EVENT_STATE.lock().unwrap();
104
105    // Take pending events first
106    let events: Vec<HookExecutionEvent> = state.pending_events.drain(..).collect();
107    state.event_handler = handler;
108
109    // Deliver pending events to the new handler
110    if let Some(ref handler) = state.event_handler {
111        for event in events {
112            handler(event);
113        }
114    }
115}
116
117/// Emit a hook event
118fn emit(event: HookExecutionEvent) {
119    let mut state = HOOK_EVENT_STATE.lock().unwrap();
120    if let Some(ref handler) = state.event_handler {
121        handler(event);
122    } else {
123        state.pending_events.push(event);
124        if state.pending_events.len() > MAX_PENDING_EVENTS {
125            state.pending_events.remove(0);
126        }
127    }
128}
129
130/// Check if a hook event should be emitted
131fn should_emit(hook_event: &str) -> bool {
132    if ALWAYS_EMITTED_HOOK_EVENTS.contains(&hook_event) {
133        return true;
134    }
135    let state = HOOK_EVENT_STATE.lock().unwrap();
136    state.all_hook_events_enabled
137}
138
139/// Emit hook started event
140pub fn emit_hook_started(hook_id: &str, hook_name: &str, hook_event: &str) {
141    if !should_emit(hook_event) {
142        return;
143    }
144
145    emit(HookExecutionEvent::Started(HookStartedEvent {
146        hook_id: hook_id.to_string(),
147        hook_name: hook_name.to_string(),
148        hook_event: hook_event.to_string(),
149    }));
150}
151
152/// Emit hook progress event
153pub fn emit_hook_progress(
154    hook_id: &str,
155    hook_name: &str,
156    hook_event: &str,
157    stdout: &str,
158    stderr: &str,
159    output: &str,
160) {
161    if !should_emit(hook_event) {
162        return;
163    }
164
165    emit(HookExecutionEvent::Progress(HookProgressEvent {
166        hook_id: hook_id.to_string(),
167        hook_name: hook_name.to_string(),
168        hook_event: hook_event.to_string(),
169        stdout: stdout.to_string(),
170        stderr: stderr.to_string(),
171        output: output.to_string(),
172    }));
173}
174
175/// Start a progress interval that periodically emits hook progress events.
176/// Returns a function that stops the interval.
177pub fn start_hook_progress_interval(
178    params: StartHookProgressParams,
179) -> Arc<dyn Fn() + Send + Sync> {
180    if !should_emit(&params.hook_event) {
181        return Arc::new(|| {});
182    }
183
184    let interval_ms = params.interval_ms.unwrap_or(1000);
185    let hook_id = params.hook_id.clone();
186    let hook_name = params.hook_name.clone();
187    let hook_event = params.hook_event.clone();
188    let get_output = params.get_output.clone();
189
190    let stopped = Arc::new(Mutex::new(false));
191    let stopped_clone = stopped.clone();
192
193    // Spawn tokio task for progress polling
194    let handle = tokio::spawn(async move {
195        let mut last_emitted_output = String::new();
196        let mut interval = interval(Duration::from_millis(interval_ms));
197
198        loop {
199            interval.tick().await;
200
201            // Check if stopped
202            if *stopped_clone.lock().unwrap() {
203                break;
204            }
205
206            let output = get_output().await;
207            if output.output == last_emitted_output {
208                continue;
209            }
210            last_emitted_output = output.output.clone();
211
212            emit_hook_progress(
213                &hook_id,
214                &hook_name,
215                &hook_event,
216                &output.stdout,
217                &output.stderr,
218                &output.output,
219            );
220        }
221    });
222
223    // Return closure that stops the task
224    Arc::new(move || {
225        let mut stopped = stopped.lock().unwrap();
226        *stopped = true;
227        handle.abort();
228    })
229}
230
231/// Parameters for emitting a hook response
232pub struct EmitHookResponseParams {
233    pub hook_id: String,
234    pub hook_name: String,
235    pub hook_event: String,
236    pub output: String,
237    pub stdout: String,
238    pub stderr: String,
239    pub exit_code: Option<i32>,
240    pub outcome: HookOutcome,
241}
242
243/// Emit hook response event
244pub fn emit_hook_response(data: EmitHookResponseParams) {
245    // Always log full hook output to debug log for verbose mode debugging
246    let output_to_log =
247        if !data.stdout.is_empty() || !data.stderr.is_empty() || !data.output.is_empty() {
248            if !data.stdout.is_empty() {
249                Some(&data.stdout)
250            } else if !data.stderr.is_empty() {
251                Some(&data.stderr)
252            } else {
253                Some(&data.output)
254            }
255        } else {
256            None
257        };
258
259    if let Some(output) = output_to_log {
260        log_for_debugging(&format!(
261            "Hook {} ({}) {:?}:\n{}",
262            data.hook_name, data.hook_event, data.outcome, output
263        ));
264    }
265
266    if !should_emit(&data.hook_event) {
267        return;
268    }
269
270    emit(HookExecutionEvent::Response(HookResponseEvent {
271        hook_id: data.hook_id,
272        hook_name: data.hook_name,
273        hook_event: data.hook_event,
274        output: data.output,
275        stdout: data.stdout,
276        stderr: data.stderr,
277        exit_code: data.exit_code,
278        outcome: data.outcome,
279    }));
280}
281
282/// Enable emission of all hook event types (beyond SessionStart and Setup)
283pub fn set_all_hook_events_enabled(enabled: bool) {
284    let mut state = HOOK_EVENT_STATE.lock().unwrap();
285    state.all_hook_events_enabled = enabled;
286}
287
288/// Clear hook event state
289pub fn clear_hook_event_state() {
290    let mut state = HOOK_EVENT_STATE.lock().unwrap();
291    state.event_handler = None;
292    state.pending_events.clear();
293    state.all_hook_events_enabled = false;
294}
295
296/// Log for debugging
297fn log_for_debugging(msg: &str) {
298    log::debug!("{}", msg);
299}