magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
//! Wall time spent in foreground work. Children share the parent's interval, not a sum.
use std::{
    path::PathBuf,
    sync::{Arc, Mutex},
    time::{Duration, Instant},
};

#[derive(Debug, Default)]
struct ActiveTime {
    completed: Duration,
    started: Option<Instant>,
}

impl ActiveTime {
    fn elapsed(&self, now: Instant) -> Duration {
        self.completed.saturating_add(
            self.started
                .map_or(Duration::ZERO, |start| now.saturating_duration_since(start)),
        )
    }

    fn finish(&mut self, now: Instant) -> Duration {
        self.completed = self.elapsed(now);
        self.started = None;
        self.completed
    }
}

#[derive(Debug, Clone, Default)]
pub(crate) struct SessionActiveTime(Arc<Mutex<ActiveTime>>);

impl SessionActiveTime {
    pub(crate) fn from_events(events: &[super::SessionEvent]) -> Self {
        let millis = events
            .iter()
            .filter_map(active_time_millis)
            .max()
            .unwrap_or(0);
        Self(Arc::new(Mutex::new(ActiveTime {
            completed: Duration::from_millis(millis),
            started: None,
        })))
    }

    pub(crate) fn elapsed(&self, now: Instant) -> Duration {
        self.0
            .lock()
            .unwrap_or_else(|error| error.into_inner())
            .elapsed(now)
    }

    pub(crate) fn is_active(&self) -> bool {
        self.0
            .lock()
            .unwrap_or_else(|error| error.into_inner())
            .started
            .is_some()
    }

    /// Called by the single foreground worker. No child starts another interval.
    pub(crate) fn start(
        &self,
        session: Option<super::Session>,
        cwd: PathBuf,
        report_error: impl Fn(String) + Send + 'static,
    ) -> ActiveTimeGuard {
        self.0
            .lock()
            .unwrap_or_else(|error| error.into_inner())
            .started = Some(Instant::now());
        ActiveTimeGuard {
            clock: self.clone(),
            session,
            cwd,
            report_error: Box::new(report_error),
        }
    }
}

pub(crate) fn active_time_millis(event: &super::SessionEvent) -> Option<u64> {
    matches!(
        event.kind(),
        Some(super::SessionEventKind::SessionActiveTime | super::SessionEventKind::Compaction)
    )
    .then(|| {
        event
            .payload
            .get("session_active_ms")
            .and_then(serde_json::Value::as_u64)
    })
    .flatten()
}

/// Drop runs on success, cancellation, early error, and unwind, outside the UI thread.
pub(crate) struct ActiveTimeGuard {
    clock: SessionActiveTime,
    session: Option<super::Session>,
    cwd: PathBuf,
    report_error: Box<dyn Fn(String) + Send>,
}

impl Drop for ActiveTimeGuard {
    fn drop(&mut self) {
        let elapsed = self
            .clock
            .0
            .lock()
            .unwrap_or_else(|error| error.into_inner())
            .finish(Instant::now());
        let millis = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
        if let Err(error) = super::record_session_event(
            self.session.as_ref(),
            &self.cwd,
            super::SessionEventKind::SessionActiveTime,
            serde_json::json!({"session_active_ms": millis}),
        ) {
            (self.report_error)(format!("could not save session active time: {error}"));
        }
    }
}