1use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Instant;
9
10use colored::Colorize;
11use serde_json::Value;
12
13use crate::display::{colors, icons, DetailLevel};
14use crate::event::EventKind;
15
16#[derive(Debug, Default)]
18pub struct RunStats {
19 pub tasks_passed: usize,
20 pub tasks_failed: usize,
21 pub tasks_skipped: usize,
22 pub total_input_tokens: u64,
23 pub total_output_tokens: u64,
24 pub total_cache_tokens: u64,
25 pub total_cost: f64,
26 pub ttft_values: Vec<u64>,
27 pub mcp_calls: u32,
28 pub mcp_retries: u32,
29 pub mcp_errors: u32,
30 pub media_stored: u32,
31 pub media_bytes: u64,
32 pub media_dedup: u32,
33 pub artifacts_count: u32,
34 pub artifacts_bytes: u64,
35 pub guardrails_passed: u32,
36 pub guardrails_failed: u32,
37 pub guardrails_escalations: u32,
38 pub structured_attempts: u32,
39 pub structured_success_layer: Option<u8>,
40 pub root_failure: Option<String>,
41 pub task_timeline: Vec<(String, String, u64, u64)>,
43 pub provider_calls: Vec<ProviderCallStat>,
45}
46
47#[derive(Debug)]
48pub struct ProviderCallStat {
49 pub task_id: String,
50 pub input_tokens: u64,
51 pub output_tokens: u64,
52 pub cache_tokens: u64,
53 pub ttft_ms: Option<u64>,
54 pub cost: f64,
55}
56
57impl RunStats {
58 pub fn apply_event(&mut self, event: &crate::event::Event) {
65 match &event.kind {
66 EventKind::ProviderResponded {
67 input_tokens,
68 output_tokens,
69 cache_read_tokens,
70 ttft_ms,
71 cost_usd,
72 ..
73 } => {
74 self.total_input_tokens += input_tokens;
75 self.total_output_tokens += output_tokens;
76 self.total_cache_tokens += cache_read_tokens;
77 self.total_cost += cost_usd;
78 if let Some(t) = ttft_ms {
79 self.ttft_values.push(*t);
80 }
81 self.provider_calls.push(ProviderCallStat {
82 task_id: event.kind.task_id().unwrap_or("?").to_string(),
83 input_tokens: *input_tokens,
84 output_tokens: *output_tokens,
85 cache_tokens: *cache_read_tokens,
86 ttft_ms: *ttft_ms,
87 cost: *cost_usd,
88 });
89 }
90
91 EventKind::McpError { .. } => {
92 self.mcp_errors += 1;
93 }
94
95 EventKind::McpInvoke { .. } => {
96 self.mcp_calls += 1;
97 }
98
99 EventKind::McpRetry { .. } => {
100 self.mcp_retries += 1;
101 }
102
103 EventKind::GuardrailPassed { .. } => {
104 self.guardrails_passed += 1;
105 }
106
107 EventKind::GuardrailFailed { .. } => {
108 self.guardrails_failed += 1;
109 }
110
111 EventKind::GuardrailEscalation { .. } => {
112 self.guardrails_escalations += 1;
113 }
114
115 EventKind::ArtifactWritten { size, .. } => {
116 self.artifacts_count += 1;
117 self.artifacts_bytes += size;
118 }
119
120 EventKind::MediaStored {
121 size_bytes,
122 deduplicated,
123 ..
124 } => {
125 self.media_stored += 1;
126 self.media_bytes += size_bytes;
127 if *deduplicated {
128 self.media_dedup += 1;
129 }
130 }
131
132 EventKind::StructuredOutputAttempt { .. } => {
133 self.structured_attempts += 1;
134 }
135
136 EventKind::StructuredOutputSuccess { layer, .. } => {
137 self.structured_success_layer = Some(*layer);
138 }
139
140 EventKind::TaskCompleted { .. } => {
141 self.tasks_passed += 1;
142 }
143
144 EventKind::TaskFailed { task_id, .. } => {
145 self.tasks_failed += 1;
146 if self.root_failure.is_none() {
147 self.root_failure = Some(task_id.to_string());
148 }
149 }
150
151 EventKind::TaskSkipped { .. } => {
152 self.tasks_skipped += 1;
153 }
154
155 _ => {}
156 }
157 }
158}
159
160pub struct CliRenderer {
161 detail: DetailLevel,
162 start: Instant,
163 pub(crate) stats: RunStats,
164 task_layers: HashMap<Arc<str>, usize>,
166 current_layer: usize,
168 term_width: u16,
170 task_starts: HashMap<String, (u64, String)>,
172 workflow_start_ms: u64,
174 last_rendered_id: Option<u64>,
177}
178
179impl CliRenderer {
180 pub fn new(detail: DetailLevel) -> Self {
181 let term_width = terminal_size::terminal_size()
182 .map(|(w, _)| w.0)
183 .unwrap_or(80);
184
185 Self {
186 detail,
187 start: Instant::now(),
188 stats: RunStats::default(),
189 task_layers: HashMap::new(),
190 current_layer: 0,
191 term_width,
192 task_starts: HashMap::new(),
193 workflow_start_ms: 0,
194 last_rendered_id: None,
195 }
196 }
197
198 pub fn last_rendered_id(&self) -> Option<u64> {
199 self.last_rendered_id
200 }
201
202 pub fn set_task_layers(&mut self, layers: HashMap<Arc<str>, usize>) {
204 self.task_layers = layers;
205 }
206
207 fn ts(&self) -> String {
209 let elapsed = self.start.elapsed().as_secs_f32();
210 format!("{:>6}", format!("+{:.1}s", elapsed))
211 .dimmed()
212 .to_string()
213 }
214
215 pub fn render_new_events(&mut self, events: &[crate::event::Event]) {
216 for event in events {
217 if self.last_rendered_id.is_none_or(|last| event.id > last) {
218 self.render(event);
219 self.last_rendered_id = Some(event.id);
220 }
221 }
222 }
223
224 pub fn render_kind(&mut self, kind: &crate::event::EventKind) {
225 let event = crate::event::Event {
226 id: 0,
227 timestamp_ms: self.start.elapsed().as_millis() as u64,
228 kind: kind.clone(),
229 };
230 self.render(&event);
231 }
232
233 pub fn render(&mut self, event: &crate::event::Event) {
235 if self.detail.is_json() {
236 match serde_json::to_string(event) {
238 Ok(json) => println!("{}", json),
239 Err(e) => {
240 eprintln!(
242 "{{\"error\":\"event_serialization_failed\",\"detail\":\"{}\"}}",
243 e.to_string().replace('"', "'")
244 );
245 }
246 }
247 return;
248 }
249
250 self.stats.apply_event(event);
252
253 match &event.kind {
254 EventKind::WorkflowStarted { .. } => {
258 self.workflow_start_ms = event.timestamp_ms;
259 }
261 EventKind::WorkflowPaused => {
262 println!("{} {} paused", self.ts(), "⏸".yellow());
263 }
264 EventKind::WorkflowResumed => {
265 println!("{} {} resumed", self.ts(), "▶".green());
266 }
267 EventKind::WorkflowAborted {
268 reason,
269 running_tasks,
270 ..
271 } => {
272 println!(
273 "{} {} {}",
274 self.ts(),
275 "⚠".red().bold(),
276 "ABORTED".red().bold()
277 );
278 println!(
279 "{} {} {}",
280 " ".repeat(6),
281 "reason:".dimmed(),
282 reason.red()
283 );
284 if !running_tasks.is_empty() {
285 let names: Vec<&str> = running_tasks.iter().map(|s| s.as_ref()).collect();
286 println!(
287 "{} {} {}",
288 " ".repeat(6),
289 "running:".dimmed(),
290 names.join(", ").yellow()
291 );
292 }
293 }
294
295 EventKind::TaskScheduled {
299 task_id,
300 dependencies,
301 } => {
302 if self.detail.show_layer_separators() {
304 if let Some(&layer) = self.task_layers.get(task_id) {
305 if layer > self.current_layer && self.current_layer > 0 {
306 println!();
307 let label = format!(" layer {} ", layer + 1);
308 let dash = "─ ".dimmed();
309 let half =
310 (self.term_width as usize / 4).saturating_sub(label.len() / 2);
311 println!(
312 "{}{}{}{}",
313 " ".repeat(14),
314 dash.to_string().repeat(half / 2),
315 label.dimmed(),
316 dash.to_string().repeat(half / 2)
317 );
318 println!();
319 }
320 self.current_layer = layer;
321 }
322 }
323
324 let deps_str = if dependencies.is_empty() {
325 "—".dimmed().to_string()
326 } else {
327 dependencies
328 .iter()
329 .map(|d| d.as_ref())
330 .collect::<Vec<_>>()
331 .join(", ")
332 };
333 let padded_id = format!("{:<14}", task_id);
335 println!(
336 "{} {} {} {} {} {}",
337 self.ts(),
338 icons::pending(),
339 " ".normal(), padded_id.bold(),
341 "scheduled".dimmed(),
342 format!("deps: {}", deps_str).dimmed()
343 );
344 }
345
346 EventKind::TaskStarted { task_id, verb, .. } => {
347 self.task_starts
348 .insert(task_id.to_string(), (event.timestamp_ms, verb.to_string()));
349 let padded_id = format!("{:<14}", task_id);
350 println!(
351 "{} {} {} {} {}",
352 self.ts(),
353 icons::running(),
354 icons::verb(verb),
355 padded_id.bold(),
356 "running".white()
357 );
358 }
359
360 EventKind::TaskCompleted {
361 task_id,
362 output,
363 duration_ms,
364 } => {
365 let dur_secs = *duration_ms as f32 / 1000.0;
366
367 let verb = self
369 .task_starts
370 .get(task_id.as_ref())
371 .map(|(_, v)| v.clone())
372 .unwrap_or_default();
373
374 if let Some((start, _)) = self.task_starts.get(task_id.as_ref()) {
376 self.stats.task_timeline.push((
377 task_id.to_string(),
378 verb.clone(),
379 start.saturating_sub(self.workflow_start_ms),
380 *duration_ms,
381 ));
382 }
383
384 let padded_id = format!("{:<14}", task_id);
385 println!(
386 "{} {} {} {} {}",
387 self.ts(),
388 icons::success(),
389 icons::verb(&verb),
390 padded_id.bold(),
391 colors::duration(dur_secs)
392 );
393
394 if self.detail.show_previews() {
396 self.render_output_preview(output);
397 }
398 }
399
400 EventKind::TaskFailed {
401 task_id,
402 error,
403 duration_ms,
404 ..
405 } => {
406 let dur_secs = *duration_ms as f32 / 1000.0;
407 let verb = self
408 .task_starts
409 .get(task_id.as_ref())
410 .map(|(_, v)| v.clone())
411 .unwrap_or_default();
412
413 if let Some((start, _)) = self.task_starts.get(task_id.as_ref()) {
415 self.stats.task_timeline.push((
416 task_id.to_string(),
417 verb.clone(),
418 start.saturating_sub(self.workflow_start_ms),
419 *duration_ms,
420 ));
421 }
422
423 let padded_id = format!("{:<14}", task_id);
424 println!(
425 "{} {} {} {} {}",
426 self.ts(),
427 icons::failed(),
428 icons::verb(&verb),
429 padded_id.bold().red(),
430 colors::duration(dur_secs)
431 );
432 println!(
433 "{} {} {} {}",
434 " ".repeat(6),
435 "│".dimmed(),
436 "error".red(),
437 error.red()
438 );
439 }
440
441 EventKind::TaskSkipped { task_id, reason } => {
442 let padded_id = format!("{:<14}", task_id);
443 println!(
444 "{} {} {} {} {}",
445 self.ts(),
446 "\u{2298}".dimmed(),
447 " ".normal(),
448 padded_id.dimmed(),
449 format!("skipped \u{2014} {}", reason).dimmed()
450 );
451 }
452
453 EventKind::TemplateResolved {
457 task_id: _,
458 template,
459 result,
460 } => {
461 if self.detail.show_template_events() {
462 println!(
463 "{}",
464 super::format_event::fmt_template_resolved(template, result)
465 );
466 }
467 }
468
469 EventKind::ProviderCalled {
470 task_id: _,
471 provider,
472 model,
473 prompt_len,
474 } => {
475 if self.detail.show_sub_events() {
476 println!(
477 "{}",
478 super::format_event::fmt_provider_called(provider, model, *prompt_len)
479 );
480 }
481 }
482
483 EventKind::ProviderResponded {
484 input_tokens,
485 output_tokens,
486 cache_read_tokens,
487 ttft_ms,
488 cost_usd,
489 ..
490 } => {
491 if self.detail.show_sub_events() {
492 println!(
493 "{}",
494 super::format_event::fmt_provider_responded(
495 *input_tokens,
496 *output_tokens,
497 *cache_read_tokens,
498 *ttft_ms,
499 )
500 );
501 if self.detail.show_sparklines() {
502 println!(
503 "{}",
504 super::format_event::fmt_provider_sparkline(
505 *output_tokens,
506 *input_tokens,
507 *cost_usd,
508 )
509 );
510 }
511 }
512 }
513
514 EventKind::ContextAssembled {
518 task_id: _,
519 sources,
520 total_tokens,
521 budget_used_pct,
522 truncated: _,
523 ..
524 } => {
525 if self.detail.show_sub_events() {
526 println!(
527 "{}",
528 super::format_event::fmt_context_assembled(
529 sources.len(),
530 *total_tokens,
531 *budget_used_pct,
532 )
533 );
534 }
535 }
536
537 EventKind::McpConnected { server_name } => {
541 if self.detail.show_sub_events() {
542 println!("{}", super::format_event::fmt_mcp_connected(server_name));
543 }
544 }
545
546 EventKind::McpError { server_name, error } => {
547 println!("{}", super::format_event::fmt_mcp_error(server_name, error));
548 }
549
550 EventKind::McpInvoke {
551 call_id,
552 mcp_server,
553 tool,
554 resource,
555 ..
556 } => {
557 if self.detail.show_sub_events() {
558 println!(
559 "{}",
560 super::format_event::fmt_mcp_invoke(
561 mcp_server,
562 tool.as_deref(),
563 resource.as_deref(),
564 call_id,
565 )
566 );
567 }
568 }
569
570 EventKind::McpResponse {
571 call_id,
572 output_len,
573 duration_ms,
574 cached,
575 is_error,
576 ..
577 } => {
578 if self.detail.show_sub_events() {
579 println!(
580 "{}",
581 super::format_event::fmt_mcp_response(
582 call_id,
583 *output_len,
584 *duration_ms,
585 *cached,
586 *is_error,
587 )
588 );
589 }
590 }
591
592 EventKind::McpRetry {
593 operation,
594 attempt,
595 max_attempts,
596 error,
597 ..
598 } => {
599 println!(
600 "{}",
601 super::format_event::fmt_mcp_retry(operation, *attempt, *max_attempts, error,)
602 );
603 }
604
605 EventKind::AgentStart {
609 task_id: _,
610 max_turns,
611 mcp_servers,
612 } => {
613 if self.detail.show_sub_events() {
614 println!(
615 "{}",
616 super::format_event::fmt_agent_start(*max_turns, mcp_servers)
617 );
618 }
619 }
620
621 EventKind::AgentTurn {
622 task_id: _,
623 turn_index,
624 kind,
625 metadata,
626 } => {
627 if self.detail.show_sub_events() {
628 println!("{}", super::format_event::fmt_agent_turn(*turn_index, kind));
629 if let Some(meta) = metadata {
630 if meta.stop_reason == "tool_use" {
631 println!("{}", super::format_event::fmt_agent_turn_tool_use());
632 }
633 }
634 }
635 }
636
637 EventKind::AgentComplete {
638 task_id: _,
639 turns,
640 stop_reason,
641 } => {
642 if self.detail.show_sub_events() {
643 println!(
644 "{}",
645 super::format_event::fmt_agent_complete(*turns, stop_reason)
646 );
647 }
648 }
649
650 EventKind::AgentSpawned {
651 parent_task_id: _,
652 child_task_id,
653 depth,
654 } => {
655 if self.detail.show_sub_events() {
656 println!(
657 "{}",
658 super::format_event::fmt_agent_spawned(child_task_id, *depth)
659 );
660 }
661 }
662
663 EventKind::GuardrailPassed {
667 guardrail_type,
668 description,
669 ..
670 } => {
671 if self.detail.show_sub_events() {
672 println!(
673 "{}",
674 super::format_event::fmt_guardrail_passed(guardrail_type, description)
675 );
676 }
677 }
678
679 EventKind::GuardrailFailed {
680 guardrail_type,
681 message,
682 ..
683 } => {
684 println!(
685 "{}",
686 super::format_event::fmt_guardrail_failed(guardrail_type, message)
687 );
688 }
689
690 EventKind::GuardrailEscalation {
691 severity, message, ..
692 } => {
693 println!(
694 "{}",
695 super::format_event::fmt_guardrail_escalation(severity, message)
696 );
697 }
698
699 EventKind::Log { level, message, .. } => {
703 println!(
704 "{}",
705 super::format_event::fmt_log(&self.ts(), level, message)
706 );
707 }
708
709 EventKind::Custom { name, payload, .. } => {
710 if self.detail.show_sub_events() {
711 println!(
712 "{}",
713 super::format_event::fmt_custom(&self.ts(), name, payload)
714 );
715 }
716 }
717
718 EventKind::ArtifactWritten {
722 path, size, format, ..
723 } => {
724 if self.detail.show_sub_events() {
725 println!(
726 "{}",
727 super::format_event::fmt_artifact_written(path, *size, format)
728 );
729 }
730 }
731
732 EventKind::ArtifactFailed { path, reason, .. } => {
733 println!("{}", super::format_event::fmt_artifact_failed(path, reason));
734 }
735
736 EventKind::MediaExtracted {
740 task_id: _,
741 block_count,
742 content_types,
743 } => {
744 if self.detail.show_sub_events() {
745 println!(
746 "{}",
747 super::format_event::fmt_media_extracted(*block_count, content_types)
748 );
749 }
750 }
751
752 EventKind::MediaStored {
753 hash,
754 path,
755 size_bytes,
756 verified,
757 deduplicated,
758 pipeline_ms,
759 ..
760 } => {
761 if self.detail.show_sub_events() {
762 println!(
763 "{}",
764 super::format_event::fmt_media_stored(*size_bytes, path, hash)
765 );
766 if self.detail.show_previews() {
767 println!(
768 "{}",
769 super::format_event::fmt_media_stored_detail(
770 *deduplicated,
771 *verified,
772 *pipeline_ms,
773 )
774 );
775 }
776 }
777 }
778
779 EventKind::MediaProcessed { .. } => {
780 }
782
783 EventKind::MediaStoreFailed {
784 hash: _, reason, ..
785 } => {
786 println!("{}", super::format_event::fmt_media_store_failed(reason));
787 }
788
789 EventKind::MediaIntegrityCheck { .. } => {
790 }
792
793 EventKind::MediaCleanup { .. } => {
794 }
796
797 EventKind::StructuredOutputAttempt {
801 layer,
802 layer_name,
803 success,
804 error,
805 ..
806 } => {
807 if self.detail.show_sub_events() {
808 println!(
809 "{}",
810 super::format_event::fmt_structured_output_attempt(
811 *layer,
812 layer_name,
813 *success,
814 error.as_deref(),
815 )
816 );
817 }
818 }
819
820 EventKind::StructuredOutputSuccess { .. } => {
821 }
823
824 EventKind::VisionContentResolved {
828 image_count,
829 total_bytes,
830 resolve_ms,
831 ..
832 } => {
833 if self.detail.show_sub_events() {
834 println!(
835 "{}",
836 super::format_event::fmt_vision_content_resolved(
837 *image_count,
838 *total_bytes,
839 *resolve_ms,
840 )
841 );
842 }
843 }
844
845 EventKind::HttpRequest { method, url, .. } => {
849 if self.detail.show_sub_events() {
850 println!("{}", super::format_event::fmt_http_request(method, url));
851 }
852 }
853
854 EventKind::HttpResponse {
855 status_code,
856 content_type,
857 content_length,
858 elapsed_ms,
859 ..
860 } => {
861 if self.detail.show_sub_events() {
862 println!(
863 "{}",
864 super::format_event::fmt_http_response(
865 *status_code,
866 content_type.as_deref(),
867 *content_length,
868 *elapsed_ms,
869 )
870 );
871 }
872 }
873
874 EventKind::ForEachCompleted {
878 task_id,
879 total,
880 succeeded,
881 failed,
882 ..
883 } => {
884 if self.detail.show_sub_events() {
885 println!(
886 "{}",
887 super::format_event::fmt_for_each_completed(
888 task_id, *total, *succeeded, *failed,
889 )
890 );
891 }
892 }
893
894 EventKind::ExecCompleted {
898 exit_code,
899 duration_ms,
900 ..
901 } => {
902 if self.detail.show_sub_events() {
903 println!(
904 "{}",
905 super::format_event::fmt_exec_completed(*exit_code, *duration_ms)
906 );
907 }
908 }
909
910 EventKind::PolicyBlocked {
914 policy_type,
915 reason,
916 ..
917 } => {
918 println!(
919 "{}",
920 super::format_event::fmt_policy_blocked(&self.ts(), policy_type, reason)
921 );
922 }
923
924 EventKind::FetchRetry {
928 url,
929 attempt,
930 max_attempts,
931 status_code,
932 backoff_ms,
933 ..
934 } => {
935 println!(
936 "{}",
937 super::format_event::fmt_fetch_retry(
938 url,
939 *attempt,
940 *max_attempts,
941 *status_code,
942 *backoff_ms,
943 )
944 );
945 }
946
947 EventKind::BootPhaseCompleted {
951 phase,
952 success,
953 duration_ms,
954 warnings,
955 } => {
956 println!(
957 "{}",
958 super::format_event::fmt_boot_phase(phase, *success, *duration_ms, warnings,)
959 );
960 }
961
962 EventKind::NativeModelLoaded {
966 model,
967 kind,
968 duration_ms,
969 is_vision,
970 ..
971 } => {
972 if self.detail.show_sub_events() {
973 println!(
974 "{}",
975 super::format_event::fmt_native_model_loaded(
976 model,
977 kind,
978 *duration_ms,
979 *is_vision,
980 )
981 );
982 }
983 }
984
985 EventKind::BindingDefaultApplied { alias, path, .. } => {
989 if self.detail.show_sub_events() {
990 println!("{}", super::format_event::fmt_binding_default(alias, path));
991 }
992 }
993
994 EventKind::BindingTransformApplied {
995 alias,
996 transform_chain,
997 ..
998 } => {
999 if self.detail.show_sub_events() {
1000 println!(
1001 "{}",
1002 super::format_event::fmt_binding_transform(alias, transform_chain)
1003 );
1004 }
1005 }
1006
1007 EventKind::BindingEnvResolved {
1008 var_name, found, ..
1009 } => {
1010 if self.detail.show_sub_events() {
1011 println!("{}", super::format_event::fmt_binding_env(var_name, *found));
1012 }
1013 }
1014
1015 EventKind::DecomposeStarted {
1019 task_id, strategy, ..
1020 } => {
1021 if self.detail.show_sub_events() {
1022 println!(
1023 "{}",
1024 super::format_event::fmt_decompose_started(task_id, strategy)
1025 );
1026 }
1027 }
1028
1029 EventKind::DecomposeCompleted {
1030 task_id,
1031 item_count,
1032 duration_ms,
1033 ..
1034 } => {
1035 if self.detail.show_sub_events() {
1036 println!(
1037 "{}",
1038 super::format_event::fmt_decompose_completed(
1039 task_id,
1040 *item_count,
1041 *duration_ms,
1042 )
1043 );
1044 }
1045 }
1046
1047 EventKind::ForEachStarted {
1051 task_id,
1052 item_count,
1053 concurrency,
1054 ..
1055 } => {
1056 if self.detail.show_sub_events() {
1057 println!(
1058 "{}",
1059 super::format_event::fmt_for_each_started(
1060 task_id,
1061 *item_count,
1062 *concurrency,
1063 )
1064 );
1065 }
1066 }
1067
1068 EventKind::ProviderInitialized {
1072 provider,
1073 model,
1074 cached,
1075 } => {
1076 if self.detail.show_sub_events() {
1077 println!(
1078 "{}",
1079 super::format_event::fmt_provider_initialized(provider, model, *cached,)
1080 );
1081 }
1082 }
1083
1084 EventKind::BuiltinToolInvoked {
1088 tool_name,
1089 duration_ms,
1090 success,
1091 ..
1092 } => {
1093 if self.detail.show_sub_events() {
1094 println!(
1095 "{}",
1096 super::format_event::fmt_builtin_tool_invoked(
1097 tool_name,
1098 *duration_ms,
1099 *success,
1100 )
1101 );
1102 }
1103 }
1104
1105 EventKind::ExtractApplied {
1109 mode,
1110 input_len,
1111 output_len,
1112 ..
1113 } => {
1114 if self.detail.show_sub_events() {
1115 println!(
1116 "{}",
1117 super::format_event::fmt_extract_applied(mode, *input_len, *output_len,)
1118 );
1119 }
1120 }
1121
1122 _ => {}
1124 }
1125 }
1126
1127 fn render_output_preview(&self, output: &Value) {
1129 for line in format_output_preview(output, self.term_width) {
1130 println!("{}", line);
1131 }
1132 }
1133
1134 pub fn render_quiet_summary(&self, total_duration_ms: u64) {
1135 super::summary::print_run_quiet_summary(&self.stats, total_duration_ms);
1136 }
1137
1138 pub fn render_summary(&self, total_duration_ms: u64, trace_path: Option<&str>) {
1140 super::summary::print_run_summary(&self.stats, self.detail, total_duration_ms, trace_path);
1141 }
1142}
1143
1144pub(crate) fn format_bytes(bytes: u64) -> String {
1150 if bytes < 1024 {
1151 format!("{} B", bytes)
1152 } else if bytes < 1024 * 1024 {
1153 format!("{:.1} KB", bytes as f64 / 1024.0)
1154 } else {
1155 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
1156 }
1157}
1158
1159use super::colors::{floor_char_boundary, stripped_len};
1160
1161pub(crate) fn format_output_preview(output: &Value, term_width: u16) -> Vec<String> {
1166 let owned;
1169 let text: &str = match output {
1170 Value::String(s) => s.as_str(),
1171 _ => {
1172 owned = serde_json::to_string_pretty(output).unwrap_or_default();
1173 &owned
1174 }
1175 };
1176
1177 if text.is_empty() || text == "null" {
1178 return Vec::new();
1179 }
1180
1181 let is_json = text.starts_with('{') || text.starts_with('[');
1182 let is_markdown = text.starts_with('#') || text.contains("\n## ");
1183
1184 let max_width = (term_width as usize).min(72).saturating_sub(16);
1185 let dashes = "\u{254c}".repeat(max_width);
1186 let size_label = format!("{} ch", text.chars().count());
1187 let padding = max_width.saturating_sub(size_label.len() + 1);
1188
1189 let mut lines = Vec::new();
1190
1191 lines.push(format!(
1193 "{} {} {}{}{}",
1194 " ".repeat(6),
1195 "\u{2502}".dimmed(),
1196 "\u{256d}\u{254c}".dimmed(),
1197 dashes.dimmed(),
1198 "\u{256e}".dimmed()
1199 ));
1200
1201 let preview_lines: Vec<String> = if is_json {
1203 vec![colors::json_preview(&text.replace('\n', " "), max_width)]
1204 } else if is_markdown {
1205 colors::markdown_preview(text, 4)
1206 .into_iter()
1207 .map(|l| {
1208 if stripped_len(&l) > max_width {
1209 let end = floor_char_boundary(&l, max_width - 1);
1210 format!("{}\u{2026}", &l[..end])
1211 } else {
1212 l
1213 }
1214 })
1215 .collect()
1216 } else {
1217 text.lines()
1218 .take(2)
1219 .map(|l| {
1220 if l.len() > max_width {
1221 let end = floor_char_boundary(l, max_width - 1);
1222 format!("{}\u{2026}", &l[..end])
1223 } else {
1224 l.to_string()
1225 }
1226 })
1227 .collect()
1228 };
1229
1230 for line in &preview_lines {
1231 let pad = max_width.saturating_sub(stripped_len(line));
1232 lines.push(format!(
1233 "{} {} {} {}{} {}",
1234 " ".repeat(6),
1235 "\u{2502}".dimmed(),
1236 "\u{2502}".dimmed(),
1237 line,
1238 " ".repeat(pad),
1239 "\u{2502}".dimmed()
1240 ));
1241 }
1242
1243 lines.push(format!(
1245 "{} {} {}{} {} {}",
1246 " ".repeat(6),
1247 "\u{2502}".dimmed(),
1248 "\u{2570}\u{254c}".dimmed(),
1249 "\u{254c}".repeat(padding).dimmed(),
1250 size_label.dimmed(),
1251 "\u{254c}\u{256f}".dimmed()
1252 ));
1253
1254 lines
1255}