codetether_agent/tui/ui/
status_bar.rs1use std::time::{SystemTime, UNIX_EPOCH};
2
3use ratatui::{
4 style::{Color, Modifier, Style},
5 text::Span,
6};
7
8use crate::tui::app::state::App;
9
10pub fn format_timestamp(timestamp: SystemTime) -> String {
11 let secs = timestamp
12 .duration_since(UNIX_EPOCH)
13 .map(|d| d.as_secs())
14 .unwrap_or_default();
15 let day = secs % 86_400;
16 let hour = day / 3_600;
17 let minute = (day % 3_600) / 60;
18 let second = day % 60;
19 format!("{hour:02}:{minute:02}:{second:02}")
20}
21
22pub fn bus_status_label_and_color(app: &App) -> (String, Color) {
23 let connected = app.state.a2a_connected;
24 let agents = app.state.worker_bridge_registered_agents.len();
25 let queued = app.state.worker_task_queue.len();
26 let processing = app
27 .state
28 .worker_bridge_processing_state
29 .unwrap_or(app.state.processing);
30
31 if connected || agents > 0 || queued > 0 {
32 let state = if processing { "active" } else { "idle" };
33 (
34 format!("BUS {state} ({agents}ag/{queued}q)"),
35 if processing {
36 Color::Green
37 } else {
38 Color::Yellow
39 },
40 )
41 } else {
42 ("BUS offline".to_string(), Color::DarkGray)
43 }
44}
45
46pub fn bus_status_badge_span(app: &App) -> Span<'static> {
47 let (label, color) = bus_status_label_and_color(app);
48 Span::styled(
49 format!(" {label} "),
50 Style::default().fg(color).add_modifier(Modifier::BOLD),
51 )
52}
53
54pub fn latency_badge_spans(app: &App) -> Option<Vec<Span<'static>>> {
55 let mut spans = Vec::new();
56
57 if app.state.processing {
58 spans.push(request_timing_span(
59 "TTFT",
60 app.state.current_request_first_token_ms,
61 Color::Cyan,
62 true,
63 ));
64 if let Some(elapsed_ms) = app.state.current_request_elapsed_ms() {
65 spans.push(Span::raw(" "));
66 spans.push(Span::styled(
67 format!(" ELAPSED {} ", format_duration_ms(elapsed_ms)),
68 Style::default()
69 .fg(Color::Yellow)
70 .add_modifier(Modifier::BOLD),
71 ));
72 }
73 return Some(spans);
74 }
75
76 if app.state.last_request_first_token_ms.is_none()
77 && app.state.last_request_last_token_ms.is_none()
78 {
79 return None;
80 }
81
82 spans.push(request_timing_span(
83 "TTFT",
84 app.state.last_request_first_token_ms,
85 Color::Cyan,
86 false,
87 ));
88 spans.push(Span::raw(" "));
89 spans.push(request_timing_span(
90 "TTL",
91 app.state.last_request_last_token_ms,
92 Color::Green,
93 false,
94 ));
95 Some(spans)
96}
97
98fn request_timing_span(
99 label: &str,
100 duration_ms: Option<u64>,
101 color: Color,
102 emphasize: bool,
103) -> Span<'static> {
104 let display = duration_ms
105 .map(format_duration_ms)
106 .unwrap_or_else(|| "…".to_string());
107 let mut style = Style::default().fg(color);
108 if emphasize {
109 style = style.add_modifier(Modifier::BOLD);
110 }
111 Span::styled(format!(" {label} {display} "), style)
112}
113
114pub fn format_duration_ms(duration_ms: u64) -> String {
115 if duration_ms >= 60_000 {
116 format!(
117 "{}m{:02}s",
118 duration_ms / 60_000,
119 (duration_ms % 60_000) / 1_000
120 )
121 } else if duration_ms >= 1_000 {
122 format!("{:.1}s", duration_ms as f64 / 1_000.0)
123 } else {
124 format!("{duration_ms}ms")
125 }
126}
127
128pub fn session_model_label(state: &crate::tui::app::state::AppState) -> Option<String> {
129 if state.processing {
130 Some("processing".to_string())
131 } else {
132 None
133 }
134}