Skip to main content

lean_ctx/tools/
ctx_summary.rs

1//! `ctx_summary` business logic (#292): record + recall AI session summaries.
2
3use crate::core::session::SessionState;
4use crate::core::session_summary;
5
6/// Dispatch a summary action. `session` is required for `record`.
7pub fn handle(
8    project_root: &str,
9    session: Option<&SessionState>,
10    action: &str,
11    query: Option<&str>,
12    top_k: usize,
13) -> String {
14    match action.trim() {
15        "" | "recall" => render_recall(project_root, query, top_k),
16        "record" => render_record(project_root, session),
17        "list" => render_list(project_root),
18        other => {
19            format!("ERR: unknown summary action '{other}'. Use: recall <query> | record | list")
20        }
21    }
22}
23
24fn render_recall(project_root: &str, query: Option<&str>, top_k: usize) -> String {
25    let Some(query) = query.map(str::trim).filter(|q| !q.is_empty()) else {
26        return "ERR: recall requires a query (e.g. \"what did I do on the graph?\")".to_string();
27    };
28    let hits = session_summary::recall(project_root, query, top_k.clamp(1, 20));
29    if hits.is_empty() {
30        return format!("No session summaries match '{query}'.");
31    }
32    let mode = hits.first().map_or("lexical", |h| h.mode);
33    let mut out = format!(
34        "session summaries for '{query}' ({} hits, {mode}):\n",
35        hits.len()
36    );
37    for h in hits {
38        let when = h.record.created_at.format("%Y-%m-%d %H:%M");
39        out.push_str(&format!(
40            "\n[{}] {} — {} (score {:.2})\n{}\n",
41            h.record.id, when, h.record.title, h.score, h.record.body
42        ));
43    }
44    out
45}
46
47fn render_record(project_root: &str, session: Option<&SessionState>) -> String {
48    let Some(session) = session else {
49        return "ERR: no active session to summarize".to_string();
50    };
51    let candidate = session_summary::build_candidate(session);
52    match session_summary::record_now(project_root, candidate) {
53        Ok(title) => format!("summary recorded: {title}"),
54        Err(e) => format!("summary: {e}"),
55    }
56}
57
58fn render_list(project_root: &str) -> String {
59    let summaries = session_summary::list(project_root);
60    if summaries.is_empty() {
61        return "No session summaries yet.".to_string();
62    }
63    let mut out = format!("session summaries ({}):\n", summaries.len());
64    for s in summaries.iter().rev() {
65        let when = s.created_at.format("%Y-%m-%d %H:%M");
66        out.push_str(&format!(
67            "  [{}] {} — {} ({} files, {} tool calls)\n",
68            s.id,
69            when,
70            s.title,
71            s.files.len(),
72            s.tool_calls
73        ));
74    }
75    out
76}