- Developer Guide
- Extending
- Extending the CLI
Extending the CLI
Hermes exposes protected extension hooks onHermesCLIso wrapper CLIs can add widgets, keybindings, and layout customizations without overriding the 1000+ linerun()method. This keeps your extension decoupled from internal changes.
HermesCLI
run()
Extension points
There are five extension seams available:
| Hook | Purpose | Override when… |
|---|---|---|
| _get_extra_tui_widgets() | Inject widgets into the layout | You need a persistent UI element (panel, status line, mini-player) |
| _register_extra_tui_keybindings(kb, *, input_area) | Add keyboard shortcuts | You need hotkeys (toggle panels, transport controls, modal shortcuts) |
| _build_tui_layout_children(**widgets) | Full control over widget ordering | You need to reorder or wrap existing widgets (rare) |
| process_command() | Add custom slash commands | You need/mycommandhandling (pre-existing hook) |
| _build_tui_style_dict() | Custom prompt_toolkit styles | You need custom colors or styling (pre-existing hook) |
_get_extra_tui_widgets()
_register_extra_tui_keybindings(kb, *, input_area)
_build_tui_layout_children(**widgets)
process_command()
/mycommand
_build_tui_style_dict()
The first three are new protected hooks. The last two already existed.
Quick start: a wrapper CLI
#!/usr/bin/env python3"""my_cli.py — Example wrapper CLI that extends Hermes."""from cli import HermesCLIfrom prompt_toolkit.layout import FormattedTextControl, Windowfrom prompt_toolkit.filters import Conditionclass MyCLI(HermesCLI): def __init__(self, **kwargs): super().__init__(**kwargs) self._panel_visible = False def _get_extra_tui_widgets(self): """Add a toggleable info panel above the status bar.""" cli_ref = self return [ Window( FormattedTextControl(lambda: "📊 My custom panel content"), height=1, filter=Condition(lambda: cli_ref._panel_visible), ), ] def _register_extra_tui_keybindings(self, kb, *, input_area): """F2 toggles the custom panel.""" cli_ref = self @kb.add("f2") def _toggle_panel(event): cli_ref._panel_visible = not cli_ref._panel_visible def process_command(self, cmd: str) -> bool: """Add a /panel slash command.""" if cmd.strip().lower() == "/panel": self._panel_visible = not self._panel_visible state = "visible" if self._panel_visible else "hidden" print(f"Panel is now {state}") return True return super().process_command(cmd)if __name__ == "__main__": cli = MyCLI() cli.run()
Run it:
cd ~/.hermes/hermes-agentsource .venv/bin/activatepython my_cli.py
Hook reference
_get_extra_tui_widgets()
_get_extra_tui_widgets()
Returns a list of prompt_toolkit widgets to insert into the TUI layout. Widgets appearbetween the spacer and the status bar— above the input area but below the main output.
def _get_extra_tui_widgets(self) -> list: return [] # default: no extra widgets
Each widget should be a prompt_toolkit container (e.g.,Window,ConditionalContainer,HSplit). UseConditionalContainerorfilter=Condition(…)to make widgets toggleable.
Window
ConditionalContainer
HSplit
ConditionalContainer
filter=Condition(...)
from prompt_toolkit.layout import ConditionalContainer, Window, FormattedTextControlfrom prompt_toolkit.filters import Conditiondef _get_extra_tui_widgets(self): return [ ConditionalContainer( Window(FormattedTextControl("Status: connected"), height=1), filter=Condition(lambda: self._show_status), ), ]
_register_extra_tui_keybindings(kb, *, input_area)
_register_extra_tui_keybindings(kb, *, input_area)
Called after Hermes registers its own keybindings and before the layout is built. Add your keybindings tokb.
kb
def _register_extra_tui_keybindings(self, kb, *, input_area): pass # default: no extra keybindings
Parameters:
- kb— TheKeyBindingsinstance for the prompt_toolkit application
- input_area— The mainTextAreawidget, if you need to read or manipulate user input
kb
KeyBindings
input_area
TextArea
def _register_extra_tui_keybindings(self, kb, *, input_area): cli_ref = self @kb.add("f3") def _clear_input(event): input_area.text = "" @kb.add("f4") def _insert_template(event): input_area.text = "/search "
Avoid conflictswith built-in keybindings:Enter(submit),Escape Enter(newline),Ctrl-C(interrupt),Ctrl-D(exit),Tab(auto-suggest accept). Function keys F2+ and Ctrl-combinations are generally safe.
Enter
Escape Enter
Ctrl-C
Ctrl-D
Tab
_build_tui_layout_children(**widgets)
_build_tui_layout_children(**widgets)
Override this only when you need full control over widget ordering. Most extensions should use_get_extra_tui_widgets()instead.
_get_extra_tui_widgets()
def _build_tui_layout_children(self, *, sudo_widget, secret_widget, approval_widget, clarify_widget, model_picker_widget=None, spinner_widget=None, spacer, status_bar, input_rule_top, image_bar, input_area, input_rule_bot, voice_status_bar, completions_menu) -> list:
The default implementation returns (anyNonewidgets are filtered out):
None
[ Window(height=0), # anchor sudo_widget, # sudo password prompt (conditional) secret_widget, # secret input prompt (conditional) approval_widget, # dangerous command approval (conditional) clarify_widget, # clarify question UI (conditional) model_picker_widget, # model picker overlay (conditional) spinner_widget, # thinking spinner (conditional) spacer, # fills remaining vertical space *self._get_extra_tui_widgets(), # YOUR WIDGETS GO HERE status_bar, # model/token/context status line input_rule_top, # ─── border above input image_bar, # attached images indicator input_area, # user text input input_rule_bot, # ─── border below input voice_status_bar, # voice mode status (conditional) completions_menu, # autocomplete dropdown]
Layout diagram
The default layout from top to bottom:
- Output area— scrolling conversation history
- Spacer
- Extra widgets— from_get_extra_tui_widgets()
- Status bar— model, context %, elapsed time
- Image bar— attached image count
- Input area— user prompt
- Voice status— recording indicator
- Completions menu— autocomplete suggestions
_get_extra_tui_widgets()
Tips
- Invalidate the displayafter state changes: callself._invalidate()to trigger a prompt_toolkit redraw.
- Access agent state:self.agent,self.model,self.conversation_historyare all available.
- Custom styles: Override_build_tui_style_dict()and add entries for your custom style classes.
- Slash commands: Overrideprocess_command(), handle your commands, and callsuper().process_command(cmd)for everything else.
- Don’t overriderun()unless absolutely necessary — the extension hooks exist specifically to avoid that coupling.
self._invalidate()
self.agent
self.model
self.conversation_history
_build_tui_style_dict()
process_command()
super().process_command(cmd)
run()