Skip to main content

roboticus_cli/cli/
sessions.rs

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('&', "&amp;")
205                    .replace('<', "&lt;")
206                    .replace('>', "&gt;")
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}