Skip to main content

omni_dev/cli/ai/
chat.rs

1//! Interactive AI chat command.
2
3use std::io::{self, Write};
4
5use anyhow::Result;
6use clap::Parser;
7use crossterm::{
8    event::{self, Event, KeyCode, KeyModifiers},
9    terminal::{disable_raw_mode, enable_raw_mode},
10};
11
12/// Interactive AI chat session.
13#[derive(Parser)]
14pub struct ChatCommand {
15    /// AI model to use (overrides environment configuration).
16    #[arg(long)]
17    pub model: Option<String>,
18}
19
20impl ChatCommand {
21    /// Executes the chat command.
22    pub async fn execute(self) -> Result<()> {
23        let ai_info = crate::utils::preflight::check_ai_credentials(self.model.as_deref())?;
24        eprintln!(
25            "Connected to {} (model: {})",
26            ai_info.provider, ai_info.model
27        );
28        eprintln!("Enter to send, Shift+Enter for newline, Ctrl+D to exit.\n");
29
30        let client = crate::claude::create_default_claude_client(self.model, None)?;
31
32        chat_loop(&client).await
33    }
34}
35
36async fn chat_loop(client: &crate::claude::client::ClaudeClient) -> Result<()> {
37    let system_prompt = "You are a helpful assistant.";
38
39    loop {
40        let input = match read_user_input() {
41            Ok(Some(text)) => text,
42            Ok(None) => {
43                eprintln!("\nGoodbye!");
44                break;
45            }
46            Err(e) => {
47                eprintln!("\nInput error: {e}");
48                break;
49            }
50        };
51
52        let trimmed = input.trim();
53        if trimmed.is_empty() {
54            continue;
55        }
56
57        let response = client.send_message(system_prompt, trimmed).await?;
58        println!("{response}\n");
59    }
60
61    Ok(())
62}
63
64/// Guard that disables raw mode on drop.
65struct RawModeGuard;
66
67impl Drop for RawModeGuard {
68    fn drop(&mut self) {
69        let _ = disable_raw_mode();
70    }
71}
72
73/// Reads multiline user input with "> " prompt.
74///
75/// Returns `Ok(Some(text))` on Enter, `Ok(None)` on Ctrl+D/Ctrl+C.
76fn read_user_input() -> Result<Option<String>> {
77    eprint!("> ");
78    io::stderr().flush()?;
79
80    enable_raw_mode()?;
81    let _guard = RawModeGuard;
82
83    let mut buffer = String::new();
84
85    loop {
86        if let Event::Key(key_event) = event::read()? {
87            match key_event.code {
88                KeyCode::Enter => {
89                    if key_event.modifiers.contains(KeyModifiers::SHIFT) {
90                        buffer.push('\n');
91                        eprint!("\r\n... ");
92                        io::stderr().flush()?;
93                    } else {
94                        eprint!("\r\n");
95                        io::stderr().flush()?;
96                        return Ok(Some(buffer));
97                    }
98                }
99                KeyCode::Char('d') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
100                    if buffer.is_empty() {
101                        return Ok(None);
102                    }
103                    eprint!("\r\n");
104                    io::stderr().flush()?;
105                    return Ok(Some(buffer));
106                }
107                KeyCode::Char('c') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
108                    return Ok(None);
109                }
110                KeyCode::Char(c) => {
111                    buffer.push(c);
112                    eprint!("{c}");
113                    io::stderr().flush()?;
114                }
115                KeyCode::Backspace if buffer.pop().is_some() => {
116                    eprint!("\x08 \x08");
117                    io::stderr().flush()?;
118                }
119                _ => {}
120            }
121        }
122    }
123}