hermes-agent/hermes_cli
Brooklyn Nicholson 25ba6783b8 feat(tui-gateway): WebSocket transport + /chat web UI, wire-compatible with Ink
Extracts the JSON-RPC transport from stdio into an abstraction so the same
dispatcher drives Ink over stdio AND browser/iOS clients over WebSocket
without duplicating handler logic. Adds a Chat page to the existing web
dashboard that exercises the full surface — streaming, tool calls, slash
commands, model picker, session resume.

Backend
-------
* tui_gateway/transport.py — Transport protocol + contextvar binding + the
  module-level StdioTransport. Stream is resolved through a callback so
  tests that monkeypatch `_real_stdout` keep working.
* tui_gateway/server.py — write_json and dispatch are now transport-aware.
  Backward compatible: no transport bound = legacy stdio path, so entry.py
  (Ink's stdio entrypoint) is unchanged externally.
* tui_gateway/ws.py — WSTransport + handle_ws coroutine. Safe to call from
  any thread: detects loop-thread deadlock and fire-and-forget schedules
  when needed, blocking run_coroutine_threadsafe + future.result otherwise.
* hermes_cli/web_server.py — mounts /api/ws on the existing FastAPI app,
  gated by the same ephemeral session token used for REST. Adds
  HERMES_DASHBOARD_DEV_TOKEN env override so Vite HMR dev can share the
  token with the backend.

Frontend
--------
* web/src/lib/gatewayClient.ts — browser WebSocket JSON-RPC client that
  mirrors ui-tui/src/gatewayClient.ts.
* web/src/lib/slashExec.ts — slash command pipeline (slash.exec with
  command.dispatch fallback + exec/plugin/alias/skill/send directive
  handling), mirrors ui-tui/src/app/createSlashHandler.ts.
* web/src/pages/ChatPage.tsx — transcript + composer driven entirely by
  the WS.
* web/src/components/SlashPopover.tsx — autocomplete popover above the
  composer, debounced complete.slash.
* web/src/components/ModelPickerDialog.tsx — two-stage provider/model
  picker; confirms by emitting /model through the slash pipeline.
* web/src/components/ToolCall.tsx — expandable tool call row (Ink-style
  chevron + context + summary/error/diff).
* web/src/App.tsx — logo links to /, Chat entry added to nav.
* web/src/pages/SessionsPage.tsx — every session row gets an Open-in-chat
  button that navigates to /chat?resume=<id> (uses session.resume).
* web/vite.config.ts — /api proxy configured with ws: true so WebSocket
  upgrades forward in dev mode; injectDevToken plugin reads
  HERMES_DASHBOARD_DEV_TOKEN and injects it into the served index.html so
  Vite HMR can authenticate against FastAPI without a separate flow.

Tests
-----
tests/hermes_cli/test_web_server.py picks up three new classes:

* TestTuiGatewayWebSocket — handshake, auth rejection, parse errors,
  unknown methods, inline + pool handler round-trips, session event
  routing, disconnect cleanup.
* TestTuiGatewayTransportParity — byte-identical envelopes for the same
  RPC over stdio vs WS (unknown method, inline handler, error envelope,
  explicit stdio transport).
* TestTuiGatewayE2EAnyPort — scripted multi-RPC conversation driven
  identically via handle_request and via WebSocket; order + shape must
  match. This is the "hermes --tui in any port" check.

Existing tests under tests/tui_gateway/ and tests/test_tui_gateway_server.py
all still pass unchanged — backward compat preserved.

Try it
------
    hermes dashboard          # builds web, serves on :9119, click Chat

Dev with HMR:

    export HERMES_DASHBOARD_DEV_TOKEN="dev-\$(openssl rand -hex 16)"
    hermes dashboard --no-open
    cd web && npm run dev     # :5173, /api + /api/ws proxied to :9119

fix(chat): insert tool rows before the streaming assistant message

Transcript used to read "user → empty assistant bubble → tool → bubble
filling in", which is disorienting: the streaming cursor sits at the top
while the "work" rows appear below it chronologically.

Now tool.start inserts the row just before the current streaming
assistant message, so the order reads "user → tools → final message".
If no streaming assistant exists yet (rare), tools still append at the
end; tool.progress / tool.complete match by id regardless of position.

fix(web-chat): font, composer, streaming caret + port GoodVibesHeart

- ChatPage root opts out of App's `font-mondwest uppercase` (dashboard
  chrome style) — adds `font-courier normal-case` so transcript prose is
  readable mono mixed-case instead of pixel-display caps.
