use crate::cowork::peek::{PeekError, PeekMessage, days_to_ymd, parse_rfc3339};
use serde_json::Value;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub fn read_session_cwd(path: &Path) -> Option<String> {
let file = fs::File::open(path).ok()?;
let mut reader = BufReader::new(file);
let mut line = String::new();
reader.read_line(&mut line).ok()?;
let val: Value = serde_json::from_str(line.trim()).ok()?;
if val.get("type").and_then(|v| v.as_str()) != Some("session_meta") {
return None;
}
val.get("payload")?
.get("cwd")?
.as_str()
.map(|s| s.to_string())
}
pub fn find_latest_session_for_cwd(
base: &Path,
target_cwd: &str,
) -> Result<Option<(PathBuf, SystemTime)>, PeekError> {
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
find_latest_session_for_cwd_at(base, target_cwd, now_secs)
}
pub(crate) fn find_latest_session_for_cwd_at(
base: &Path,
target_cwd: &str,
now_epoch_secs: i64,
) -> Result<Option<(PathBuf, SystemTime)>, PeekError> {
let today_days = now_epoch_secs.div_euclid(86400);
let mut candidates: Vec<(PathBuf, SystemTime)> = Vec::new();
for offset in -1i64..=7i64 {
let (year, month, day) = days_to_ymd(today_days - offset);
let day_dir = base
.join(format!("{year:04}"))
.join(format!("{month:02}"))
.join(format!("{day:02}"));
let Ok(entries) = fs::read_dir(&day_dir) else {
continue;
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension().and_then(|s| s.to_str()) != Some("jsonl") {
continue;
}
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.starts_with("rollout-") {
continue;
}
if let Some(cwd) = read_session_cwd(&path) {
if cwd == target_cwd {
if let Ok(metadata) = entry.metadata() {
if let Ok(mtime) = metadata.modified() {
candidates.push((path.clone(), mtime));
}
}
}
}
}
}
Ok(candidates.into_iter().max_by_key(|(_, m)| *m))
}
pub fn parse_codex_jsonl(
path: &Path,
since: Option<&str>,
limit: usize,
) -> Result<(Vec<PeekMessage>, bool), PeekError> {
let since_cutoff: Option<i64> = match since {
Some(raw) => Some(parse_rfc3339(raw).ok_or_else(|| {
PeekError::Parse(format!("invalid `since` RFC3339 timestamp: {raw}"))
})?),
None => None,
};
let file = fs::File::open(path)?;
let reader = BufReader::new(file);
let mut all: Vec<PeekMessage> = Vec::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let val: Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(_) => continue,
};
if val.get("type").and_then(|v| v.as_str()) != Some("response_item") {
continue;
}
let Some(payload) = val.get("payload") else {
continue;
};
if payload.get("type").and_then(|v| v.as_str()) != Some("message") {
continue;
}
let role = payload.get("role").and_then(|v| v.as_str()).unwrap_or("");
if role != "user" && role != "assistant" {
continue;
}
let text = match payload.get("content") {
Some(Value::Array(blocks)) => {
let parts: Vec<String> = blocks
.iter()
.filter_map(|b| b.get("text").and_then(|v| v.as_str()).map(String::from))
.collect();
parts.join("\n")
}
Some(Value::String(s)) => s.clone(),
_ => continue,
};
if text.is_empty() {
continue;
}
let at = val
.get("timestamp")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if let Some(cutoff) = since_cutoff {
if let Some(msg_ts) = parse_rfc3339(&at) {
if msg_ts <= cutoff {
continue;
}
}
}
all.push(PeekMessage {
role: role.to_string(),
at,
text,
});
}
let total = all.len();
let truncated = total > limit;
let start = total.saturating_sub(limit);
let tail = all.split_off(start);
Ok((tail, truncated))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn reads_session_meta_cwd_from_first_line() {
let fixture = Path::new(
"tests/fixtures/cowork/codex/2026/04/13/rollout-2026-04-13T12-00-00-fake.jsonl",
);
let cwd = read_session_cwd(fixture).expect("read cwd");
assert_eq!(cwd, "/tmp/fake-project");
}
#[test]
fn parses_codex_messages_filtering_event_and_reasoning() {
let fixture = Path::new(
"tests/fixtures/cowork/codex/2026/04/13/rollout-2026-04-13T12-00-00-fake.jsonl",
);
let (messages, truncated) = parse_codex_jsonl(fixture, None, 30).expect("parse");
assert_eq!(messages.len(), 4);
assert_eq!(messages[0].text, "codex: hello");
assert_eq!(messages[1].text, "codex: hi back");
assert_eq!(messages[2].text, "codex: continue");
assert_eq!(messages[3].text, "codex: continuing");
assert!(messages[0].at < messages[3].at);
assert!(!truncated);
}
#[test]
fn honors_limit_by_tail_and_sets_truncated_codex() {
let fixture = Path::new(
"tests/fixtures/cowork/codex/2026/04/13/rollout-2026-04-13T12-00-00-fake.jsonl",
);
let (messages, truncated) = parse_codex_jsonl(fixture, None, 2).expect("parse");
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].text, "codex: continue");
assert_eq!(messages[1].text, "codex: continuing");
assert!(truncated);
}
const FIXED_NOW_EPOCH_2026_04_13: i64 = {
20556_i64 * 86400 + 12 * 3600
};
#[test]
fn walks_codex_dir_filtering_by_cwd() {
let base = Path::new("tests/fixtures/cowork/codex");
let result =
find_latest_session_for_cwd_at(base, "/tmp/fake-project", FIXED_NOW_EPOCH_2026_04_13)
.expect("find session");
assert!(result.is_some());
let (path, _mtime) = result.unwrap();
assert!(
path.to_string_lossy()
.contains("rollout-2026-04-13T12-00-00-fake.jsonl")
);
}
#[test]
fn walks_codex_dir_excludes_other_projects() {
let base = Path::new("tests/fixtures/cowork/codex");
let result =
find_latest_session_for_cwd_at(base, "/tmp/fake-project", FIXED_NOW_EPOCH_2026_04_13)
.expect("find session");
let path = result.unwrap().0;
assert!(!path.to_string_lossy().contains("otherproject"));
}
#[test]
fn scan_window_covers_utc_tomorrow_for_positive_offset_tz() {
let base = Path::new("tests/fixtures/cowork/codex");
let now_epoch = 20555_i64 * 86400 + 21 * 3600;
let result = find_latest_session_for_cwd_at(base, "/tmp/fake-project", now_epoch)
.expect("find session");
assert!(
result.is_some(),
"expected to find the 2026/04/13 fixture at UTC 2026-04-12T21:00 \
(= 05:00 local in +08:00)"
);
let (path, _) = result.unwrap();
assert!(
path.to_string_lossy().contains("2026/04/13"),
"expected the 04/13 fixture (local today), got {path:?}"
);
}
#[test]
fn scan_window_excludes_sessions_outside_the_9_day_span() {
let base = Path::new("tests/fixtures/cowork/codex");
let now_epoch = 20565_i64 * 86400;
let result = find_latest_session_for_cwd_at(base, "/tmp/fake-project", now_epoch)
.expect("find session");
assert!(
result.is_none(),
"04/13 fixture must fall outside the 04/15..04/23 window, got {result:?}"
);
}
}