Skip to content

Commit 7cc27ce

Browse files
committed
Add enum value support to input fields and improve error handling
1 parent 96ceca6 commit 7cc27ce

2 files changed

Lines changed: 81 additions & 8 deletions

File tree

src/tui/app.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ pub struct InputField {
120120
pub field_type: String,
121121
pub required: bool,
122122
pub description: Option<String>,
123+
/// Allowed values when the schema specifies "enum" (e.g. elicitation requestedSchema).
124+
pub enum_values: Option<Vec<String>>,
123125
}
124126

125127
impl App {
@@ -486,6 +488,7 @@ impl App {
486488
.entry(field_name.clone())
487489
.or_default()
488490
.push(c);
491+
self.error_message = None;
489492
} else if self.tool_call_input_mode {
490493
self.tool_call_inputs
491494
.entry(field_name.clone())
@@ -509,6 +512,7 @@ impl App {
509512
if let Some(value) = self.elicitation_inputs.get_mut(field_name) {
510513
value.pop();
511514
}
515+
self.error_message = None;
512516
} else if self.tool_call_input_mode {
513517
if let Some(value) = self.tool_call_inputs.get_mut(field_name) {
514518
value.pop();
@@ -668,6 +672,7 @@ impl App {
668672
field_type: "string".to_string(),
669673
required: arg.required.unwrap_or(false),
670674
description: arg.description.clone(),
675+
enum_values: None,
671676
})
672677
.collect()
673678
} else {
@@ -830,6 +835,17 @@ impl App {
830835
if let Some(value_str) = self.elicitation_inputs.get(&field.name) {
831836
let value_str = value_str.trim();
832837
if !value_str.is_empty() {
838+
if let Some(allowed) = &field.enum_values {
839+
if !allowed.iter().any(|v| v == value_str) {
840+
self.error_message = Some(format!(
841+
"Invalid value for '{}': \"{}\" is not allowed. Choose one of: {}.",
842+
field.name,
843+
value_str,
844+
allowed.join(", ")
845+
));
846+
return;
847+
}
848+
}
833849
let json_value = match field.field_type.as_str() {
834850
"number" | "integer" => {
835851
if let Ok(num) = value_str.parse::<i64>() {
@@ -1014,11 +1030,23 @@ fn parse_input_schema(schema: &Value) -> Vec<InputField> {
10141030

10151031
let required = required_fields.contains(name);
10161032

1033+
let enum_values = prop.get("enum").and_then(|e| e.as_array()).map(|arr| {
1034+
arr.iter()
1035+
.filter_map(|v| {
1036+
v.as_str()
1037+
.map(String::from)
1038+
.or_else(|| v.as_i64().map(|n| n.to_string()))
1039+
.or_else(|| v.as_f64().map(|n| n.to_string()))
1040+
})
1041+
.collect::<Vec<String>>()
1042+
});
1043+
10171044
fields.push(InputField {
10181045
name: name.clone(),
10191046
field_type,
10201047
required,
10211048
description,
1049+
enum_values,
10221050
});
10231051
}
10241052
}

src/tui/ui.rs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,16 @@ fn render_content(f: &mut Frame, app: &App, area: Rect) {
9696
return;
9797
}
9898

99+
// When in elicitation form, show errors inside the form overlay instead of replacing content
99100
if let Some(error) = &app.error_message {
100-
let error_widget = Paragraph::new(error.as_str())
101-
.block(Block::default().borders(Borders::ALL).title("Error"))
102-
.style(Style::default().fg(Color::Red))
103-
.wrap(Wrap { trim: true });
104-
f.render_widget(error_widget, area);
105-
return;
101+
if !app.elicitation_input_mode {
102+
let error_widget = Paragraph::new(error.as_str())
103+
.block(Block::default().borders(Borders::ALL).title("Error"))
104+
.style(Style::default().fg(Color::Red))
105+
.wrap(Wrap { trim: true });
106+
f.render_widget(error_widget, area);
107+
return;
108+
}
106109
}
107110

108111
match app.current_tab {
@@ -627,6 +630,16 @@ fn render_tool_input_form(f: &mut Frame, app: &App) {
627630
)));
628631
}
629632

633+
if let Some(opts) = &field.enum_values {
634+
if !opts.is_empty() {
635+
let hint = format!(" Allowed: {}", opts.join(", "));
636+
lines.push(Line::from(Span::styled(
637+
hint,
638+
Style::default().fg(Color::DarkGray),
639+
)));
640+
}
641+
}
642+
630643
let value_style = if is_current {
631644
Style::default()
632645
.fg(Color::Green)
@@ -798,9 +811,17 @@ fn render_elicitation_form(f: &mut Frame, app: &App) {
798811
.map(|p| p.message.lines().count())
799812
.unwrap_or(0)
800813
.max(1);
814+
let error_lines = app
815+
.error_message
816+
.as_ref()
817+
.map(|e| e.lines().count())
818+
.unwrap_or(0);
801819
let popup_width = area.width.saturating_sub(10).min(80);
802-
let popup_height = (message_lines as u16 + (app.input_fields.len() as u16 * 3) + 8)
803-
.min(area.height.saturating_sub(4));
820+
let popup_height = (message_lines as u16
821+
+ (error_lines as u16).saturating_add(2)
822+
+ (app.input_fields.len() as u16 * 3)
823+
+ 8)
824+
.min(area.height.saturating_sub(4));
804825

805826
let popup_area = Rect {
806827
x: (area.width.saturating_sub(popup_width)) / 2,
@@ -827,6 +848,20 @@ fn render_elicitation_form(f: &mut Frame, app: &App) {
827848

828849
let mut lines = Vec::new();
829850

851+
if let Some(error) = &app.error_message {
852+
lines.push(Line::from(Span::styled(
853+
"Error:",
854+
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
855+
)));
856+
for line in error.lines() {
857+
lines.push(Line::from(Span::styled(
858+
line,
859+
Style::default().fg(Color::Red),
860+
)));
861+
}
862+
lines.push(Line::from(""));
863+
}
864+
830865
if let Some(pending) = &app.pending_elicitation {
831866
lines.push(Line::from(Span::styled(
832867
&pending.message,
@@ -878,6 +913,16 @@ fn render_elicitation_form(f: &mut Frame, app: &App) {
878913
)));
879914
}
880915

916+
if let Some(opts) = &field.enum_values {
917+
if !opts.is_empty() {
918+
let hint = format!(" Allowed: {}", opts.join(", "));
919+
lines.push(Line::from(Span::styled(
920+
hint,
921+
Style::default().fg(Color::DarkGray),
922+
)));
923+
}
924+
}
925+
881926
let value_style = if is_current {
882927
Style::default()
883928
.fg(Color::Green)

0 commit comments

Comments
 (0)