KDE/Plasma tray monitor and controller for Hermes Gateway (Nous Research Hermes Agent)
Project description
tray4hermes
A real KDE Plasma 5 tray — the tray4hermes icon slots in next to your other SNI apps. Screenshot from a Manjaro KDE session where this tray is live.
Canonical: English (this file)
Other languages: Čeština
A KDE/Plasma system-tray monitor and controller for Hermes Gateway, the messaging service included with Hermes Agent by Nous Research.
tray4hermes is read-only with respect to Hermes Agent. It controls the gateway via
systemctl --user, persists one small JSON file of its own, and reads everything else. It does not store tokens, does not configure providers, does not edit~/.hermes/config.yaml. All of that lives in Hermes Agent itself.
Overview
hermes-gateway runs as a systemd --user service. tray4hermes adds a KDE
tray interface for monitoring and controlling it. The application:
- shows the gateway state directly in the KDE panel (green/orange/red)
- lets you Start / Stop / Restart without opening a terminal
- switches profiles (
default,work,off…) from a menu - displays logs in a viewer with coloured levels, per-level filters, search, traceback awareness and a time-window filter
- speaks English and Czech, switchable at runtime without a restart
- reads Hermes runtime state and uses the standard
systemctlAPI.
⚠️ Disclaimer: tray4hermes is a passive convenience addon, not an official Hermes Agent component. Hermes Agent runs perfectly well without it. Use it if you like it; ignore it if you don't.
Features
- 📊 Live status icon in the system tray (🟢 active, 🟠 warming, 🔵 activating, ⚫ inactive, 🔴 failed, ⚪ unknown)
- ▶️ Start / Stop / Restart of
hermes-gateway.servicein a single click - 🔄 Profile switcher submenu, driven by
~/.hermes/profiles/ - 📋 Log viewer — see below, it's quite polished
- ⚙️ Open Hermes config in your default editor
- 💻 Launch Hermes CLI in a new terminal
- 🛠️ Settings dialog — UI language plus the defaults the log viewer starts with, stored alongside the rest of the tray state
- 🌍 English and Czech UI — pick a language in the Settings dialog and
the tray re-labels itself immediately, no restart;
tray4hermes -L en(orcs) overrides the stored choice for a single run, andtray4hermes --languagelists what the installed build ships
Log viewer
The log viewer reads ~/.hermes/logs/gateway.log and provides:
- Coloured log levels:
DEBUGgray,INFOwhite,WARNINGyellow,ERRORred,CRITICALred + full-row highlight - Line-number gutter like Qt Creator / VS Code
- Per-level filters on a toolbar row of their own, so they and the
controls above them stay visible instead of collapsing into the
»overflow popup — show only what you want - TRACEBACK toggle — a separate category for stack traces; turn it off to see only messages, or on to keep both
- Time-window filter (All / 5m / 15m / 1h / 6h / 24h) — show only logs from the last hour, etc.
- Reverse order — flip to
journalctlstyle (newest at top); every new entry appears first - Max lines spinbox — rolling buffer (0 = unlimited). Steps by 1; type any value manually
- Search in a collapsible find bar under the log (
Ctrl+Fopens and focuses it,F3next,Shift+F3prev,Esccloses). It deliberately sits outside the toolbar, so it stays reachable at any window width - Auto-scroll toggle (default ON; OFF = preserve position on refresh). While investigating, the viewer preserves a manually selected scroll position even when auto-scroll is ON; it only follows the live edge when you are already viewing that edge
- Word-wrap toggle
- Copy / Clear / Refresh actions
- Settings dialog (font size, max lines, per-level visibility, …)
- Remembers window size/position between opens
- Persisted — all settings are saved to
~/.config/tray4hermes/state.json
Architecture
┌──────────────────────────────────────────────────────┐
│ Hermes Agent (Nous Research) │
│ • hermes-gateway.service (systemd --user) │
│ • Hermes Desktop (Electron consumer) │
│ • CLI / TUI / MCP / Plugins │
└──────────────────────┬───────────────────────────────┘
│ shares
▼
~/.hermes/ ← single source of truth
├── config.yaml
├── auth.json
├── gateway_state.json
├── logs/gateway.log
└── profiles/<name>/
▲
│ reads (read-only)
┌──────────────────────┴───────────────────────────────┐
│ tray4hermes (this package) │
│ • systray icon • Start/Stop/Restart │
│ • profile switcher • log viewer │
│ • writes only: ~/.config/tray4hermes/state.json │
└──────────────────────────────────────────────────────┘
The package has zero coupling to the Hermes Agent source code. It only
knows about files in ~/.hermes/, the systemd unit name, and the path
to the hermes CLI. The tray can be uninstalled at any time without
affecting the gateway.
State machine
The tray combines two sources of truth into six discrete states:
| Code | Icon | Meaning |
|---|---|---|
active |
🟢 | Gateway running, at least one platform connected |
warming |
🟠 | Gateway running, credentials/platforms still initialising |
activating |
🔵 | systemd is starting the service |
inactive |
⚫ | Gateway stopped |
failed |
🔴 | systemd unit failed |
unknown |
⚪ | Cannot determine state (both sources unavailable) |
gateway_state.json is the primary source when fresh (< 1 hour old);
systemctl is-active is the fallback. The two-source design avoids
the OAuth warm-up race where the systemd unit shows active for a few
seconds before the first model call actually succeeds.
Requirements
- Linux (developed primarily on Manjaro KDE; should work on any distro with KDE Plasma 5, see Platform support)
- KDE Plasma 5 (Plasma 6 uses Qt6 — tray4hermes is Qt5; Qt6 port is in the Roadmap)
- Python ≥ 3.11
- A running
hermes-gateway.serviceundersystemd --user loginctl enable-linger $USERfor autostart after logout
Installation
End-user (system-wide)
uv pip install --system tray4hermes
# or with pipx:
pipx install tray4hermes
Then enable autostart:
# Find where the .desktop file ended up after pip install
DESKTOP_FILE=$(python3 -c "import tray4hermes, os; \
print(os.path.join(os.path.dirname(tray4hermes.__file__), 'data', 'tray4hermes.desktop'))")
cp "$DESKTOP_FILE" ~/.config/autostart/tray4hermes.desktop
# Adjust the Exec= line to point at your installed tray4hermes script
# (the wheel ships it at <prefix>/bin/tray4hermes).
Development (editable)
git clone https://github.com/MoDD0/tray4hermes.git
cd tray4hermes
uv pip install --system -e ".[dev]"
./scripts/dev.sh # installs deps + runs tests
Launch the tray:
# Either via the installed console script:
tray4hermes
# Or via the watchdog wrapper (auto-restart on crash):
./run.sh
# Or as a module:
python -m tray4hermes
Platform support
Primarily developed and tested on Manjaro KDE (rolling release,
Plasma 5, xcb X11 backend).
Should work natively on any Linux that has:
- Qt5 with PyQt5 (
python -c "from PyQt5.QtWidgets import QSystemTrayIcon; print('OK')"should succeed). PySide2 is not supported — every module imports PyQt5 directly. - DBus session bus (
echo $DBUS_SESSION_BUS_ADDRESSshould be set — typically automatic in a desktop session) - A system tray implementation respecting the freedesktop.org SNI spec (KDE Plasma, Xfce, LXQt, Cinnamon, MATE, GNOME with extension, Elementary OS with extension…)
- systemd --user (or something compatible — OpenRC / runit
alternatives have a slightly different
hermes-gatewayAPI; an adapter inpaths.pywould be needed)
Distro status
Only one row here is actually tested. The rest is reasoning from how close the distro is to that one — useful, but not a promise.
| Distro / DE | Status | Notes |
|---|---|---|
| Manjaro KDE | ✅ tested | the platform this is developed and tested on |
| Arch Linux + KDE | 🟡 expected to work | practically identical to Manjaro, just package from extra not AUR |
| CachyOS KDE | 🟡 expected to work | Arch-based, practically identical |
| Ubuntu + KDE | 🟡 expected to work | see below |
| Fedora + KDE | 🟡 expected to work | dnf install python3-pyqt5 |
| openSUSE + KDE | 🟡 expected to work | zypper install python3-PyQt5 |
Ubuntu with Unity / GNOME / others
This is a tray application, so the main difference is tray support:
- Ubuntu + KDE Plasma (
apt install kubuntu-desktop) — should work out-of-the-box, you may need to installpython3-pyqt5viaaptorpip install PyQt5for yourself. - Ubuntu + Unity (22.04+) — Unity uses its own indicator tray,
not SNI. We would need to adapt
icons.pyandapp.pyso they register via DBus atcom.canonical.Unity.LauncherEntry. Technically possible, but not implemented today. If you run Unity and want this — open an issue. - Ubuntu + GNOME 41+ — GNOME intentionally does not support tray without an SNI extension. You have to install an extension like AppIndicator Support, then it should work.
- Ubuntu + Cinnamon / MATE / XFCE — all support SNI tray, should work out-of-the-box.
Practical recommendation for Ubuntu users
Short version: install KDE Plasma and it will work with the highest probability. Not the smallest package, but the cleanest path.
# 1. Install KDE Plasma desktop (meta-package, ~600 MB)
sudo apt install kde-plasma-desktop
# 2. Log out. On the login screen, choose the "Plasma" session, log in.
# 3. In a terminal inside the KDE session:
sudo apt install python3-pyqt5 # system PyQt5
python3 -m pip install --user tray4hermes # or from GitHub
python3 -m tray4hermes # start the tray
# 4. For autostart after login:
# The .desktop file ships inside the installed wheel at:
# /usr/local/lib/python3.*/site-packages/tray4hermes/data/tray4hermes.desktop
# (resolve with `python3 -c "import tray4hermes; import os; \
# print(os.path.join(os.path.dirname(tray4hermes.__file__), 'data', 'tray4hermes.desktop'))"`)
# Copy it to ~/.config/autostart/ and edit the Exec= path to point at
# your installed tray4hermes.
Why we recommend this:
- Hermes Agent runs on Ubuntu exactly the same as on Manjaro (both are Linux, both have systemd --user, both have Python 3.11+). No inherent incompatibility with Ubuntu.
- KDE Plasma's tray implements the SNI spec most thoroughly of any DE — no workaround, no extension, no "maybe works".
- Higher chance of first-try success. Instead of 75–95% with GNOME / Cinnamon + workaround, you get ~99%.
Alternative for purists: if you insist on GNOME and really want
this to work there, follow the instructions in the Ubuntu + GNOME 41+
section above. But it's not a trivial first experience.
Future testing
A dedicated Ubuntu VM: if the community brings real demand for
Ubuntu support, we'd happily spin up a simple Ubuntu LTS VM
(VirtualBox or LXC) with KDE Plasma in CI and run a smoke test —
tray4hermes starts → menu icon appears → click Start/Stop → verify
that systemctl --user responds. Cost would be ~1–2 days of setup +
scripts. We don't have it right now, because demand is currently
zero (Manjaro/KDE covers ~95% of users who have written to us).
If you'd like it, add it to the Roadmap. Open an issue with a +1
and a vote, or just show up to a contributing call. 🙂
Troubleshooting
If the tray doesn't show on another distro:
# 1. Verify Qt5 sees a system tray
python -c "from PyQt5.QtWidgets import QSystemTrayIcon, QApplication; \
app = QApplication([]); print('available:', QSystemTrayIcon.isSystemTrayAvailable())"
# 2. Check whether DBus is running
echo "DBus session bus: $DBUS_SESSION_BUS_ADDRESS"
# 3. Try explicit XDG, and run in the foreground so errors reach the terminal
export XDG_CURRENT_DESKTOP=KDE
export XDG_SESSION_TYPE=x11
python -m tray4hermes
There is no --debug flag: the tray writes its failures to stderr, so
running it from a terminal as above is the whole debug story. tray4hermes --version and tray4hermes --language are the only other arguments.
If QSystemTrayIcon.isSystemTrayAvailable() returns False, it's a
distro/desktop combo problem, not a tray4hermes bug. You can:
- open an issue with the output of
python -c "import sys; print(sys.platform, …)"plus your Qt version (PyQt5.QtCore.PYQT_VERSION_STR) - ask in the Hermes community (or directly with us in issues, we'll
help extend
paths.pyfor your setup)
We want it to work everywhere Hermes Agent has a chance of running. So distro-specific PRs are very welcome.
Security
This package does not handle any credentials, tokens, or secrets.
The only file it writes is ~/.config/tray4hermes/state.json, which
contains the currently-selected profile name, log-viewer preferences,
and a schema version:
{
"version": 1,
"selected_profile": "default",
"log_settings": {
"max_lines": 2000,
"auto_scroll": true,
"word_wrap": false,
"font_size": 9,
"show_levels": ["ERROR", "WARNING", "INFO", "DEBUG", "CRITICAL", "TRACE"],
"show_tracebacks": true,
"time_window_minutes": 0,
"reverse_order": false
}
}
If you find a security issue, please open a private issue via GitHub's "Report a vulnerability" feature (Settings → Security → Report a vulnerability). Do not disclose vulnerabilities in public issues or commits.
Threat model
| Vector | Mitigation |
|---|---|
RCE via malicious gateway_state.json |
Loaded as JSON only; no eval/exec/shell. Strict shape. |
| Log injection | Log viewer is read-only; uses QPlainTextEdit (escapes HTML). |
| Profile path injection | Profile name validated by Hermes Agent itself (hermes profile use returns non-zero on missing). |
| Lock file race | O_CREAT|O_EXCL + PID liveness probe + recursive single retry. |
Filesystem exhaustion on state.json write |
Atomic tmp + os.replace; directory created with parents=True. |
The tray is sandboxed against the rest of Hermes Agent: even a
zero-day in tray4hermes cannot read auth.json, the .env file, or
trigger a model call. The worst it can do is crash and be restarted
by the watchdog — at which point it just reads the (still intact)
state and re-renders the tray icon.
Development
Run tests
./scripts/dev.sh # install + pytest
./scripts/dev.sh tests/test_state.py -v # specific file
Tests use QT_QPA_PLATFORM=offscreen (set in tests/conftest.py) so
they run headless, without a display server.
Continuous integration
GitHub Actions runs on every push to main
and on every pull request: lint (ruff check + ruff format --check)
and the full test suite on Python 3.11, 3.12 and 3.13 — the versions
this package claims to support. A separate job refuses any commit that
lowers the package version.
Lint & format
uv run ruff check src tests
uv run ruff format src tests
Security scan
uv run bandit -c pyproject.toml -r src
Pre-commit hooks (optional but recommended)
uv pip install --system pre-commit
pre-commit install
pre-commit run --all-files
Project layout
tray4hermes/
├── pyproject.toml # PEP 621, uv-friendly, exact-pinned deps
├── LICENSE # MIT
├── README.md # generated from docs/i18n/en.md — don't edit
├── CONTRIBUTING.md
├── CLAUDE.md # briefing for AI assistants working here
├── .claude/ # architecture, conventions, session log, specs
├── .gitignore # incl. secret patterns
├── .pre-commit-config.yaml
├── run.sh # watchdog wrapper
├── docs/
│ ├── README.cs.md # generated from docs/i18n/cs.md — don't edit
│ ├── i18n/
│ │ ├── en.md # canonical README source (source of truth)
│ │ └── cs.md # Czech translation of en.md
│ └── images/
│ ├── preview.png # screenshot for README
│ └── tray4hermes.png # package logo
├── scripts/
│ ├── dev.sh # install + test convenience
│ ├── i18n_build.py # docs/i18n/*.md → README.md + docs/README.cs.md
│ ├── i18n_compile.sh # .pot extraction + .po → .mo catalogs
│ ├── i18n_lint.py # heading parity across README translations
│ └── versioning.py # SemVer bump helper
├── src/
│ └── tray4hermes/
│ ├── __init__.py # __version__ (single source of truth)
│ ├── __main__.py # python -m tray4hermes (argparse entry)
│ ├── app.py # HermesTray — icon, menu, poll timer
│ ├── state.py # @dataclass + aggregation logic
│ ├── paths.py # all filesystem paths and tunables
│ ├── icons.py # QPainter icon factory
│ ├── lock.py # single-instance lock
│ ├── log_dialog.py # LogDialog — editor, gutter, toolbars, find bar
│ ├── log_settings.py # viewer preferences: dataclass, persistence, dialog
│ ├── log_parse.py # level / timestamp / traceback parsing (no Qt widgets)
│ ├── log_theme.py # colours, fonts and filter constants shared by both dialogs
│ ├── tray_settings.py # global settings dialog (language, defaults)
│ ├── i18n.py # gettext wrapper + runtime language switch
│ ├── py.typed # PEP 561 marker
│ ├── _locales/ # UI catalogs, shipped inside the wheel
│ │ ├── tray4hermes.pot
│ │ └── cs/LC_MESSAGES/tray4hermes.{po,mo}
│ └── data/
│ └── tray4hermes.desktop # ships inside the wheel
└── tests/
├── conftest.py
├── test_app.py # Qt offscreen — tray menu and actions
├── test_cli.py # `python -m tray4hermes` argument handling
├── test_gateway_control.py # systemctl actions, profile switching
├── test_i18n_build.py # README build script
├── test_i18n_parity.py # build + lint scripts wired into pytest
├── test_i18n_runtime.py # gettext catalogs, msgid language
├── test_launchers.py # editor / terminal resolution and spawning
├── test_lock.py # single-instance lock
├── test_log_parse.py # level / timestamp parsing, both regex engines
├── test_log_refresh.py # what the viewer shows when the log is unreadable
├── test_log_settings.py # viewer settings persistence
├── test_log_shortcuts.py # Ctrl+F / F3 / Shift+F3 / Esc, real key path
├── test_log_toolbar.py # toolbar rows stay on screen at realistic widths
├── test_readme_freshness.py # compiled READMEs match their sources
├── test_readme_parity.py # image parity between en.md and cs.md
├── test_settings_dialog.py # log-viewer settings dialog
├── test_state.py # state aggregation and persistence
├── test_tray_settings.py # global tray settings
└── test_versioning.py # SemVer bump helper
License
MIT — see LICENSE.
Hosting
Primary host: GitHub → https://github.com/MoDD0/tray4hermes
- All development, issues, pull requests, releases, and CI happen there.
- This is where contributors fork from and push branches to.
- Releases are tagged here first; everything else cascades from here.
Contributing
Issues, comments, suggestions — all welcome. Whether it's:
- 🐛 Bug report — ideally with the relevant part of
~/.hermes/logs/gateway.logand whateverpython -m tray4hermesprints when run from a terminal - 💡 Feature request — short description of what it would be for. We don't mind "wild" ideas (different backend, Wayland support, custom icons…). Worth a discussion.
- 🎨 UI tweak — colours, layout, fonts, tooltips. This is the area where contributor taste matters most.
- 📖 Documentation — screenshots of the log viewer are missing; feel free to add them
- 🌍 Localization — the UI runs through
gettextand ships English and Czech. Another language means one.pofile undersrc/tray4hermes/_locales/; the README translations live separately indocs/i18n/. Both routes are described in CONTRIBUTING.md - 🐧 Distro-specific fixes — see Platform support. Every distro we test on is a contribution away
How to send a PR / patch
git clone https://github.com/MoDD0/tray4hermes.git
cd tray4hermes
./scripts/dev.sh # installs deps, runs tests
# make your change
./scripts/dev.sh -v # re-run tests with verbose
uv run ruff check src tests
uv run ruff format src tests
git commit -m "description"
git push
Rules (flexible):
- Don't break the tests. The whole suite must stay green —
./scripts/dev.shruns it. - Don't add new runtime dependencies without discussion — the
package has one runtime dependency (
PyQt5), and we want to keep it that way. - No secrets in the diff
(
grep -rE "sk-[a-z0-9]{16,}|api_key.*[a-z0-9]{20,}"). - No writes to
~/.hermes/*from inside the tray — that area is owned by Hermes Agent. - MIT-compatible contributions. If you're adding code, stick to the MIT license.
If you like this project and have an idea, just open an issue — we'll discuss it. Even "this word doesn't sound right in the translation" is welcome feedback.
Roadmap (next-up ideas)
A few things on my mind that aren't done yet. If any of these interests you more than the rest, speak up:
- Wayland support — currently
xcb(X11) only because of the KDE Plasma tray API; Plasma 6 + Qt6 would open Wayland. PRs welcome. - Custom icons per status — SVG icons instead of
QPainterraster - Log search across sessions (FTS5 over the sessions DB)
- Notifications on ERROR — toast over D-Bus when the gateway writes a traceback (I like it, but it's a matter of taste)
- Settings export/import — sharing log-viewer presets between profiles
- Ubuntu LTS CI VM — see Platform support → Future testing
- Plasma 6 / Qt6 port — for the day Manjaro ships Plasma 6 by default
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tray4hermes-2.0.17.tar.gz.
File metadata
- Download URL: tray4hermes-2.0.17.tar.gz
- Upload date:
- Size: 103.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a65cbbdbe0a3fdba5e43c8730b81eff442873f3f994d2ba756b9a552c0e83a6
|
|
| MD5 |
0fb8f58767ad9ba975b679a4d593adcc
|
|
| BLAKE2b-256 |
613eddf929733e7b1733c7240bf983a87d0bdc1c348663d9a1f9aace1b7bbca1
|
Provenance
The following attestation bundles were made for tray4hermes-2.0.17.tar.gz:
Publisher:
publish.yml on MoDD0/tray4hermes
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tray4hermes-2.0.17.tar.gz -
Subject digest:
1a65cbbdbe0a3fdba5e43c8730b81eff442873f3f994d2ba756b9a552c0e83a6 - Sigstore transparency entry: 2256393917
- Sigstore integration time:
-
Permalink:
MoDD0/tray4hermes@ffefe896c729dcee0be6522d54d54416c185e177 -
Branch / Tag:
refs/tags/v2.0.17 - Owner: https://github.com/MoDD0
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ffefe896c729dcee0be6522d54d54416c185e177 -
Trigger Event:
release
-
Statement type:
File details
Details for the file tray4hermes-2.0.17-py3-none-any.whl.
File metadata
- Download URL: tray4hermes-2.0.17-py3-none-any.whl
- Upload date:
- Size: 62.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b66f47cfaafd5d18a3de3a64eeab4a18bf53cc0c444597a31a2810563f60239f
|
|
| MD5 |
b162088ec7928031dc6a7b2f63e6119d
|
|
| BLAKE2b-256 |
092e6b992bbb3d54d6fa10ed40a145c1566cb15a3d4b859b7ac2449631597165
|
Provenance
The following attestation bundles were made for tray4hermes-2.0.17-py3-none-any.whl:
Publisher:
publish.yml on MoDD0/tray4hermes
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tray4hermes-2.0.17-py3-none-any.whl -
Subject digest:
b66f47cfaafd5d18a3de3a64eeab4a18bf53cc0c444597a31a2810563f60239f - Sigstore transparency entry: 2256393929
- Sigstore integration time:
-
Permalink:
MoDD0/tray4hermes@ffefe896c729dcee0be6522d54d54416c185e177 -
Branch / Tag:
refs/tags/v2.0.17 - Owner: https://github.com/MoDD0
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ffefe896c729dcee0be6522d54d54416c185e177 -
Trigger Event:
release
-
Statement type: