codetether-agent 4.5.7

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
use std::collections::HashMap;

use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph, Wrap},
};

use crate::telemetry::PROVIDER_METRICS;
use crate::tui::app::state::App;
use crate::tui::chat::message::MessageType;
use crate::tui::token_display::TokenDisplay;

#[derive(Debug, Default)]
struct ToolLatencySummary {
    count: usize,
    failures: usize,
    total_ms: u64,
    max_ms: u64,
    last_ms: u64,
}

pub fn render_latency(f: &mut Frame, area: Rect, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(8), Constraint::Length(3)])
        .split(area);

    let mut lines = vec![
        Line::from("Latency Inspector"),
        Line::from(""),
        Line::from("Process-local timing from the current TUI runtime."),
        Line::from("TTFT/TTL measure Enter to first/last assistant text observed in the TUI."),
        Line::from(format!("Workspace: {}", app.state.cwd_display)),
    ];

    if app.state.processing {
        let elapsed = app
            .state
            .current_request_elapsed_ms()
            .map(format_duration)
            .unwrap_or_else(|| "running".to_string());
        let ttft = app
            .state
            .current_request_first_token_ms
            .map(format_duration)
            .unwrap_or_else(|| "waiting".to_string());
        lines.push(Line::from(format!(
            "Current request: in flight for {elapsed} | TTFT {ttft}"
        )));
    } else {
        lines.push(Line::from("Current request: idle"));
    }

    match (
        app.state.last_request_first_token_ms,
        app.state.last_request_last_token_ms,
    ) {
        (Some(ttft_ms), Some(ttl_ms)) => lines.push(Line::from(format!(
            "Last request: TTFT {} | TTL {}",
            format_duration(ttft_ms),
            format_duration(ttl_ms)
        ))),
        (Some(ttft_ms), None) => lines.push(Line::from(format!(
            "Last request: TTFT {} | TTL unavailable",
            format_duration(ttft_ms)
        ))),
        (None, Some(ttl_ms)) => lines.push(Line::from(format!(
            "Last request: TTFT unavailable | TTL {}",
            format_duration(ttl_ms)
        ))),
        (None, None) => lines.push(Line::from("Last request: no token timing yet")),
    }

    if let Some(latency_ms) = app.state.last_completion_latency_ms {
        let model = app
            .state
            .last_completion_model
            .clone()
            .unwrap_or_else(|| "unknown".to_string());
        let prompt = app.state.last_completion_prompt_tokens.unwrap_or_default();
        let output = app.state.last_completion_output_tokens.unwrap_or_default();
        lines.push(Line::from(format!(
            "Last model round-trip: {model} in {} ({} in / {} out)",
            format_duration(latency_ms),
            prompt,
            output
        )));
    } else {
        lines.push(Line::from("Last model round-trip: no request timing yet"));
    }

    if let Some(latency_ms) = app.state.last_tool_latency_ms {
        let tool = app
            .state
            .last_tool_name
            .clone()
            .unwrap_or_else(|| "unknown".to_string());
        let outcome = if app.state.last_tool_success.unwrap_or(false) {
            "ok"
        } else {
            "fail"
        };
        lines.push(Line::from(format!(
            "Last tool: {tool} in {} [{outcome}]",
            format_duration(latency_ms)
        )));
    } else {
        lines.push(Line::from("Last tool: no timed tool executions yet"));
    }

    lines.push(Line::from(""));
    lines.push(section_heading("Token Usage Snapshot"));
    let token_display = TokenDisplay::new();
    for line in token_display.create_detailed_display().into_iter().take(10) {
        lines.push(Line::from(line));
    }
    lines.push(Line::from(""));
    lines.push(section_heading("Provider Round-Trip Latency"));

    let mut provider_snapshots = PROVIDER_METRICS.all_snapshots();
    provider_snapshots.sort_by(|a, b| {
        b.request_count
            .cmp(&a.request_count)
            .then_with(|| b.avg_latency_ms.total_cmp(&a.avg_latency_ms))
            .then_with(|| a.provider.cmp(&b.provider))
    });
    if provider_snapshots.is_empty() {
        lines.push(Line::from("  No provider requests recorded yet."));
    } else {
        for snapshot in provider_snapshots.iter().take(6) {
            lines.push(Line::from(format!(
                "  {}: avg {} | p50 {} | p95 {} | {} reqs | {} avg TPS",
                snapshot.provider,
                format_duration(snapshot.avg_latency_ms.round() as u64),
                format_duration(snapshot.p50_latency_ms.round() as u64),
                format_duration(snapshot.p95_latency_ms.round() as u64),
                snapshot.request_count,
                format_tps(snapshot.avg_tps)
            )));
        }
    }

    lines.push(Line::from(""));
    lines.push(section_heading("Tool Latency In Chat"));

    let tool_summaries = summarize_tool_latencies(app);
    if tool_summaries.is_empty() {
        lines.push(Line::from(
            "  No timed tool executions recorded in the current chat buffer.",
        ));
    } else {
        for (tool_name, stats) in tool_summaries.iter().take(6) {
            let avg_ms = stats.total_ms / stats.count.max(1) as u64;
            lines.push(Line::from(format!(
                "  {}: avg {} | max {} | {} runs | {} failures",
                tool_name,
                format_duration(avg_ms),
                format_duration(stats.max_ms),
                stats.count,
                stats.failures
            )));
        }
    }

    lines.push(Line::from(""));
    lines.push(section_heading("Recent Timed Tools"));
    let recent = recent_tool_latencies(app, 8);
    if recent.is_empty() {
        lines.push(Line::from("  No recent timed tools yet."));
    } else {
        for entry in recent {
            lines.push(Line::from(format!("  {entry}")));
        }
    }

    let widget = Paragraph::new(lines)
        .block(Block::default().borders(Borders::ALL).title("Latency"))
        .wrap(Wrap { trim: false });
    f.render_widget(widget, chunks[0]);

    let footer = Paragraph::new(Line::from(vec![
        Span::styled(
            " LATENCY ",
            Style::default().fg(Color::Black).bg(Color::Cyan),
        ),
        Span::raw(" | "),
        Span::styled("Esc", Style::default().fg(Color::Yellow)),
        Span::raw(": Back | "),
        Span::styled("/chat", Style::default().fg(Color::Yellow)),
        Span::raw(": Return | "),
        Span::styled(
            app.state.status.clone(),
            Style::default().fg(if app.state.processing {
                Color::Yellow
            } else {
                Color::Green
            }),
        ),
    ]));
    f.render_widget(footer, chunks[1]);
}

