feat(hermes-agent): native Hermes status, chat panel, and launcher provider#934
feat(hermes-agent): native Hermes status, chat panel, and launcher provider#934Nomadcxx wants to merge 29 commits into
Conversation
…ovider Adds a Noctalia plugin providing bar status, chat panel, tool-call approvals, interrupts, session management, and a >hermes launcher command for Hermes. - Bar widget: traffic-light status with summary popup - Panel: streaming chat, inline approvals, interrupt, sessions - Launcher: >hermes one-shot prompts and session control - Settings: bridge config, poll interval, defaults, display options - Bridge: local authenticated HTTP bridge with token auth, 1MB body limit, 0o600 state file permissions Supersedes noctalia-dev#898 (read-only status) and covers noctalia-dev#931 use case (remote Hermes via bridgeHost setting).
Automatic Code Quality ReviewFile: hermes-agent/BarWidget.qml
+ border.width: 1
+ border.width: 1File: hermes-agent/Panel.qml
+ border.width: 1File: hermes-agent/components/SummaryPopup.qml
+ border.width: 1 |
CI code-quality review flagged hardcoded border.width: 1 values. Replaced with Style.borderS per AGENTS.md Style singleton requirement.
…rst run Bridge now auto-detects HERMES_HOME (env, ~/.hermes, ~/.config/hermes, ~/.local/share/hermes), the hermes binary (PATH, ~/.local/bin/hermes, ~/.hermes/bin/hermes), gateway status from gateway_state.json, and the default model/provider from config.yaml. New /detect endpoint returns all detected values. QML auto-configures on first run (empty pluginSettings) and saves detected paths and model to settings. Settings UI collapses advanced fields (bridge host/port, state file, hermes home, hermes command) behind a toggle. Users only see model selection by default.
Automatic Code Quality ReviewFile: hermes-agent/BarWidget.qml
+ border.width: 1
+ border.width: 1File: hermes-agent/Panel.qml
+ border.width: 1File: hermes-agent/components/SummaryPopup.qml
+ border.width: 1 |
|
The |
…uto-start Four bugs prevented the plugin from working on a fresh install: 1. autoConfigure() never ran because the first-run check tested Object.keys(pluginSettings).length === 0, but Noctalia populates pluginSettings with manifest defaults before Component.onCompleted fires. Replaced with an explicit 'configured' flag in defaultSettings. 2. Race condition: autoConfigure() ran immediately after ensureBridge() in Component.onCompleted, but the bridge process was not listening yet. Now autoConfigure() is chained after the bridge health check succeeds, with a retry timer polling /health up to 20 times. 3. Bridge auto-detect comparison failed because QML passes expanded paths (e.g. /home/user/.hermes) but run_server() compared against the literal '~/.hermes'. Now checks both tilde and expanded form. 4. No handling for when the Hermes gateway is not running. Added /gateway/start and /gateway/status endpoints to the bridge, a 'Start gateway' action in the bar context menu and summary popup, and an autoStartGateway setting (default true) that starts the gateway during autoConfigure() if it is offline/stopped/unknown. The bridge tries 'hermes gateway start' (systemd/launchd) first, falling back to a detached 'hermes gateway run' process.
Update: Auto-detect and gateway auto-start fixesPushed commit 1. First-run detection never triggered
2. Race condition: bridge not ready when autoConfigure ran
3. Bridge auto-detect path comparison failedQML passes expanded paths (e.g. 4. No handling for gateway not runningAdded gateway lifecycle support:
CI: code-quality ✅ manifest-check ✅ |
…teway start Race condition: on fresh install, the bridge creates the token file when it starts, but the FileView may not have reloaded it by the time autoConfigure() runs. This caused /gateway/start to fail silently with 401 (no token in header). Fix: gate refreshState() and autoConfigure() behind the token FileView's onLoaded callback via a bridgeOnlinePending flag. When the bridge comes online, onBridgeOnline() sets the flag and reloads the token file. The onLoaded callback then runs refreshState() and autoConfigure() with the token guaranteed to be loaded.
…n NButton NButton does not have a 'flat' property. The QML engine logged 'Cannot assign to non-existent property flat' at Settings.qml:116 each time the settings popup opened, which caused the Loader to fail rendering the entire Settings.qml content — resulting in an empty popup showing only the wrapper header and Apply button. Replaced flat: true with outlined: true, which gives the intended less-prominent appearance for the Advanced settings toggle.
…ng call sites Three pluginApi accesses were missing the ?. operator: - Main.qml: root.pluginApi.openPanel inside withCurrentScreen callback - BarWidget.qml: pluginApi.manifest in context menu settings action - BarWidget.qml: pluginApi.manifest in SummaryPopup onSettings handler
…ins into add-hermes-agent
2f7c37e to
3597572
Compare
|
Hi @nomadx 👋 — great plugin, we've been running it in production here. We forked it to add a client-only mode for Hermes running on a remote server (no local bridge, no
The bridge protocol is unchanged — everything goes through the existing Our fork + commits: https://github.com/FelipeMayerDev/noctalia-custom-plugins/commits/main/hermes-agent/ Happy to open a PR against your branch ( |
Client-only mode lets users run the plugin without a local Hermes installation. The bridge runs on a remote server, reached via SSH tunnel. Adds statePollTimer for HTTP polling, idle fallback in bar widget when gateway is running, and a server helper script. Co-authored-by: FelipeMayerDev <65250731+FelipeMayerDev@users.noreply.github.com>
|
Thanks @FelipeMayerDev, cherry-picked your client-only mode into the PR in commit f8ea895. All seven plugin files are in, bridge protocol untouched. A few things I had to change for the upstream contribution rules: reverted the manifest repository URL back to the noctalia-plugins registry, added your name to the author field, and bumped the version to 1.1.0. Left your fork-level AGENTS.md and root README out since those are fork-specific. The idle-fallback fix in BarWidget was a real gap I had not caught. Good catch. Co-authored-by trailer is on the commit. Thanks for the clean diff. |
…es for client-only mode - Test button in Settings pings /health on configured host/port/token - Shows Connected, Unreachable, or Wrong token result - ensureBridge() now reports remote bridge address when unreachable - statePollTimer calls ensureBridge() instead of refreshState() for health check - onClientOnlyModeChanged calls ensureBridge() to connect after toggle - getJson/postJson distinguish status 0 (network) from 403 (auth) from HTTP errors Co-authored-by: FelipeMayerDev <65250731+FelipeMayerDev@users.noreply.github.com>
…oints - Delete SessionHeader.qml, ToolEventRow.qml (zero references) - Delete hermes_attention.py, hermes_status_check.py, hermes_status_hook.py (not called) - Add /gateway/start and /gateway/stop bridge endpoints (hermes gateway start/stop) - Add stopGateway() to Main.qml, call on client-only mode switch - Gate start-gateway context menu behind !clientOnlyMode - Hide autoStartGateway toggle in settings when client-only mode active Co-authored-by: FelipeMayerDev <65250731+FelipeMayerDev@users.noreply.github.com>
…idgeToken binding Two bugs in client-only mode toggle cycle: 1. bridgeToken binding break: tokenFileView.onLoaded imperatively assigned root.bridgeToken, destroying the QML binding. After toggling clientOnlyMode, bridgeToken stayed stuck on the local token → 403 from remote bridge. Fix: make bridgeToken readonly, add separate bridgeTokenFromFile property. 2. Toggle cycle: bridgeHost/bridgePort were single settings for both modes. Toggling clientOnlyMode OFF left bridgeHost pointing at remote IP, so normal mode sent local token to remote bridge → 403. Also startBridge() tried to bind local bridge on remote IP. Fix: mode-dependent bindings — 127.0.0.1:19777 in normal mode, configured remote host/port in client-only mode. Also removed stopGateway() from onClientOnlyModeChanged (was hitting remote bridge), added maybeStartGateway() for clean local gateway restart on toggle back to normal mode, and added diagnostic logging.
…esAvatar, fix trap + setBridgeError deep-copy - Merge getJson/postJson into shared requestJson() (-25 lines) - Extract HermesAvatar.qml component, replace 4 avatar duplications (-60 lines) - Fix postJson log typo (was logging 'getJson') - Remove dead displayText branch (session.running check was no-op) - Fix SummaryPopup indentation - Deep-copy state in setBridgeError for QML change detection - Move trap before bridge start + add EXIT signal - Detect bridge startup failure during token wait loop net: -63 lines, 1 new file
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
What changed since the first versionBridge toggle fix Toggling client-only mode off then on again gave 403. Two causes:
Client-only mode Test Connection button in settings validates the remote bridge token. Error messages distinguish connection failures, auth failures (403), and other HTTP errors. Bridge resilience Auto-restarts on health check failure. The serve script detects startup failures during the token wait loop and exits instead of hanging 10 seconds. Cleanup trap covers EXIT and fires before the bridge starts. Code cleanup
Net: -63 lines, 1 new file. |
The |
- saveSettings now calls setModel so popup Apply button works - New Session clears messages/events/approval in bridge state - Added Reset button to Panel (calls createSession for server-side clear) - Fixed HermesAvatar.qml deployment to installed plugin - Removed unused bridgeOnline reference from createSession log - Added clearChat function for local state reset
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
|
Update: 2026-06-23 Bug fixes and a model-picker overhaul since the last push. Bugs fixed Model apply. The popup Apply button only saved plugin settings, never called New Session. The bridge created a new Hermes RPC session but kept old messages and events in state. The panel looked unchanged. Fixed in the bridge: Reset. Added a Reset button next to New Session in the chat panel. Clears messages without a server roundtrip. Disabled when the pane is empty. Toggle cycle. Switching between normal mode and client-only mode broke bridge contact because Bridge token binding. A mutable Provider list The Settings dropdown now merges three sources: provider config models from HermesAvatar component. Extracted from four duplicate avatar blocks across BarWidget, Panel, and SummaryPopup. Fixes a Main.qml load failure when the file was missing from installed dirs. Commits: |
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
…l, and diagnostics
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
… fix - Add GET /sessions endpoint dispatching session.list RPC - SessionPopup.qml: scrollable popup listing 10 recent sessions - resumeSession loads messages from SessionDB directly for instant feedback - Prompt auto-creates gateway session, preserves loaded history - Debounced Timer scrolls chat to bottom on state changes
|
Update: session picker feature Latest commits add a session history picker to the chat panel header. What's new:
How it works: The bridge reads messages directly from Hermes' SQLite Repo differences to note: The standalone repo ( Commits: |
Automatic Code Quality ReviewFile: hermes-agent/Panel.qml
+ tooltipText: pluginApi?.tr("panel.sessions") || "Sessions"File: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: falseFile: hermes-agent/components/SessionPopup.qml
+ text: pluginApi?.tr("panel.sessions") || "Sessions"
+ spacing: 2
+ text: pluginApi?.tr("panel.noSessions") || "No recent sessions" |
This is intentional. The parent passes an already-scaled icon size to the avatar component. Setting |
Automatic Code Quality ReviewFile: hermes-agent/components/HermesAvatar.qml
+ applyUiScale: false |
hermes-agentadds a native Noctalia interface for Hermes: bar status, chat panel with streaming and tool-call approvals, session management, and a>hermeslauncher command.Bar widget
Traffic-light status: online, busy, needs-you, degraded, offline. Click the pill for a popup with model, provider, background jobs, MCP servers, and pending approvals.
Chat panel
Send prompts, stream responses, approve tool calls inline, interrupt a running job, start or resume sessions.
Launcher
Type
>hermesin the Noctalia launcher. Opens the panel, starts a session, resumes the latest, or fires a one-shot prompt.Settings
Bridge host and port, state file path, Hermes home, poll interval, default provider and model, auto-start bridge, hide-when-idle, pin panel, show tool activity.
Bridge
scripts/hermes_bridge.pyruns locally on127.0.0.1with HTTP endpoints for health, state, session, prompt, interrupt, approvals, and one-shot. QML surfaces call the bridge and watch a JSON state file.On first start the bridge generates a bearer token (
secrets.token_urlsafe(32)), saves it to~/.cache/noctalia-hermes/bridge.token(0o600), and checks every request withsecrets.compare_digest(). Bodies cap at 1 MB. State files are0o600. Errors return a generic code only.Overlap with #898 and #931
#898 (
hermes-status) is a read-only traffic light. This bar widget matches that indicator and adds the summary popup, panel, approvals, interrupts, sessions, and launcher. Status-only users can ignore the panel.#931 (
hermes-ssh-chat) is an SSH terminal for remote Hermes. This plugin uses a local bridge. Remote Hermes: wraphermesCommandin SSH, or run the bridge on the remote host and setbridgeHost.#898 and #931 both work. If the registry wants one Hermes plugin instead of three, this one has the wider surface. I'll leave that to maintainers.
Manifest id matches folder, preview.png 960×540, i18n via
pluginApi?.tr(), N* widgets, Logger,Settings.qmlsaveSettings(). CI passed manifest-check and code-quality.MIT