pub mod stream_renderer;
use crate::error::{HindsightError, Result};
use crate::parser::ExecutionNode;
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::fs::File;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, Receiver};
use std::time::Duration;
pub struct SessionWatcher {
file_path: PathBuf,
position: u64,
_watcher: RecommendedWatcher,
rx: Receiver<Result<Event>>,
}
impl SessionWatcher {
pub fn new<P: AsRef<Path>>(file_path: P) -> Result<Self> {
let file_path = file_path.as_ref().to_path_buf();
if !file_path.exists() {
return Err(HindsightError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Session file not found: {}", file_path.display()),
)));
}
let position = std::fs::metadata(&file_path).map(|m| m.len()).unwrap_or(0);
let (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(
move |res: notify::Result<Event>| {
let _ = tx.send(
res.map_err(|e| HindsightError::FileWatcher(format!("Watch error: {}", e))),
);
},
notify::Config::default(),
)
.map_err(|e| HindsightError::FileWatcher(format!("Failed to create watcher: {}", e)))?;
watcher
.watch(file_path.as_ref(), RecursiveMode::NonRecursive)
.map_err(|e| HindsightError::FileWatcher(format!("Failed to watch file: {}", e)))?;
Ok(SessionWatcher {
file_path,
position,
_watcher: watcher,
rx,
})
}
pub fn read_new_nodes(&mut self) -> Result<Vec<ExecutionNode>> {
let mut file = File::open(&self.file_path)?;
file.seek(SeekFrom::Start(self.position))?;
let reader = BufReader::new(file);
let mut new_nodes = Vec::new();
for line in reader.lines() {
let line = line?;
self.position += line.len() as u64 + 1;
if line.trim().is_empty() {
continue;
}
match serde_json::from_str::<ExecutionNode>(&line) {
Ok(node) => new_nodes.push(node),
Err(e) => {
eprintln!("Warning: Failed to parse line: {}", e);
}
}
}
Ok(new_nodes)
}
pub fn wait_for_changes(&self, timeout: Duration) -> bool {
match self.rx.recv_timeout(timeout) {
Ok(Ok(event)) => {
matches!(event.kind, EventKind::Modify(_))
}
Ok(Err(_)) => false,
Err(_) => false, }
}
pub fn poll(&mut self) -> Result<Vec<ExecutionNode>> {
while let Ok(Ok(_event)) = self.rx.try_recv() {
}
self.read_new_nodes()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_watcher_creation() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "{{\"type\":\"user\"}}").unwrap();
temp_file.flush().unwrap();
let watcher = SessionWatcher::new(temp_file.path());
assert!(watcher.is_ok());
}
#[test]
fn test_read_new_nodes() {
use crate::parser::models::ToolUse;
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "{{\"type\":\"user\"}}").unwrap();
temp_file.flush().unwrap();
let mut watcher = SessionWatcher::new(temp_file.path()).unwrap();
assert!(watcher.position > 0);
let new_node = ExecutionNode {
uuid: Some("test-uuid".to_string()),
parent_uuid: None,
timestamp: Some(1000),
node_type: "tool_use".to_string(),
message: None,
tool_use: Some(ToolUse {
name: "Read".to_string(),
input: serde_json::json!({"file": "test.rs"}),
id: Some("tool-1".to_string()),
}),
tool_result: None,
tool_use_result: None,
thinking: None,
progress: None,
token_usage: None,
extra: None,
};
writeln!(temp_file, "{}", serde_json::to_string(&new_node).unwrap()).unwrap();
temp_file.flush().unwrap();
let new_nodes = watcher.read_new_nodes().unwrap();
assert_eq!(new_nodes.len(), 1);
assert_eq!(new_nodes[0].node_type, "tool_use");
}
}