Skip to main content

smc/cmd/
recent.rs

1/// smc recent — show most recent messages across all sessions.
2use 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
11// ── Opts ───────────────────────────────────────────────────────────────────
12
13pub struct RecentOpts {
14    pub limit: usize,
15    pub role: Option<String>,
16    pub project: Option<String>,
17    pub max_tokens: usize,
18}
19
20// ── Records ────────────────────────────────────────────────────────────────
21
22#[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
33// ── run ────────────────────────────────────────────────────────────────────
34
35pub 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}