@@ -45,6 +45,7 @@ type ElicitationDialog struct {
4545 enumIndexes map [int ]int // selected index for enum fields
4646 currentField int
4747 keyMap elicitationKeyMap
48+ fieldErrors map [int ]string // validation error messages per field
4849}
4950
5051type elicitationKeyMap struct {
@@ -60,6 +61,7 @@ func NewElicitationDialog(message string, schema any, _ map[string]any) Dialog {
6061 inputs : make ([]textinput.Model , len (fields )),
6162 boolValues : make (map [int ]bool ),
6263 enumIndexes : make (map [int ]int ),
64+ fieldErrors : make (map [int ]string ),
6365 keyMap : elicitationKeyMap {
6466 Up : key .NewBinding (key .WithKeys ("up" , "shift+tab" )),
6567 Down : key .NewBinding (key .WithKeys ("down" , "tab" )),
@@ -126,6 +128,9 @@ func (d *ElicitationDialog) handleKeyPress(msg tea.KeyPressMsg) (layout.Model, t
126128
127129// toggleCurrentSelection toggles boolean or cycles enum for the current field.
128130func (d * ElicitationDialog ) toggleCurrentSelection () {
131+ // Clear error when user interacts with the field
132+ delete (d .fieldErrors , d .currentField )
133+
129134 switch d .currentFieldType () {
130135 case "boolean" :
131136 d .boolValues [d .currentField ] = ! d .boolValues [d .currentField ]
@@ -147,16 +152,26 @@ func (d *ElicitationDialog) submit() (layout.Model, tea.Cmd) {
147152 cmd := d .close (tools .ElicitationActionAccept , nil )
148153 return d , cmd
149154 }
150- if content , ok := d .collectValues (); ok {
151- cmd := d .close (tools .ElicitationActionAccept , content )
152- return d , cmd
155+
156+ // Clear previous errors and validate
157+ d .fieldErrors = make (map [int ]string )
158+ content , firstErrorIdx := d .collectAndValidate ()
159+
160+ if firstErrorIdx >= 0 {
161+ // Focus the first field with an error
162+ d .focusField (firstErrorIdx )
163+ return d , nil
153164 }
154- return d , nil
165+
166+ cmd := d .close (tools .ElicitationActionAccept , content )
167+ return d , cmd
155168}
156169
157170func (d * ElicitationDialog ) updateCurrentInput (msg tea.KeyPressMsg ) (layout.Model , tea.Cmd ) {
158171 // Only text-based fields (not boolean/enum) use the text input
159172 if d .isTextInputField () {
173+ // Clear error for current field when user types
174+ delete (d .fieldErrors , d .currentField )
160175 var cmd tea.Cmd
161176 d .inputs [d .currentField ], cmd = d .inputs [d .currentField ].Update (msg )
162177 return d , cmd
@@ -168,11 +183,19 @@ func (d *ElicitationDialog) moveFocus(delta int) {
168183 if len (d .fields ) == 0 {
169184 return
170185 }
171- if len (d .inputs ) > 0 {
186+ newField := (d .currentField + delta + len (d .fields )) % len (d .fields )
187+ d .focusField (newField )
188+ }
189+
190+ // focusField moves focus to the specified field index.
191+ func (d * ElicitationDialog ) focusField (idx int ) {
192+ if idx < 0 || idx >= len (d .fields ) {
193+ return
194+ }
195+ if len (d .inputs ) > 0 && d .currentField < len (d .inputs ) {
172196 d .inputs [d .currentField ].Blur ()
173197 }
174- // Wrap around when cycling through fields
175- d .currentField = (d .currentField + delta + len (d .fields )) % len (d .fields )
198+ d .currentField = idx
176199 // Only focus text input for fields that use it
177200 if d .isTextInputField () {
178201 d .inputs [d .currentField ].Focus ()
@@ -192,8 +215,11 @@ func (d *ElicitationDialog) close(action tools.ElicitationAction, content map[st
192215 return CloseWithElicitationResponse (action , content )
193216}
194217
195- func (d * ElicitationDialog ) collectValues () (map [string ]any , bool ) {
218+ // collectAndValidate validates all fields and returns the collected values.
219+ // Returns the content map and the index of the first field with an error (-1 if valid).
220+ func (d * ElicitationDialog ) collectAndValidate () (map [string ]any , int ) {
196221 content := make (map [string ]any )
222+ firstErrorIdx := - 1
197223
198224 for i , field := range d .fields {
199225 switch field .Type {
@@ -203,7 +229,10 @@ func (d *ElicitationDialog) collectValues() (map[string]any, bool) {
203229 idx := d .enumIndexes [i ]
204230 if idx < 0 || idx >= len (field .EnumValues ) {
205231 if field .Required {
206- return nil , false
232+ d .fieldErrors [i ] = "Selection required"
233+ if firstErrorIdx < 0 {
234+ firstErrorIdx = i
235+ }
207236 }
208237 continue
209238 }
@@ -212,40 +241,65 @@ func (d *ElicitationDialog) collectValues() (map[string]any, bool) {
212241 val := strings .TrimSpace (d .inputs [i ].Value ())
213242 if val == "" {
214243 if field .Required {
215- return nil , false
244+ d .fieldErrors [i ] = "This field is required"
245+ if firstErrorIdx < 0 {
246+ firstErrorIdx = i
247+ }
216248 }
217249 continue
218250 }
219- parsed , ok := d .parseFieldValue (val , field )
220- if ! ok {
221- return nil , false
251+ parsed , errMsg := d .parseAndValidateField (val , field )
252+ if errMsg != "" {
253+ d .fieldErrors [i ] = errMsg
254+ if firstErrorIdx < 0 {
255+ firstErrorIdx = i
256+ }
257+ continue
222258 }
223259 content [field .Name ] = parsed
224260 }
225261 }
226- return content , true
262+ return content , firstErrorIdx
227263}
228264
229- // parseFieldValue parses and validates a field value based on its type .
230- func (d * ElicitationDialog ) parseFieldValue (val string , field ElicitationField ) (any , bool ) {
265+ // parseAndValidateField parses and validates a field value, returning the parsed value and an error message .
266+ func (d * ElicitationDialog ) parseAndValidateField (val string , field ElicitationField ) (any , string ) {
231267 if val == "" {
232- return nil , false
268+ return nil , ""
233269 }
234270
235271 switch field .Type {
236272 case "number" :
237273 f , err := strconv .ParseFloat (val , 64 )
238- return f , err == nil && validateNumberField (f , field )
274+ if err != nil {
275+ return nil , "Must be a valid number"
276+ }
277+ if errMsg := validateNumberFieldWithMessage (f , field ); errMsg != "" {
278+ return nil , errMsg
279+ }
280+ return f , ""
239281
240282 case "integer" :
241283 n , err := strconv .ParseInt (val , 10 , 64 )
242- return n , err == nil && validateNumberField (float64 (n ), field )
284+ if err != nil {
285+ return nil , "Must be a whole number"
286+ }
287+ if errMsg := validateNumberFieldWithMessage (float64 (n ), field ); errMsg != "" {
288+ return nil , errMsg
289+ }
290+ return n , ""
243291
244292 case "enum" :
245- return val , slices .Contains (field .EnumValues , val )
293+ if ! slices .Contains (field .EnumValues , val ) {
294+ return nil , "Invalid selection"
295+ }
296+ return val , ""
246297
247298 default : // string
248- return val , validateStringField (val , field )
299+ if errMsg := validateStringFieldWithMessage (val , field ); errMsg != "" {
300+ return nil , errMsg
301+ }
302+ return val , ""
249303 }
250304}
251305
@@ -301,7 +355,14 @@ func (d *ElicitationDialog) renderField(content *Content, i int, field Elicitati
301355 if field .Required {
302356 label += "*"
303357 }
304- content .AddContent (styles .DialogContentStyle .Bold (true ).Render (label ))
358+
359+ // Check if this field has an error
360+ hasError := d .fieldErrors [i ] != ""
361+ labelStyle := styles .DialogContentStyle .Bold (true )
362+ if hasError {
363+ labelStyle = labelStyle .Foreground (styles .Error )
364+ }
365+ content .AddContent (labelStyle .Render (label ))
305366
306367 // Render field input based on type
307368 isFocused := i == d .currentField
@@ -314,6 +375,12 @@ func (d *ElicitationDialog) renderField(content *Content, i int, field Elicitati
314375 d .inputs [i ].SetWidth (contentWidth )
315376 content .AddContent (d .inputs [i ].View ())
316377 }
378+
379+ // Show error message if present
380+ if hasError {
381+ errorStyle := styles .DialogContentStyle .Foreground (styles .Error ).Italic (true )
382+ content .AddContent (errorStyle .Render (" ⚠ " + d .fieldErrors [i ]))
383+ }
317384}
318385
319386func (d * ElicitationDialog ) renderBooleanField (content * Content , i int , isFocused bool ) {
0 commit comments