- Composer: textarea + send button wrapped as one bordered unit with
  `focus-within` ring; `font-sans` dropped (it mapped to `Collapse`
  display). Heights stretch together via `items-stretch`; button is a
  flush cap with `border-l` divider.
- Streaming caret no longer wraps to a new line when the assistant
  renders a block element. Markdown now takes a `streaming` prop and
  injects the caret inside the last block (paragraph, list item, code)
  so it hugs the trailing character. Caret sized in em units.
- EmptyState gets a blinking caret + <kbd> shortcut chips.
- Port ui-tui's GoodVibesHeart easter egg to the web: typing "thanks" /
  "ty" / "ily" / "good bot" flashes a Lucide heart next to the
  connection badge (same regex, same 650ms beat, same palette as
  ui-tui/src/app/useMainApp.ts).
2026-04-23 21:11:04 -04:00
..
__init__.py chore: release v0.11.0 (2026.4.23) (#14791) 2026-04-23 15:31:59 -07:00
auth.py fix(auth): refuse to touch real auth.json during pytest; delete sandbox-escaping test (#14729) 2026-04-23 13:50:21 -07:00
auth_commands.py fix(auth): unify credential source removal — every source sticks (#13427) 2026-04-21 01:52:49 -07:00
backup.py fix(backup): handle files with pre-1980 timestamps 2026-04-20 00:47:40 -07:00
banner.py refactor: remove dead code — 1,784 lines across 77 files (#9180) 2026-04-13 16:32:04 -07:00
callbacks.py fix: ESC cancels secret/sudo prompts, clearer skip messaging (#9902) 2026-04-14 16:11:37 -07:00
claw.py Normalize claw workspace paths for Windows 2026-04-22 18:15:27 -07:00
cli_output.py refactor: remove dead code — 1,784 lines across 77 files (#9180) 2026-04-13 16:32:04 -07:00
clipboard.py feat: fix img pasting in new ink plus newline after tools 2026-04-11 13:14:32 -05:00
codex_models.py feat(codex): add gpt-5.5 and wire live model discovery into picker (#14720) 2026-04-23 13:32:43 -07:00
colors.py feat: respect NO_COLOR env var and TERM=dumb (#4079) 2026-03-30 17:07:21 -07:00
commands.py feat(gateway): expose plugin slash commands natively on all platforms + decision-capable command hook 2026-04-22 16:23:21 -07:00
completion.py fix: preserve profile name completion in dynamic shell completion 2026-04-14 10:45:42 -07:00
config.py feat(delegation): bump default child_timeout_seconds to 600s (#14809) 2026-04-23 16:14:55 -07:00
copilot_auth.py fix(copilot): resolve GHE token poisoning when GITHUB_TOKEN is set 2026-04-13 05:12:36 -07:00
cron.py feat(cron): track delivery failures in job status (#6042) 2026-04-07 22:49:01 -07:00
curses_ui.py feat: ungate Tool Gateway — subscription-based access with per-tool opt-in 2026-04-16 12:36:49 -07:00
debug.py style(debug): add missing blank line between LogSnapshot and helpers 2026-04-22 16:34:05 -05:00
default_soul.py fix: reset default SOUL.md to baseline identity text (#3159) 2026-03-26 01:34:27 -07:00
dingtalk_auth.py test(dingtalk): cover QR device-flow auth + OpenClaw branding disclosure 2026-04-17 05:08:07 -07:00
doctor.py feat: add Step Plan provider support (salvage #6005) 2026-04-22 02:59:58 -07:00
dump.py refactor: remove smart_model_routing feature (#12732) 2026-04-19 18:12:55 -07:00
env_loader.py fix(cli): ensure project .env is sanitized before loading 2026-04-22 05:51:44 -07:00
gateway.py fix(gateway): drain-aware hermes update + faster still-working pings (#14736) 2026-04-23 14:01:57 -07:00
hooks.py feat: shell hooks — wire shell scripts as Hermes hook callbacks 2026-04-20 20:53:51 -07:00
logs.py feat: component-separated logging with session context and filtering (#7991) 2026-04-11 17:23:36 -07:00
main.py fix(gateway): drain-aware hermes update + faster still-working pings (#14736) 2026-04-23 14:01:57 -07:00
mcp_config.py fix(mcp): consolidate OAuth handling, pick up external token refreshes (#11383) 2026-04-16 21:57:10 -07:00
memory_setup.py fix(memory): discover user-installed memory providers from $HERMES_HOME/plugins/ (#10529) 2026-04-15 14:25:40 -07:00
model_normalize.py fix(copilot): normalize vendor-prefixed and dash-notation model IDs (#6879) (#11561) 2026-04-17 04:19:36 -07:00
model_switch.py fix: resolve_alias prefers highest version + merges static catalog 2026-04-23 23:18:33 +05:30
models.py feat(codex): add gpt-5.5 and wire live model discovery into picker (#14720) 2026-04-23 13:32:43 -07:00
nous_subscription.py fix(fal): extend whitespace-only FAL_KEY handling to all call sites 2026-04-21 02:04:21 -07:00
pairing.py fix(pairing): handle null user_name in pairing list display 2026-04-23 02:34:11 -07:00
platforms.py feat(cron): honor hermes tools config for the cron platform (#14798) 2026-04-23 15:48:50 -07:00
plugins.py fix(image-gen): force-refresh plugin providers in long-lived sessions 2026-04-23 03:01:18 -07:00
plugins_cmd.py feat(plugins): make all plugins opt-in by default 2026-04-20 04:46:45 -07:00
profiles.py fix(profiles): stage profile imports to prevent directory clobbering 2026-04-23 03:02:34 -07:00
providers.py feat: add Step Plan provider support (salvage #6005) 2026-04-22 02:59:58 -07:00
runtime_provider.py fix(kimi-coding): add KIMI_CODING_API_KEY fallback + api_mode detection for /coding endpoint 2026-04-21 19:48:39 -07:00
setup.py feat: add Xiaomi MiMo v2.5-pro and v2.5 model support (#14635) 2026-04-23 10:06:25 -07:00
skills_config.py refactor: remove dead code — 1,784 lines across 77 files (#9180) 2026-04-13 16:32:04 -07:00
skills_hub.py Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor 2026-04-17 08:59:33 -05:00
skin_engine.py fix(skins): don't inherit status_bar_* into light-mode skins 2026-04-22 13:20:02 -07:00
status.py feat: add Step Plan provider support (salvage #6005) 2026-04-22 02:59:58 -07:00
timeouts.py fix(config): add stale timeout settings 2026-04-20 00:52:50 -07:00
tips.py feat(agent): make API retry count configurable via agent.api_max_retries (#14730) 2026-04-23 13:59:32 -07:00
tools_config.py fix(image-gen): persist plugin provider on reconfigure 2026-04-23 01:56:09 -07:00
uninstall.py feat(uninstall): offer to remove named profiles when uninstalling from default 2026-04-18 19:18:13 -07:00
voice.py fix(tui): ignore SIGPIPE so stderr back-pressure can't kill the gateway 2026-04-23 16:18:15 -07:00
web_server.py feat(tui-gateway): WebSocket transport + /chat web UI, wire-compatible with Ink 2026-04-23 21:11:04 -04:00
webhook.py feat(webhook): direct delivery mode for zero-LLM push notifications (#12473) 2026-04-19 05:18:19 -07:00