lean-ctx 3.8.3

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
//! `/api/tree` — a collapsible directory → file → symbol hierarchy built from the
//! real project index (`graph_index`). Powers the Explorer tab. No mock data: the
//! tree mirrors indexed files and their extracted symbols.

use crate::dashboard::routes::helpers::detect_project_root_for_dashboard;
use std::collections::BTreeMap;

pub(super) fn get_route(
    path: &str,
    _query_str: &str,
) -> Option<(&'static str, &'static str, String)> {
    match path {
        "/api/tree" => Some(tree()),
        _ => None,
    }
}

#[derive(Default)]
struct DirNode {
    dirs: BTreeMap<String, DirNode>,
    files: Vec<FileLeaf>,
}

struct SymLeaf {
    name: String,
    kind: String,
    line: usize,
    exported: bool,
}

struct FileLeaf {
    name: String,
    path: String,
    language: String,
    lines: usize,
    symbols: Vec<SymLeaf>,
}

fn tree() -> (&'static str, &'static str, String) {
    let root = detect_project_root_for_dashboard();
    let project = super::project_basename(&root);
    let index = crate::core::graph_index::load_or_build(&root);

    // Group symbols by their (relative) file path.
    let mut syms_by_file: std::collections::HashMap<
        &str,
        Vec<&crate::core::graph_index::SymbolEntry>,
    > = std::collections::HashMap::new();
    for sym in index.symbols.values() {
        syms_by_file.entry(sym.file.as_str()).or_default().push(sym);
    }

    let mut tree_root = DirNode::default();
    let mut file_count = 0usize;
    let mut symbol_count = 0usize;

    for (path, entry) in &index.files {
        file_count += 1;
        let mut symbols: Vec<SymLeaf> = syms_by_file
            .get(path.as_str())
            .map(|v| {
                v.iter()
                    .map(|s| SymLeaf {
                        name: s.name.clone(),
                        kind: s.kind.clone(),
                        line: s.start_line,
                        exported: s.is_exported,
                    })
                    .collect()
            })
            .unwrap_or_default();
        symbols.sort_by(|a, b| a.line.cmp(&b.line).then_with(|| a.name.cmp(&b.name)));
        symbol_count += symbols.len();

        let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
        if parts.is_empty() {
            continue;
        }
        let (dirs, file_name) = parts.split_at(parts.len() - 1);
        let mut node = &mut tree_root;
        for dir in dirs {
            node = node.dirs.entry((*dir).to_string()).or_default();
        }
        node.files.push(FileLeaf {
            name: file_name[0].to_string(),
            path: path.clone(),
            language: entry.language.clone(),
            lines: entry.line_count,
            symbols,
        });
    }

    let children = serialize_dir(&mut tree_root);
    let val = serde_json::json!({
        "project": project,
        "file_count": file_count,
        "symbol_count": symbol_count,
        "tree": children,
    });
    ("200 OK", "application/json", val.to_string())
}

/// Serialize a directory's children (sub-dirs first, then files), each sorted.
fn serialize_dir(node: &mut DirNode) -> Vec<serde_json::Value> {
    let mut out: Vec<serde_json::Value> = Vec::new();
    for (name, child) in &mut node.dirs {
        let kids = serialize_dir(child);
        let file_count = count_files(child);
        out.push(serde_json::json!({
            "type": "dir",
            "name": name,
            "files": file_count,
            "children": kids,
        }));
    }
    node.files.sort_by(|a, b| a.name.cmp(&b.name));
    for f in &node.files {
        let syms: Vec<serde_json::Value> = f
            .symbols
            .iter()
            .map(|s| {
                serde_json::json!({
                    "name": s.name,
                    "kind": s.kind,
                    "line": s.line,
                    "exported": s.exported,
                })
            })
            .collect();
        out.push(serde_json::json!({
            "type": "file",
            "name": f.name,
            "path": f.path,
            "language": f.language,
            "lines": f.lines,
            "symbol_count": syms.len(),
            "symbols": syms,
        }));
    }
    out
}

fn count_files(node: &DirNode) -> usize {
    node.files.len() + node.dirs.values().map(count_files).sum::<usize>()
}