Skip to main content

smc/cmd/
context.rs

1/// smc context — show messages around a specific line in a session.
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 ContextOpts {
14    pub session: String,
15    pub line: usize,
16    pub context: usize,
17    pub max_tokens: usize,
18}
19
20// ── Records ────────────────────────────────────────────────────────────────
21
22#[derive(Serialize, Debug)]
23struct ContextRecord {
24    #[serde(rename = "type")]
25    record_type: &'static str,
26    line: usize,
27    role: String,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    timestamp: Option<String>,
30    text: String,
31    is_target: bool,
32}
33
34// ── run ────────────────────────────────────────────────────────────────────
35
36pub fn run<W: Write>(opts: &ContextOpts, file: &SessionFile, em: &mut Emitter<W>) -> Result<()> {
37    let f = std::fs::File::open(&file.path)?;
38    let reader = std::io::BufReader::new(f);
39
40    use std::io::BufRead;
41    let mut messages: Vec<(usize, Record)> = Vec::new();
42
43    for (line_num, line) in reader.lines().enumerate() {
44        let Ok(line) = line else { continue };
45        if line.trim().is_empty() {
46            continue;
47        }
48        let Ok(record) = serde_json::from_str::<Record>(&line) else { continue };
49        if record.is_message() {
50            messages.push((line_num + 1, record));
51        }
52    }
53
54    let target_idx = messages
55        .iter()
56        .position(|(ln, _)| *ln >= opts.line)
57        .unwrap_or(messages.len().saturating_sub(1));
58
59    let start = target_idx.saturating_sub(opts.context);
60    let end = std::cmp::min(messages.len(), target_idx + opts.context + 1);
61
62    for (i, (line_num, record)) in messages[start..end].iter().enumerate() {
63        let msg = record.as_message().unwrap();
64        let text = msg.text_content();
65        let preview: String = text.chars().take(500).collect();
66
67        let rec = ContextRecord {
68            record_type: "context",
69            line: *line_num,
70            role: record.role().to_string(),
71            timestamp: msg.timestamp.clone(),
72            text: preview,
73            is_target: start + i == target_idx,
74        };
75
76        if !em.emit(&rec)? {
77            break;
78        }
79    }
80
81    em.flush()?;
82    Ok(())
83}