@@ -37,46 +37,83 @@ type ElicitationField struct {
3737}
3838
3939// ElicitationDialog implements Dialog for MCP elicitation requests.
40+ //
41+ // When a schema is provided, fields are rendered as a form.
42+ // When no schema is provided, a single free-form text input (responseInput)
43+ // is shown so the user can type an answer.
4044type ElicitationDialog struct {
4145 BaseDialog
42- message string
43- fields []ElicitationField
44- inputs []textinput.Model
45- boolValues map [int ]bool
46- enumIndexes map [int ]int // selected index for enum fields
47- currentField int
48- keyMap elicitationKeyMap
49- fieldErrors map [int ]string // validation error messages per field
46+ title string
47+ message string
48+ fields []ElicitationField
49+ inputs []textinput.Model
50+ boolValues map [int ]bool
51+ enumIndexes map [int ]int // selected index for enum fields
52+ currentField int
53+ keyMap elicitationKeyMap
54+ fieldErrors map [int ]string // validation error messages per field
55+ responseInput textinput.Model // free-form text input used when len(fields) == 0
5056}
5157
5258type elicitationKeyMap struct {
53- Up , Down , Enter , Escape , Space key.Binding
59+ Up , Down , Tab , ShiftTab , Enter , Escape , Space key.Binding
60+ }
61+
62+ // hasFreeFormInput returns true when no schema fields exist and the dialog
63+ // shows a single free-form text input instead.
64+ func (d * ElicitationDialog ) hasFreeFormInput () bool {
65+ return len (d .fields ) == 0
5466}
5567
5668// NewElicitationDialog creates a new elicitation dialog.
57- func NewElicitationDialog (message string , schema any , _ map [string ]any ) Dialog {
69+ func NewElicitationDialog (message string , schema any , meta map [string ]any ) Dialog {
5870 fields := parseElicitationSchema (schema )
71+
72+ // Determine dialog title from meta, defaulting to "Question"
73+ title := "Question"
74+ if meta != nil {
75+ if t , ok := meta ["cagent/title" ].(string ); ok && t != "" {
76+ title = t
77+ }
78+ }
79+
5980 d := & ElicitationDialog {
81+ title : title ,
6082 message : message ,
6183 fields : fields ,
6284 inputs : make ([]textinput.Model , len (fields )),
6385 boolValues : make (map [int ]bool ),
6486 enumIndexes : make (map [int ]int ),
6587 fieldErrors : make (map [int ]string ),
6688 keyMap : elicitationKeyMap {
67- Up : key .NewBinding (key .WithKeys ("up" , "shift+tab" )),
68- Down : key .NewBinding (key .WithKeys ("down" , "tab" )),
69- Enter : key .NewBinding (key .WithKeys ("enter" )),
70- Escape : key .NewBinding (key .WithKeys ("esc" )),
71- Space : key .NewBinding (key .WithKeys ("space" )),
89+ Up : key .NewBinding (key .WithKeys ("up" )),
90+ Down : key .NewBinding (key .WithKeys ("down" )),
91+ Tab : key .NewBinding (key .WithKeys ("tab" )),
92+ ShiftTab : key .NewBinding (key .WithKeys ("shift+tab" )),
93+ Enter : key .NewBinding (key .WithKeys ("enter" )),
94+ Escape : key .NewBinding (key .WithKeys ("esc" )),
95+ Space : key .NewBinding (key .WithKeys ("space" )),
7296 },
7397 }
98+
99+ // If no schema fields, add a free-form text input for the response
100+ if len (fields ) == 0 {
101+ ti := textinput .New ()
102+ ti .SetStyles (styles .DialogInputStyle )
103+ ti .SetWidth (defaultWidth )
104+ ti .Prompt = ""
105+ ti .Placeholder = "Type your response"
106+ ti .CharLimit = defaultCharLimit
107+ ti .Focus ()
108+ d .responseInput = ti
109+ }
110+
74111 d .initInputs ()
75112 return d
76113}
77114
78115func (d * ElicitationDialog ) Init () tea.Cmd {
79- if len (d .inputs ) > 0 {
116+ if d . hasFreeFormInput () || len (d .inputs ) > 0 {
80117 return textinput .Blink
81118 }
82119 return nil
@@ -88,7 +125,12 @@ func (d *ElicitationDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
88125 cmd := d .SetSize (msg .Width , msg .Height )
89126 return d , cmd
90127 case tea.PasteMsg :
91- // Forward paste to text input if current field uses one
128+ // Forward paste to the active text input
129+ if d .hasFreeFormInput () {
130+ var cmd tea.Cmd
131+ d .responseInput , cmd = d .responseInput .Update (msg )
132+ return d , cmd
133+ }
92134 if d .isTextInputField () {
93135 var cmd tea.Cmd
94136 d .inputs [d .currentField ], cmd = d .inputs [d .currentField ].Update (msg )
@@ -112,17 +154,24 @@ func (d *ElicitationDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
112154
113155func (d * ElicitationDialog ) handleKeyPress (msg tea.KeyPressMsg ) (layout.Model , tea.Cmd ) {
114156 switch {
115- case key .Matches (msg , d .keyMap .Space ) && ! d .isTextInputField ():
116- // Only handle space for boolean/enum fields; let it pass through to text input otherwise
117- d .toggleCurrentSelection ( )
157+ case key .Matches (msg , d .keyMap .Space ) && ! d .isTextInputField () && ! d . hasFreeFormInput () :
158+ // Space cycles forward through options, same as down arrow
159+ d .moveSelection ( 1 )
118160 return d , nil
119161 case key .Matches (msg , d .keyMap .Escape ):
120162 cmd := d .close (tools .ElicitationActionCancel , nil )
121163 return d , cmd
122164 case key .Matches (msg , d .keyMap .Up ):
123- d .moveFocus (- 1 )
165+ // Up/down navigate within selection fields (enum/boolean)
166+ d .moveSelection (- 1 )
124167 return d , nil
125168 case key .Matches (msg , d .keyMap .Down ):
169+ d .moveSelection (1 )
170+ return d , nil
171+ case key .Matches (msg , d .keyMap .ShiftTab ):
172+ d .moveFocus (- 1 )
173+ return d , nil
174+ case key .Matches (msg , d .keyMap .Tab ):
126175 d .moveFocus (1 )
127176 return d , nil
128177 case key .Matches (msg , d .keyMap .Enter ):
@@ -132,17 +181,21 @@ func (d *ElicitationDialog) handleKeyPress(msg tea.KeyPressMsg) (layout.Model, t
132181 }
133182}
134183
135- // toggleCurrentSelection toggles boolean or cycles enum for the current field.
136- func (d * ElicitationDialog ) toggleCurrentSelection () {
137- // Clear error when user interacts with the field
184+ // moveSelection moves the selection up/down within a boolean or enum field.
185+ func (d * ElicitationDialog ) moveSelection (delta int ) {
138186 delete (d .fieldErrors , d .currentField )
139187
140188 switch d .currentFieldType () {
141189 case "boolean" :
190+ // Boolean only has two options: toggle
142191 d .boolValues [d .currentField ] = ! d .boolValues [d .currentField ]
143192 case "enum" :
144193 field := d .fields [d .currentField ]
145- d .enumIndexes [d .currentField ] = (d .enumIndexes [d .currentField ] + 1 ) % len (field .EnumValues )
194+ n := len (field .EnumValues )
195+ if n == 0 {
196+ return
197+ }
198+ d .enumIndexes [d .currentField ] = (d .enumIndexes [d .currentField ] + delta + n ) % n
146199 }
147200}
148201
@@ -154,17 +207,22 @@ func (d *ElicitationDialog) currentFieldType() string {
154207}
155208
156209func (d * ElicitationDialog ) submit () (layout.Model , tea.Cmd ) {
157- if len (d .fields ) == 0 {
158- cmd := d .close (tools .ElicitationActionAccept , nil )
210+ // Free-form response: no schema fields, just a text input
211+ if d .hasFreeFormInput () {
212+ val := strings .TrimSpace (d .responseInput .Value ())
213+ var content map [string ]any
214+ if val != "" {
215+ content = map [string ]any {"response" : val }
216+ }
217+ cmd := d .close (tools .ElicitationActionAccept , content )
159218 return d , cmd
160219 }
161220
162- // Clear previous errors and validate
221+ // Schema-based form: validate all fields
163222 d .fieldErrors = make (map [int ]string )
164223 content , firstErrorIdx := d .collectAndValidate ()
165224
166225 if firstErrorIdx >= 0 {
167- // Focus the first field with an error
168226 d .focusField (firstErrorIdx )
169227 return d , nil
170228 }
@@ -174,9 +232,12 @@ func (d *ElicitationDialog) submit() (layout.Model, tea.Cmd) {
174232}
175233
176234func (d * ElicitationDialog ) updateCurrentInput (msg tea.KeyPressMsg ) (layout.Model , tea.Cmd ) {
177- // Only text-based fields (not boolean/enum) use the text input
235+ if d .hasFreeFormInput () {
236+ var cmd tea.Cmd
237+ d .responseInput , cmd = d .responseInput .Update (msg )
238+ return d , cmd
239+ }
178240 if d .isTextInputField () {
179- // Clear error for current field when user types
180241 delete (d .fieldErrors , d .currentField )
181242 var cmd tea.Cmd
182243 d .inputs [d .currentField ], cmd = d .inputs [d .currentField ].Update (msg )
@@ -314,7 +375,7 @@ func (d *ElicitationDialog) View() string {
314375 contentWidth := d .ContentWidth (dialogWidth , 2 )
315376
316377 content := NewContent (contentWidth )
317- content .AddTitle ("MCP Server Request" )
378+ content .AddTitle (d . title )
318379 content .AddSeparator ()
319380 content .AddContent (styles .DialogContentStyle .Width (contentWidth ).Render (d .message ))
320381
@@ -326,17 +387,21 @@ func (d *ElicitationDialog) View() string {
326387 content .AddSpace ()
327388 }
328389 }
390+ } else if d .hasFreeFormInput () {
391+ content .AddSeparator ()
392+ d .responseInput .SetWidth (contentWidth )
393+ content .AddContent (d .responseInput .View ())
329394 }
330395
331396 content .AddSpace ()
332397 if len (d .fields ) > 0 {
333398 if d .hasSelectionFields () {
334- content .AddHelpKeys ("↑/↓" , "navigate " , "space " , "change " , "enter" , "submit" , "esc" , "cancel" )
399+ content .AddHelpKeys ("↑/↓" , "select " , "tab " , "next field " , "enter" , "submit" , "esc" , "cancel" )
335400 } else {
336- content .AddHelpKeys ("↑/↓ " , "navigate " , "enter" , "submit" , "esc" , "cancel" )
401+ content .AddHelpKeys ("tab " , "next field " , "enter" , "submit" , "esc" , "cancel" )
337402 }
338403 } else {
339- content .AddHelpKeys ("enter" , "confirm " , "esc" , "cancel" )
404+ content .AddHelpKeys ("enter" , "submit " , "esc" , "cancel" )
340405 }
341406
342407 return styles .DialogStyle .Width (dialogWidth ).Render (content .Build ())
@@ -441,7 +506,7 @@ func (d *ElicitationDialog) handleMouseClick(msg tea.MouseClickMsg) (layout.Mode
441506
442507 // Compute the Y offset where fields start by measuring the rendered header.
443508 header := lipgloss .JoinVertical (lipgloss .Left ,
444- styles .DialogTitleStyle .Width (contentWidth ).Render ("MCP Server Request" ),
509+ styles .DialogTitleStyle .Width (contentWidth ).Render (d . title ),
445510 RenderSeparator (contentWidth ),
446511 styles .DialogContentStyle .Width (contentWidth ).Render (d .message ),
447512 RenderSeparator (contentWidth ),
0 commit comments