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