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 ContextOpts {
14 pub session: String,
15 pub line: usize,
16 pub context: usize,
17 pub max_tokens: usize,
18}
19
20#[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
34pub 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}