1use crate::agent::{AgentConfig, AgentSession};
4use crate::api::{Claude, Session as ClaudeSession};
5use crate::deepseek::{DeepSeek, Session as DeepSeekSession};
6use crate::utils::prettify;
7use anyhow::{anyhow, Context, Result};
8use std::io::{self, Read, Write};
9use std::sync::atomic::Ordering;
10use std::sync::mpsc;
11use std::sync::Arc;
12use std::thread;
13use std::time::Duration;
14
15#[cfg(unix)]
16fn setup_raw_mode() -> Result<libc::termios> {
17 use std::os::unix::io::AsRawFd;
18 eprintln!("DEBUG: setup_raw_mode called");
19
20 let stdin_fd = io::stdin().as_raw_fd();
21
22 let original_termios = unsafe {
23 let mut t: libc::termios = std::mem::zeroed();
24 libc::tcgetattr(stdin_fd, &mut t);
25 t
26 };
27
28 let mut raw_termios = original_termios;
29 raw_termios.c_lflag &= !(libc::ICANON | libc::ECHO);
30 raw_termios.c_cc[libc::VMIN] = 0;
31 raw_termios.c_cc[libc::VTIME] = 1;
32
33 unsafe {
34 libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw_termios);
35 }
36
37 eprintln!("DEBUG: raw mode set");
38 Ok(original_termios)
39}
40
41#[cfg(not(unix))]
42fn setup_raw_mode() -> Result<libc::termios> {
43 Err(anyhow!("Raw mode not supported on this platform"))
44}
45
46#[cfg(unix)]
47fn restore_raw_mode(original_termios: libc::termios) -> Result<()> {
48 use std::os::unix::io::AsRawFd;
49
50 let stdin_fd = io::stdin().as_raw_fd();
51 unsafe {
52 libc::tcsetattr(stdin_fd, libc::TCSANOW, &original_termios);
53 }
54 eprintln!("DEBUG: raw mode restored");
55 Ok(())
56}
57
58#[cfg(not(unix))]
59fn restore_raw_mode(_: libc::termios) -> Result<()> {
60 Ok(())
61}
62
63#[cfg(unix)]
64fn read_char() -> Result<Option<u8>> {
65 let mut buf = [0u8; 1];
66 let mut stdin = io::stdin();
67 match stdin.read_exact(&mut buf) {
68 Ok(_) => Ok(Some(buf[0])),
69 Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
70 Err(e) => Err(anyhow!("Read error: {}", e)),
71 }
72}
73
74#[cfg(not(unix))]
75fn read_char() -> Result<Option<u8>> {
76 Ok(None)
77}
78
79fn setup_interrupt_monitor(interrupt_flag: Arc<std::sync::atomic::AtomicBool>) -> mpsc::Receiver<()> {
80 let (tx, rx) = mpsc::channel();
81
82 thread::spawn(move || {
83 loop {
84 if let Ok(Some(ch)) = read_char() {
85 if ch == 27 { interrupt_flag.store(true, Ordering::Relaxed);
87 eprintln!("\nš Interrupt detected (ESC)!");
88 let _ = tx.send(());
89 break;
90 }
91 }
92 thread::sleep(Duration::from_millis(10));
93 }
94 });
95
96 rx
97}
98
99pub async fn run_agent_cli(
100 use_deepseek: bool,
101 use_opus: bool,
102 use_haiku: bool,
103) -> Result<()> {
104 println!("š¤ Starting Toast Agent...\n");
105
106 let config = AgentConfig::default();
107 let mut session = AgentSession::new(config);
108
109 if use_deepseek {
110 run_with_deepseek(&mut session, use_opus, use_haiku).await
111 } else {
112 run_with_claude(&mut session, use_opus, use_haiku).await
113 }
114}
115
116async fn run_with_claude(
117 session: &mut AgentSession,
118 use_opus: bool,
119 use_haiku: bool,
120) -> Result<()> {
121 let config_dir = dirs::config_dir()
122 .ok_or_else(|| anyhow!("Could not determine config directory"))?
123 .join("toast");
124
125 let cookie = std::fs::read_to_string(config_dir.join("cookie"))
126 .context("Failed to read cookie")?
127 .trim()
128 .to_string();
129
130 let org_id = if let Ok(id) = std::fs::read_to_string(config_dir.join("org_id")) {
131 id.trim().to_string()
132 } else {
133 crate::utils::extract_org_id_from_cookie(&cookie)
134 .ok_or_else(|| anyhow!("Could not extract org_id from cookie"))?
135 };
136
137 let claude_session = ClaudeSession {
138 cookie,
139 user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0".to_string(),
140 organization_id: org_id,
141 };
142
143 let model = if use_opus {
144 crate::config::OPUS_MODEL
145 } else if use_haiku {
146 crate::config::HAIKU_MODEL
147 } else {
148 crate::config::SONNET_MODEL
149 };
150
151 let claude = Claude::new(claude_session, model)?;
152 println!("Connected to Claude ({model})");
153 println!("š” Press ESC during tool execution to interrupt\n");
154
155 let chat_id = claude.create_chat().await.context("Failed to create chat")?;
156
157 let system_prompt = session.agent().get_system_prompt();
158 claude.send_message(&chat_id, &system_prompt, &[]).await
159 .context("Failed to send system prompt")?;
160
161 let original_termios = setup_raw_mode()?;
162
163 let interrupt_flag = session.interrupt_flag();
164 let _interrupt_rx = setup_interrupt_monitor(interrupt_flag);
165
166 let mut stdout = io::stdout();
167
168 loop {
169 print!("You: ");
170 stdout.flush()?;
171
172 let mut input = String::new();
173
174 loop {
175 match read_char() {
176 Ok(Some(ch)) => {
177 if ch == b'\n' || ch == b'\r' {
178 println!();
179 break;
180 } else if ch == 27 {
181 println!("\nā¹ļø Cancelled input");
182 input.clear();
183 break;
184 } else if ch == 127 {
185 if !input.is_empty() {
186 input.pop();
187 print!("\x08 \x08");
188 stdout.flush()?;
189 }
190 } else if ch >= 32 && ch < 127 {
191 input.push(ch as char);
192 print!("{}", ch as char);
193 stdout.flush()?;
194 }
195 }
196 Ok(None) => {
197 thread::sleep(Duration::from_millis(10));
198 }
199 Err(_) => break,
200 }
201 }
202
203 let input = input.trim();
204 if input.is_empty() {
205 continue;
206 }
207 if matches!(input, "exit" | "quit" | "/exit" | "x") {
208 break;
209 }
210
211 let response = claude.send_message(&chat_id, input, &[]).await
212 .context("Failed to send message")?;
213
214 println!("\nClaude: {}\n", prettify(&response));
215
216 process_agent_response_claude(session, &claude, &chat_id, &response).await?;
217
218 session.agent().reset();
219 }
220
221 restore_raw_mode(original_termios)?;
222
223 claude.delete_chat(&chat_id).await.ok();
224 println!("\nš Goodbye!");
225 Ok(())
226}
227
228async fn run_with_deepseek(
229 session: &mut AgentSession,
230 use_opus: bool,
231 use_haiku: bool,
232) -> Result<()> {
233 let config_dir = dirs::config_dir()
234 .ok_or_else(|| anyhow!("Could not determine config directory"))?
235 .join("toast")
236 .join("deepseek");
237
238 let auth_token = std::fs::read_to_string(config_dir.join("auth_token"))
239 .context("Failed to read auth token")?
240 .trim()
241 .to_string();
242
243 let cookies = serde_json::from_str(
244 &std::fs::read_to_string(config_dir.join("cookies.json"))
245 .context("Failed to read cookies")?
246 )?;
247
248 let deepseek_session = DeepSeekSession { auth_token, cookies };
249 let mut deepseek = DeepSeek::new(deepseek_session)?;
250
251 let model = if use_opus {
252 "deepseek-r1"
253 } else if use_haiku {
254 "deepseek-lite"
255 } else {
256 "deepseek-r1"
257 };
258
259 println!("Connected to DeepSeek ({model})");
260 println!("š” Press ESC during tool execution to interrupt\n");
261
262 let chat_id = deepseek.create_chat_session().await
263 .context("Failed to create chat session")?;
264
265 let thinking_mode = if model == "deepseek-r1" {
266 crate::deepseek::ThinkingMode::Detailed
267 } else {
268 crate::deepseek::ThinkingMode::Simple
269 };
270 let search_mode = crate::deepseek::SearchMode::Disabled;
271
272 let system_prompt = session.agent().get_system_prompt();
273
274 let original_termios = setup_raw_mode()?;
275
276 let interrupt_flag = session.interrupt_flag();
277 let _interrupt_rx = setup_interrupt_monitor(interrupt_flag);
278
279 let mut stdout = io::stdout();
280 let mut first_message = true;
281
282 loop {
283 print!("You: ");
284 stdout.flush()?;
285
286 let mut input = String::new();
287
288 loop {
289 match read_char() {
290 Ok(Some(ch)) => {
291 if ch == b'\n' || ch == b'\r' {
292 println!();
293 break;
294 } else if ch == 27 {
295 println!("\nā¹ļø Cancelled input");
296 input.clear();
297 break;
298 } else if ch == 127 {
299 if !input.is_empty() {
300 input.pop();
301 print!("\x08 \x08");
302 stdout.flush()?;
303 }
304 } else if ch >= 32 && ch < 127 {
305 input.push(ch as char);
306 print!("{}", ch as char);
307 stdout.flush()?;
308 }
309 }
310 Ok(None) => {
311 thread::sleep(Duration::from_millis(10));
312 }
313 Err(_) => break,
314 }
315 }
316
317 let input = input.trim();
318 if input.is_empty() {
319 continue;
320 }
321 if matches!(input, "exit" | "quit" | "/exit" | "x") {
322 break;
323 }
324
325 let system_prompt_opt = if first_message {
326 first_message = false;
327 Some(system_prompt.as_str())
328 } else {
329 None
330 };
331
332 let response = deepseek.chat_completion(
333 &chat_id,
334 input,
335 None,
336 thinking_mode,
337 search_mode,
338 system_prompt_opt,
339 ).await.context("Failed to send message")?;
340
341 println!("\nDeepSeek: {}\n", prettify(&response));
342
343 process_agent_response_deepseek(
344 session,
345 &mut deepseek,
346 &chat_id,
347 &response,
348 thinking_mode,
349 search_mode,
350 ).await?;
351
352 session.agent().reset();
353 }
354
355 restore_raw_mode(original_termios)?;
356
357 println!("\nš Goodbye!");
358 Ok(())
359}
360
361async fn process_agent_response_claude(
362 session: &mut AgentSession,
363 claude: &Claude,
364 chat_id: &str,
365 response: &str,
366) -> Result<()> {
367 let tool_results = session.agent().process_tool_calls(response).await?;
368
369 if !tool_results.is_empty() {
370 let mut result_message = String::from("Tool execution results:\n\n");
371 for (tool_name, output) in tool_results {
372 result_message.push_str(&format!("[{tool_name}]\n{output}\n\n"));
373 }
374
375 let follow_up = claude.send_message(chat_id, &result_message, &[]).await
376 .context("Failed to send tool results")?;
377
378 println!("Claude: {}\n", prettify(&follow_up));
379
380 Box::pin(process_agent_response_claude(session, claude, chat_id, &follow_up)).await?;
381 }
382
383 Ok(())
384}
385
386async fn process_agent_response_deepseek(
387 session: &mut AgentSession,
388 deepseek: &mut DeepSeek,
389 chat_id: &str,
390 response: &str,
391 thinking_mode: crate::deepseek::ThinkingMode,
392 search_mode: crate::deepseek::SearchMode,
393) -> Result<()> {
394 let tool_results = session.agent().process_tool_calls(response).await?;
395
396 if !tool_results.is_empty() {
397 let mut result_message = String::from("Tool execution results:\n\n");
398 for (tool_name, output) in tool_results {
399 result_message.push_str(&format!("[{tool_name}]\n{output}\n\n"));
400 }
401
402 let follow_up = deepseek.chat_completion(
403 chat_id,
404 &result_message,
405 None,
406 thinking_mode,
407 search_mode,
408 None,
409 ).await.context("Failed to send tool results")?;
410
411 println!("DeepSeek: {}\n", prettify(&follow_up));
412
413 Box::pin(process_agent_response_deepseek(
414 session,
415 deepseek,
416 chat_id,
417 &follow_up,
418 thinking_mode,
419 search_mode,
420 )).await?;
421 }
422
423 Ok(())
424}