hermes-agent/ui-tui/README.md
2026-04-15 10:20:56 -05:00

16 KiB

Hermes TUI

React + Ink terminal UI for Hermes. TypeScript owns the screen. Python owns sessions, tools, model calls, and most command logic.

hermes --tui

What runs

The client entrypoint is src/entry.tsx. It exits early if stdin is not a TTY, starts GatewayClient, then renders App.

GatewayClient spawns:

python -m tui_gateway.entry

Interpreter resolution order is: HERMES_PYTHONPYTHON$VIRTUAL_ENV/bin/python./.venv/bin/python./venv/bin/pythonpython3 (or python on Windows).

The transport is newline-delimited JSON-RPC over stdio:

ui-tui/src                  tui_gateway/
-----------                 -------------
entry.tsx                   entry.py
  -> GatewayClient            -> request loop
  -> App                      -> server.py RPC handlers

stdin/stdout: JSON-RPC requests, responses, events
stderr: captured into an in-memory log ring

Malformed stdout lines are treated as protocol noise and surfaced as gateway.protocol_error. Stderr lines become gateway.stderr. Neither writes directly into the terminal.

Running it

From the repo root, the normal path is:

hermes --tui

The CLI expects ui-tui/node_modules to exist. If the TUI deps are missing:

cd ui-tui
npm install

Local package commands:

npm run dev
npm start
npm run build
npm run lint
npm run fmt
npm run fix

There is no package-local test script today.

App model

src/app.tsx is the center of the UI. It holds:

  • transcript and streaming state
  • queued messages and input history
  • session lifecycle
  • tool progress and reasoning text
  • prompt flows for approval, clarify, sudo, and secret input
  • slash command routing
  • tab completion and path completion
  • theme state from gateway skin data

The UI renders as a normal Ink tree with Static transcript output, a live streaming assistant row, prompt overlays, queue preview, status rule, input line, and completion list.

The intro panel is driven by session.info and rendered through branding.tsx.

Hotkeys and interactions

Current input behavior is split across app.tsx, components/textInput.tsx, and the prompt/picker components.

Main chat input

Key Behavior
Enter Submit the current draft
empty Enter twice If queued messages exist and the agent is busy, interrupt the current run. If queued messages exist and the agent is idle, send the next queued message
Shift+Enter / Alt+Enter Insert a newline in the current draft
\ + Enter Append the line to the multiline buffer (fallback for terminals without modifier support)
Ctrl+C Interrupt active run, or clear the current draft, or exit if nothing is pending
Ctrl+D Exit
Ctrl+G Open $EDITOR with the current draft
Ctrl+L New session (same as /clear)
Ctrl+V / Alt+V Paste clipboard image (same as /paste)
Tab Apply the active completion
Up/Down Cycle completions if the completion list is open; otherwise edit queued messages first, then walk input history
Left/Right Move the cursor
modified Left/Right Move by word when the terminal sends Ctrl or Meta with the arrow key
Home / Ctrl+A Start of line
End / Ctrl+E End of line
Backspace Delete the character to the left of the cursor
Delete Delete the character to the right of the cursor
modified Backspace Delete the previous word
modified Delete Delete the next word
Ctrl+W Delete the previous word
Ctrl+U Delete from the cursor back to the start of the line
Ctrl+K Delete from the cursor to the end of the line
Meta+B / Meta+F Move by word
!cmd Run a shell command through the gateway
{!cmd} Inline shell interpolation before send; queued drafts keep the raw text until they are sent

Notes:

  • Tab only applies completions when completions are present and you are not in multiline mode.
  • Queue/history navigation only applies when you are not in multiline mode.
  • PgUp / PgDn are left to the terminal emulator; the TUI does not handle them.

Prompt and picker modes

Context Keys Behavior
approval prompt Up/Down, Enter Move and confirm the selected approval choice
approval prompt o, s, a, d Quick-pick once, session, always, deny
approval prompt Esc, Ctrl+C Deny
clarify prompt with choices Up/Down, Enter Move and confirm the selected choice
clarify prompt with choices single-digit number Quick-pick the matching numbered choice
clarify prompt with choices Enter on "Other" Switch into free-text entry
clarify free-text mode Enter Submit typed answer
sudo / secret prompt Enter Submit typed value
sudo / secret prompt Ctrl+C Cancel by sending an empty response
resume picker Up/Down, Enter Move and resume the selected session
resume picker 1-9 Quick-pick one of the first nine visible sessions
resume picker Esc, Ctrl+C Close the picker

Notes:

  • Clarify free-text mode and masked prompts use ink-text-input, so text editing there follows the library's default bindings rather than components/textInput.tsx.
  • When a blocking prompt is open, the main chat input hotkeys are suspended.
  • Clarify mode has no dedicated cancel shortcut in the current client. Sudo and secret prompts only expose Ctrl+C cancellation from the app-level blocked handler.

