1use chrono::{DateTime, Local};
4
5use mi6_core::{Event, EventType};
6
7use super::string::{truncate_from_start, truncate_str, truncate_with_prefix};
8use super::table::{COL_APP, COL_EVENT, COL_PID, COL_SESSION, COL_SOURCE, COL_TIME};
9
10pub fn framework_abbrev(framework: Option<&str>) -> &'static str {
12 match framework {
13 Some("claude") => "CLA",
14 Some("gemini") => "GEM",
15 Some("cursor") => "CUR",
16 Some("opencode") => "OPE",
17 Some("codex") => "COD",
18 Some(_) => "???",
19 None => "---",
20 }
21}
22
23pub fn get_display_event_type(event: &Event) -> String {
26 if event.event_type == EventType::ApiRequest {
27 if event.tokens_input == Some(0)
29 && event.tokens_output == Some(0)
30 && let Some(ref metadata) = event.metadata
31 && let Ok(json) = serde_json::from_str::<serde_json::Value>(metadata)
32 && let Some(event_type) = json.get("event_type").and_then(|v| v.as_str())
33 {
34 return match event_type {
35 "conversation_starts" => "ConvStart".to_string(),
36 "user_prompt" => "UserPrompt".to_string(),
37 "tool_decision" => "ToolDecision".to_string(),
38 "tool_result" => "ToolResult".to_string(),
39 "api_request" => "ApiMeta".to_string(),
40 _ => "ApiRequest".to_string(),
41 };
42 }
43 "ApiRequest".to_string()
44 } else {
45 event.event_type.to_string()
46 }
47}
48
49pub fn print_event_row(event: &Event, show_app: bool, details_width: usize, raw_mode: bool) {
54 let local_time: DateTime<Local> = event.timestamp.into();
55 let time_str = local_time.format("%Y-%m-%d %H:%M:%S").to_string();
56 let time = truncate_str(&time_str, COL_TIME);
57 let event_type_str = get_display_event_type(event);
58 let event_type = truncate_str(&event_type_str, COL_EVENT);
59 let session_short = truncate_str(&event.session_id, COL_SESSION);
60 let pid_str = event.pid.map_or_else(|| "-".to_string(), |p| p.to_string());
61 let source = source_abbrev(event.source.as_deref());
62 let details_str = format_details(event);
63 let details = truncate_str(&details_str, details_width);
64
65 if show_app {
66 let app = framework_abbrev(event.framework.as_deref());
67 if raw_mode {
68 print!(
69 "{:<COL_TIME$} {:<COL_APP$} {:<COL_EVENT$} {:<COL_SESSION$} {:<COL_PID$} {:<COL_SOURCE$} {}\r\n",
70 time, app, event_type, session_short, pid_str, source, details
71 );
72 } else {
73 println!(
74 "{:<COL_TIME$} {:<COL_APP$} {:<COL_EVENT$} {:<COL_SESSION$} {:<COL_PID$} {:<COL_SOURCE$} {}",
75 time, app, event_type, session_short, pid_str, source, details
76 );
77 }
78 } else if raw_mode {
79 print!(
80 "{:<COL_TIME$} {:<COL_EVENT$} {:<COL_SESSION$} {:<COL_PID$} {:<COL_SOURCE$} {}\r\n",
81 time, event_type, session_short, pid_str, source, details
82 );
83 } else {
84 println!(
85 "{:<COL_TIME$} {:<COL_EVENT$} {:<COL_SESSION$} {:<COL_PID$} {:<COL_SOURCE$} {}",
86 time, event_type, session_short, pid_str, source, details
87 );
88 }
89}
90
91fn source_abbrev(source: Option<&str>) -> &'static str {
93 match source {
94 Some("hook") => "hook",
95 Some("otel") => "otel",
96 Some("transcript") => "tscr",
97 Some(_) => "?",
98 None => "-",
99 }
100}
101
102pub fn format_details(event: &Event) -> String {
104 let mut parts: Vec<String> = Vec::new();
105
106 if let Some(ref mode) = event.permission_mode
108 && mode != "default"
109 {
110 parts.push(format!("mode={}", mode));
111 }
112
113 match &event.event_type {
115 EventType::PreToolUse | EventType::PostToolUse => {
116 format_tool_use_details(event, &mut parts);
117 }
118 EventType::Notification => format_notification_details(event, &mut parts),
119 EventType::SessionStart => format_session_start_details(event, &mut parts),
120 EventType::ApiRequest => format_api_request_details(event, &mut parts),
121 _ => format_default_details(event, &mut parts),
122 }
123
124 parts.join(" ")
125}
126
127fn parse_payload(event: &Event) -> Option<serde_json::Value> {
129 event
130 .payload
131 .as_ref()
132 .and_then(|p| serde_json::from_str(p).ok())
133}
134
135fn format_tool_use_details(event: &Event, parts: &mut Vec<String>) {
137 if let Some(ref tool) = event.tool_name {
138 parts.push(format!("tool={}", tool));
139 }
140
141 if let Some(ref subagent) = event.subagent_type {
142 parts.push(format!("subagent={}", subagent));
143 }
144
145 if let Some(ref agent_id) = event.spawned_agent_id {
146 let short_id = truncate_from_start(agent_id, 12);
147 parts.push(format!("spawned={}", short_id));
148 }
149}
150
151fn format_notification_details(event: &Event, parts: &mut Vec<String>) {
153 let Some(payload) = parse_payload(event) else {
154 return;
155 };
156
157 let ntype = payload
158 .get("notification_type")
159 .and_then(|v| v.as_str())
160 .unwrap_or("?");
161 parts.push(format!("type={}", ntype));
162}
163
164fn format_session_start_details(event: &Event, parts: &mut Vec<String>) {
166 if let Some(payload) = parse_payload(event) {
167 let source = payload
168 .get("source")
169 .and_then(|v| v.as_str())
170 .unwrap_or("?");
171 parts.push(format!("source={}", source));
172 }
173
174 if let Some(ref path) = event.transcript_path {
175 let short_path = truncate_with_prefix(path, 40);
176 parts.push(format!("transcript={}", short_path));
177 }
178}
179
180fn format_api_request_details(event: &Event, parts: &mut Vec<String>) {
182 if event.tokens_input == Some(0)
184 && event.tokens_output == Some(0)
185 && let Some(details) = format_activity_event_details(event)
186 {
187 parts.push(details);
188 return;
189 }
190
191 if let Some(ref model) = event.model {
193 let short_model = model
195 .replace("claude-", "c-")
196 .replace("sonnet", "son")
197 .replace("opus", "op")
198 .replace("haiku", "hai");
199 parts.push(format!("model={}", short_model));
200 }
201
202 let tokens_in = event.tokens_input.unwrap_or(0);
204 let tokens_out = event.tokens_output.unwrap_or(0);
205 let total = tokens_in + tokens_out;
206 parts.push(format!("in={} out={} ({})", tokens_in, tokens_out, total));
207
208 if let Some(cache_read) = event.tokens_cache_read
210 && cache_read > 0
211 {
212 parts.push(format!("cache={}", cache_read));
213 }
214
215 if let Some(cost) = event.cost_usd {
217 parts.push(format!("${:.4}", cost));
218 }
219}
220
221fn format_activity_event_details(event: &Event) -> Option<String> {
223 let metadata = event.metadata.as_ref()?;
224 let json: serde_json::Value = serde_json::from_str(metadata).ok()?;
225 let event_type = json.get("event_type")?.as_str()?;
226
227 let mut parts: Vec<String> = Vec::new();
228
229 match event_type {
230 "conversation_starts" => {
231 if let Some(v) = json.get("approval_policy").and_then(|v| v.as_str()) {
232 parts.push(format!("approval={}", v));
233 }
234 if let Some(v) = json.get("sandbox_policy").and_then(|v| v.as_str()) {
235 parts.push(format!("sandbox={}", v));
236 }
237 if let Some(v) = json.get("provider").and_then(|v| v.as_str()) {
238 parts.push(format!("provider={}", v));
239 }
240 }
241 "user_prompt" => {
242 if let Some(v) = json.get("prompt_length").and_then(|v| v.as_i64()) {
243 parts.push(format!("len={}", v));
244 }
245 }
246 "tool_decision" => {
247 if let Some(v) = json.get("tool_name").and_then(|v| v.as_str()) {
248 parts.push(format!("tool={}", v));
249 }
250 if let Some(v) = json.get("decision").and_then(|v| v.as_str()) {
251 parts.push(format!("decision={}", v));
252 }
253 }
254 "tool_result" => {
255 if let Some(v) = json.get("tool_name").and_then(|v| v.as_str()) {
256 parts.push(format!("tool={}", v));
257 }
258 if let Some(v) = json.get("success").and_then(|v| v.as_bool()) {
259 parts.push(if v {
260 "ok".to_string()
261 } else {
262 "FAIL".to_string()
263 });
264 }
265 if let Some(v) = json.get("duration_ms").and_then(|v| v.as_i64()) {
266 parts.push(format!("{}ms", v));
267 }
268 }
269 "api_request" => {
270 if let Some(v) = json.get("status_code").and_then(|v| v.as_i64()) {
271 parts.push(format!("status={}", v));
272 }
273 if let Some(v) = json.get("attempt").and_then(|v| v.as_i64())
274 && v > 1
275 {
276 parts.push(format!("attempt={}", v));
277 }
278 if let Some(v) = json.get("duration_ms").and_then(|v| v.as_i64()) {
279 parts.push(format!("{}ms", v));
280 }
281 }
282 _ => return None,
283 }
284
285 if parts.is_empty() {
286 None
287 } else {
288 Some(parts.join(" "))
289 }
290}
291
292fn format_default_details(event: &Event, parts: &mut Vec<String>) {
294 if let Some(ref cwd) = event.cwd {
295 let short_cwd = truncate_with_prefix(cwd, 30);
296 parts.push(format!("cwd={}", short_cwd));
297 }
298}