1use 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#[derive(Parser)]
14pub struct AiCommand {
15 #[command(subcommand)]
17 pub command: AiSubcommands,
18}
19
20#[derive(Subcommand)]
22pub enum AiSubcommands {
23 Chat(ChatCommand),
25}
26
27impl AiCommand {
28 pub async fn execute(self) -> Result<()> {
30 match self.command {
31 AiSubcommands::Chat(cmd) => cmd.execute().await,
32 }
33 }
34}
35
36#[derive(Parser)]
38pub struct ChatCommand {
39 #[arg(long)]
41 pub model: Option<String>,
42}
43
44impl ChatCommand {
45 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
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}