fn section_heading(title: &str) -> Line<'static> {
    Line::from(vec![Span::styled(
        format!("  {title}"),
        Style::default()
            .fg(Color::Cyan)
            .add_modifier(Modifier::BOLD),
    )])
}

fn summarize_tool_latencies(app: &App) -> Vec<(String, ToolLatencySummary)> {
    let mut by_tool: HashMap<String, ToolLatencySummary> = HashMap::new();

    for message in &app.state.messages {
        if let MessageType::ToolResult {
            name,
            success,
            duration_ms: Some(duration_ms),
            ..
        } = &message.message_type
        {
            let entry = by_tool.entry(name.clone()).or_default();
            entry.count += 1;
            entry.total_ms += *duration_ms;
            entry.max_ms = entry.max_ms.max(*duration_ms);
            entry.last_ms = *duration_ms;
            if !*success {
                entry.failures += 1;
            }
        }
    }

    let mut summaries: Vec<_> = by_tool.into_iter().collect();
    summaries.sort_by(|a, b| {
        let a_avg = a.1.total_ms / a.1.count.max(1) as u64;
        let b_avg = b.1.total_ms / b.1.count.max(1) as u64;
        b_avg
            .cmp(&a_avg)
            .then_with(|| b.1.last_ms.cmp(&a.1.last_ms))
            .then_with(|| a.0.cmp(&b.0))
    });
    summaries
}

fn recent_tool_latencies(app: &App, limit: usize) -> Vec<String> {
    app.state
        .messages
        .iter()
        .rev()
        .filter_map(|message| {
            if let MessageType::ToolResult {
                name,
                success,
                duration_ms: Some(duration_ms),
                ..
            } = &message.message_type
            {
                let icon = if *success { "OK" } else { "ER" };
                Some(format!("[{icon}] {name} {}", format_duration(*duration_ms)))
            } else {
                None
            }
        })
        .take(limit)
        .collect()
}

fn format_duration(ms: u64) -> String {
    match ms {
        0..=999 => format!("{ms}ms"),
        1_000..=9_999 => format!("{:.2}s", ms as f64 / 1_000.0),
        10_000..=59_999 => format!("{:.1}s", ms as f64 / 1_000.0),
        _ => format!("{:.1}m", ms as f64 / 60_000.0),
    }
}

fn format_tps(tps: f64) -> String {
    if tps >= 100.0 {
        format!("{tps:.0}")
    } else if tps >= 10.0 {
        format!("{tps:.1}")
    } else {
        format!("{tps:.2}")
    }
}