Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
16beb01
feat(hermes-agent): native Hermes status, chat panel, and launcher pr…
Nomadcxx Jun 20, 2026
c8d4658
fix(hermes-agent): use Style.borderS instead of hardcoded border width
Nomadcxx Jun 20, 2026
60eb399
feat(hermes-agent): auto-detect Hermes home, gateway, and model on fi…
Nomadcxx Jun 20, 2026
8670c34
style(hermes-agent): use Style.borderS instead of hardcoded border width
Nomadcxx Jun 20, 2026
ecd14d5
fix(hermes-agent): fix auto-detect on first install and add gateway a…
Nomadcxx Jun 20, 2026
5ad4917
fix(hermes-agent): wait for bridge token before auto-configure and ga…
Nomadcxx Jun 20, 2026
b9b4624
fix(hermes-agent): replace non-existent flat property with outlined o…
Nomadcxx Jun 20, 2026
4f9b4ea
fix(hermes-agent): use optional chaining for pluginApi in all remaini…
Nomadcxx Jun 20, 2026
c862bb8
chore: update plugin registry
Nomadcxx Jun 20, 2026
5c74c2c
fix(hermes-agent): replace non-existent flat property with outlined
Nomadcxx Jun 20, 2026
3f11dd3
fix(hermes-agent): auto-restart bridge on health check failure
Nomadcxx Jun 20, 2026
3597572
Merge branch 'add-hermes-agent' of github.com:Nomadcxx/legacy-v4-plug…
Nomadcxx Jun 20, 2026
f8ea895
feat(hermes-agent): add client-only mode for remote bridge over SSH
Nomadcxx Jun 21, 2026
6c8481d
chore: update registry for hermes-agent v1.1.0
Nomadcxx Jun 21, 2026
0d99a79
fix(hermes-agent): add Test connection button and better error messag…
Nomadcxx Jun 21, 2026
de7df36
refactor(hermes-agent): remove dead code, add gateway start/stop endp…
Nomadcxx Jun 21, 2026
1cfd3fc
fix(hermes-agent): use labelColor instead of color on NLabel, fix inv…
Nomadcxx Jun 21, 2026
63c82d7
fix(hermes-agent): remove dead startGateway signal from SummaryPopup …
Nomadcxx Jun 21, 2026
87a7aa3
fix(hermes-agent): mode-dependent bridgeHost/bridgePort + readonly br…
Nomadcxx Jun 22, 2026
b2d76b6
refactor(hermes-agent): audit fixes - merge requestJson, extract Herm…
Nomadcxx Jun 22, 2026
f42badb
fix(hermes-agent): session reset, model apply, and message clearing
Nomadcxx Jun 22, 2026
e541ceb
fix(hermes-agent): filter model list to only configured providers
Nomadcxx Jun 22, 2026
c830f38
fix(hermes-agent): providers list mirrors hermes --tui (configured + …
Nomadcxx Jun 22, 2026
2ff09b3
fix(hermes-agent): show all providers from Hermes model catalog
Nomadcxx Jun 22, 2026
7f658c3
docs(hermes-agent): rewrite README with architecture, model picker, t…
Nomadcxx Jun 22, 2026
ea32480
docs(hermes-agent): add repo source install, clarify bridge modes
Nomadcxx Jun 22, 2026
8db3019
docs(hermes-agent): comprehensive bridge mode guide with token, tunne…
Nomadcxx Jun 23, 2026
2ee0505
feat(hermes-agent): session picker popup, resume with history, scroll…
Nomadcxx Jun 23, 2026
e2658cf
fix(hermes-agent): remove tr fallbacks, hardcoded spacing → Style.mar…
Nomadcxx Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions hermes-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
settings.json
bridge.token
263 changes: 263 additions & 0 deletions hermes-agent/BarWidget.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
import "components" as Components

Item {
id: root

property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0

readonly property var mainInstance: pluginApi?.mainInstance
readonly property var state: mainInstance?.state || ({})
readonly property var bridge: state.bridge || ({})
readonly property var hermes: state.hermes || ({})
readonly property var session: state.session || ({})
readonly property var summary: state.summary || ({})
readonly property string gatewayStatus: (summary.gateway && summary.gateway.status) || "unknown"
readonly property var cfg: pluginApi?.pluginSettings || ({})
readonly property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
readonly property bool clientOnlyMode: cfg.clientOnlyMode ?? defaults.clientOnlyMode ?? false
readonly property string hermesIconPath: pluginApi?.pluginDir ? "file://" + pluginApi.pluginDir + "/assets/hermes-icon.png" : ""

readonly property string screenName: screen ? screen.name : ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)

readonly property bool hideWhenIdle: cfg.hideWhenIdle ?? defaults.hideWhenIdle ?? false
readonly property string bridgeStatus: bridge.status || "offline"
readonly property string hermesStatus: hermes.status || "unknown"
readonly property bool bridgeOnline: bridgeStatus === "online"
// When the bridge is online but Hermes has not reported a lifecycle status yet
// (no session run / status hook), fall back to the gateway: running gateway = idle.
readonly property string status: !bridgeOnline ? "offline"
: (hermesStatus !== "unknown" ? hermesStatus
: (gatewayStatus === "running" ? "idle" : "unknown"))
readonly property bool shouldHide: hideWhenIdle && status === "idle"

readonly property string statusIcon: {
switch (status) {
case "offline": return "power";
case "idle": return "circle-check";
case "busy": return "loader";
case "attention": return "bell-ringing";
case "degraded": return "alert-circle";
case "error": return "alert-triangle";
default: return "sparkles";
}
}

readonly property color statusColor: {
switch (status) {
case "offline": return Color.mError;
case "idle": return Color.mPrimary;
case "busy": return Color.mPrimary;
case "attention": return "#f59e0b";
case "degraded": return "#f97316";
case "error": return Color.mError;
default: return Color.mOnSurface;
}
}

readonly property string statusText: {
switch (status) {
case "offline": return pluginApi?.tr("status.offline");
case "idle": return pluginApi?.tr("status.idle");
case "busy": return pluginApi?.tr("status.busy");
case "attention": return pluginApi?.tr("status.attention");
case "degraded": return pluginApi?.tr("status.degraded");
case "error": return pluginApi?.tr("status.error");
default: return pluginApi?.tr("status.unknown");
}
}

readonly property string displayText: {
if (isBarVertical) return "";
if (status === "idle") return "";
if (status === "unknown") return "";
if (status === "attention") return "!";
return statusText;
}

readonly property string tooltipText: {
var model = hermes.model ? " · " + hermes.model : "";
var err = bridge.error ? "\n" + bridge.error : "";
return "Hermes: " + statusText + model + err;
}

function openHermesPanel() {
if (mainInstance && typeof mainInstance.openPreferredPanel === "function") {
mainInstance.openPreferredPanel(root.screen, root);
return;
}
pluginApi?.openPanel(root.screen, root);
}

readonly property real contentWidth: {
if (shouldHide) return 0;
return isBarVertical ? capsuleHeight : content.implicitWidth + Style.marginM * 2;
}
readonly property real contentHeight: {
if (shouldHide) return 0;
return isBarVertical ? content.implicitHeight + Style.marginM * 2 : capsuleHeight;
}

implicitWidth: contentWidth
implicitHeight: contentHeight
visible: !shouldHide

Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
radius: Style.radiusL
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth

Item {
id: content
anchors.centerIn: parent
implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth
implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight

RowLayout {
id: rowLayout
visible: !root.isBarVertical
spacing: Style.marginS

Item {
Layout.preferredWidth: root.barFontSize + 6
Layout.preferredHeight: root.barFontSize + 6
Layout.alignment: Qt.AlignVCenter

Components.HermesAvatar {
anchors.fill: parent
iconPath: root.hermesIconPath
fallbackIcon: root.statusIcon
statusColor: root.statusColor
iconSize: root.barFontSize
dotSize: 7 * Style.uiScaleRatio
}
}

NText {
visible: root.displayText !== ""
text: root.displayText
pointSize: root.barFontSize
applyUiScale: false
font.weight: Style.fontWeightSemiBold
color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
}
}

ColumnLayout {
id: colLayout
visible: root.isBarVertical
spacing: Style.marginXS

Item {
Layout.preferredWidth: root.barFontSize + 8
Layout.preferredHeight: root.barFontSize + 8
Layout.alignment: Qt.AlignHCenter

Components.HermesAvatar {
anchors.fill: parent
iconPath: root.hermesIconPath
fallbackIcon: root.statusIcon
statusColor: root.statusColor
iconSize: root.barFontSize
dotSize: 7 * Style.uiScaleRatio
}
}
}
}
}

MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor

onClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
summaryPopup.toggleAt(root, root.screen);
} else if (mouse.button === Qt.RightButton) {
summaryPopup.close();
PanelService.showContextMenu(contextMenu, root, screen);
}
}

onEntered: TooltipService.show(root, root.tooltipText, BarService.getTooltipDirection(root.screenName))
onExited: TooltipService.hide()
}

NPopupContextMenu {
id: contextMenu
screen: root.screen

model: {
var items = [
{ "label": pluginApi?.tr("bar.openPanel"), "action": "open", "icon": "message-circle" },
{ "label": pluginApi?.tr("bar.newSession"), "action": "new-session", "icon": "plus" },
{ "label": pluginApi?.tr("bar.interrupt"), "action": "interrupt", "icon": "octagon" },
{ "label": pluginApi?.tr("bar.refresh"), "action": "refresh", "icon": "refresh" }
];
if (!root.clientOnlyMode && (root.status === "offline" || root.status === "unknown")) {
items.push({ "label": pluginApi?.tr("bar.startGateway"), "action": "start-gateway", "icon": "power" });
}
items.push({ "label": pluginApi?.tr("settings.title"), "action": "settings", "icon": "settings" });
return items;
}

onTriggered: function(action) {
contextMenu.close();
PanelService.closeContextMenu(root.screen);
if (action === "open") {
root.openHermesPanel();
} else if (action === "new-session") {
mainInstance?.createSession();
root.openHermesPanel();
} else if (action === "interrupt") {
mainInstance?.interrupt();
} else if (action === "refresh") {
mainInstance?.refreshState();
} else if (action === "start-gateway") {
mainInstance?.startGateway();
} else if (action === "settings") {
BarService.openPluginSettings(root.screen, pluginApi?.manifest);
}
}
}

