1use super::*;
2
3pub async fn cmd_sessions_list(url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
4 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
5 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
6 let c = RoboticusClient::new(url)?;
7 let data = c.get("/api/sessions").await.map_err(|e| {
8 RoboticusClient::check_connectivity_hint(&*e);
9 e
10 })?;
11 if json {
12 println!("{}", serde_json::to_string_pretty(&data)?);
13 return Ok(());
14 }
15 heading("Sessions");
16 let sessions = data["sessions"].as_array();
17 match sessions {
18 Some(arr) if !arr.is_empty() => {
19 let widths = [14, 18, 28, 22];
20 table_header(&["ID", "Agent", "Nickname", "Updated"], &widths);
21 for s in arr {
22 let id = truncate_id(s["id"].as_str().unwrap_or(""), 11);
23 let agent = s["agent_id"].as_str().unwrap_or("").to_string();
24 let nickname = s["nickname"].as_str().unwrap_or("\u{2014}").to_string();
25 let updated = s["updated_at"].as_str().unwrap_or("").to_string();
26 table_row(
27 &[
28 format!("{MONO}{id}{RESET}"),
29 agent,
30 nickname,
31 format!("{DIM}{updated}{RESET}"),
32 ],
33 &widths,
34 );
35 }
36 eprintln!();
37 eprintln!(" {DIM}{} session(s){RESET}", arr.len());
38 }
39 _ => empty_state("No sessions found"),
40 }
41 eprintln!();
42 Ok(())
43}
44
45pub async fn cmd_session_detail(
46 url: &str,
47 id: &str,
48 json: bool,
49) -> Result<(), Box<dyn std::error::Error>> {
50 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
51 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
52 let c = RoboticusClient::new(url)?;
53 let session = c.get(&format!("/api/sessions/{id}")).await.map_err(|e| {
54 RoboticusClient::check_connectivity_hint(&*e);
55 e
56 })?;
57 let messages = c.get(&format!("/api/sessions/{id}/messages")).await?;
58 if json {
59 let combined = serde_json::json!({ "session": session, "messages": messages });
60 println!("{}", serde_json::to_string_pretty(&combined)?);
61 return Ok(());
62 }
63 let nickname = session["nickname"].as_str().unwrap_or("\u{2014}");
64 heading(&format!("Session {}", truncate_id(id, 12)));
65 kv_mono("ID", id);
66 kv("Nickname", nickname);
67 kv("Agent", session["agent_id"].as_str().unwrap_or(""));
68 kv("Created", session["created_at"].as_str().unwrap_or(""));
69 kv("Updated", session["updated_at"].as_str().unwrap_or(""));
70 let msgs = messages["messages"].as_array();
71 match msgs {
72 Some(arr) if !arr.is_empty() => {
73 eprintln!();
74 eprintln!(" {BOLD}Messages ({}):{RESET}", arr.len());
75 eprintln!(" {DIM}{}{RESET}", "\u{2500}".repeat(56));
76 for m in arr {
77 let role = m["role"].as_str().unwrap_or("?");
78 let content = m["content"].as_str().unwrap_or("");
79 let time = m["created_at"].as_str().unwrap_or("");
80 let role_color = match role {
81 "user" => CYAN,
82 "assistant" => GREEN,
83 "system" => YELLOW,
84 _ => DIM,
85 };
86 let short_time = if time.len() > 19 { &time[11..19] } else { time };
87 eprintln!(
88 " {role_color}\u{25b6}{RESET} {role_color}{BOLD}{role}{RESET} {DIM}{short_time}{RESET}"
89 );
90 for line in content.lines() {
91 eprintln!(" {line}");
92 }
93 eprintln!();
94 }
95 }
96 _ => {
97 eprintln!();
98 empty_state("No messages in this session");
99 }
100 }
101 eprintln!();
102 Ok(())
103}
104
105pub async fn cmd_session_create(
106 url: &str,
107 agent_id: &str,
108) -> Result<(), Box<dyn std::error::Error>> {
109 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
110 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
111 let c = RoboticusClient::new(url)?;
112 let body = serde_json::json!({ "agent_id": agent_id });
113 let result = c.post("/api/sessions", body).await.map_err(|e| {
114 RoboticusClient::check_connectivity_hint(&*e);
115 e
116 })?;
117 let session_id = result["session_id"].as_str().unwrap_or("unknown");
118 eprintln!();
119 eprintln!(" {OK} Session created: {MONO}{session_id}{RESET}");
120 eprintln!();
121 Ok(())
122}
123
124pub async fn cmd_session_export(
125 base_url: &str,
126 session_id: &str,
127 format: &str,
128 output: Option<&str>,
129) -> Result<(), Box<dyn std::error::Error>> {
130 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
131 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
132 let resp = super::http_client()?
133 .get(format!("{base_url}/api/sessions/{session_id}"))
134 .send()
135 .await?;
136 if !resp.status().is_success() {
137 eprintln!(" Session not found: {session_id}");
138 return Ok(());
139 }
140 let session: serde_json::Value = resp.json().await?;
141 let resp2 = super::http_client()?
142 .get(format!("{base_url}/api/sessions/{session_id}/messages"))
143 .send()
144 .await?;
145 let body: serde_json::Value = resp2.json().await.unwrap_or_default();
146 let messages: Vec<serde_json::Value> = body
147 .get("messages")
148 .and_then(|v| v.as_array())
149 .cloned()
150 .unwrap_or_default();
151 let content = match format {
152 "json" => {
153 let export = serde_json::json!({ "session": session, "messages": messages, "exported_at": chrono::Utc::now().to_rfc3339() });
154 serde_json::to_string_pretty(&export)?
155 }
156 "markdown" => {
157 let mut md = String::new();
158 md.push_str(&format!("# Session {}\n\n", session_id));
159 md.push_str(&format!(
160 "**Agent:** {}\n",
161 session
162 .get("agent_id")
163 .and_then(|v| v.as_str())
164 .unwrap_or("?")
165 ));
166 md.push_str(&format!(
167 "**Created:** {}\n\n",
168 session
169 .get("created_at")
170 .and_then(|v| v.as_str())
171 .unwrap_or("?")
172 ));
173 md.push_str("---\n\n");
174 for msg in &messages {
175 let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("?");
176 let content = msg.get("content").and_then(|v| v.as_str()).unwrap_or("");
177 let ts = msg.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
178 md.push_str(&format!("### {} *({ts})*\n\n{content}\n\n", role));
179 }
180 md
181 }
182 "html" => {
183 let mut html = String::new();
184 html.push_str("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Roboticus Session Export</title><style>");
185 html.push_str("body{font-family:-apple-system,sans-serif;max-width:800px;margin:40px auto;padding:0 20px;background:#1a1a2e;color:#e0e0e0}");
186 html.push_str(
187 "h1{color:#8b5cf6}.msg{margin:16px 0;padding:12px 16px;border-radius:8px}",
188 );
189 html.push_str(".user{background:#2a2a4a;border-left:3px solid #8b5cf6}.assistant{background:#1e3a2e;border-left:3px solid #22c55e}");
190 html.push_str(".system{background:#3a2a1e;border-left:3px solid #f59e0b}.role{font-weight:bold;font-size:.85em}.time{font-size:.75em;color:#888}");
191 html.push_str("</style></head><body>");
192 html.push_str(&format!("<h1>Session {}</h1>", session_id));
193 for msg in &messages {
194 let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("?");
195 let content = msg.get("content").and_then(|v| v.as_str()).unwrap_or("");
196 let ts = msg.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
197 let class = match role {
198 "user" => "user",
199 "assistant" => "assistant",
200 "system" => "system",
201 _ => "msg",
202 };
203 let escaped = content
204 .replace('&', "&")
205 .replace('<', "<")
206 .replace('>', ">")
207 .replace('\n', "<br>");
208 html.push_str(&format!("<div class=\"msg {class}\"><div class=\"role\">{role} <span class=\"time\">{ts}</span></div><div>{escaped}</div></div>"));
209 }
210 html.push_str("</body></html>");
211 html
212 }
213 _ => {
214 eprintln!(" Unknown format: {format}. Use json, html, or markdown.");
215 return Ok(());
216 }
217 };
218 match output {
219 Some(path) => {
220 std::fs::write(path, &content)?;
221 eprintln!(" {OK} Exported to {path}");
222 }
223 None => print!("{content}"),
224 }
225 Ok(())
226}
227
228pub async fn cmd_sessions_backfill_nicknames(url: &str) -> Result<(), Box<dyn std::error::Error>> {
229 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
230 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
231 let c = RoboticusClient::new(url)?;
232 let result = c
233 .post("/api/sessions/backfill-nicknames", serde_json::json!({}))
234 .await
235 .map_err(|e| {
236 RoboticusClient::check_connectivity_hint(&*e);
237 e
238 })?;
239 let count = result["backfilled"].as_u64().unwrap_or(0);
240 eprintln!();
241 eprintln!(" {OK} Backfilled nicknames for {ACCENT}{count}{RESET} session(s)");
242 eprintln!();
243 Ok(())
244}