1use crate::tools::{Tool, ToolResult};
2use anyhow::{anyhow, Context, Result};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use std::io::IsTerminal;
7use std::time::Duration;
8use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader, Stdin};
9use tokio::time::timeout;
10
11pub struct PromptUserTool;
13
14impl PromptUserTool {
15 pub fn new() -> Self {
16 Self
17 }
18
19 fn supports_interactive() -> bool {
20 std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
21 }
22
23 fn default_required() -> bool {
24 true
25 }
26
27 async fn prompt_interactively(&self, args: &PromptUserArgs) -> Result<NormalizedResponse> {
28 if !Self::supports_interactive() {
29 return Err(anyhow!(
30 "Interactive prompting is unavailable (stdin/stdout not a TTY). Provide `prefilled_response` instead."
31 ));
32 }
33
34 self.print_prompt_header(args).await?;
35 let stdin = io::stdin();
36 let mut reader = BufReader::new(stdin);
37 match args.input_type {
38 PromptInputType::MultilineText => self.collect_multiline(&mut reader, args).await,
39 _ => self.collect_single_line(&mut reader, args).await,
40 }
41 }
42
43 async fn collect_single_line(
44 &self,
45 reader: &mut BufReader<Stdin>,
46 args: &PromptUserArgs,
47 ) -> Result<NormalizedResponse> {
48 let mut stdout = io::stdout();
49 loop {
50 stdout.write_all(b"> ").await?;
51 stdout.flush().await?;
52 let mut buffer = String::new();
53 let bytes = self
54 .read_line(reader, &mut buffer, args.timeout_seconds)
55 .await?;
56 if bytes == 0 {
57 return Err(anyhow!("User input closed before a value was provided"));
58 }
59 let trimmed = buffer.trim_end().to_string();
60
61 if trimmed.is_empty() {
62 if let Some(res) = self.empty_fallback(args)? {
63 return Ok(res);
64 }
65 self.write_warning(
66 "A response is required. Please provide a value or configure a default.",
67 )
68 .await?;
69 continue;
70 }
71
72 match self.parse_user_input(&trimmed, args) {
73 Ok(resp) => return Ok(resp),
74 Err(err) => {
75 self.write_warning(&format!("{}", err)).await?;
76 }
77 }
78 }
79 }
80
81 async fn collect_multiline(
82 &self,
83 reader: &mut BufReader<Stdin>,
84 args: &PromptUserArgs,
85 ) -> Result<NormalizedResponse> {
86 let mut stdout = io::stdout();
87 stdout
88 .write_all(
89 b"Enter your response. Finish by typing '/end' on its own line or '/skip' to leave empty.\n",
90 )
91 .await?;
92 stdout.flush().await?;
93
94 loop {
95 let mut lines = Vec::new();
96 loop {
97 stdout.write_all(b"| ").await?;
98 stdout.flush().await?;
99 let mut buffer = String::new();
100 let bytes = self
101 .read_line(reader, &mut buffer, args.timeout_seconds)
102 .await?;
103 if bytes == 0 {
104 return Err(anyhow!(
105 "User input closed before multiline response finished"
106 ));
107 }
108 let trimmed = buffer.trim_end();
109 if trimmed.eq_ignore_ascii_case("/end") {
110 break;
111 }
112 if trimmed.eq_ignore_ascii_case("/skip") {
113 if let Some(res) = self.empty_fallback(args)? {
114 return Ok(res);
115 }
116 self.write_warning(
117 "This prompt is required. Please provide content before finishing.",
118 )
119 .await?;
120 continue;
121 }
122 lines.push(trimmed.to_string());
123 }
124
125 if lines.is_empty() {
126 if let Some(res) = self.empty_fallback(args)? {
127 return Ok(res);
128 }
129 self.write_warning("A response is required. Please try again.")
130 .await?;
131 continue;
132 }
133
134 let combined = lines.join("\n");
135 return self.normalize_text_value(combined, args, false, false);
136 }
137 }
138
139 async fn read_line(
140 &self,
141 reader: &mut BufReader<Stdin>,
142 buffer: &mut String,
143 timeout_secs: Option<u64>,
144 ) -> Result<usize> {
145 buffer.clear();
146 if let Some(secs) = timeout_secs {
147 match timeout(Duration::from_secs(secs), reader.read_line(buffer)).await {
148 Ok(res) => Ok(res.context("Failed to read user input")?),
149 Err(_) => Err(anyhow!(
150 "Timed out waiting for user input after {} seconds",
151 secs
152 )),
153 }
154 } else {
155 Ok(reader
156 .read_line(buffer)
157 .await
158 .context("Failed to read user input")?)
159 }
160 }
161
162 async fn write_warning(&self, message: &str) -> Result<()> {
163 let mut stdout = io::stdout();
164 stdout
165 .write_all(format!("⚠️ {}\n", message).as_bytes())
166 .await?;
167 stdout.flush().await?;
168 Ok(())
169 }
170
171 async fn print_prompt_header(&self, args: &PromptUserArgs) -> Result<()> {
172 let mut stdout = io::stdout();
173 let mut section = String::new();
174 section.push_str("\n🔸 User Input Required\n");
175 section.push_str(&format!("{}\n", args.prompt.trim()));
176 if let Some(instructions) = &args.instructions {
177 section.push_str(instructions.trim());
178 section.push('\n');
179 }
180 if let Some(placeholder) = &args.placeholder {
181 section.push_str(&format!("Hint: {}\n", placeholder));
182 }
183 if !args.options.is_empty() {
184 section.push_str("Options:\n");
185 for (idx, opt) in args.options.iter().enumerate() {
186 let label = opt.label.as_deref().unwrap_or("(option)");
187 let mut line = format!(" [{}] {}", idx + 1, label);
188 if let Some(code) = &opt.short_code {
189 line.push_str(&format!(" (code: {})", code));
190 }
191 if let Some(desc) = &opt.description {
192 line.push_str(&format!(" — {}", desc));
193 }
194 if let Some(preview) = value_preview(&opt.value) {
195 line.push_str(&format!(" [value: {}]", preview));
196 }
197 line.push('\n');
198 section.push_str(&line);
199 }
200 if args.allow_freeform {
201 section.push_str(" (Custom values are allowed.)\n");
202 }
203 }
204 if let Some(validation) = &args.validation_hint {
205 section.push_str(&format!("Validation: {}\n", validation));
206 }
207
208 match args.input_type {
209 PromptInputType::MultilineText => {
210 section.push_str(
211 "Enter text over multiple lines. Type '/end' on its own line when finished.\n",
212 );
213 }
214 PromptInputType::Boolean => {
215 section.push_str("Respond with yes/no, true/false, or y/n.\n");
216 }
217 PromptInputType::Number => {
218 section.push_str("Enter a numeric value.\n");
219 }
220 PromptInputType::SingleSelect | PromptInputType::MultiSelect => {
221 section.push_str(
222 "Choose by number, label, or short code. Separate multiple choices with commas.\n",
223 );
224 }
225 PromptInputType::Json => {
226 section.push_str("Provide a JSON value (object, array, string, etc.).\n");
227 }
228 PromptInputType::Text => {}
229 }
230
231 stdout.write_all(section.as_bytes()).await?;
232 stdout.flush().await?;
233 Ok(())
234 }
235
236 fn empty_fallback(&self, args: &PromptUserArgs) -> Result<Option<NormalizedResponse>> {
237 if let Some(default_value) = &args.default_value {
238 let mut normalized = self.normalize_prefill(default_value.clone(), args)?;
239 normalized.used_default = true;
240 return Ok(Some(normalized));
241 }
242 if !args.required {
243 return Ok(Some(NormalizedResponse::empty()));
244 }
245 Ok(None)
246 }
247
248 fn parse_user_input(&self, raw: &str, args: &PromptUserArgs) -> Result<NormalizedResponse> {
249 match args.input_type {
250 PromptInputType::Text => self.normalize_text_value(raw.to_string(), args, false, false),
251 PromptInputType::MultilineText => {
252 self.normalize_text_value(raw.to_string(), args, false, false)
253 }
254 PromptInputType::Boolean => {
255 let value = parse_bool(raw)
256 .ok_or_else(|| anyhow!("Could not interpret '{}' as yes/no", raw))?;
257 Ok(NormalizedResponse::from_bool(value))
258 }
259 PromptInputType::Number => {
260 let value: f64 = raw
261 .parse()
262 .map_err(|_| anyhow!("Could not interpret '{}' as a number", raw))?;
263 self.normalize_number_value(value, args, false, false)
264 }
265 PromptInputType::SingleSelect => self.resolve_single_selection(raw, args, false, false),
266 PromptInputType::MultiSelect => self.resolve_multi_selection(raw, args, false, false),
267 PromptInputType::Json => {
268 let value: Value = serde_json::from_str(raw)
269 .map_err(|err| anyhow!("Invalid JSON input: {}", err))?;
270 Ok(NormalizedResponse::from_json(value))
271 }
272 }
273 }
274
275 fn normalize_prefill(&self, value: Value, args: &PromptUserArgs) -> Result<NormalizedResponse> {
276 match args.input_type {
277 PromptInputType::Text | PromptInputType::MultilineText => {
278 let text = value_to_owned_string(&value).ok_or_else(|| {
279 anyhow!("prefilled_response must be a string for text prompts")
280 })?;
281 self.normalize_text_value(text, args, false, true)
282 }
283 PromptInputType::Boolean => {
284 let as_bool = match value {
285 Value::Bool(b) => Some(b),
286 Value::String(s) => parse_bool(&s),
287 Value::Number(n) => {
288 if let Some(i) = n.as_i64() {
289 if i == 0 {
290 Some(false)
291 } else if i == 1 {
292 Some(true)
293 } else {
294 None
295 }
296 } else {
297 None
298 }
299 }
300 _ => None,
301 }
302 .ok_or_else(|| anyhow!("prefilled_response must be boolean"))?;
303 Ok(NormalizedResponse::from_prefilled_bool(as_bool))
304 }
305 PromptInputType::Number => {
306 let numeric = match &value {
307 Value::Number(num) => num.as_f64(),
308 Value::String(s) => s.parse().ok(),
309 _ => None,
310 }
311 .ok_or_else(|| anyhow!("prefilled_response must be numeric"))?;
312 self.normalize_number_value(numeric, args, false, true)
313 }
314 PromptInputType::SingleSelect => {
315 if value.is_null() && !args.required {
316 return Ok(NormalizedResponse::empty());
317 }
318 self.match_prefilled_selection(value, args, false, true)
319 }
320 PromptInputType::MultiSelect => self.match_prefilled_multi(value, args, false, true),
321 PromptInputType::Json => Ok(NormalizedResponse::from_prefilled_json(value)),
322 }
323 }
324
325 fn normalize_text_value(
326 &self,
327 mut text: String,
328 args: &PromptUserArgs,
329 used_default: bool,
330 used_prefill: bool,
331 ) -> Result<NormalizedResponse> {
332 let len = text.chars().count();
333 if let Some(min) = args.min_length {
334 if len < min {
335 return Err(anyhow!("Response must be at least {} characters", min));
336 }
337 }
338 if let Some(max) = args.max_length {
339 if len > max {
340 text.truncate(max);
341 }
342 }
343 Ok(NormalizedResponse::from_string(
344 text,
345 used_default,
346 used_prefill,
347 ))
348 }
349
350 fn normalize_number_value(
351 &self,
352 value: f64,
353 args: &PromptUserArgs,
354 used_default: bool,
355 used_prefill: bool,
356 ) -> Result<NormalizedResponse> {
357 if let Some(min) = args.min_value {
358 if value < min {
359 return Err(anyhow!("Value must be >= {}", min));
360 }
361 }
362 if let Some(max) = args.max_value {
363 if value > max {
364 return Err(anyhow!("Value must be <= {}", max));
365 }
366 }
367 if let Some(step) = args.step {
368 if step > 0.0 {
369 let quotient = value / step;
370 let nearest = quotient.round();
371 if (quotient - nearest).abs() > 1e-6 {
372 return Err(anyhow!("Value must be a multiple of {}", step));
373 }
374 }
375 }
376 Ok(NormalizedResponse::from_number(
377 value,
378 used_default,
379 used_prefill,
380 ))
381 }
382
383 fn resolve_single_selection(
384 &self,
385 raw: &str,
386 args: &PromptUserArgs,
387 used_default: bool,
388 used_prefill: bool,
389 ) -> Result<NormalizedResponse> {
390 if args.options.is_empty() && !args.allow_freeform {
391 return Err(anyhow!(
392 "No options provided. Set `allow_freeform` to true for free-text answers."
393 ));
394 }
395
396 if args.options.is_empty() {
397 return Ok(NormalizedResponse::from_string(
398 raw.to_string(),
399 used_default,
400 used_prefill,
401 ));
402 }
403
404 match match_option(raw, &args.options) {
405 Some((label, value)) => Ok(NormalizedResponse::from_selection(
406 value,
407 Some(label),
408 used_default,
409 used_prefill,
410 )),
411 None if args.allow_freeform => Ok(NormalizedResponse::from_string(
412 raw.to_string(),
413 used_default,
414 used_prefill,
415 )),
416 None => Err(anyhow!("'{}' did not match any available options", raw)),
417 }
418 }
419
420 fn resolve_multi_selection(
421 &self,
422 raw: &str,
423 args: &PromptUserArgs,
424 used_default: bool,
425 used_prefill: bool,
426 ) -> Result<NormalizedResponse> {
427 if args.options.is_empty() && !args.allow_freeform {
428 return Err(anyhow!(
429 "Multi-select prompts require options unless `allow_freeform` is true"
430 ));
431 }
432 let tokens: Vec<_> = raw
433 .split(',')
434 .map(|t| t.trim())
435 .filter(|t| !t.is_empty())
436 .collect();
437 if tokens.is_empty() {
438 return Err(anyhow!("Provide at least one selection"));
439 }
440
441 let mut values = Vec::new();
442 let mut labels = Vec::new();
443 for token in tokens {
444 if let Some((label, value)) = match_option(token, &args.options) {
445 values.push(value);
446 labels.push(label);
447 } else if args.allow_freeform {
448 values.push(Value::String(token.to_string()));
449 } else {
450 return Err(anyhow!("'{}' did not match any available options", token));
451 }
452 }
453 Ok(NormalizedResponse::from_multi_selection(
454 values,
455 labels,
456 used_default,
457 used_prefill,
458 ))
459 }
460
461 fn match_prefilled_selection(
462 &self,
463 value: Value,
464 args: &PromptUserArgs,
465 used_default: bool,
466 used_prefill: bool,
467 ) -> Result<NormalizedResponse> {
468 if args.options.is_empty() {
469 if args.allow_freeform {
470 return Ok(NormalizedResponse::from_json(value));
471 }
472 return Err(anyhow!(
473 "prefilled_response must correspond to a provided option"
474 ));
475 }
476
477 for opt in &args.options {
478 if opt.value == value {
479 let label = opt
480 .label
481 .clone()
482 .or_else(|| value_to_owned_string(&opt.value));
483 return Ok(NormalizedResponse::from_selection(
484 opt.value.clone(),
485 label,
486 used_default,
487 used_prefill,
488 ));
489 }
490 }
491
492 if args.allow_freeform {
493 return Ok(NormalizedResponse::from_json(value));
494 }
495
496 Err(anyhow!("prefilled_response value did not match any option"))
497 }
498
499 fn match_prefilled_multi(
500 &self,
501 value: Value,
502 args: &PromptUserArgs,
503 used_default: bool,
504 used_prefill: bool,
505 ) -> Result<NormalizedResponse> {
506 let values = if let Value::Array(arr) = value {
507 arr
508 } else if let Some(s) = value_to_owned_string(&value) {
509 s.split(',')
510 .map(|t| Value::String(t.trim().to_string()))
511 .collect()
512 } else {
513 return Err(anyhow!(
514 "prefilled_response must be an array or comma-delimited string"
515 ));
516 };
517
518 if values.is_empty() {
519 return Err(anyhow!(
520 "prefilled_response must contain at least one entry"
521 ));
522 }
523
524 let mut resolved_values = Vec::new();
525 let mut labels = Vec::new();
526 for val in values {
527 if let Some(opt_label) =
528 args.options
529 .iter()
530 .find(|opt| opt.value == val)
531 .and_then(|opt| {
532 opt.label
533 .clone()
534 .or_else(|| value_to_owned_string(&opt.value))
535 })
536 {
537 resolved_values.push(val.clone());
538 labels.push(opt_label);
539 } else if args.allow_freeform {
540 resolved_values.push(val.clone());
541 } else {
542 return Err(anyhow!(
543 "prefilled_response contained a value not present in options"
544 ));
545 }
546 }
547
548 Ok(NormalizedResponse::from_multi_selection(
549 resolved_values,
550 labels,
551 used_default,
552 used_prefill,
553 ))
554 }
555}
556
557impl Default for PromptUserTool {
558 fn default() -> Self {
559 Self::new()
560 }
561}
562
563#[async_trait]
564impl Tool for PromptUserTool {
565 fn name(&self) -> &str {
566 "prompt_user"
567 }
568
569 fn description(&self) -> &str {
570 "Prompts the human user for structured input (text, boolean, number, selections, or JSON)."
571 }
572
573 fn parameters(&self) -> Value {
574 json!({
575 "type": "object",
576 "properties": {
577 "prompt": {"type": "string", "description": "Friendly prompt shown to the user."},
578 "input_type": {
579 "type": "string",
580 "description": "Type of input expected from the user.",
581 "enum": [
582 "text",
583 "multiline_text",
584 "boolean",
585 "number",
586 "single_select",
587 "multi_select",
588 "json"
589 ],
590 "default": "text"
591 },
592 "placeholder": {"type": "string"},
593 "instructions": {"type": "string", "description": "Extra instructions displayed before collecting input."},
594 "required": {"type": "boolean", "default": true},
595 "options": {
596 "type": "array",
597 "description": "List of allowed options for select-style prompts.",
598 "items": {
599 "type": "object",
600 "properties": {
601 "label": {"type": "string"},
602 "description": {"type": "string"},
603 "short_code": {"type": "string", "description": "Short alias like 'a', 'b', or 'high'."},
604 "value": {"description": "JSON value returned when this option is chosen."}
605 },
606 "required": ["value"]
607 }
608 },
609 "allow_freeform": {
610 "type": "boolean",
611 "description": "Allow responses outside the provided options for selection prompts.",
612 "default": false
613 },
614 "default_value": {
615 "description": "Default value used when the user skips the prompt."
616 },
617 "prefilled_response": {
618 "description": "Provide a value here to bypass interactive prompting (useful for automated flows)."
619 },
620 "min_length": {"type": "integer", "minimum": 0},
621 "max_length": {"type": "integer", "minimum": 1},
622 "min_value": {"type": "number"},
623 "max_value": {"type": "number"},
624 "step": {
625 "type": "number",
626 "description": "Restrict numeric responses to multiples of this value."
627 },
628 "validation_hint": {"type": "string", "description": "Text displayed to the user describing validation requirements."},
629 "metadata": {"type": "object", "description": "Arbitrary metadata echoed back with the response."},
630 "timeout_seconds": {
631 "type": "integer",
632 "minimum": 1,
633 "description": "Abort prompting if no input is received within this many seconds."
634 }
635 },
636 "required": ["prompt", "input_type"]
637 })
638 }
639
640 async fn execute(&self, args: Value) -> Result<ToolResult> {
641 let params: PromptUserArgs =
642 serde_json::from_value(args).context("Failed to parse prompt_user arguments")?;
643
644 let response = if let Some(prefill) = ¶ms.prefilled_response {
645 match self.normalize_prefill(prefill.clone(), ¶ms) {
646 Ok(mut resp) => {
647 resp.used_prefill = true;
648 resp
649 }
650 Err(err) => return Ok(ToolResult::failure(err.to_string())),
651 }
652 } else {
653 match self.prompt_interactively(¶ms).await {
654 Ok(resp) => resp,
655 Err(err) => return Ok(ToolResult::failure(err.to_string())),
656 }
657 };
658
659 let payload = PromptUserPayload {
660 prompt: params.prompt,
661 input_type: params.input_type.as_str().to_string(),
662 response: response.value,
663 display_value: response.display_value,
664 selections: response.selection_labels,
665 metadata: params.metadata,
666 used_default: response.used_default,
667 used_prefill: response.used_prefill,
668 };
669
670 let output = serde_json::to_string(&payload)?;
671 Ok(ToolResult::success(output))
672 }
673}
674
675#[derive(Debug, Clone, Deserialize)]
676#[serde(rename_all = "snake_case")]
677#[derive(Default)]
678enum PromptInputType {
679 #[default]
680 Text,
681 MultilineText,
682 Boolean,
683 Number,
684 SingleSelect,
685 MultiSelect,
686 Json,
687}
688
689impl PromptInputType {
690 fn as_str(&self) -> &'static str {
691 match self {
692 PromptInputType::Text => "text",
693 PromptInputType::MultilineText => "multiline_text",
694 PromptInputType::Boolean => "boolean",
695 PromptInputType::Number => "number",
696 PromptInputType::SingleSelect => "single_select",
697 PromptInputType::MultiSelect => "multi_select",
698 PromptInputType::Json => "json",
699 }
700 }
701}
702
703#[derive(Debug, Clone, Deserialize)]
704struct PromptOption {
705 #[serde(default)]
706 label: Option<String>,
707 #[serde(default)]
708 description: Option<String>,
709 #[serde(default)]
710 short_code: Option<String>,
711 value: Value,
712}
713
714#[derive(Debug, Deserialize)]
715struct PromptUserArgs {
716 prompt: String,
717 #[serde(default)]
718 input_type: PromptInputType,
719 #[serde(default)]
720 placeholder: Option<String>,
721 #[serde(default)]
722 instructions: Option<String>,
723 #[serde(default = "PromptUserTool::default_required")]
724 required: bool,
725 #[serde(default)]
726 options: Vec<PromptOption>,
727 #[serde(default)]
728 allow_freeform: bool,
729 #[serde(default)]
730 default_value: Option<Value>,
731 #[serde(default)]
732 prefilled_response: Option<Value>,
733 #[serde(default)]
734 min_length: Option<usize>,
735 #[serde(default)]
736 max_length: Option<usize>,
737 #[serde(default)]
738 min_value: Option<f64>,
739 #[serde(default)]
740 max_value: Option<f64>,
741 #[serde(default)]
742 step: Option<f64>,
743 #[serde(default)]
744 validation_hint: Option<String>,
745 #[serde(default)]
746 metadata: Option<Value>,
747 #[serde(default)]
748 timeout_seconds: Option<u64>,
749}
750
751#[derive(Debug, Serialize)]
752struct PromptUserPayload {
753 prompt: String,
754 input_type: String,
755 response: Value,
756 #[serde(skip_serializing_if = "Option::is_none")]
757 display_value: Option<String>,
758 #[serde(skip_serializing_if = "Option::is_none")]
759 selections: Option<Vec<String>>,
760 #[serde(skip_serializing_if = "Option::is_none")]
761 metadata: Option<Value>,
762 used_default: bool,
763 used_prefill: bool,
764}
765
766#[derive(Debug)]
767struct NormalizedResponse {
768 value: Value,
769 display_value: Option<String>,
770 selection_labels: Option<Vec<String>>,
771 used_default: bool,
772 used_prefill: bool,
773}
774
775impl NormalizedResponse {
776 fn empty() -> Self {
777 Self {
778 value: Value::Null,
779 display_value: None,
780 selection_labels: None,
781 used_default: false,
782 used_prefill: false,
783 }
784 }
785
786 fn from_string(value: String, used_default: bool, used_prefill: bool) -> Self {
787 Self {
788 display_value: Some(value.clone()),
789 value: Value::String(value),
790 selection_labels: None,
791 used_default,
792 used_prefill,
793 }
794 }
795
796 fn from_bool(value: bool) -> Self {
797 Self {
798 display_value: Some(value.to_string()),
799 value: Value::Bool(value),
800 selection_labels: None,
801 used_default: false,
802 used_prefill: false,
803 }
804 }
805
806 fn from_prefilled_bool(value: bool) -> Self {
807 Self {
808 display_value: Some(value.to_string()),
809 value: Value::Bool(value),
810 selection_labels: None,
811 used_default: false,
812 used_prefill: true,
813 }
814 }
815
816 fn from_number(value: f64, used_default: bool, used_prefill: bool) -> Self {
817 Self {
818 display_value: Some(value.to_string()),
819 value: json!(value),
820 selection_labels: None,
821 used_default,
822 used_prefill,
823 }
824 }
825
826 fn from_json(value: Value) -> Self {
827 Self {
828 selection_labels: None,
829 display_value: value_to_owned_string(&value),
830 value,
831 used_default: false,
832 used_prefill: false,
833 }
834 }
835
836 fn from_prefilled_json(value: Value) -> Self {
837 Self {
838 selection_labels: None,
839 display_value: value_to_owned_string(&value),
840 value,
841 used_default: false,
842 used_prefill: true,
843 }
844 }
845
846 fn from_selection(
847 value: Value,
848 label: Option<String>,
849 used_default: bool,
850 used_prefill: bool,
851 ) -> Self {
852 Self {
853 selection_labels: label.clone().map(|l| vec![l.clone()]),
854 display_value: label,
855 value,
856 used_default,
857 used_prefill,
858 }
859 }
860
861 fn from_multi_selection(
862 values: Vec<Value>,
863 labels: Vec<String>,
864 used_default: bool,
865 used_prefill: bool,
866 ) -> Self {
867 Self {
868 selection_labels: if labels.is_empty() {
869 None
870 } else {
871 Some(labels)
872 },
873 display_value: None,
874 value: Value::Array(values),
875 used_default,
876 used_prefill,
877 }
878 }
879}
880
881fn parse_bool(input: &str) -> Option<bool> {
882 match input.trim().to_lowercase().as_str() {
883 "y" | "yes" | "true" | "t" | "1" => Some(true),
884 "n" | "no" | "false" | "f" | "0" => Some(false),
885 _ => None,
886 }
887}
888
889fn match_option(token: &str, options: &[PromptOption]) -> Option<(String, Value)> {
890 if options.is_empty() {
891 return None;
892 }
893 let trimmed = token.trim();
894 let lower = trimmed.to_lowercase();
895 let numeric_choice = trimmed.parse::<usize>().ok();
896
897 for (idx, opt) in options.iter().enumerate() {
898 if let Some(choice) = numeric_choice {
899 if choice == idx + 1 {
900 let label = opt
901 .label
902 .clone()
903 .or_else(|| value_to_owned_string(&opt.value))
904 .unwrap_or_else(|| format!("Option {}", choice));
905 return Some((label, opt.value.clone()));
906 }
907 }
908
909 if let Some(label) = &opt.label {
910 if label.to_lowercase() == lower {
911 return Some((label.clone(), opt.value.clone()));
912 }
913 }
914
915 if let Some(code) = &opt.short_code {
916 if code.to_lowercase() == lower {
917 let label = opt.label.clone().unwrap_or_else(|| code.clone());
918 return Some((label, opt.value.clone()));
919 }
920 }
921
922 if let Some(value_repr) = value_to_owned_string(&opt.value) {
923 if value_repr.to_lowercase() == lower {
924 let label = opt.label.clone().unwrap_or(value_repr.clone());
925 return Some((label, opt.value.clone()));
926 }
927 }
928 }
929
930 None
931}
932
933fn value_to_owned_string(value: &Value) -> Option<String> {
934 match value {
935 Value::Null => None,
936 Value::Bool(b) => Some(b.to_string()),
937 Value::Number(n) => Some(n.to_string()),
938 Value::String(s) => Some(s.clone()),
939 Value::Array(_) | Value::Object(_) => Some(value.to_string()),
940 }
941}
942
943fn value_preview(value: &Value) -> Option<String> {
944 let repr = value_to_owned_string(value)?;
945 if repr.len() > 48 {
946 Some(format!("{}…", &repr[..45]))
947 } else {
948 Some(repr)
949 }
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955
956 #[tokio::test]
957 async fn test_prompt_user_prefilled_text() {
958 let tool = PromptUserTool::new();
959 let args = json!({
960 "prompt": "Provide a status update",
961 "input_type": "text",
962 "prefilled_response": "All systems nominal"
963 });
964
965 let result = tool.execute(args).await.unwrap();
966 assert!(result.success);
967
968 let payload: Value = serde_json::from_str(&result.output).unwrap();
969 assert_eq!(payload["response"], "All systems nominal");
970 assert_eq!(payload["used_prefill"], true);
971 }
972
973 #[tokio::test]
974 async fn test_prompt_user_prefilled_select() {
975 let tool = PromptUserTool::new();
976 let args = json!({
977 "prompt": "Choose environment",
978 "input_type": "single_select",
979 "options": [
980 {"label": "Production", "short_code": "prod", "value": "prod"},
981 {"label": "Staging", "short_code": "stage", "value": "stage"}
982 ],
983 "prefilled_response": "stage"
984 });
985
986 let result = tool.execute(args).await.unwrap();
987 assert!(result.success, "error: {:?}", result.error);
988 let payload: Value = serde_json::from_str(&result.output).unwrap();
989 assert_eq!(payload["response"], "stage");
990 assert_eq!(payload["selections"].as_array().unwrap()[0], "Staging");
991 }
992
993 #[tokio::test]
994 async fn test_prompt_user_prefilled_multi_select() {
995 let tool = PromptUserTool::new();
996 let args = json!({
997 "prompt": "Select tags",
998 "input_type": "multi_select",
999 "options": [
1000 {"label": "Urgent", "short_code": "u", "value": "urgent"},
1001 {"label": "Follow-up", "short_code": "f", "value": "follow"}
1002 ],
1003 "prefilled_response": ["urgent", "follow"]
1004 });
1005
1006 let result = tool.execute(args).await.unwrap();
1007 assert!(result.success);
1008 let payload: Value = serde_json::from_str(&result.output).unwrap();
1009 assert_eq!(payload["response"].as_array().unwrap().len(), 2);
1010 assert_eq!(payload["used_prefill"], true);
1011 }
1012
1013 #[tokio::test]
1014 async fn test_prompt_user_missing_prefill_fails_when_noninteractive() {
1015 if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
1017 eprintln!("Skipping test: running in interactive terminal");
1018 return;
1019 }
1020
1021 let tool = PromptUserTool::new();
1022 let args = json!({
1023 "prompt": "Need manual input",
1024 "input_type": "text"
1025 });
1026
1027 let result = tool.execute(args).await.unwrap();
1028 assert!(!result.success);
1029 assert!(result
1030 .error
1031 .unwrap_or_default()
1032 .contains("Interactive prompting is unavailable"));
1033 }
1034
1035 #[tokio::test]
1036 async fn test_prompt_user_invalid_prefill_option() {
1037 let tool = PromptUserTool::new();
1038 let args = json!({
1039 "prompt": "Pick a lane",
1040 "input_type": "single_select",
1041 "options": [
1042 {"label": "Blue", "value": "blue"}
1043 ],
1044 "prefilled_response": "red"
1045 });
1046
1047 let result = tool.execute(args).await.unwrap();
1048 assert!(!result.success);
1049 }
1050}