collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use crossterm::terminal;
use ratatui::prelude::Color;

use crate::tui::theme::Theme;

/// Summary data collected at exit time.
pub struct ExitSummary {
    pub session_id: String,
    pub tool_total: usize,
    pub tool_success: usize,
    pub tool_failure: usize,
    pub wall_secs: u64,
    pub total_tokens: u64,
    pub api_calls: u32,
    pub model_name: String,
    pub theme: Theme,
}

/// Print a clean, theme-aware goodbye summary to stdout.
///
/// Layout adapts to terminal width; the box never overflows.
pub fn print_goodbye(s: &ExitSummary) {
    let term_width = terminal::size().map(|(w, _)| w as usize).unwrap_or(80);
    let box_width = term_width.clamp(44, 72);
    let inner = box_width - 2; // content area between borders

    let c = ThemeAnsi::from(&s.theme);

    let success_rate = if s.tool_total > 0 {
        s.tool_success as f64 / s.tool_total as f64 * 100.0
    } else {
        0.0
    };

    let tokens_str = if s.total_tokens >= 1000 {
        format!("{:.1}k", s.total_tokens as f64 / 1000.0)
    } else {
        s.total_tokens.to_string()
    };

    let session_short = if s.session_id.len() > 36 {
        &s.session_id[..36]
    } else {
        &s.session_id
    };

    // ── Border ──────────────────────────────────────────────────────────────
    let top = format!(
        "{border}{line}{r}",
        border = c.border,
        line = "".repeat(inner),
        r = c.reset,
    );
    let bot = format!(
        "{border}{line}{r}",
        border = c.border,
        line = "".repeat(inner),
        r = c.reset,
    );

    println!();
    println!("{top}");

    // ── Title ───────────────────────────────────────────────────────────────
    row_text(
        &c,
        inner,
        &format!(
            "{}collet signing off — see you on the next run!{}",
            c.accent, c.reset
        ),
    );

    row_blank(&c, inner);

    // ── Interaction Summary ─────────────────────────────────────────────────
    row_section(&c, inner, "Interaction Summary");
    row_kv(&c, inner, "Session ID:", session_short, &c.dim);
    row_kv(
        &c,
        inner,
        "Tool Calls:",
        &format!(
            "{}  ( {}{}{} {}{}{} )",
            s.tool_total, c.success, s.tool_success, c.reset, c.error, s.tool_failure, c.reset
        ),
        &c.text,
    );
    row_kv(
        &c,
        inner,
        "Success Rate:",
        &format!("{:.1}%", success_rate),
        &c.text,
    );

    row_blank(&c, inner);

    // ── Performance ─────────────────────────────────────────────────────────
    row_section(&c, inner, "Performance");
    row_kv(
        &c,
        inner,
        "Wall Time:",
        &format!("{:.1}s", s.wall_secs as f64),
        &c.text,
    );
    row_kv(&c, inner, "Tokens Used:", &tokens_str, &c.text);
    row_kv(&c, inner, "API Calls:", &s.api_calls.to_string(), &c.text);
    row_kv(&c, inner, "Model:", &s.model_name, &c.text);

    row_blank(&c, inner);

    // ── Tip ─────────────────────────────────────────────────────────────────
    row_text(
        &c,
        inner,
        &format!("{}Tip: Resume with: collet --continue{}", c.info, c.reset),
    );
    row_text(
        &c,
        inner,
        &format!(
            "{}     or: collet --resume {}{}{}",
            c.info, c.dim, session_short, c.reset
        ),
    );

    println!("{bot}");
    println!();
}

// ── Row helpers ─────────────────────────────────────────────────────────────

fn row_blank(c: &ThemeAnsi, inner: usize) {
    println!(
        "{b}{pad}{r}",
        b = c.border,
        pad = " ".repeat(inner),
        r = c.reset
    );
}

fn row_text(c: &ThemeAnsi, inner: usize, text: &str) {
    let vis = visible_width(text);
    let pad = inner.saturating_sub(vis + 2);
    println!(
        "{b}{r} {text}{pad} {b}{r}",
        b = c.border,
        r = c.reset,
        pad = " ".repeat(pad),
    );
}

fn row_section(c: &ThemeAnsi, inner: usize, label: &str) {
    let colored = format!(
        "{bold}{accent}{label}{reset}",
        bold = "\x1b[1m",
        accent = c.accent,
        reset = c.reset
    );
    let vis = visible_width(&colored);
    let pad = inner.saturating_sub(vis + 2);
    println!(
        "{b}{r} {colored}{pad} {b}{r}",
        b = c.border,
        r = c.reset,
        pad = " ".repeat(pad),
    );
}

fn row_kv(c: &ThemeAnsi, inner: usize, key: &str, value: &str, val_color: &str) {
    let key_width = 20;
    let key_padded = format!("{:<width$}", key, width = key_width);
    let line = format!(
        "{dim}{key_padded}{reset}{vc}{value}{reset}",
        dim = c.dim,
        reset = c.reset,
        vc = val_color,
    );
    let vis = visible_width(&line);
    let pad = inner.saturating_sub(vis + 2);
    println!(
        "{b}{r} {line}{pad} {b}{r}",
        b = c.border,
        r = c.reset,
        pad = " ".repeat(pad),
    );
}

// ── Theme → ANSI conversion ────────────────────────────────────────────────

struct ThemeAnsi {
    accent: String,
    border: String,
    text: String,
    dim: String,
    success: String,
    error: String,
    info: String,
    reset: &'static str,
}

impl ThemeAnsi {
    fn from(theme: &Theme) -> Self {
        Self {
            accent: color_to_ansi(theme.accent),
            border: color_to_ansi(theme.border),
            text: color_to_ansi(theme.text),
            dim: color_to_ansi(theme.text_dim),
            success: color_to_ansi(theme.success),
            error: color_to_ansi(theme.error),
            info: color_to_ansi(theme.info),
            reset: "\x1b[0m",
        }
    }
}

fn color_to_ansi(c: Color) -> String {
    match c {
        Color::Rgb(r, g, b) => format!("\x1b[38;2;{r};{g};{b}m"),
        Color::Black => "\x1b[30m".to_string(),
        Color::Red => "\x1b[31m".to_string(),
        Color::Green => "\x1b[32m".to_string(),
        Color::Yellow => "\x1b[33m".to_string(),
        Color::Blue => "\x1b[34m".to_string(),
        Color::Magenta => "\x1b[35m".to_string(),
        Color::Cyan => "\x1b[36m".to_string(),
        Color::White => "\x1b[37m".to_string(),
        Color::Gray => "\x1b[90m".to_string(),
        Color::DarkGray => "\x1b[90m".to_string(),
        Color::LightRed => "\x1b[91m".to_string(),
        Color::LightGreen => "\x1b[92m".to_string(),
        Color::LightYellow => "\x1b[93m".to_string(),
        Color::LightBlue => "\x1b[94m".to_string(),
        Color::LightMagenta => "\x1b[95m".to_string(),
        Color::LightCyan => "\x1b[96m".to_string(),
        _ => "\x1b[37m".to_string(), // fallback white
    }
}

/// Approximate visible character width, stripping ANSI escape sequences.
fn visible_width(s: &str) -> usize {
    let mut width = 0;
    let mut in_escape = false;
    for ch in s.chars() {
        if ch == '\x1b' {
            in_escape = true;
        } else if in_escape {
            if ch.is_alphabetic() {
                in_escape = false;
            }
        } else {
            width += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
        }
    }
    width
}