Skip to main content

omni_dev/cli/
ai.rs

1//! AI commands
2
3use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5use crossterm::{
6    event::{self, Event, KeyCode, KeyModifiers},
7    terminal::{disable_raw_mode, enable_raw_mode},
8};
9use std::io::{self, Write};
10
11/// AI operations
12#[derive(Parser)]
13pub struct AiCommand {
14    /// The AI subcommand to execute
15    #[command(subcommand)]
16    pub command: AiSubcommand,
17}
18
19/// AI subcommands
20#[derive(Subcommand)]
21pub enum AiSubcommand {
22    /// Interactive AI chat session
23    Chat(ChatCommand),
24}
25
26impl AiCommand {
27    /// Execute the AI command
28    pub fn execute(self) -> Result<()> {
29        match self.command {
30            AiSubcommand::Chat(cmd) => cmd.execute(),
31        }
32    }
33}
34
35/// Interactive AI chat session
36#[derive(Parser)]
37pub struct ChatCommand {
38    /// AI model to use (overrides environment configuration)
39    #[arg(long)]
40    pub model: Option<String>,
41}
42
43impl ChatCommand {
44    /// Execute the chat command
45    pub fn execute(self) -> Result<()> {
46        let ai_info = crate::utils::preflight::check_ai_credentials(self.model.as_deref())?;
47        eprintln!(
48            "Connected to {} (model: {})",
49            ai_info.provider, ai_info.model
50        );
51        eprintln!("Enter to send, Shift+Enter for newline, Ctrl+D to exit.\n");
52
53        let client = crate::claude::create_default_claude_client(self.model.clone(), None)?;
54
55        let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
56        rt.block_on(chat_loop(&client))
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/// Read 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}