1use std::path::Path;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use crate::error::{AcpCliError, Result};
5use crate::session::history::load_history;
6use crate::session::persistence::SessionRecord;
7use crate::session::pid;
8use crate::session::scoping::{find_git_root, session_dir, session_key};
9
10pub fn sessions_new(agent: &str, cwd: &str, name: Option<&str>) -> Result<()> {
12 let cwd_path = Path::new(cwd);
13 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
14 let dir_str = resolved_dir.to_string_lossy();
15 let session_name = name.unwrap_or("");
16 let key = session_key(agent, &dir_str, session_name);
17
18 let now = SystemTime::now()
19 .duration_since(UNIX_EPOCH)
20 .unwrap_or_default()
21 .as_secs();
22
23 let record = SessionRecord {
24 id: key.clone(),
25 agent: agent.to_string(),
26 cwd: resolved_dir.clone(),
27 name: name.map(|s| s.to_string()),
28 created_at: now,
29 closed: false,
30 acp_session_id: None,
31 };
32
33 let path = session_dir().join(format!("{key}.json"));
34 record.save(&path).map_err(AcpCliError::Io)?;
35
36 println!("Session created:");
37 println!(" id: {key}");
38 println!(" agent: {agent}");
39 println!(" cwd: {}", resolved_dir.display());
40 if let Some(n) = name {
41 println!(" name: {n}");
42 }
43 println!(" file: {}", path.display());
44
45 Ok(())
46}
47
48pub fn sessions_list(agent: Option<&str>, cwd: Option<&str>) -> Result<()> {
50 let dir = session_dir();
51 if !dir.exists() {
52 println!("No sessions found.");
53 return Ok(());
54 }
55
56 let entries = std::fs::read_dir(&dir).map_err(AcpCliError::Io)?;
57 let mut found = false;
58
59 for entry in entries {
60 let entry = entry.map_err(AcpCliError::Io)?;
61 let path = entry.path();
62 if path.extension().and_then(|e| e.to_str()) != Some("json") {
63 continue;
64 }
65
66 let record = match SessionRecord::load(&path).map_err(AcpCliError::Io)? {
67 Some(r) => r,
68 None => continue,
69 };
70
71 if let Some(a) = agent
73 && record.agent != a
74 {
75 continue;
76 }
77
78 if let Some(c) = cwd {
80 let cwd_path = Path::new(c);
81 let resolved = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
82 if record.cwd != resolved {
83 continue;
84 }
85 }
86
87 if !found {
88 println!("{:<12} {:<10} {:<6} CWD", "ID (short)", "AGENT", "STATUS");
89 println!("{}", "-".repeat(72));
90 found = true;
91 }
92
93 let short_id = &record.id[..12.min(record.id.len())];
94 let status = if record.closed { "closed" } else { "open" };
95 println!(
96 "{:<12} {:<10} {:<6} {}",
97 short_id,
98 record.agent,
99 status,
100 record.cwd.display()
101 );
102 }
103
104 if !found {
105 println!("No sessions found.");
106 }
107
108 Ok(())
109}
110
111pub fn sessions_close(agent: &str, cwd: &str, name: Option<&str>) -> Result<()> {
113 let cwd_path = Path::new(cwd);
114 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
115 let dir_str = resolved_dir.to_string_lossy();
116 let session_name = name.unwrap_or("");
117 let key = session_key(agent, &dir_str, session_name);
118
119 let path = session_dir().join(format!("{key}.json"));
120 let mut record = match SessionRecord::load(&path).map_err(AcpCliError::Io)? {
121 Some(r) => r,
122 None => {
123 return Err(AcpCliError::NoSession {
124 agent: agent.to_string(),
125 cwd: cwd.to_string(),
126 });
127 }
128 };
129
130 if record.closed {
131 println!("Session is already closed.");
132 return Ok(());
133 }
134
135 record.closed = true;
136 record.save(&path).map_err(AcpCliError::Io)?;
137
138 let short_id = &record.id[..12.min(record.id.len())];
139 println!("Session {short_id} closed.");
140 Ok(())
141}
142
143pub fn sessions_show(agent: &str, cwd: &str, name: Option<&str>) -> Result<()> {
145 let cwd_path = Path::new(cwd);
146 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
147 let dir_str = resolved_dir.to_string_lossy();
148 let session_name = name.unwrap_or("");
149 let key = session_key(agent, &dir_str, session_name);
150
151 let path = session_dir().join(format!("{key}.json"));
152 let record = match SessionRecord::load(&path).map_err(AcpCliError::Io)? {
153 Some(r) => r,
154 None => {
155 return Err(AcpCliError::NoSession {
156 agent: agent.to_string(),
157 cwd: cwd.to_string(),
158 });
159 }
160 };
161
162 let status = if record.closed { "closed" } else { "open" };
163 let created = format_timestamp(record.created_at);
164
165 println!("ID: {}", record.id);
166 println!("Agent: {}", record.agent);
167 println!("CWD: {}", record.cwd.display());
168 if let Some(ref n) = record.name {
169 println!("Name: {n}");
170 }
171 println!("Created at: {created}");
172 println!("Status: {status}");
173 if let Some(ref acp_id) = record.acp_session_id {
174 println!("ACP Session: {acp_id}");
175 }
176
177 Ok(())
178}
179
180pub fn sessions_history(agent: &str, cwd: &str, name: Option<&str>) -> Result<()> {
182 let cwd_path = Path::new(cwd);
183 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
184 let dir_str = resolved_dir.to_string_lossy();
185 let session_name = name.unwrap_or("");
186 let key = session_key(agent, &dir_str, session_name);
187
188 let sess_path = session_dir().join(format!("{key}.json"));
190 if SessionRecord::load(&sess_path)
191 .map_err(AcpCliError::Io)?
192 .is_none()
193 {
194 return Err(AcpCliError::NoSession {
195 agent: agent.to_string(),
196 cwd: cwd.to_string(),
197 });
198 }
199
200 let entries = load_history(&key).map_err(AcpCliError::Io)?;
201
202 if entries.is_empty() {
203 println!("No conversation history.");
204 return Ok(());
205 }
206
207 for entry in &entries {
208 let ts = format_timestamp(entry.timestamp);
209 println!("[{ts}] {}:", entry.role);
210 println!("{}", entry.content);
211 println!();
212 }
213
214 Ok(())
215}
216
217pub fn cancel_prompt(agent: &str, cwd: &str, name: Option<&str>) -> Result<()> {
219 let cwd_path = Path::new(cwd);
220 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
221 let dir_str = resolved_dir.to_string_lossy();
222 let session_name = name.unwrap_or("");
223 let key = session_key(agent, &dir_str, session_name);
224
225 match pid::read_pid(&key) {
226 Some(active_pid) => {
227 let ret = unsafe { libc::kill(active_pid as libc::pid_t, libc::SIGTERM) };
230 if ret == 0 {
231 println!("Sent SIGTERM to process {active_pid}.");
232 } else {
233 eprintln!("Failed to send signal to process {active_pid}.");
234 }
235 Ok(())
236 }
237 None => {
238 println!("No active prompt.");
239 Ok(())
240 }
241 }
242}
243
244pub fn session_status(agent: &str, cwd: &str, name: Option<&str>) -> Result<()> {
246 let cwd_path = Path::new(cwd);
247 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
248 let dir_str = resolved_dir.to_string_lossy();
249 let session_name = name.unwrap_or("");
250 let key = session_key(agent, &dir_str, session_name);
251
252 let sess_path = session_dir().join(format!("{key}.json"));
253 let record = match SessionRecord::load(&sess_path).map_err(AcpCliError::Io)? {
254 Some(r) => r,
255 None => {
256 return Err(AcpCliError::NoSession {
257 agent: agent.to_string(),
258 cwd: cwd.to_string(),
259 });
260 }
261 };
262
263 let active_pid = pid::read_pid(&key);
264 let status = if record.closed {
265 "closed"
266 } else if active_pid.is_some() {
267 "running"
268 } else {
269 "idle"
270 };
271
272 println!("Agent: {}", record.agent);
273 println!("Session: {}", record.id);
274 println!("Status: {status}");
275 if let Some(p) = active_pid {
276 println!("PID: {p}");
277 }
278
279 Ok(())
280}
281
282pub async fn set_mode(agent: &str, cwd: &str, name: Option<&str>, mode: &str) -> Result<()> {
286 let cwd_path = Path::new(cwd);
287 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
288 let dir_str = resolved_dir.to_string_lossy();
289 let session_name = name.unwrap_or("");
290 let key = session_key(agent, &dir_str, session_name);
291
292 let mut client = crate::queue::client::QueueClient::connect(&key)
293 .await
294 .map_err(|_| AcpCliError::Connection("No active session. Start a prompt first.".into()))?;
295
296 client.set_mode(mode).await
297}
298
299pub async fn set_config(
303 agent: &str,
304 cwd: &str,
305 name: Option<&str>,
306 key: &str,
307 value: &str,
308) -> Result<()> {
309 let cwd_path = Path::new(cwd);
310 let resolved_dir = find_git_root(cwd_path).unwrap_or_else(|| cwd_path.to_path_buf());
311 let dir_str = resolved_dir.to_string_lossy();
312 let session_name = name.unwrap_or("");
313 let session_key = session_key(agent, &dir_str, session_name);
314
315 let mut client = crate::queue::client::QueueClient::connect(&session_key)
316 .await
317 .map_err(|_| AcpCliError::Connection("No active session. Start a prompt first.".into()))?;
318
319 client.set_config(key, value).await
320}
321
322fn format_timestamp(ts: u64) -> String {
324 let secs = ts;
325 let days_since_epoch = secs / 86400;
327 let time_of_day = secs % 86400;
328 let hours = time_of_day / 3600;
329 let minutes = (time_of_day % 3600) / 60;
330 let seconds = time_of_day % 60;
331
332 let z = days_since_epoch as i64 + 719468;
334 let era = if z >= 0 { z } else { z - 146096 } / 146097;
335 let doe = (z - era * 146097) as u64;
336 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
337 let y = yoe as i64 + era * 400;
338 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
339 let mp = (5 * doy + 2) / 153;
340 let d = doy - (153 * mp + 2) / 5 + 1;
341 let m = if mp < 10 { mp + 3 } else { mp - 9 };
342 let y = if m <= 2 { y + 1 } else { y };
343
344 format!("{y:04}-{m:02}-{d:02} {hours:02}:{minutes:02}:{seconds:02}")
345}