Skip to main content

claude_rust_tools/infrastructure/
ask_user_tool.rs

1use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
2
3use claude_rust_errors::{AppError, AppResult};
4use claude_rust_types::{PermissionLevel, Tool};
5use serde_json::{Value, json};
6
7pub struct AskUserTool {
8    paused: Arc<AtomicBool>,
9}
10
11impl AskUserTool {
12    pub fn new(paused: Arc<AtomicBool>) -> Self {
13        Self { paused }
14    }
15}
16
17#[async_trait::async_trait]
18impl Tool for AskUserTool {
19    fn name(&self) -> &str {
20        "ask_user_question"
21    }
22
23    fn description(&self) -> &str {
24        "Ask the user a question and wait for their response. Use this when you need clarification or input from the user."
25    }
26
27    fn input_schema(&self) -> Value {
28        json!({
29            "type": "object",
30            "properties": {
31                "question": {
32                    "type": "string",
33                    "description": "The question to ask the user"
34                }
35            },
36            "required": ["question"]
37        })
38    }
39
40    fn permission_level(&self) -> PermissionLevel {
41        PermissionLevel::ReadOnly
42    }
43
44    async fn execute(&self, input: Value) -> AppResult<String> {
45        let question = input
46            .get("question")
47            .and_then(|q| q.as_str())
48            .ok_or_else(|| AppError::Tool("missing 'question' field".into()))?
49            .to_string();
50
51        let paused = self.paused.clone();
52
53        let answer = tokio::task::spawn_blocking(move || {
54            paused.store(true, Ordering::Relaxed);
55            let result = prompt_interactive(&question);
56            paused.store(false, Ordering::Relaxed);
57            result
58        })
59        .await
60        .map_err(|e| AppError::Tool(format!("ask user task failed: {e}")))??;
61
62        Ok(answer)
63    }
64}
65
66fn prompt_interactive(question: &str) -> AppResult<String> {
67    use std::io::Write;
68    use crossterm::terminal;
69
70    let w = terminal::size().map(|(w, _)| w as usize).unwrap_or(80);
71    let outer = w.saturating_sub(4).max(20);
72    let inner = outer.saturating_sub(2);
73    let avail = inner.saturating_sub(4);
74
75    let top_label = "─ Question ";
76    let top_fill = inner.saturating_sub(top_label.len());
77
78    let q_lines = wrap_text(question, avail);
79
80    let mut out = std::io::stdout();
81    let _ = writeln!(out);
82    let _ = writeln!(out, "  \x1b[36m\x1b[1m╭{top_label}{}\x1b[0m", "─".repeat(top_fill));
83    for line in &q_lines {
84        let pad = avail.saturating_sub(line.len());
85        let _ = writeln!(out, "  \x1b[36m│\x1b[0m  {line}{}  \x1b[36m│\x1b[0m", " ".repeat(pad));
86    }
87    let sep_fill = "─".repeat(inner);
88    let _ = writeln!(out, "  \x1b[36m\x1b[2m├{sep_fill}┤\x1b[0m");
89    let input_pad = " ".repeat(avail);
90    let _ = writeln!(out, "  \x1b[36m│\x1b[0m  \x1b[1m\x1b[36m❯\x1b[0m {input_pad}  \x1b[36m│\x1b[0m");
91    let _ = writeln!(out, "  \x1b[36m\x1b[1m╰{}\x1b[0m", "─".repeat(inner));
92    let input_row = q_lines.len() + 3;
93    let _ = write!(out, "\x1b[{input_row}A\r\x1b[6C");
94    let _ = out.flush();
95
96    terminal::enable_raw_mode().map_err(|e| AppError::Tool(e.to_string()))?;
97    let result = read_answer(avail);
98    terminal::disable_raw_mode().ok();
99
100    let lines_to_clear = q_lines.len() + 4;
101    let _ = write!(out, "\x1b[{}B\r\x1b[J", lines_to_clear.saturating_sub(1));
102    let _ = out.flush();
103
104    result
105}
106
107fn read_answer(max_len: usize) -> AppResult<String> {
108    use std::io::Write;
109    use crossterm::event::{self, KeyCode, KeyModifiers};
110
111    let mut buf = String::new();
112    let mut out = std::io::stdout();
113
114    loop {
115        if !event::poll(std::time::Duration::from_millis(50)).unwrap_or(false) {
116            continue;
117        }
118        if let event::Event::Key(k) = event::read().map_err(|e| AppError::Tool(e.to_string()))? { match (k.code, k.modifiers) {
119            (KeyCode::Enter, _) => break,
120            (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
121                return Err(AppError::Interrupted);
122            }
123            (KeyCode::Backspace, _) => {
124                if buf.pop().is_some() {
125                    let visible = buf.chars().take(max_len).collect::<String>();
126                    let pad = max_len.saturating_sub(visible.len());
127                    let _ = write!(out, "\r\x1b[6C{visible}{} \r\x1b[{}C",
128                        " ".repeat(pad), 6 + visible.len());
129                    let _ = out.flush();
130                }
131            }
132            (KeyCode::Char(c), _) if buf.len() < max_len => {
133                buf.push(c);
134                let _ = write!(out, "{c}");
135                let _ = out.flush();
136            }
137            _ => {}
138        } }
139    }
140    Ok(buf)
141}
142
143fn wrap_text(text: &str, width: usize) -> Vec<String> {
144    if width == 0 { return vec![text.to_string()]; }
145    let mut lines = Vec::new();
146    for paragraph in text.split('\n') {
147        if paragraph.is_empty() { lines.push(String::new()); continue; }
148        let mut current = String::new();
149        for word in paragraph.split_whitespace() {
150            if current.is_empty() {
151                current = word.to_string();
152            } else if current.len() + 1 + word.len() <= width {
153                current.push(' ');
154                current.push_str(word);
155            } else {
156                lines.push(current);
157                current = word.to_string();
158            }
159        }
160        if !current.is_empty() { lines.push(current); }
161    }
162    if lines.is_empty() { lines.push(String::new()); }
163    lines
164}