diff --git a/src/fast_agent/ui/elicitation_form.py b/src/fast_agent/ui/elicitation_form.py index 3327c305..4efc33f1 100644 --- a/src/fast_agent/ui/elicitation_form.py +++ b/src/fast_agent/ui/elicitation_form.py @@ -7,6 +7,7 @@ from mcp.types import ElicitRequestedSchema from prompt_toolkit import Application from prompt_toolkit.buffer import Buffer +from prompt_toolkit.filters import Condition from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous @@ -25,6 +26,8 @@ from fast_agent.ui.elicitation_style import ELICITATION_STYLE +text_navigation_mode = False + class SimpleNumberValidator(Validator): """Simple number validator with real-time feedback.""" @@ -68,7 +71,7 @@ def __init__( self, min_length: Optional[int] = None, max_length: Optional[int] = None, - pattern: Optional[str] = None + pattern: Optional[str] = None, ): self.min_length = min_length self.max_length = max_length @@ -314,6 +317,10 @@ def _build_form(self): ] ) + # Use field navigation mode as default + global text_navigation_mode + text_navigation_mode = False + # Key bindings kb = KeyBindings() @@ -325,31 +332,49 @@ def focus_next_with_refresh(event): def focus_previous_with_refresh(event): focus_previous(event) + # Toggle between text navigation mode and field navigation mode + @kb.add("c-t") + def toggle_text_navigation_mode(event): + global text_navigation_mode + text_navigation_mode = not text_navigation_mode + event.app.invalidate() # Force redraw the app to update toolbar + # Arrow key navigation - let radio lists handle up/down first - @kb.add("down") + @kb.add("down", filter=Condition(lambda: not text_navigation_mode)) def focus_next_arrow(event): focus_next(event) - @kb.add("up") + @kb.add("up", filter=Condition(lambda: not text_navigation_mode)) def focus_previous_arrow(event): focus_previous(event) - @kb.add("right", eager=True) + @kb.add("right", eager=True, filter=Condition(lambda: not text_navigation_mode)) def focus_next_right(event): focus_next(event) - @kb.add("left", eager=True) + @kb.add("left", eager=True, filter=Condition(lambda: not text_navigation_mode)) def focus_previous_left(event): focus_previous(event) - # Enter always submits - @kb.add("c-m") - def submit(event): + # Enter submits in field navigation mode + @kb.add("c-m", filter=Condition(lambda: not text_navigation_mode)) + def submit_enter(event): self._accept() - # Ctrl+J inserts newlines - @kb.add("c-j") - def insert_newline(event): + # Ctrl+J inserts newlines in field navigation mode + @kb.add("c-j", filter=Condition(lambda: not text_navigation_mode)) + def insert_newline_cj(event): + # Insert a newline at the cursor position + event.current_buffer.insert_text("\n") + # Mark this field as multiline when user adds a newline + for field_name, widget in self.field_widgets.items(): + if isinstance(widget, Buffer) and widget == event.current_buffer: + self.multiline_fields.add(field_name) + break + + # Enter inserts new lines in text navigation mode + @kb.add("c-m", filter=Condition(lambda: text_navigation_mode)) + def insert_newline_enter(event): # Insert a newline at the cursor position event.current_buffer.insert_text("\n") # Mark this field as multiline when user adds a newline @@ -358,6 +383,11 @@ def insert_newline(event): self.multiline_fields.add(field_name) break + # deactivate ctrl+j in text navigation mode + @kb.add("c-j", filter=Condition(lambda: text_navigation_mode)) + def _(event): + pass + # ESC should ALWAYS cancel immediately, no matter what @kb.add("escape", eager=True, is_global=True) def cancel(event): @@ -369,18 +399,40 @@ def get_toolbar(): if hasattr(self, "_toolbar_hidden") and self._toolbar_hidden: return FormattedText([]) - return FormattedText( - [ - ( - "class:bottom-toolbar.text", - " /↑↓→← navigate. submit. insert new line. cancel. ", - ), - ( - "class:bottom-toolbar.text", - " Auto-Cancel further elicitations from this Server.", - ), - ] - ) + mode_label = "TEXT MODE" if text_navigation_mode else "FIELD MODE" + mode_color = "ansired" if text_navigation_mode else "ansigreen" + + arrow_up = "↑" + arrow_down = "↓" + arrow_left = "←" + arrow_right = "→" + + if text_navigation_mode: + actions_line = ( + " cancel. Auto-Cancel further elicitations from this Server." + ) + navigation_tail = ( + " | toggle text mode. navigate. insert new line." + ) + else: + actions_line = ( + " submit. cancel. Auto-Cancel further elicitations " + "from this Server." + ) + navigation_tail = ( + " | toggle text mode. " + f"/{arrow_up}{arrow_down}{arrow_right}{arrow_left} navigate. " + " insert new line." + ) + + formatted_segments = [ + ("class:bottom-toolbar.text", actions_line), + ("", "\n"), + ("class:bottom-toolbar.text", " | "), + (f"fg:{mode_color} bg:ansiblack", f" {mode_label} "), + ("class:bottom-toolbar.text", navigation_tail), + ] + return FormattedText(formatted_segments) # Store toolbar function reference for later control self._get_toolbar = get_toolbar @@ -388,7 +440,7 @@ def get_toolbar(): # Create toolbar window that we can reference later self._toolbar_window = Window( - FormattedTextControl(get_toolbar), height=1, style="class:bottom-toolbar" + FormattedTextControl(get_toolbar), height=2, style="class:bottom-toolbar" ) # Add toolbar to the layout diff --git a/src/fast_agent/ui/elicitation_style.py b/src/fast_agent/ui/elicitation_style.py index b46237f7..1631be0f 100644 --- a/src/fast_agent/ui/elicitation_style.py +++ b/src/fast_agent/ui/elicitation_style.py @@ -53,7 +53,7 @@ "completion-menu.meta.completion": "bg:ansiblack fg:ansiblue", "completion-menu.meta.completion.current": "bg:ansibrightblack fg:ansiblue", # Toolbar - matching enhanced_prompt.py exactly - "bottom-toolbar": "fg:ansiblack bg:ansigray", - "bottom-toolbar.text": "fg:ansiblack bg:ansigray", + "bottom-toolbar": "fg:#ansiblack bg:#ansigray", + "bottom-toolbar.text": "fg:#ansiblack bg:#ansigray", } )