Hermes WebUI — Messages UI InventoryEvery message-area element & combination, wired to the real static/style.css.  ·  Two-stage proposal (#536) →
Theme
1 · Empty state

First load / no messages

Renders inside #messages when S.messages is empty. Logo + title + subtitle + 3 suggestion buttons.

.empty-state

What can I help with?

Ask anything, run commands, explore files, or manage your scheduled tasks.

2 · User messages

Right-aligned bubble, attachments, and edit mode

User rows have no avatar/label — the right-edge alignment and tinted bubble identify the sender. Timestamp + edit/copy live in a .msg-foot below the bubble, revealed on hover (forced visible here).

.msg-row[data-role="user"] — plain

How do I run the dev server and point it at a specific workspace path?

10:42
.msg-files — attachments above body (right-aligned)
📎 architecture-notes.pdf 📎 Q1-forecast.xlsx 📎 meeting.docx 📎 screenshot.png

Please review these docs and summarise the key decisions.

.msg-edit-area + .msg-edit-bar — edit mode
3 · Assistant — markdown basics

Paragraphs, emphasis, lists, blockquote, hr, links

Assistant output is a single .msg-row.assistant-turn that holds one role header + an .assistant-turn-blocks column of one-or-more .assistant-segment children. Each segment may contain a .thinking-card, a .msg-body, and its own .msg-foot (copy / regen). This lets a turn stream reasoning → text → tool calls → more text without repeating the Hermes avatar each time.

.msg-body — rich prose
H Hermes

Running the dev server

You can start Hermes with the built-in launcher. The simplest path is no docker, no proxy — the CLI handles everything.

Prerequisites

  • Node >= 18
  • A workspace directory you own
    • Read/write permissions
    • No existing .hermes folder
  • An API key set via HERMES_API_KEY

Steps

  1. Clone the repo
  2. Run npm install
  3. Start with npm run dev -- --workspace ~/code
Tip: the --workspace flag accepts absolute or ~-prefixed paths. Relative paths are resolved against the CWD.

For full setup options see the configuration guide.

.msg-body table
HHermes

Model comparison:

ModelContextGood forCost / 1M in
Opus 4.61MDeep reasoning, long code$15.00
Sonnet 4.61MDaily driver, agents$3.00
Haiku 4.5200kFast tasks, tool loops$0.80
4 · Code blocks

Plain, with header, with copy button, multi-language

pre + code (no header)
HHermes
npm install
npm run dev -- --workspace ~/code
.pre-header + pre + .code-copy-btn
HHermes
typescript
export async function startServer(opts: ServerOptions) {
  const port = opts.port ?? 3000;
  const app = createApp();
  app.listen(port, () => {
    console.log(`Hermes listening on :${port}`);
  });
  return app;
}
python
from hermes import Agent

def main() -> None:
    agent = Agent(model="claude-opus-4-6")
    reply = agent.run("Summarise today's commits")
    print(reply)

if __name__ == "__main__":
    main()
json
{
  "model": "claude-sonnet-4-6",
  "stream": true,
  "tools": ["bash", "edit_file", "search"]
}
5 · Inline media

Images (default & zoomed) and downloadable links

.msg-media-img (default + .msg-media-img--full)
HHermes

Here's the screenshot you asked for (click to zoom):

demo

And the full-width variant:

demo-full
.msg-media-link — non-image downloads
HHermes
6 · Math & diagrams

KaTeX inline / block & Mermaid block

.katex-inline + .katex-block
HHermes

Inline math: \(E = mc^2\) and the quadratic formula below:

$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$

A tidier form: \(\sum_{i=1}^{n} i = \frac{n(n+1)}{2}\).

$$\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}$$
.mermaid-block (pre-render placeholder)
HHermes

The request flow:

graph LR
  U[User] --> C[Composer]
  C --> API[/api/chat/]
  API --> M((Model))
  M --> T{tool?}
  T -- yes --> X[Tool Runner]
  T -- no  --> R[Reply]
  X --> R
  R --> U
