Skip to main content

omni_dev/cli/
ai.rs

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