Interaction rules

  • Plain text entered while the agent is busy is queued instead of sent immediately.
  • Slash commands and !cmd do not queue; they execute immediately even while a run is active.
  • Queue auto-drains after each assistant response, unless a queued item is currently being edited.
  • Up/Down prioritizes queued-message editing over history. History only activates when there is no queue to edit.
  • Queued drafts keep their original !cmd and {!cmd} text while you edit them. Shell commands and interpolation run when the queued item is actually sent.
  • If you load a queued item into the input and resubmit plain text, that queue item is replaced, removed from the queue preview, and promoted to send next. If the agent is still busy, the edited item is moved to the front of the queue and sent after the current run completes.
  • Completion requests are debounced by 60 ms. Input starting with / uses complete.slash. A trailing token that starts with ./, ../, ~/, /, or @ uses complete.path.
  • Text pastes are inserted inline directly into the draft. Nothing is newline-flattened.
  • Ctrl+G writes the current draft, including any multiline buffer, to a temp file, temporarily swaps screen buffers, launches $EDITOR, then restores the TUI and submits the saved text if the editor exits cleanly.
  • Input history is stored in ~/.hermes/.hermes_history or under HERMES_HOME.

Rendering

Assistant output is rendered in one of two ways:

  • if the payload already contains ANSI, messageLine.tsx prints it directly
  • otherwise components/markdown.tsx renders a small Markdown subset into Ink components

The Markdown renderer handles headings, lists, block quotes, tables, fenced code blocks, diff coloring, inline code, emphasis, links, and plain URLs.

Tool/status activity is shown in a live activity lane. Transcript rows stay focused on user/assistant turns.

Prompt flows

The Python gateway can pause the main loop and request structured input:

  • approval.request: allow once, allow for session, allow always, or deny
  • clarify.request: pick from choices or type a custom answer
  • sudo.request: masked password entry
  • secret.request: masked value entry for a named env var
  • session.list: used by SessionPicker for /resume

These are stateful UI branches in app.tsx, not separate screens.

Commands

The local slash handler covers the built-ins that need direct client behavior:

  • /help
  • /quit, /exit, /q
  • /clear
  • /new
  • /compact
  • /resume
  • /copy
  • /paste
  • /details
  • /logs
  • /statusbar, /sb
  • /queue
  • /undo
  • /retry

Notes:

  • /copy sends the selected assistant response through OSC 52.
  • /paste with no args asks the gateway for clipboard image attachment state.
  • /paste does not manage text paste entries; text paste is inline-only.
  • /details [hidden|collapsed|expanded|cycle] controls thinking/tool-detail visibility.
  • /statusbar toggles the status rule on/off.

Anything else falls through to:

  1. slash.exec
  2. command.dispatch

That lets Python own aliases, plugins, skills, and registry-backed commands without duplicating the logic in the TUI.

Event surface

Primary event types the client handles today:

Event Payload
gateway.ready { skin? }
session.info session metadata for banner + tool/skill panels
message.start start assistant streaming
message.delta { text, rendered? }
message.complete { text, rendered?, usage, status }
thinking.delta { text }
reasoning.delta { text }
reasoning.available { text }
status.update { kind, text }
tool.start { tool_id, name, context? }
tool.progress { name, preview }
tool.complete { tool_id, name }
clarify.request { question, choices?, request_id }
approval.request { command, description }
sudo.request { request_id }
secret.request { prompt, env_var, request_id }
background.complete { task_id, text }
btw.complete { text }
error { message }
gateway.stderr synthesized from child stderr
gateway.protocol_error synthesized from malformed stdout

Theme model

The client starts with DEFAULT_THEME from theme.ts, then merges in gateway skin data from gateway.ready.

Current branding overrides:

  • agent name
  • prompt symbol
  • welcome text
  • goodbye text

Current color overrides:

  • banner title, accent, border, body, dim
  • label, ok, error, warn

branding.tsx uses those values for the logo, session panel, and update notice.

File map

ui-tui/src/
  entry.tsx              TTY gate + render()
  app.tsx                main state machine and UI
  gatewayClient.ts       child process + JSON-RPC bridge
  theme.ts               default palette + skin merge
  constants.ts           display constants, hotkeys, tool labels
  types.ts               shared client-side types
  banner.ts              ASCII art data

  components/
    branding.tsx         banner + session summary
    markdown.tsx         Markdown-to-Ink renderer
    maskedPrompt.tsx     masked input for sudo / secrets
    messageLine.tsx      transcript rows
    prompts.tsx          approval + clarify flows
    queuedMessages.tsx   queued input preview
    sessionPicker.tsx    session resume picker
    textInput.tsx        custom line editor
    thinking.tsx         spinner, reasoning, tool activity

  lib/
    history.ts           persistent input history
    osc52.ts             OSC 52 clipboard copy
    text.ts              text helpers, ANSI detection, previews

Related Python side:

tui_gateway/
  entry.py               stdio entrypoint
  server.py              RPC handlers and session logic
  render.py              optional rich/ANSI bridge

Notes

  • No dead code: main.tsx, altScreen.tsx, commandPalette.tsx, and lib/slash.ts have been removed.