1use 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#[derive(Parser)]
13pub struct AiCommand {
14 #[command(subcommand)]
16 pub command: AiSubcommand,
17}
18
19#[derive(Subcommand)]
21pub enum AiSubcommand {
22 Chat(ChatCommand),
24}
25
26impl AiCommand {
27 pub fn execute(self) -> Result<()> {
29 match self.command {
30 AiSubcommand::Chat(cmd) => cmd.execute(),
31 }
32 }
33}
34
35#[derive(Parser)]
37pub struct ChatCommand {
38 #[arg(long)]
40 pub model: Option<String>,
41}
42
43impl ChatCommand {
44 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
88struct RawModeGuard;
90
91impl Drop for RawModeGuard {
92 fn drop(&mut self) {
93 let _ = disable_raw_mode();
94 }
95}
96
97fn 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}