1use crate::control;
4use crate::store::{parse_since_duration, TaskStore};
5use std::io::{self, BufRead, Write};
6
7pub async fn interactive(session_id: &str) -> anyhow::Result<()> {
9 let store = crate::cli::init_store().await?;
10 let model = control::get_model(&store).await?;
11 let agent = control::get_agent(&store).await?;
12
13 let session_label = if session_id == TaskStore::DEFAULT_SESSION {
14 String::new()
15 } else {
16 format!(" [{session_id}]")
17 };
18
19 println!("orch control session ({agent}:{model}){session_label}");
20 println!("Type /model, /agent to manage. Ctrl+C to exit.");
21 println!("---");
22
23 let stdin = io::stdin();
24 let mut stdout = io::stdout();
25
26 loop {
27 print!("orch> ");
28 stdout.flush()?;
29
30 let mut line = String::new();
31 let bytes = stdin.lock().read_line(&mut line)?;
32 if bytes == 0 {
33 break;
34 }
35
36 let message = line.trim();
37 if message.is_empty() {
38 continue;
39 }
40 if message == "exit" || message == "quit" {
41 break;
42 }
43
44 match control::maybe_handle_control_command(&store, session_id, None, message).await? {
45 Some(response) => {
46 println!("{response}");
47 println!();
48 }
49 None => match control::send_message(&store, session_id, "cli", None, message).await {
50 Ok(response) => {
51 println!("{response}");
52 println!();
53 }
54 Err(e) => {
55 eprintln!("error: {e}");
56 }
57 },
58 }
59 }
60
61 Ok(())
62}
63
64pub async fn single_message(session_id: &str, message: &str) -> anyhow::Result<()> {
66 let store = crate::cli::init_store().await?;
67 if let Some(response) =
68 control::maybe_handle_control_command(&store, session_id, None, message).await?
69 {
70 println!("{response}");
71 return Ok(());
72 }
73
74 let response = control::send_message(&store, session_id, "cli", None, message).await?;
75 println!("{response}");
76 Ok(())
77}
78
79pub async fn history(
81 session_id: &str,
82 search: Option<String>,
83 since: Option<String>,
84 limit: i64,
85 with_cost: bool,
86) -> anyhow::Result<()> {
87 let store = crate::cli::init_store().await?;
88
89 let since_ts: Option<String> = since
91 .as_deref()
92 .map(parse_since_duration)
93 .transpose()
94 .map_err(|e| anyhow::anyhow!("{e}"))?;
95
96 let messages = if let Some(query) = search {
97 store
98 .search_control_messages(session_id, &query, since_ts.as_deref(), limit)
99 .await?
100 } else {
101 store
102 .list_control_messages(session_id, since_ts.as_deref(), limit)
103 .await?
104 };
105
106 if messages.is_empty() {
107 println!("No messages found.");
108 return Ok(());
109 }
110
111 for msg in &messages {
112 let role_label = match msg.role.as_str() {
113 "user" => "you",
114 "assistant" => msg.agent.as_deref().unwrap_or("assistant"),
115 _ => &msg.role,
116 };
117 let model_info = msg
118 .model
119 .as_deref()
120 .map(|m| format!(" ({m})"))
121 .unwrap_or_default();
122
123 let cost_info = if with_cost {
124 let mut parts = Vec::new();
125 if let Some(cost) = msg.cost_usd {
126 parts.push(format!("${cost:.4}"));
127 }
128 if let (Some(input), Some(output)) = (msg.input_tokens, msg.output_tokens) {
129 parts.push(format!(
130 "in:{} out:{}",
131 format_number(input),
132 format_number(output)
133 ));
134 } else if let Some(tokens) = msg.tokens_used {
135 parts.push(format!("{} tok", format_number(tokens)));
136 }
137 if parts.is_empty() {
138 String::new()
139 } else {
140 format!(" [{}]", parts.join(", "))
141 }
142 } else {
143 String::new()
144 };
145
146 println!(
147 "[{}] {}{}{}",
148 msg.created_at, role_label, model_info, cost_info
149 );
150 for line in msg.content.lines() {
151 println!(" {line}");
152 }
153 println!();
154 }
155
156 Ok(())
157}
158
159pub async fn stats(session_id: &str) -> anyhow::Result<()> {
161 let store = crate::cli::init_store().await?;
162
163 let summary = store.get_session_cost_summary(session_id).await?;
164 let fallback_model = control::get_model(&store).await?;
165 let fallback_agent = control::get_agent(&store).await?;
166 let current_model = summary.primary_model.clone().unwrap_or(fallback_model);
167 let current_agent = summary.primary_agent.clone().unwrap_or(fallback_agent);
168
169 if summary.total_messages == 0 {
170 println!("No messages found in session '{}'.", session_id);
171 return Ok(());
172 }
173
174 let session_label = if session_id == TaskStore::DEFAULT_SESSION {
175 "default".to_string()
176 } else {
177 session_id.to_string()
178 };
179
180 println!(
181 "Session: {} ({}:{})",
182 session_label, current_agent, current_model
183 );
184 println!(
185 "Messages: {} ({} user, {} assistant)",
186 summary.total_messages,
187 summary.total_messages - summary.assistant_messages,
188 summary.assistant_messages
189 );
190
191 if summary.total_tokens > 0 {
192 println!("Total tokens: {}", format_number(summary.total_tokens));
193 if summary.total_input_tokens > 0 || summary.total_output_tokens > 0 {
194 let input = summary.total_input_tokens;
195 let output = summary.total_output_tokens;
196 println!(" Input: {}", format_number(input));
197 println!(" Output: {}", format_number(output));
198 }
199 }
200
201 println!("Estimated cost: ${:.4}", summary.total_cost_usd);
202
203 if !summary.by_model.is_empty() {
204 println!("\nBreakdown by model:");
205 for (model, count, tokens, cost) in &summary.by_model {
206 let tokens_str = if *tokens > 0 {
207 format!(", {} tok", format_number(*tokens))
208 } else {
209 String::new()
210 };
211 println!(" {}: {} msg{} (${:.4})", model, count, tokens_str, cost);
212 }
213 }
214
215 Ok(())
216}
217
218fn format_number(n: i64) -> String {
219 let s = n.to_string();
220 let mut result = String::new();
221 for (count, c) in s.chars().rev().enumerate() {
222 if count > 0 && count % 3 == 0 {
223 result.push(',');
224 }
225 result.push(c);
226 }
227 result.chars().rev().collect()
228}