1use 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 if let Some(prev) = loaded {
20 let summary = prev.format_compact();
21 *session = prev;
22 format!("Session loaded.\n{summary}")
23 } else {
24 let id_str = session_id.unwrap_or("latest");
25 format!("No session found (id: {id_str}). Starting fresh.")
26 }
27 }
28
29 "save" => {
30 match session.save() {
31 Ok(()) => format!("Session {} saved (v{}).", session.id, session.version),
32 Err(e) => format!("Save failed: {e}"),
33 }
34 }
35
36 "task" => {
37 let desc = value.unwrap_or("(no description)");
38 session.set_task(desc, None);
39 format!("Task set: {desc}")
40 }
41
42 "finding" => {
43 let summary = value.unwrap_or("(no summary)");
44 let (file, line, text) = parse_finding_value(summary);
45 session.add_finding(file.as_deref(), line, text);
46 format!("Finding added: {summary}")
47 }
48
49 "decision" => {
50 let desc = value.unwrap_or("(no description)");
51 session.add_decision(desc, None);
52 format!("Decision recorded: {desc}")
53 }
54
55 "reset" => {
56 let _ = session.save();
57 let old_id = session.id.clone();
58 *session = SessionState::new();
59 crate::core::budget_tracker::BudgetTracker::global().reset();
60 format!("Session reset. Previous: {old_id}. New: {}", session.id)
61 }
62
63 "list" => {
64 let sessions = SessionState::list_sessions();
65 if sessions.is_empty() {
66 return "No sessions found.".to_string();
67 }
68 let mut lines = vec![format!("Sessions ({}):", sessions.len())];
69 for s in sessions.iter().take(10) {
70 let task = s.task.as_deref().unwrap_or("(no task)");
71 let task_short: String = task.chars().take(40).collect();
72 lines.push(format!(
73 " {} v{} | {} calls | {} tok | {}",
74 s.id, s.version, s.tool_calls, s.tokens_saved, task_short
75 ));
76 }
77 if sessions.len() > 10 {
78 lines.push(format!(" ... +{} more", sessions.len() - 10));
79 }
80 lines.join("\n")
81 }
82
83 "cleanup" => {
84 let removed = SessionState::cleanup_old_sessions(7);
85 format!("Cleaned up {removed} old session(s) (>7 days).")
86 }
87
88 "snapshot" => match session.save_compaction_snapshot() {
89 Ok(snapshot) => {
90 format!(
91 "Compaction snapshot saved ({} bytes).\n{snapshot}",
92 snapshot.len()
93 )
94 }
95 Err(e) => format!("Snapshot failed: {e}"),
96 },
97
98 "restore" => {
99 let snapshot = if let Some(id) = session_id {
100 SessionState::load_compaction_snapshot(id)
101 } else {
102 SessionState::load_latest_snapshot()
103 };
104 match snapshot {
105 Some(s) => format!("Session restored from compaction snapshot:\n{s}"),
106 None => "No compaction snapshot found. Session continues fresh.".to_string(),
107 }
108 }
109
110 "resume" => session.build_resume_block(),
111
112 "profile" => {
113 use crate::core::profiles;
114 if let Some(name) = value {
115 if let Ok(p) = profiles::set_active_profile(name) {
116 format!(
117 "Profile switched to '{name}'.\n\
118 Read mode: {}, Budget: {} tokens, CRP: {}, Density: {}",
119 p.read.default_mode,
120 p.budget.max_context_tokens,
121 p.compression.crp_mode,
122 p.compression.output_density,
123 )
124 } else {
125 let available: Vec<String> =
126 profiles::list_profiles().iter().map(|p| p.name.clone()).collect();
127 format!(
128 "Profile '{name}' not found. Available: {}",
129 available.join(", ")
130 )
131 }
132 } else {
133 let name = profiles::active_profile_name();
134 let p = profiles::active_profile();
135 let list = profiles::list_profiles();
136 let mut out = format!(
137 "Active profile: {name}\n\
138 Read: {}, Budget: {} tok, CRP: {}, Density: {}\n\n\
139 Available profiles:",
140 p.read.default_mode,
141 p.budget.max_context_tokens,
142 p.compression.crp_mode,
143 p.compression.output_density,
144 );
145 for info in &list {
146 let marker = if info.name == name { " *" } else { " " };
147 out.push_str(&format!(
148 "\n{marker} {:<14} ({}) {}",
149 info.name, info.source, info.description
150 ));
151 }
152 out.push_str("\n\nSwitch: ctx_session action=profile value=<name>");
153 out
154 }
155 }
156
157 "budget" => {
158 use crate::core::budget_tracker::BudgetTracker;
159 let snap = BudgetTracker::global().check();
160 snap.format_compact()
161 }
162
163 "role" => {
164 use crate::core::roles;
165 if let Some(name) = value {
166 match roles::set_active_role(name) {
167 Ok(r) => {
168 crate::core::budget_tracker::BudgetTracker::global().reset();
169 format!(
170 "Role switched to '{name}'.\n\
171 Shell: {}, Budget: {} tokens / {} shell / ${:.2}\n\
172 Tools: {}",
173 r.role.shell_policy,
174 r.limits.max_context_tokens,
175 r.limits.max_shell_invocations,
176 r.limits.max_cost_usd,
177 if r.tools.allowed.iter().any(|a| a == "*") {
178 let denied = if r.tools.denied.is_empty() {
179 "none".to_string()
180 } else {
181 format!("denied: {}", r.tools.denied.join(", "))
182 };
183 format!("* (all), {denied}")
184 } else {
185 r.tools.allowed.join(", ")
186 }
187 )
188 }
189 Err(e) => {
190 let available: Vec<String> =
191 roles::list_roles().iter().map(|r| r.name.clone()).collect();
192 format!("{e}. Available: {}", available.join(", "))
193 }
194 }
195 } else {
196 let name = roles::active_role_name();
197 let r = roles::active_role();
198 let list = roles::list_roles();
199 let mut out = format!(
200 "Active role: {name}\n\
201 Description: {}\n\
202 Shell policy: {}, Budget: {} tokens / {} shell / ${:.2}\n\n\
203 Available roles:",
204 r.role.description,
205 r.role.shell_policy,
206 r.limits.max_context_tokens,
207 r.limits.max_shell_invocations,
208 r.limits.max_cost_usd,
209 );
210 for info in &list {
211 let marker = if info.is_active { " *" } else { " " };
212 out.push_str(&format!(
213 "\n{marker} {:<14} ({}) {}",
214 info.name, info.source, info.description
215 ));
216 }
217 out.push_str("\n\nSwitch: ctx_session action=role value=<name>");
218 out
219 }
220 }
221
222 _ => format!("Unknown action: {action}. Use: status, load, save, task, finding, decision, reset, list, cleanup, snapshot, restore, resume, profile, role, budget"),
223 }
224}
225
226fn parse_finding_value(value: &str) -> (Option<String>, Option<u32>, &str) {
227 if let Some(dash_pos) = value.find(" \u{2014} ").or_else(|| value.find(" - ")) {
229 let location = &value[..dash_pos];
230 let sep_len = 3;
231 let text = &value[dash_pos + sep_len..];
232
233 if let Some(colon_pos) = location.rfind(':') {
234 let file = &location[..colon_pos];
235 if let Ok(line) = location[colon_pos + 1..].parse::<u32>() {
236 return (Some(file.to_string()), Some(line), text);
237 }
238 }
239 return (Some(location.to_string()), None, text);
240 }
241 (None, None, value)
242}