Components.SummaryPopup {
id: summaryPopup
pluginApi: root.pluginApi
state: root.state
screen: root.screen

onOpenPanel: root.openHermesPanel()
onNewSession: {
mainInstance?.createSession();
root.openHermesPanel();
}
onInterrupt: mainInstance?.interrupt()
onRefresh: mainInstance?.refreshState()
onSettings: BarService.openPluginSettings(root.screen, pluginApi?.manifest)
}
}
118 changes: 118 additions & 0 deletions hermes-agent/LauncherProvider.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import QtQuick
import qs.Commons

Item {
id: root

property var pluginApi: null
property var launcher: null
property string name: "Hermes"
property bool handleSearch: false
property string supportedLayouts: "list"

readonly property var mainInstance: pluginApi?.mainInstance
readonly property var cfg: pluginApi?.pluginSettings || ({})
readonly property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
readonly property string prefix: cfg.launcherPrefix ?? defaults.launcherPrefix ?? ">hermes"

function handleCommand(searchText) {
return searchText.startsWith(prefix);
}

function commands() {
return [{
"name": prefix,
"description": pluginApi?.tr("launcher.commandDescription"),
"icon": "sparkles",
"isTablerIcon": true,
"isImage": false,
"onActivate": function() {
if (launcher) launcher.setSearchText(prefix + " ");
}
}];
}

function closeLauncher() {
if (launcher) launcher.close();
}

function openPanel() {
if (mainInstance && typeof mainInstance.openPreferredPanel === "function") {
mainInstance.openPreferredPanel(null, null);
} else {
pluginApi?.openPanel(pluginApi?.panelOpenScreen);
}
closeLauncher();
}

function getResults(searchText) {
if (!searchText.startsWith(prefix)) return [];

var content = searchText.slice(prefix.length).replace(/^\s+/, "");
var results = [];

if (content.length > 0) {
results.push({
"name": pluginApi?.tr("launcher.ask"),
"description": content,
"icon": "send",
"isTablerIcon": true,
"isImage": false,
"_score": 1000,
"onActivate": function() {
mainInstance?.oneShot(content);
root.openPanel();
}
});
} else {
results.push({
"name": pluginApi?.tr("launcher.typePrompt"),
"description": prefix + " plan the next change",
"icon": "message-circle",
"isTablerIcon": true,
"isImage": false,
"_score": 1000,
"onActivate": function() {}
});
}

results.push({
"name": pluginApi?.tr("launcher.openPanel"),
"description": pluginApi?.tr("launcher.openPanelDescription"),
"icon": "panel-right-open",
"isTablerIcon": true,
"isImage": false,
"_score": 950,
"onActivate": root.openPanel
});

results.push({
"name": pluginApi?.tr("launcher.newSession"),
"description": pluginApi?.tr("launcher.newSessionDescription"),
"icon": "plus",
"isTablerIcon": true,
"isImage": false,
"_score": 900,
"onActivate": function() {
mainInstance?.createSession();
root.openPanel();
}
});

results.push({
"name": pluginApi?.tr("launcher.resumeLatest"),
"description": pluginApi?.tr("launcher.resumeLatestDescription"),
"icon": "history",
"isTablerIcon": true,
"isImage": false,
"_score": 850,
"onActivate": function() {
var storedId = ((mainInstance?.state || {}).session || {}).stored_id || "";
if (storedId !== "") mainInstance?.resumeSession(storedId);
root.openPanel();
}
});

return results;
}
}
Loading