use super::*;
pub(super) fn hydrate_diagnostic_event(
ui_state: &mut state::MissionControlState,
event: &SessionEvent,
) {
let Some(message) = sanitized_payload_str(&event.payload, "message") else {
return;
};
let level =
sanitized_payload_str(&event.payload, "level").unwrap_or_else(|| "warning".to_string());
ui_state.apply_output_event_at(
&OutputEvent::Diagnostic { level, message },
Some(event.timestamp),
);
}
pub(super) fn hydrate_hook_diagnostic_event(
ui_state: &mut state::MissionControlState,
event: &SessionEvent,
) {
let payload = &event.payload;
let Some(message) = sanitized_payload_str(payload, "message") else {
return;
};
let diagnostic = HookDiagnostic {
phase: parse_hook_phase(
sanitized_payload_str(payload, "phase")
.as_deref()
.unwrap_or("after_tool"),
),
tool_name: sanitized_payload_str(payload, "tool")
.or_else(|| sanitized_payload_str(payload, "target_tool"))
.unwrap_or_else(|| "unknown".to_string()),
label: sanitized_payload_str(payload, "label").unwrap_or_else(|| "hook".to_string()),
category: match sanitized_payload_str(payload, "category").as_deref() {
Some("exit") => HookFailureCategory::Exit,
Some("timeout") => HookFailureCategory::Timeout,
Some("stdin") => HookFailureCategory::Stdin,
Some("output-limit") => HookFailureCategory::OutputLimit,
_ => HookFailureCategory::Runner,
},
policy: match sanitized_payload_str(payload, "policy").as_deref() {
Some("ignore") => HookFailurePolicy::Ignore,
Some("block") => HookFailurePolicy::Block,
Some("fail") => HookFailurePolicy::Fail,
_ => HookFailurePolicy::Warn,
},
target_ran: payload
.get("target_ran")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
message,
};
ui_state.apply_output_event_at(
&OutputEvent::HookDiagnostic {
diagnostic: Box::new(diagnostic),
},
Some(event.timestamp),
);
}
pub(super) fn parse_hook_phase(value: &str) -> HookPhase {
match value {
"before_tool" => HookPhase::Before,
"after_assistant" => HookPhase::AfterAssistant,
"after_reasoning" => HookPhase::AfterReasoning,
_ => HookPhase::After,
}
}
pub(super) fn hydrate_hook_lifecycle_event(
ui_state: &mut state::MissionControlState,
event: &SessionEvent,
tool_activity_ids: &std::collections::HashMap<String, ActivityId>,
started_hooks: &mut std::collections::HashSet<ActivityId>,
hook_activity_sequence: &mut usize,
) {
let payload = &event.payload;
let status = payload
.get("status")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
let id = payload
.get("activity_id")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
.map(ActivityId::new)
.unwrap_or_else(|| {
*hook_activity_sequence = hook_activity_sequence.saturating_add(1);
ActivityId::new(format!("history/hook-lifecycle-{hook_activity_sequence}"))
});
let parent_id = payload
.get("tool_call_id")
.and_then(serde_json::Value::as_str)
.and_then(|call_id| tool_activity_ids.get(call_id).cloned());
let metadata = hook_lifecycle_metadata(payload);
match status {
"started" => {
started_hooks.insert(id.clone());
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id,
parent_id,
kind: ActivityKind::Hook,
status: ActivityStatus::Running,
metadata,
});
}
"success" | "failed" | "blocked" | "ignored" => {
if !started_hooks.contains(&id) {
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id,
kind: ActivityKind::Hook,
status: ActivityStatus::Running,
metadata: metadata.clone(),
});
started_hooks.insert(id.clone());
}
ui_state.apply_historical_activity_event(ActivityEvent::Finished {
id,
status: if status == "success" {
ActivityStatus::Success
} else {
ActivityStatus::Failed
},
metadata: Some(metadata),
});
}
_ => {}
}
}
fn hook_lifecycle_metadata(payload: &serde_json::Value) -> ActivityMetadata {
let phase = sanitized_payload_str(payload, "phase").unwrap_or_else(|| "hook".to_string());
let label = sanitized_payload_str(payload, "label").unwrap_or_else(|| "hook".to_string());
let target_tool =
sanitized_payload_str(payload, "target_tool").unwrap_or_else(|| "unknown".to_string());
let status = sanitized_payload_str(payload, "status").unwrap_or_else(|| "unknown".to_string());
let policy = sanitized_payload_str(payload, "policy").unwrap_or_else(|| "warn".to_string());
let category = sanitized_payload_str(payload, "category");
let mut fields = vec![
("phase".to_string(), phase.clone()),
("hook_label".to_string(), label.clone()),
("target_tool".to_string(), target_tool.clone()),
("status".to_string(), status.clone()),
("policy".to_string(), policy.clone()),
];
if let Some(category) = &category {
fields.push(("category".to_string(), category.clone()));
}
if let Some(target_ran) = payload
.get("target_ran")
.and_then(serde_json::Value::as_bool)
{
fields.push(("target_ran".to_string(), target_ran.to_string()));
}
if let Some(elapsed_ms) = payload
.get("elapsed_ms")
.and_then(serde_json::Value::as_u64)
{
fields.push(("elapsed_ms".to_string(), elapsed_ms.to_string()));
}
let mut detail = format!("recorded {phase} hook '{label}' for tool '{target_tool}' {status}");
if let Some(category) = category {
detail.push_str(&format!(" category={category} policy={policy}"));
}
if let Some(message) = sanitized_payload_str(payload, "message") {
detail.push_str(&format!(": {message}"));
}
ActivityMetadata {
label: format!("{phase} hook {label} on {target_tool}"),
detail: Some(redact_sensitive_text(&detail)),
fields,
}
}