7 · Thinking / reasoning

Bordered panel (collapsed / open, animated), live loader, streaming cursor

Thinking cards are rendered at the top of an .assistant-segment. They're now bordered gold-tinted panels (no more left-rule-only look) and expand/collapse with a max-height + opacity transition. Click the header in either example below to see the animation live.

.thinking-card (collapsed, inside .assistant-segment)
HHermes
💡 Thought for 4.3s
The user asked about the dev server...

Here's the shortest path…

.thinking-card.open (animated — max-height + opacity)
HHermes
💡 Thought for 4.3s
The user is asking about launching the dev server.
Options: npm script, docker, or the bundled CLI.
The CLI is the simplest — no container runtime needed.
I should show the exact commands and the --workspace flag,
then mention the env var for the API key at the end.

Here's the shortest path…

.thinking — live 3-dot loader (pre-reasoning)
HHermes
Thinking
[data-live-assistant="1"] — streaming cursor at end of last child
HHermes

Sure — the simplest way is to run npm run dev. The CLI will pick up the default

8 · Tool cards

Running, done, expanded, subagent, error, multi-card toggle

Tool cards sit in .tool-card-row wrappers (no longer nested under .msg-row). The details panel now animates open/closed via max-height + opacity — click any header below to see the transition.

.tool-card.tool-card-running (collapsed, pulsing dot)
bash npm run build
.tool-card — done, collapsed
📄 read_file static/style.css · 1155 lines
.tool-card.open — args table + result snippet + Show more (animated detail)
bash grep -rn "msg-role" static/ · exit 0 · 380ms
command: grep -rn "msg-role" static/
cwd: /Users/aron/hermes-webui
timeout: 30000
static/style.css:430:  .msg-role{font-size:12px;font-weight:500...}
static/style.css:431:  .msg-role.user{color:rgba(124,185,255,0.65);}
static/style.css:432:  .msg-role.assistant{color:rgba(201,168,76,0.6);}
static/ui.js:1141:    const roleEl = el('div', 'msg-role ' + role);
.tool-card.tool-card-subagent — delegated work
🤖 Subagent Explore · Map chat messages UI elements
🤖 Delegate task Plan · Propose redesign variants
.tool-card (error snippet)
bash npm run typecheck · exit 1 · 2.3s
command: npm run typecheck
src/server.ts:42:7 - error TS2345: Argument of type 'string | undefined'
  is not assignable to parameter of type 'number'.

