1use std::io::Write;
3
4use anyhow::Result;
5use serde::Serialize;
6
7use crate::models::Record;
8use crate::output::Emitter;
9use crate::util::discover::SessionFile;
10
11pub struct RecentOpts {
14 pub limit: usize,
15 pub role: Option<String>,
16 pub project: Option<String>,
17 pub max_tokens: usize,
18}
19
20#[derive(Serialize, Debug)]
23struct RecentRecord {
24 #[serde(rename = "type")]
25 record_type: &'static str,
26 project: String,
27 session_id: String,
28 role: String,
29 timestamp: String,
30 text: String,
31}
32
33pub fn run<W: Write>(opts: &RecentOpts, files: &[SessionFile], em: &mut Emitter<W>) -> Result<()> {
36 let filtered: Vec<&SessionFile> = files
37 .iter()
38 .filter(|f| {
39 if let Some(proj) = &opts.project {
40 f.project_name.to_lowercase().contains(&proj.to_lowercase())
41 } else {
42 true
43 }
44 })
45 .collect();
46
47 let mut all: Vec<RecentRecord> = Vec::new();
48
49 for file in &filtered {
50 let Ok(f) = std::fs::File::open(&file.path) else { continue };
51
52 use std::io::BufRead;
53 let reader = std::io::BufReader::new(f);
54
55 let mut last_lines: Vec<String> = Vec::new();
56 for line in reader.lines() {
57 let Ok(line) = line else { continue };
58 if line.trim().is_empty() {
59 continue;
60 }
61 last_lines.push(line);
62 if last_lines.len() > opts.limit * 2 + 50 {
63 last_lines.drain(..last_lines.len() - opts.limit - 25);
64 }
65 }
66
67 for line in last_lines.iter().rev().take(opts.limit + 10) {
68 let Ok(record) = serde_json::from_str::<Record>(line) else { continue };
69 let Some(msg) = record.as_message() else { continue };
70
71 let role = record.role().to_string();
72 if let Some(rf) = &opts.role {
73 if role != *rf {
74 continue;
75 }
76 }
77
78 let ts = msg.timestamp.clone().unwrap_or_default();
79 let text = msg.text_content();
80 let preview: String = text.chars().take(120).collect::<String>().replace('\n', " ");
81
82 all.push(RecentRecord {
83 record_type: "recent",
84 project: file.project_name.clone(),
85 session_id: file.session_id.clone(),
86 role,
87 timestamp: ts,
88 text: preview,
89 });
90 }
91 }
92
93 all.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
94
95 let show = std::cmp::min(opts.limit, all.len());
96 for rec in all.iter().take(show) {
97 if !em.emit(rec)? {
98 break;
99 }
100 }
101
102 let summary = crate::output::SummaryRecord {
103 record_type: "summary",
104 count: show,
105 files_scanned: Some(filtered.len()),
106 elapsed_ms: 0,
107 };
108 em.emit(&summary)?;
109
110 em.flush()?;
111 Ok(())
112}