hermes-bubbles
Health Uyari
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Uyari
- process.env — Environment variable access in scripts/audit-classes.mjs
- process.env — Environment variable access in scripts/install-plugin.mjs
- network request — Outbound network request in scripts/install-plugin.mjs
- process.env — Environment variable access in scripts/probe-contrast.mjs
- network request — Outbound network request in scripts/probe-contrast.mjs
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Bubble-style chat page for the Hermes web dashboard — replaces the built-in xterm /chat with a native streaming message stream over the tui_gateway JSON-RPC socket.
hermes-bubbles
A bubble-style chat page for the Hermes web dashboard (hermes dashboard).
The built-in /chat is an xterm.js terminal wired to /api/pty, running the real TUI — so it looks like a terminal: a character stream, monospace text, no message boundaries. This plugin replaces it with a native message stream where each reply, tool call and approval prompt is its own block, and streaming output grows inside the bubble it belongs to.
It does not go through a PTY. It speaks the dashboard's own tui_gateway JSON-RPC socket (/api/ws) — the same channel the desktop app uses — so it receives structured events: message.delta is a token chunk, tool.start/tool.complete are a pair to correlate, approval is a request that must be answered.

Open sessions are tabs along the top, stored sessions live in a collapsible panel on the right, and the transcript in the middle holds bubbles, one-line tool rows, collapsible thinking blocks between them, and a locator rail for jumping back to something you said earlier.
Features
| Feature | Status | Notes |
|---|---|---|
| Streaming bubbles | Done | message.delta appends into the current bubble, with a blinking caret while the turn is live |
| Markdown replies | Done | Headings, bold/italic/strikethrough, inline code, fenced blocks with a language label and copy button, nested lists, quotes, rules, tables with alignment. Unclosed fences and lone ** render as half-finished rather than throwing |
| Tool cards | Done | tool_id pairs start/complete; collapsed by default, showing type, summary and duration |
| Task list | Done | todo.updated snapshots render as a panel above the composer; collapsible, and the collapsed state is remembered. A tool call seals the reply bubble, so text after a tool starts a new bubble below the card instead of growing the one above it |
| Subagents | Done | One delegate_task batch is one card: concurrent children are rows on it, each expanding to its last tool, recent progress and summary |
| Desktop notifications | Done | Pending approvals/clarify (toasts stay until dismissed) and finished turns ring the desktop only while the window is in the background; the switch in the tab strip reports permission too, asks for it on click, and pops a confirmation toast; still rings while you are on another dashboard route |
| Composer auto-grow | Done | The input box follows the draft's height (32–192px) and then scrolls, instead of staying one row and hiding everything after the first line |
| Session history | Done | session.history projects into the same bubble structure, so a reload keeps the conversation |
| Session management | Done | Rename and delete from the history panel, which also shows a status lamp: lit = open in a tab, pulsing = running |
| Project management | Done | Each project row has a ⋯ menu. An explicit project can be renamed inline (projects.update) or deleted for real (projects.delete, which drops the entry and its folders but never the sessions or the directory); an auto-discovered repo has no backend row, so it is hidden client-side the way the desktop sidebar does it, with "Show N hidden" to bring it back. The ungrouped bucket gets neither |
| Session as context | Done | Every history row has a ⇩ button, and the row itself can be dragged into the composer: the plugin reads that session's transcript (REST /api/sessions/{id}/messages, read-only — it never becomes the active session), folds it into a bounded plain-text block (newest messages first, per-message cap, tool and hidden rows skipped) and puts it in the draft, ready for your own question above/below it |
| Tabs survive navigation | Done | Switch to Config and back — open tabs and in-flight replies are still there |
| Approval & clarify cards | Done | Both render from the server's own choices; multi_select is joined into the string the wire expects; one option per line. Every clarify question ends with a free-text row (the desktop app's "Other"), mutually exclusive with a single-select pick and additive on multi-select |
| Message actions | Done | Timestamp, branch in new chat, copy, regenerate — matching the desktop app. Every glyph is an inline SVG on one 24-unit grid at one stroke weight (geometry from Lucide, ISC), not a text character: three font glyphs sitting next to each other render at three sizes and three baselines, which is exactly what made the row look uneven |
| History panel | Done | The desktop column and the phone drawer both animate open and shut. The column grows from 0 to its 17rem and gives the width back on close, staying mounted until hb-panel-out ends so the exit is a slide rather than a cut |
| Edit a sent message | Done | Edit a user bubble and sending rewrites the turn from there, truncating what followed |
| Mid-turn input | Done | Send while a turn runs; the backend queues, steers or redirects, and the result is reported inline |
| Multi-session | Done | One socket carries several sessions; events are routed by session_id, each tab has its own timeline |
| Locator rail | Done | One tick per user message down the right edge; hover to preview, click to jump. Hidden on phones, where it only costs width |
| Mobile / narrow screens | Done | Under 768px the tab strip is REPLACED, not restyled: two round buttons float over the transcript (refresh this chat on the left, a "···" on the right that slides a drawer in from the right edge — no dimming, animated both ways). The drawer's top row starts a new chat, and every session row keeps its three tools behind one "···". The composer is the same control set on both sizes: a "···" menu (model / reasoning / notifications) on the left, round upload and send on the right. Hover-only action rows stay visible and the locator rail is dropped |
| Slash commands | Done | / opens a menu; the catalog is fetched once and filtered locally, not truncated |
| Attachments | Done | +, drag-drop and paste; routed to image.attach_bytes / pdf.attach / file.attach |
| Model & reasoning | Done | model.options listing, session-scoped switching, seven reasoning tiers. A switch the gateway wants confirmed (expensive model, data-policy provider, or a large live context) opens a dialog with the guard's own message and an accept control that re-sends the pick confirmed — nothing is written until you say yes |
| Approval mode | Done | A chip under the composer sets the dangerous-command approval mode (manual / smart / off, config.set key=approvals.mode). It is profile-wide — the menu says so — because the backend has no per-conversation override |
| Live metrics | Done | Context bar and percentage, cache hit rate, output speed, cost, updating from session.usage |
| Running indicator | Done | A flicking whale tail (animated PNG, the same asset the DeepSeek harness uses) beside the step that is running: the thinking header while reasoning streams, a tool row while it executes. It is an alpha mask over currentColor, so it takes the theme's colour; engines without mask support, and users who asked for reduced motion, get the same tail as a static SVG |
| Between two messages | Done | Everything that is not a message — thinking segments, tool calls, delegation batches, machinery notices — is folded into one block per run, capped at 190px and clipped at its bottom edge while collapsed. The header counts what is inside (▸ 3 thoughts · 8 calls 42s), the rows stay in the DOM but are inert so nothing under the clip can be focused or clicked, and expanding reveals all of them (bounded at 60vh, scrolls inside). A run of one row stays a plain row |
| Interface language | Done | English by default; follows the dashboard's language switch without a reload |
Requirements
- Hermes Agent web dashboard, tested against 0.22.2
- Node 20+ to build (developed on 24.21.0)
Install
The plugin ships as a dashboard plugin, which is a directory layout rather than
a package. There are two paths.
From a clone (for development, or if you want the tests):
git clone https://github.com/NaimioChan/hermes-bubbles
cd hermes-bubbles
npm install --include=dev # on Windows, NODE_ENV=production silently skips devDependencies
npm test
npm run install-plugin # builds and copies into $HERMES_HOME/plugins/hermes-bubbles/dashboard/
hermes plugins enable hermes-bubbles
With Hermes' own installer (needs no Node — the built bundle is committed):
hermes plugins install https://github.com/NaimioChan/hermes-bubbles
hermes plugins enable hermes-bubbles
Either way a user plugin is discovered but not loaded until it is named inplugins.enabled. With hermes plugins enable that is done for you; otherwise:
hermes config set plugins.enabled '["hermes-bubbles"]'
curl -H "X-Hermes-Session-Token: <token>" http://127.0.0.1:9119/api/dashboard/plugins/rescan
<token> is window.__HERMES_SESSION_TOKEN__ from the dashboard page. No restart
needed. Open http://127.0.0.1:9119/chat.
The bundle lives in dashboard/dist/ and is committed, because the dashboard
scanner only walks <plugins root>/<name>/dashboard/manifest.json and a pinned
install runs no build step. After changing the source, npm run install-plugin
rewrites both that copy and the installed one, so they never drift.
How it works
src/
├── core/ pure logic — no DOM, no sockets, fully unit-tested
│ ├── types.ts timeline model (bubbles, tool cards, notices, usage)
│ ├── reduce.ts events → timeline
│ ├── history.ts stored rows → timeline
│ ├── context.ts another session → a bounded prompt block, plus the drag payload
│ ├── sessions.ts multi-session store: tabs, event routing, history replay
│ ├── markdown.ts Markdown → node tree (never HTML)
│ ├── rail.ts locator-rail geometry
│ ├── requests.ts approval / clarify parsing and answer shaping
│ ├── slash.ts slash token, completion, dispatch parsing
│ ├── attachments.ts attachment queue and size/type screening
│ ├── metrics.ts usage formatting and pressure grading
│ ├── i18n.ts string catalog and timestamp formatting
│ └── dedupe.ts cross-socket duplicate gate
├── gateway/ transport
│ ├── channel.ts JSON-RPC channel (requests, events, server→client requests)
│ └── client.ts socket lifecycle, reconnect backoff, ensureOpen
├── app/
│ ├── controller.ts session control: RPC, routing, models, attachments, resync
│ └── persistent.ts the controller outlives a route change
├── ui/ rendering
│ ├── ChatPage.ts page skeleton and wiring
│ ├── TabStrip.ts open sessions
│ ├── HistoryPanel.ts stored sessions, rename/delete, status lamp
│ ├── Composer.ts input, slash menu, attachment tray, metrics
│ ├── Bubble.ts bubbles, thinking blocks, tool rows, subagent batches
│ ├── Markdown.ts renders the node tree via React.createElement
│ ├── LocatorRail.ts the jump rail
│ ├── RequestCards.ts approval and clarify cards
│ ├── ModelPicker.ts model and reasoning picker
│ ├── ConfirmDialog.ts accept/cancel for a switch the gateway held back
│ └── style.css the small amount of hand-written CSS
└── index.ts registration entry
plugin.yaml manifest Hermes' plugin loader reads (and the catalogue requires)
__init__.py registers nothing; makes the plugin loadable
dashboard/ the layout the dashboard scanner walks — built, committed
Nothing in core/ may reference window, document, WebSocket or fetch — that is what makes the edge cases testable. The transport takes an injected socket factory, so tests drive the whole chain with a fake socket.
The protocol is Hermes' internal implementation with no OpenAPI description, so docs/01-protocol-contract.md records the version it was verified against (hermes-agent 0.22.2, commit d923a5eb487) and a checklist to re-run after an upgrade.
Verification
npm test # 553 unit tests, 26 files, no browser
npm run typecheck # tsc --noEmit
node scripts/verify.mjs # 93 checks against a real dashboard (needs one on 9119)
node scripts/audit-classes.mjs
npm test covers event reduction, history projection, multi-session routing, Markdown parsing (including a property check that every prefix of a streamed message parses), slash completion, the attachment queue, usage metrics, deduplication, the JSON-RPC channel, and the controller — plus four source-level guards:
hook-order— a hook after an earlyreturnthrows React #310 and blanks the page; neither types nor logic tests can see it.layout-classes— the plugin renders inside the dashboard's precompiled Tailwind bundle, so a class the host never used applies nothing at all, silently.theme-vars— likewise for CSS variables; the host genuinely does not define--color-foregroundor--color-background.architecture—core/stays DOM-free and no component usesinnerHTML.
scripts/verify.mjs drives a real browser against a live gateway: it types, sends, streams, opens menus, switches tabs, renames and deletes sessions, navigates away and back, and measures computed styles. Checks that cannot be meaningful in a given run (the model happened not to emit a code block) report SKIP rather than passing vacuously.
scripts/verify-clarify.mjs is an on-demand check for the request cards, kept separate because it depends on the model choosing to call clarify — a check that depends on that would fail at random.
scripts/verify-confirm.mjs covers the confirmation dialog for a held-back model switch. The large-context guard only fires once a session has enough measured context, so the check is on demand: --edit-config backs up config.yaml, drops model.switch_context_confirm_tokens to 1, asserts the cancel and accept paths, then restores the file byte for byte.
Design notes
The host's CSS is the floor, not a framework. The plugin renders inside the dashboard's precompiled stylesheet, which only contains the classes the dashboard's own source used. Anything invented that the host lacks fails silently — no error, no warning, the property just never applies. Three visible bugs came from exactly that (resize-none, max-h-[70vh], text-[0.625rem]). New styles go into src/ui/style.css as explicit hb- classes, and audit-classes.mjs lists anything that would silently do nothing.
Tag-level host rules intrude too: a global code { display: inline; color: var(--background) } lands on the <code> inside every <pre>, which both shrank code blocks to their text width and painted them near-invisible. Overriding per class is the fix.
Markdown never becomes HTML. The parser emits a node tree and the renderer builds elements with React.createElement, so text is escaped by React and HTML injection is removed rather than sanitised around. The parser is also built to run on half a sentence: every streaming delta re-parses the whole message, so an unclosed fence, a lone ** or a table whose separator row has not arrived are normal inputs.
One unit, stated once. Timeline timestamps are milliseconds everywhere. The backend sends seconds, and exactly one function scales them on the way in. Mixing that up is why a 02:46 turn once displayed as 19:32.
The controller outlives the page. Navigating to another dashboard tab unmounts this one. Disposing the controller there would close the socket and every open session, so it is a document-level singleton: unmount releases a reference, remount reuses the instance. A blocking approval or clarify that arrives while the page is unmounted is parked and replayed, not refused — the backend waits up to an hour for an answer and "the user was on another tab" is not a refusal.
Compatibility
| hermes-agent | 0.22.2 (commit d923a5eb487) |
| Plugin SDK contract | 1.1.0 |
| Node | 24.21.0 (developed) |
| Bundle | ~180 KiB, React not included |
| Backend route | /api/plugins/hermes-bubbles/capabilities |
hermes plugins validate |
14/14 checks pass |
| Catalogue rule 9 | one warning: plugin_api.py imports tui_gateway.contracts.registry read-only |
The backend route asks once, before the page renders, whether every method and event it needs still exists. When the protocol drifts it fails loudly with the missing names instead of rendering an empty conversation.
Limitations
- User plugins must be registered by hand. The dashboard only loads what is in
plugins.enabled. - Only
approvalandclarifyare handled among server→client requests.sudo,secret, vault prompts and the like get-32601, which fails fast rather than hanging — deliberate, but those flows do not work on this page. - No protocol version guarantee. This JSON-RPC surface is an internal implementation and may drift with
hermes update; the capabilities route is the first gate, the contract document the second. - UI state does not survive a full page reload. Open tabs persist across dashboard navigation, not across a browser refresh.
- Tool history is lossy. The backend only carries raw content for
write_file,patchandskill_manage; other tools keep a one-line summary.
License
MIT
中文版:README.zh-CN.md
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi