lean_ctx/tools/
ctx_session.rs1use crate::core::session::SessionState;
2
3pub fn handle(
4 session: &mut SessionState,
5 action: &str,
6 value: Option<&str>,
7 session_id: Option<&str>,
8) -> String {
9 match action {
10 "status" => session.format_compact(),
11
12 "load" => {
13 let loaded = if let Some(id) = session_id {
14 SessionState::load_by_id(id)
15 } else {
16 SessionState::load_latest()
17 };
18
19 match loaded {
20 Some(prev) => {
21 let summary = prev.format_compact();
22 *session = prev;
23 format!("Session loaded.\n{summary}")
24 }
25 None => {
26 let id_str = session_id.unwrap_or("latest");
27 format!("No session found (id: {id_str}). Starting fresh.")
28 }
29 }
30 }
31
32 "save" => {
33 match session.save() {
34 Ok(()) => format!("Session {} saved (v{}).", session.id, session.version),
35 Err(e) => format!("Save failed: {e}"),
36 }
37 }
38
39 "task" => {
40 let desc = value.unwrap_or("(no description)");
41 session.set_task(desc, None);
42 format!("Task set: {desc}")
43 }
44
45 "finding" => {
46 let summary = value.unwrap_or("(no summary)");
47 let (file, line, text) = parse_finding_value(summary);
48 session.add_finding(file.as_deref(), line, text);
49 format!("Finding added: {summary}")
50 }
51
52 "decision" => {
53 let desc = value.unwrap_or("(no description)");
54 session.add_decision(desc, None);
55 format!("Decision recorded: {desc}")
56 }
57
58 "reset" => {
59 let _ = session.save();
60 let old_id = session.id.clone();
61 *session = SessionState::new();
62 format!("Session reset. Previous: {old_id}. New: {}", session.id)
63 }
64
65 "list" => {
66 let sessions = SessionState::list_sessions();
67 if sessions.is_empty() {
68 return "No sessions found.".to_string();
69 }
70 let mut lines = vec![format!("Sessions ({}):", sessions.len())];
71 for s in sessions.iter().take(10) {
72 let task = s.task.as_deref().unwrap_or("(no task)");
73 let task_short: String = task.chars().take(40).collect();
74 lines.push(format!(
75 " {} v{} | {} calls | {} tok | {}",
76 s.id, s.version, s.tool_calls, s.tokens_saved, task_short
77 ));
78 }
79 if sessions.len() > 10 {
80 lines.push(format!(" ... +{} more", sessions.len() - 10));
81 }
82 lines.join("\n")
83 }
84
85 "cleanup" => {
86 let removed = SessionState::cleanup_old_sessions(7);
87 format!("Cleaned up {removed} old session(s) (>7 days).")
88 }
89
90 "snapshot" => match session.save_compaction_snapshot() {
91 Ok(snapshot) => {
92 format!(
93 "Compaction snapshot saved ({} bytes).\n{snapshot}",
94 snapshot.len()
95 )
96 }
97 Err(e) => format!("Snapshot failed: {e}"),
98 },
99
100 "restore" => {
101 let snapshot = if let Some(id) = session_id {
102 SessionState::load_compaction_snapshot(id)
103 } else {
104 SessionState::load_latest_snapshot()
105 };
106 match snapshot {
107 Some(s) => format!("Session restored from compaction snapshot:\n{s}"),
108 None => "No compaction snapshot found. Session continues fresh.".to_string(),
109 }
110 }
111
112 _ => format!("Unknown action: {action}. Use: status, load, save, task, finding, decision, reset, list, cleanup, snapshot, restore"),
113 }
114}
115
116fn parse_finding_value(value: &str) -> (Option<String>, Option<u32>, &str) {
117 if let Some(dash_pos) = value.find(" \u{2014} ").or_else(|| value.find(" - ")) {
119 let location = &value[..dash_pos];
120 let sep_len = 3;
121 let text = &value[dash_pos + sep_len..];
122
123 if let Some(colon_pos) = location.rfind(':') {
124 let file = &location[..colon_pos];
125 if let Ok(line) = location[colon_pos + 1..].parse::<u32>() {
126 return (Some(file.to_string()), Some(line), text);
127 }
128 }
129 return (Some(location.to_string()), None, text);
130 }
131 (None, None, value)
132}