Skip to main content

KDE/Plasma tray monitor and controller for Hermes Gateway (Nous Research Hermes Agent)

Project description

tray4hermes

version: 2.0.18 CI License: MIT Python 3.11+ Code style: ruff

KDE Plasma tray with tray4hermes icon

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 systemctl API.

⚠️ 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.service in a single click
  • 🔄 Profile switcher submenu, driven by ~/.hermes/profiles/
  • 📋 Log viewersee 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 (or cs) overrides the stored choice for a single run, and tray4hermes --language lists what the installed build ships

Log viewer

The log viewer reads ~/.hermes/logs/gateway.log and provides:

  • Coloured log levels: DEBUG gray, INFO white, WARNING yellow, ERROR red, CRITICAL red + 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 journalctl style (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+F opens and focuses it, F3 next, Shift+F3 prev, Esc closes). 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.service under systemd --user
  • loginctl enable-linger $USER for 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:

  1. Qt5 with PyQt5 (python -c "from PyQt5.QtWidgets import QSystemTrayIcon; print('OK')" should succeed). PySide2 is not supported — every module imports PyQt5 directly.
  2. DBus session bus (echo $DBUS_SESSION_BUS_ADDRESS should be set — typically automatic in a desktop session)
  3. A system tray implementation respecting the freedesktop.org SNI spec (KDE Plasma, Xfce, LXQt, Cinnamon, MATE, GNOME with extension, Elementary OS with extension…)
  4. systemd --user (or something compatible — OpenRC / runit alternatives have a slightly different hermes-gateway API; an adapter in paths.py would 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 install python3-pyqt5 via apt or pip install PyQt5 for yourself.
  • Ubuntu + Unity (22.04+) — Unity uses its own indicator tray, not SNI. We would need to adapt icons.py and app.py so they register via DBus at com.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:

  1. 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.
  2. KDE Plasma's tray implements the SNI spec most thoroughly of any DE — no workaround, no extension, no "maybe works".
  3. 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.py for 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: GitHubhttps://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.log and whatever python -m tray4hermes prints 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 gettext and ships English and Czech. Another language means one .po file under src/tray4hermes/_locales/; the README translations live separately in docs/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):

  1. Don't break the tests. The whole suite must stay green — ./scripts/dev.sh runs it.
  2. Don't add new runtime dependencies without discussion — the package has one runtime dependency (PyQt5), and we want to keep it that way.
  3. No secrets in the diff (grep -rE "sk-[a-z0-9]{16,}|api_key.*[a-z0-9]{20,}").
  4. No writes to ~/.hermes/* from inside the tray — that area is owned by Hermes Agent.
  5. 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 QPainter raster
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tray4hermes-2.0.18.tar.gz (103.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tray4hermes-2.0.18-py3-none-any.whl (62.0 kB view details)

Uploaded Python 3

File details

Details for the file tray4hermes-2.0.18.tar.gz.

File metadata

  • Download URL: tray4hermes-2.0.18.tar.gz
  • Upload date:
  • Size: 103.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for tray4hermes-2.0.18.tar.gz
Algorithm Hash digest
SHA256 cdc3f127f5267c106721e0192216be8e69f9909389a2e3c422e2223da464270b
MD5 71afbe877cf6efec8be9ce073360308c
BLAKE2b-256 35d42590bd1e94292767d29b240ed7e14ac96a067e755cd6e5ad860231ca2278

See more details on using hashes here.

Provenance

The following attestation bundles were made for tray4hermes-2.0.18.tar.gz:

Publisher: publish.yml on MoDD0/tray4hermes

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tray4hermes-2.0.18-py3-none-any.whl.

File metadata

  • Download URL: tray4hermes-2.0.18-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

Hashes for tray4hermes-2.0.18-py3-none-any.whl
Algorithm Hash digest
SHA256 9a50a6eee0fde95d47844c6af80510a8ffbce192f3a5843f469bec31ba06fbf5
MD5 3efc2864afdfe94e21b6e8c1af2961d6
BLAKE2b-256 72f6acc55e72cbcde5ab14d3bfe5fdb7670d0c71d41d1a9959f315e2cb746c1a

See more details on using hashes here.

Provenance

The following attestation bundles were made for tray4hermes-2.0.18-py3-none-any.whl:

Publisher: publish.yml on MoDD0/tray4hermes

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page