oxicode/cli/commands/
sessions.rs1use crate::store::session::{AgentMessage, SessionManager};
4use anyhow::Result;
5use uuid::Uuid;
6
7pub async fn handle_sessions() -> Result<()> {
9 let cwd = std::env::current_dir()
10 .unwrap_or_else(|_| std::path::PathBuf::from("."))
11 .to_string_lossy()
12 .to_string();
13 let sessions = crate::store::session::SessionManager::list(&cwd, None).await?;
14 if sessions.is_empty() {
15 println!("No sessions found.");
16 } else {
17 println!("Sessions:");
18 println!(
19 "{:<20} {:>6} {:<30} {:>12}",
20 "NAME", "MSG", "PREVIEW", "TIME"
21 );
22 println!("{:-<20} {:-<6} {:-<30} {:-<12}", "", "", "", "");
23 for session in &sessions {
24 let name = session.name.as_deref().unwrap_or("-");
25 let preview = if session.first_message.len() > 28 {
26 format!("{}...", &session.first_message[..28])
27 } else {
28 session.first_message.clone()
29 };
30 let time = chrono::DateTime::<chrono::Local>::from(session.modified)
31 .format("%m-%d %H:%M")
32 .to_string();
33 println!(
34 "{:<20} {:>6} {:<30} {:>12}",
35 &name[..name.len().min(20)],
36 session.message_count,
37 preview,
38 time
39 );
40 }
41 }
42 Ok(())
43}
44
45pub async fn handle_tree(session_id: &str) -> Result<()> {
47 let manager = SessionManager::new().await?;
48 show_tree(&manager, session_id).await
49}
50
51pub async fn handle_fork(parent_id: &str, entry_id: &str) -> Result<()> {
53 let manager = SessionManager::new().await?;
54 fork_session(&manager, parent_id, entry_id).await
55}
56
57pub async fn handle_delete(session_id: &str) -> Result<()> {
59 let manager = SessionManager::new().await?;
60 delete_session(&manager, session_id).await
61}
62
63#[allow(dead_code)]
65pub async fn list_sessions(manager: &SessionManager) -> Result<()> {
66 let sessions = manager.list_sessions().await?;
67
68 if sessions.is_empty() {
69 println!("No sessions found.");
70 return Ok(());
71 }
72
73 println!("Sessions:");
74 println!("{:<36} {:<20} UPDATED", "ID", "BRANCH");
75 println!("{:-<36} {:-<20} {:-<20}", "", "", "");
76
77 for meta in sessions {
78 let branch_str = if let Some(ref pid) = meta.parent_id {
79 format!("forked from {}", &pid.to_string()[..8])
80 } else {
81 "root".to_string()
82 };
83 let updated = chrono::DateTime::from_timestamp_millis(meta.updated_at)
84 .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
85 .unwrap_or_else(|| "unknown".to_string());
86 println!("{:<36} {:<20} {}", meta.id, branch_str, updated);
87 }
88
89 Ok(())
90}
91
92pub async fn show_tree(manager: &SessionManager, session_id: &str) -> Result<()> {
94 let id = if session_id.is_empty() {
95 let sessions = manager.list_sessions().await?;
97 match sessions.first() {
98 Some(s) => s.id,
99 None => {
100 println!("No sessions found.");
101 return Ok(());
102 }
103 }
104 } else {
105 Uuid::parse_str(session_id)?
106 };
107
108 let tree = manager.get_tree(id)?;
109 let branch_info = manager.get_branch_info(id).await?;
110
111 if let Some(info) = branch_info
112 && let Some(ref pid) = info.parent_session_id
113 {
114 println!("Session: {} (branched from {})", id, pid);
115 } else {
116 println!("Session: {} (root)", id);
117 }
118 println!();
119
120 for node in &tree {
122 let role_marker = match &node.entry.message {
123 AgentMessage::User { .. } => "U",
124 AgentMessage::Assistant { .. } => "A",
125 AgentMessage::System { .. } => "S",
126 _ => "-",
127 };
128
129 let content_preview = truncate(&node.entry.content(), 60);
130 let prefix = if node.entry.parent_id.is_some() {
131 "├─"
132 } else {
133 "└─"
134 };
135
136 println!(
137 " {}{} [{:.8}] {}",
138 prefix, role_marker, node.entry.id, content_preview
139 );
140 }
141
142 Ok(())
143}
144
145pub async fn fork_session(
147 manager: &SessionManager,
148 parent_id_str: &str,
149 entry_id_str: &str,
150) -> Result<()> {
151 let sessions = manager.list_sessions().await?;
152 let info = sessions
153 .iter()
154 .find(|s| s.id.to_string().starts_with(parent_id_str))
155 .ok_or_else(|| anyhow::anyhow!("Session not found: {}", parent_id_str))?;
156 let entry_id = Uuid::parse_str(entry_id_str)
157 .map_err(|_| anyhow::anyhow!("Invalid entry ID: {}", entry_id_str))?;
158 let (new_session_id, _) = manager.branch_from(info.id, entry_id).await?;
159 println!("Created forked session: {}", new_session_id);
160 println!("File: {}", manager.session_path(&new_session_id).display());
161 Ok(())
162}
163
164pub async fn delete_session(manager: &SessionManager, session_id: &str) -> Result<()> {
166 let sessions = manager.list_sessions().await?;
167 let info = sessions
168 .iter()
169 .find(|s| s.id.to_string().starts_with(session_id))
170 .ok_or_else(|| anyhow::anyhow!("Session not found: {}", session_id))?;
171 let path = manager.session_path(&info.id);
172 manager.delete(info.id).await?;
173 println!("Deleted session: {}", path.display());
174 Ok(())
175}
176
177fn truncate(s: &str, max_len: usize) -> String {
179 if s.len() <= max_len {
180 return s.to_string();
181 }
182 let boundary = s
183 .char_indices()
184 .take_while(|(i, _)| *i <= max_len.saturating_sub(3))
185 .last()
186 .map(|(i, c)| i + c.len_utf8())
187 .unwrap_or(0);
188 format!("{}...", &s[..boundary])
189}