bridge-echo 0.2.0

HTTP bridge for Claude Code CLI
use std::sync::Arc;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;

#[derive(Clone, Debug)]
pub struct ActiveRequest {
    pub id: u64,
    pub channel: String,
    pub sender: String,
    pub message_preview: String,
    pub started_at: Instant,
    pub started_unix: u64,
    pub alerts_sent: Vec<u64>,
}

#[derive(Clone, Debug, serde::Serialize)]
pub struct ActiveSnapshot {
    pub id: u64,
    pub channel: String,
    pub message_preview: String,
    pub started_unix: u64,
    pub elapsed_secs: u64,
}

#[derive(Clone, Debug, serde::Serialize)]
pub struct CompletedRequest {
    pub id: u64,
    pub channel: String,
    pub message_preview: String,
    pub response_preview: String,
    pub started_unix: u64,
    pub completed_unix: u64,
    pub duration_secs: u64,
}

const MAX_COMPLETED: usize = 50;

#[derive(Default)]
struct Inner {
    next_id: u64,
    active: Vec<ActiveRequest>,
    completed: Vec<CompletedRequest>,
}

#[derive(Clone)]
pub struct RequestTracker {
    inner: Arc<RwLock<Inner>>,
}

impl RequestTracker {
    pub fn new() -> Self {
        Self {
            inner: Arc::new(RwLock::new(Inner::default())),
        }
    }

    pub async fn start(&self, channel: &str, sender: &str, message: &str) -> u64 {
        let mut inner = self.inner.write().await;
        let id = inner.next_id;
        inner.next_id += 1;

        let preview = if message.len() > 80 {
            format!("{}...", &message[..80])
        } else {
            message.to_string()
        };

        let now_unix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        inner.active.push(ActiveRequest {
            id,
            channel: channel.to_string(),
            sender: sender.to_string(),
            message_preview: preview,
            started_at: Instant::now(),
            started_unix: now_unix,
            alerts_sent: Vec::new(),
        });

        id
    }

    /// Check if a sender has an active request on a different channel.
    /// Used for multi-channel conversation detection.
    pub async fn has_active_on_other_channel(&self, sender: &str, channel: &str) -> bool {
        let inner = self.inner.read().await;
        inner
            .active
            .iter()
            .any(|r| r.sender == sender && r.channel != channel)
    }

    pub async fn complete(&self, id: u64, response: &str) {
        let mut inner = self.inner.write().await;

        let pos = inner.active.iter().position(|r| r.id == id);
        let Some(pos) = pos else { return };
        let req = inner.active.remove(pos);

        let duration = req.started_at.elapsed().as_secs();
        let now_unix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let response_preview = if response.len() > 80 {
            format!("{}...", &response[..80])
        } else {
            response.to_string()
        };

        inner.completed.push(CompletedRequest {
            id,
            channel: req.channel,
            message_preview: req.message_preview,
            response_preview,
            started_unix: req.started_unix,
            completed_unix: now_unix,
            duration_secs: duration,
        });

        if inner.completed.len() > MAX_COMPLETED {
            let drain = inner.completed.len() - MAX_COMPLETED;
            inner.completed.drain(..drain);
        }
    }

    pub async fn active_snapshot(&self) -> Vec<ActiveSnapshot> {
        let inner = self.inner.read().await;
        inner
            .active
            .iter()
            .map(|r| ActiveSnapshot {
                id: r.id,
                channel: r.channel.clone(),
                message_preview: r.message_preview.clone(),
                started_unix: r.started_unix,
                elapsed_secs: r.started_at.elapsed().as_secs(),
            })
            .collect()
    }

    pub async fn completed_snapshot(&self) -> Vec<CompletedRequest> {
        let inner = self.inner.read().await;
        inner.completed.clone()
    }

    pub async fn mark_alerted(&self, id: u64, threshold_min: u64) {
        let mut inner = self.inner.write().await;
        if let Some(req) = inner.active.iter_mut().find(|r| r.id == id) {
            if !req.alerts_sent.contains(&threshold_min) {
                req.alerts_sent.push(threshold_min);
            }
        }
    }

    pub async fn active_requests_for_alerting(&self) -> Vec<(u64, String, String, u64, Vec<u64>)> {
        let inner = self.inner.read().await;
        inner
            .active
            .iter()
            .map(|r| {
                (
                    r.id,
                    r.channel.clone(),
                    r.message_preview.clone(),
                    r.started_at.elapsed().as_secs(),
                    r.alerts_sent.clone(),
                )
            })
            .collect()
    }
}