claude_rust_tools/infrastructure/
ask_user_tool.rs1use 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 fn is_read_only(&self, _input: &Value) -> bool { true }
45 fn requires_user_interaction(&self) -> bool { true }
46
47 async fn execute(&self, input: Value) -> AppResult<String> {
48 let question = input
49 .get("question")
50 .and_then(|q| q.as_str())
51 .ok_or_else(|| AppError::Tool("missing 'question' field".into()))?
52 .to_string();
53
54 let paused = self.paused.clone();
55
56 let answer = tokio::task::spawn_blocking(move || {
57 paused.store(true, Ordering::Relaxed);
58 let result = prompt_interactive(&question);
59 paused.store(false, Ordering::Relaxed);
60 result
61 })
62 .await
63 .map_err(|e| AppError::Tool(format!("ask user task failed: {e}")))??;
64
65 Ok(answer)
66 }
67}
68
69fn prompt_interactive(question: &str) -> AppResult<String> {
70 use std::io::Write;
71 use crossterm::terminal;
72
73 let w = terminal::size().map(|(w, _)| w as usize).unwrap_or(80);
74 let outer = w.saturating_sub(4).max(20);
75 let inner = outer.saturating_sub(2);
76 let avail = inner.saturating_sub(4);
77
78 let top_label = "─ Question ";
79 let top_fill = inner.saturating_sub(top_label.len());
80
81 let q_lines = wrap_text(question, avail);
82
83 let mut out = std::io::stdout();
84 let _ = writeln!(out);
85 let _ = writeln!(out, " \x1b[36m\x1b[1m╭{top_label}{}\x1b[0m", "─".repeat(top_fill));
86 for line in &q_lines {
87 let pad = avail.saturating_sub(line.len());
88 let _ = writeln!(out, " \x1b[36m│\x1b[0m {line}{} \x1b[36m│\x1b[0m", " ".repeat(pad));
89 }
90 let sep_fill = "─".repeat(inner);
91 let _ = writeln!(out, " \x1b[36m\x1b[2m├{sep_fill}┤\x1b[0m");
92 let input_pad = " ".repeat(avail);
93 let _ = writeln!(out, " \x1b[36m│\x1b[0m \x1b[1m\x1b[36m❯\x1b[0m {input_pad} \x1b[36m│\x1b[0m");
94 let _ = writeln!(out, " \x1b[36m\x1b[1m╰{}\x1b[0m", "─".repeat(inner));
95 let input_row = q_lines.len() + 3;
96 let _ = write!(out, "\x1b[{input_row}A\r\x1b[6C");
97 let _ = out.flush();
98
99 terminal::enable_raw_mode().map_err(|e| AppError::Tool(e.to_string()))?;
100 let result = read_answer(avail);
101 terminal::disable_raw_mode().ok();
102
103 let lines_to_clear = q_lines.len() + 4;
104 let _ = write!(out, "\x1b[{}B\r\x1b[J", lines_to_clear.saturating_sub(1));
105 let _ = out.flush();
106
107 result
108}
109
110fn read_answer(max_len: usize) -> AppResult<String> {
111 use std::io::Write;
112 use crossterm::event::{self, KeyCode, KeyModifiers};
113
114 let mut buf = String::new();
115 let mut out = std::io::stdout();
116
117 loop {
118 if !event::poll(std::time::Duration::from_millis(50)).unwrap_or(false) {
119 continue;
120 }
121 if let event::Event::Key(k) = event::read().map_err(|e| AppError::Tool(e.to_string()))? { match (k.code, k.modifiers) {
122 (KeyCode::Enter, _) => break,
123 (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
124 return Err(AppError::Interrupted);
125 }
126 (KeyCode::Backspace, _) => {
127 if buf.pop().is_some() {
128 let visible = buf.chars().take(max_len).collect::<String>();
129 let pad = max_len.saturating_sub(visible.len());
130 let _ = write!(out, "\r\x1b[6C{visible}{} \r\x1b[{}C",
131 " ".repeat(pad), 6 + visible.len());
132 let _ = out.flush();
133 }
134 }
135 (KeyCode::Char(c), _) if buf.len() < max_len => {
136 buf.push(c);
137 let _ = write!(out, "{c}");
138 let _ = out.flush();
139 }
140 _ => {}
141 } }
142 }
143 Ok(buf)
144}
145
146fn wrap_text(text: &str, width: usize) -> Vec<String> {
147 if width == 0 { return vec![text.to_string()]; }
148 let mut lines = Vec::new();
149 for paragraph in text.split('\n') {
150 if paragraph.is_empty() { lines.push(String::new()); continue; }
151 let mut current = String::new();
152 for word in paragraph.split_whitespace() {
153 if current.is_empty() {
154 current = word.to_string();
155 } else if current.len() + 1 + word.len() <= width {
156 current.push(' ');
157 current.push_str(word);
158 } else {
159 lines.push(current);
160 current = word.to_string();
161 }
162 }
163 if !current.is_empty() { lines.push(current); }
164 }
165 if lines.is_empty() { lines.push(String::new()); }
166 lines
167}