use std::sync::Arc;
use axum::{
Json,
extract::{Path as AxumPath, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Serialize;
use tracing::warn;
use crate::daemon::state::DaemonState;
use super::parse_id;
fn tail_lines(text: &str, n: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
lines[start..].join("\n")
}
#[derive(Debug, Serialize)]
pub struct ActivityResponse {
pub raw_pane: String,
pub runtime_active: bool,
pub pane_stale: bool,
pub state: String,
pub summary: String,
pub confidence: f32,
pub cache_hit: bool,
pub input_tokens: u32,
pub output_tokens: u32,
pub latency_ms: u64,
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub classification: Option<String>,
pub pending_decision: Option<String>,
pub proposed_default: Option<String>,
}
pub async fn get_session_activity(
State(state): State<Arc<DaemonState>>,
AxumPath(id_str): AxumPath<String>,
) -> impl IntoResponse {
let id = match parse_id(&id_str) {
Ok(id) => id,
Err((code, msg)) => return (code, msg).into_response(),
};
let mgr = state.session_manager().await;
let record = match mgr.get(&id).await {
Ok(r) => r,
Err(_) => {
return (StatusCode::NOT_FOUND, format!("session {id_str} not found")).into_response();
}
};
let pane_text = mgr
.capture_pane(&id, 60)
.await
.unwrap_or_else(|_| String::new());
let runtime_active = mgr.tmux_driver().session_exists(&record.tmux_name);
let (raw_pane, pane_stale) = if !runtime_active && pane_text.is_empty() {
let snapshot = if let Some(p) = record.scrollback_path.as_deref() {
tokio::fs::read_to_string(p)
.await
.ok()
.map(|s| tail_lines(&s, 60))
.filter(|s| !s.is_empty())
} else {
None
};
match snapshot {
Some(snap) => (snap, true),
None => (String::new(), true), }
} else {
(pane_text, false)
};
let monitor = state.activity_monitor();
let result = match monitor.check(&id_str, &raw_pane).await {
Ok(r) => r,
Err(e) => {
warn!(session = %id_str, "activity check error (non-key): {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("activity check failed: {e}"),
)
.into_response();
}
};
let api_key_present = std::env::var("OPENROUTER_API_KEY").is_ok();
let classification = if api_key_present {
Some(format!("{:?}", result.verdict.state).to_lowercase())
} else {
None
};
Json(ActivityResponse {
raw_pane,
runtime_active,
pane_stale,
state: format!("{:?}", result.verdict.state).to_lowercase(),
summary: result.verdict.summary,
confidence: result.verdict.confidence,
cache_hit: result.cache_hit,
input_tokens: result.cost.input_tokens,
output_tokens: result.cost.output_tokens,
latency_ms: result.cost.latency_ms,
total_input_tokens: result.tally.total_input_tokens,
total_output_tokens: result.tally.total_output_tokens,
classification,
pending_decision: record.pending_decision,
proposed_default: record.proposed_default,
})
.into_response()
}