42       app.listen(opts.port, () => {
         ~~~~~~~~~
.tool-cards-toggle — Expand/Collapse All (≥2 cards)
📄read_filepackage.json
🔎grep"listen" in src/
bashnpm run typecheck · exit 0 · 4.1s
9 · Meta affordances

Role timestamp tooltip, footer action toolbar, token-usage badge

Assistant timestamps live on the .msg-role title attribute (hover for full date). Copy/regen buttons sit in the per-segment .msg-foot, 45% opacity at rest, full on turn hover. The .msg-usage badge is always visible at the bottom of the turn.

Full hover state — .msg-foot actions + .msg-usage
H Hermes

Built and type-checked successfully — server is running on :3000.

3.2K in · 481 out · ~$0.012
10 · Full composition

User turn → assistant turn (segment 1: thinking + body + tool cards) → usage

A realistic turn: one role header up top, then the segment hosting a thinking card plus the first body; tool cards follow as siblings of the turn inside .messages-inner; the usage badge closes the turn.

All-in-one turn
📎 server.ts

The build fails — can you type-check and explain?

10:40
HHermes
💡Thought for 2.1s
Attached server.ts — probably typing issue.
Run typecheck to confirm, then patch.

The build fails because opts.port can be undefined. Two fixes below — pick the one that matches your intent.

Option A — require the port

export function startServer(opts: { port: number }) {
  app.listen(opts.port);
}

Option B — default to 3000

export function startServer(opts: { port?: number } = {}) {
  const port = opts.port ?? 3000;
  app.listen(port);
}

I ran the checks below to confirm.

11.4K in · 612 out · ~$0.049
📄read_filesrc/server.ts · 58 lines
path: src/server.ts
export function startServer(opts: ServerOptions) {
  app.listen(opts.port, () => { ... });
}
bashnpm run typecheck · exit 1 · 2.3s
✏️edit_filesrc/server.ts +1 / -1
11 · Bubble layout

Opt-in via body.bubble-layout — extra bubble padding for assistant too

The default layout already right-aligns user messages (the redesign adopted it globally), so this toggle mostly affects additional padding / boundary handling. Flip the Bubble layout toggle in the header to see the mode applied.

Conversation sample

Can you add a retry button next to the regenerate one?

HHermes

Yes — it can share .msg-action-btn and live in the same .msg-actions container. I'll wire it up on _lastError.

Perfect, go for it.

12 · System / inline notes

Compression, cancellation, errors — rendered as italicised assistant messages

Italic system notices (still italic — info, not errors)
HHermes

[Context was auto-compressed to continue the conversation]

Task cancelled.

.assistant-segment[data-error="1"] — real error card, red accent, no italic
HHermes

Error: Connection lost. Your last message was saved — refresh to continue.

Error: Upstream rate-limited (429). Retrying in 30s…

12b · Turn boundaries & date separators

Right-alignment separates user turns · day-change separator

The dashed divider before each user turn was removed — the right-edge bubble alignment is its own visual break, so only a small vertical gap (10px top margin) remains between turns. Day changes still get a centred .msg-date-sep.

.msg-date-sep — Today / Yesterday / weekday / date
Yesterday

Can you summarise the PR I opened earlier?

HHermes

Yes — three files changed, net +42 / -18. Main change is the new rail variable…

Today

Did CI pass overnight?

HHermes

All green — three jobs, 4m 12s total. Here's the breakdown:

13 · Overlay cards (adjacent to transcript)

Approval & Clarify cards + reconnect banner

.approval-card — 4 button variants (once / session / always / deny)
⚠ Approval required
The agent wants to run a shell command in /Users/aron/hermes-webui.
rm -rf node_modules && npm install
.clarify-card — choice buttons + free-text fallback
? Clarification needed
Which environment should I deploy this to?
Pick a choice, or type your own answer below.
Reconnect / mid-stream recovery banner
⚠ A response may have been in progress when you last left. Reload messages?
⚠ Agent run exited with non-zero status (code 1). Check the logs.
14 · Structure & data-attribute cheat sheet

Wrappers and state markers produced by renderMessages()

Wrappers

  • .msg-row[data-role="user"] — one user turn (right-aligned bubble, 60% max-width)
  • .msg-row.assistant-turn[data-role="assistant"] — one assistant turn; contains one .msg-role and one .assistant-turn-blocks
  • .assistant-turn-blocks — flex-column holder for segments
  • .assistant-segment — a single logical chunk inside a turn: optional .thinking-card + optional .msg-body + optional .msg-foot
  • .assistant-segment-anchor — hidden segment kept as a DOM anchor for tool cards when the model emitted no text
  • .tool-card-row — per-tool-card wrapper, sibling of the turn inside .messages-inner
  • .msg-foot — per-segment (or per-user-row) footer holding .msg-time + .msg-actions

Data attributes & IDs

  • data-role="user|assistant" — role marker on the row
  • data-msgIdx="N" — index into S.messages; on user rows and assistant segments
  • data-raw-text="…" — plain-text source for copy (now lives on .assistant-segment for assistant output)
  • data-live-assistant="1" — the segment that's currently streaming
  • data-editing="1" — row is in edit mode
  • data-error="1" — error state; applies to .msg-row (user) or .assistant-segment
  • id="liveAssistantTurn" — on the turn that contains the streaming segment
  • .tool-card-row[data-live-tid="…"] — live tool-call card (removed when the turn settles)
  • data-mermaid-id, data-katex, data-rendered — block rendering state