Skip to main content

codetether_agent/tui/ui/
status_bar.rs

1use 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}