agtrace-providers 0.8.0

Internal provider adapters for the agtrace CLI. Not intended for direct use.
Documentation
use crate::traits::{LogDiscovery, ProbeResult, SessionIndex};
use crate::{Error, Result};
use agtrace_core::project_hash_from_root;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

use super::io::{extract_gemini_header, extract_project_hash_from_gemini_file};

pub struct GeminiDiscovery;

impl LogDiscovery for GeminiDiscovery {
    fn id(&self) -> &'static str {
        "gemini"
    }

    fn probe(&self, path: &Path) -> ProbeResult {
        if !path.is_file() {
            return ProbeResult::NoMatch;
        }

        if let Ok(metadata) = std::fs::metadata(path)
            && metadata.len() == 0
        {
            return ProbeResult::NoMatch;
        }

        let filename = path.file_name().and_then(|f| f.to_str()).unwrap_or("");
        if filename.starts_with("session-") && filename.ends_with(".json") {
            ProbeResult::match_high()
        } else {
            ProbeResult::NoMatch
        }
    }

    fn resolve_log_root(&self, project_root: &Path) -> Option<PathBuf> {
        let hash = project_hash_from_root(&project_root.to_string_lossy());
        Some(PathBuf::from(hash.to_string()))
    }

    fn scan_sessions(&self, log_root: &Path) -> Result<Vec<SessionIndex>> {
        let mut sessions: HashMap<String, SessionIndex> = HashMap::new();

        if !log_root.exists() {
            return Ok(Vec::new());
        }

        for entry in WalkDir::new(log_root)
            .max_depth(3)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();

            if self.probe(path) == ProbeResult::NoMatch {
                continue;
            }

            let header = match extract_gemini_header(path) {
                Ok(h) => h,
                Err(_) => continue,
            };

            let session_id = match header.session_id {
                Some(id) => id,
                None => continue,
            };

            sessions
                .entry(session_id.clone())
                .or_insert_with(|| SessionIndex {
                    session_id: session_id.clone(),
                    timestamp: header.timestamp.clone(),
                    latest_mod_time: None, // Will be computed after all files are collected
                    main_file: path.to_path_buf(),
                    sidechain_files: Vec::new(),
                    project_root: None,
                    repository_hash: None, // Computed at index time
                    snippet: header.snippet.clone(),
                    parent_session_id: None, // Gemini doesn't have subagents
                    spawned_by: None,
                });
        }

        // NOTE: Compute latest_mod_time for each session after all files are collected
        // This tracks when the session was last active (most recent file modification)
        // Critical for watch mode to identify "most recently updated" vs "most recently created" sessions
        for session in sessions.values_mut() {
            let all_files = vec![session.main_file.as_path()];
            session.latest_mod_time = crate::get_latest_mod_time_rfc3339(&all_files);
        }

        Ok(sessions.into_values().collect())
    }

    fn extract_session_id(&self, path: &Path) -> Result<String> {
        let header = extract_gemini_header(path)?;
        header
            .session_id
            .ok_or_else(|| Error::Parse(format!("No session_id in file: {}", path.display())))
    }

    fn extract_project_hash(&self, path: &Path) -> Result<Option<agtrace_types::ProjectHash>> {
        Ok(extract_project_hash_from_gemini_file(path))
    }

    fn find_session_files(&self, log_root: &Path, session_id: &str) -> Result<Vec<PathBuf>> {
        let mut matching_files = Vec::new();

        for entry in WalkDir::new(log_root)
            .max_depth(3)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();

            if self.probe(path) == ProbeResult::NoMatch {
                continue;
            }

            if let Ok(header) = extract_gemini_header(path)
                && header.session_id.as_deref() == Some(session_id)
            {
                matching_files.push(path.to_path_buf());
            }
        }

        Ok(matching_files)
    }

    fn is_sidechain_file(&self, _path: &Path) -> Result<bool> {
        // Gemini doesn't support sidechains
        Ok(false)
    }
}