use std::sync::Arc;
use axum::Router;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::Json;
use axum::routing::{get, post};
use serde::Serialize;
use crate::agent::context::ConversationContext;
use crate::agent::session::SessionStore;
use super::state::AppState;
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/api/sessions", get(list_sessions))
.route(
"/api/sessions/{id}",
get(get_session).delete(delete_session),
)
.route("/api/sessions/{id}/resume", post(resume_session))
}
#[derive(Serialize)]
struct SessionListItem {
id: String,
timestamp: String,
completed: bool,
task_preview: Option<String>,
}
async fn list_sessions(State(state): State<Arc<AppState>>) -> Json<Vec<SessionListItem>> {
let working_dir = state.working_dir().await;
let store = SessionStore::new(&working_dir);
let sessions = store.list().await;
let mut items = Vec::with_capacity(sessions.len());
for (id, timestamp, completed) in sessions {
let task_preview = store.load(&id).await.ok().and_then(|snap| {
snap.user_task.or_else(|| {
snap.messages
.iter()
.find(|m| m.role == "user")
.and_then(|m| m.content.as_ref().map(|c| c.text_content()))
.map(|s| {
let preview: String = s.chars().take(80).collect();
if s.len() > 80 {
format!("{preview}…")
} else {
preview
}
})
})
});
items.push(SessionListItem {
id,
timestamp,
completed,
task_preview,
});
}
Json(items)
}
async fn get_session(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<crate::agent::session::SessionSnapshot>, StatusCode> {
let working_dir = state.working_dir().await;
let store = SessionStore::new(&working_dir);
store
.load(&id)
.await
.map(Json)
.map_err(|_| StatusCode::NOT_FOUND)
}
#[derive(Serialize)]
struct ResumeResponse {
resumed: bool,
session_id: String,
message_count: usize,
}
async fn resume_session(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<ResumeResponse>, StatusCode> {
if state.is_agent_active() {
return Err(StatusCode::CONFLICT);
}
let working_dir = state.working_dir().await;
let store = SessionStore::new(&working_dir);
let snapshot = store.load(&id).await.map_err(|_| StatusCode::NOT_FOUND)?;
let message_count = snapshot.messages.len();
let ctx = ConversationContext::restore_with_budget(
snapshot.system_prompt.clone(),
snapshot.messages.clone(),
snapshot.last_reasoning.clone(),
state.config.context_max_tokens,
state.config.compaction_threshold,
);
*state.context.lock().await = Some(ctx);
Ok(Json(ResumeResponse {
resumed: true,
session_id: id,
message_count,
}))
}
#[derive(Serialize)]
struct DeleteResponse {
deleted: bool,
}
async fn delete_session(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<DeleteResponse>, StatusCode> {
let working_dir = state.working_dir().await;
let session_dir = crate::config::project_data_dir(&working_dir).join("sessions");
let path = session_dir.join(format!("{id}.json"));
tokio::fs::remove_file(&path)
.await
.map(|_| Json(DeleteResponse { deleted: true }))
.map_err(|_| StatusCode::NOT_FOUND)
}