use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use regex::Regex; use serde_json::Value; use crate::codex_config::get_codex_config_dir; use crate::session_manager::{SessionMessage, SessionMeta}; use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary}; const PROVIDER_ID: &str = "codex"; pub fn scan_sessions() -> Vec { let root = get_codex_config_dir().join("sessions"); let mut files = Vec::new(); collect_jsonl_files(&root, &mut files); let mut sessions = Vec::new(); for path in files { if let Some(meta) = parse_session(&path) { sessions.push(meta); } } sessions } pub fn load_messages(path: &Path) -> Result, String> { let file = File::open(path).map_err(|e| format!("Failed to open session file: {e}"))?; let reader = BufReader::new(file); let mut messages = Vec::new(); for line in reader.lines() { let line = match line { Ok(value) => value, Err(_) => continue, }; let value: Value = match serde_json::from_str(&line) { Ok(parsed) => parsed, Err(_) => continue, }; if value.get("type").and_then(Value::as_str) != Some("response_item") { continue; } let payload = match value.get("payload") { Some(payload) => payload, None => continue, }; if payload.get("type").and_then(Value::as_str) != Some("message") { continue; } let role = payload .get("role") .and_then(Value::as_str) .unwrap_or("unknown") .to_string(); let content = payload.get("content").map(extract_text).unwrap_or_default(); if content.trim().is_empty() { continue; } let ts = value.get("timestamp").and_then(parse_timestamp_to_ms); messages.push(SessionMessage { role, content, ts }); } Ok(messages) } fn parse_session(path: &Path) -> Option { let file = File::open(path).ok()?; let reader = BufReader::new(file); let mut session_id: Option = None; let mut project_dir: Option = None; let mut created_at: Option = None; let mut last_active_at: Option = None; let mut summary: Option = None; for line in reader.lines() { let line = match line { Ok(value) => value, Err(_) => continue, }; let value: Value = match serde_json::from_str(&line) { Ok(parsed) => parsed, Err(_) => continue, }; if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) { if created_at.is_none() { created_at = Some(ts); } last_active_at = Some(ts); } if value.get("type").and_then(Value::as_str) == Some("session_meta") { if let Some(payload) = value.get("payload") { if session_id.is_none() { session_id = payload .get("id") .and_then(Value::as_str) .map(|s| s.to_string()); } if project_dir.is_none() { project_dir = payload .get("cwd") .and_then(Value::as_str) .map(|s| s.to_string()); } if let Some(ts) = payload.get("timestamp").and_then(parse_timestamp_to_ms) { created_at.get_or_insert(ts); } } continue; } if value.get("type").and_then(Value::as_str) != Some("response_item") { continue; } let payload = match value.get("payload") { Some(payload) => payload, None => continue, }; if payload.get("type").and_then(Value::as_str) != Some("message") { continue; } let text = payload.get("content").map(extract_text).unwrap_or_default(); if text.trim().is_empty() { continue; } summary = Some(text); } let session_id = session_id.or_else(|| infer_session_id_from_filename(path)); let session_id = session_id?; let title = project_dir .as_deref() .and_then(path_basename) .map(|value| value.to_string()); let summary = summary.map(|text| truncate_summary(&text, 160)); Some(SessionMeta { provider_id: PROVIDER_ID.to_string(), session_id: session_id.clone(), title, summary, project_dir, created_at, last_active_at, source_path: Some(path.to_string_lossy().to_string()), resume_command: Some(format!("codex resume {session_id}")), }) } fn infer_session_id_from_filename(path: &Path) -> Option { let file_name = path.file_name()?.to_string_lossy(); let re = Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") .ok()?; re.find(&file_name).map(|mat| mat.as_str().to_string()) } fn collect_jsonl_files(root: &Path, files: &mut Vec) { if !root.exists() { return; } let entries = match std::fs::read_dir(root) { Ok(entries) => entries, Err(_) => return, }; for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { collect_jsonl_files(&path, files); } else if path.extension().and_then(|ext| ext.to_str()) == Some("jsonl") { files.push(path); } } }