fix: detect installed Hermes runtime and restore its official icon

This commit is contained in:
austinpower1258
2026-08-06 22:24:14 -07:00
parent 1dcf57676d
commit b36be1d285
6 changed files with 77 additions and 15 deletions
@@ -1,15 +1,13 @@
{
"images" : [
{
"filename" : "HermesAgent.svg",
"idiom" : "universal"
"filename" : "HermesAgent.png",
"idiom" : "universal",
"scale" : "1x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#101820"/>
<path d="M20 47V17h7v12h10V17h7v30h-7V35H27v12h-7Z" fill="#F4F7FA"/>
<path d="M15 13h34v6H15z" fill="#4AD7D1"/>
<path d="M15 45h34v6H15z" fill="#CFA9FF"/>
<path d="M18 13l5-6 5 6M36 13l5-6 5 6M18 51l5 6 5-6M36 51l5 6 5-6" fill="none" stroke="#F4F7FA" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 472 B

@@ -10,7 +10,11 @@ extension CmuxVaultAgentRegistration {
defaultValue: "Hermes Agent"
),
iconAssetName: "AgentIcons/HermesAgent",
detect: CmuxVaultAgentDetectRule(processNames: ["hermes", "hermes-agent"]),
detect: CmuxVaultAgentDetectRule(
processNames: ["hermes", "hermes-agent"],
alternateProcessNames: ["python", "python3"],
alternateArgvContainsAny: ["hermes-agent/hermes"]
),
sessionIdSource: .persistedStore(.hermesStateDB),
resumeCommand: "{{executable}} --resume {{sessionId}}",
cwd: .preserve
@@ -2,8 +2,14 @@ import Foundation
extension CmuxVaultAgentRegistration {
func processDetectedSnapshotIsRestorable(for process: VaultObservedAgentProcess) -> Bool {
guard id == "campfire" else { return true }
return process.environment["CAMPFIRE_SESSION_ROLE"] == "host"
switch id {
case "campfire":
return process.environment["CAMPFIRE_SESSION_ROLE"] == "host"
case "hermes-agent":
return process.isInteractiveHermesAgentInvocation
default:
return true
}
}
}
+61
View File
@@ -43,6 +43,16 @@ struct VaultObservedAgentProcess: Sendable {
arguments.piCompatibleSessionID(startingAt: piCompatibleSessionArgumentStartIndex)
}
/// Whether this Hermes process owns an interactive conversation that cmux can restore.
///
/// The installed launcher execs Python with `hermes-agent/hermes` as its script, so the
/// first Hermes argument is not always at index one. Management subcommands such as
/// `gateway`, `doctor`, and `update` must not become resumable chat panes.
var isInteractiveHermesAgentInvocation: Bool {
guard let command = firstHermesAgentPositionalArgument else { return true }
return command.compare("chat", options: [.caseInsensitive, .literal]) == .orderedSame
}
var openCodeExecutableArgumentIndex: Int? {
if let first = arguments.first,
Self.argumentLooksLikeOpenCode(first) {
@@ -78,6 +88,57 @@ struct VaultObservedAgentProcess: Sendable {
}
}
private var firstHermesAgentPositionalArgument: String? {
var index = hermesAgentArgumentStartIndex
while index < arguments.endIndex {
let argument = arguments[index]
if argument == "--" {
let nextIndex = arguments.index(after: index)
return nextIndex < arguments.endIndex ? arguments[nextIndex] : nil
}
guard argument.hasPrefix("-") else { return argument }
let option = argument
.split(separator: "=", maxSplits: 1)
.first
.map(String.init) ?? argument
if !argument.contains("="), Self.hermesTopLevelOptionConsumesValue(option) {
index = min(index + 2, arguments.endIndex)
} else {
index += 1
}
}
return nil
}
private var hermesAgentArgumentStartIndex: Int {
if let entrypointIndex = arguments.indices.first(where: {
Self.argumentLooksLikeHermesAgentEntrypoint(arguments[$0])
}) {
return arguments.index(after: entrypointIndex)
}
return arguments.isEmpty
? arguments.startIndex
: arguments.index(after: arguments.startIndex)
}
private static func argumentLooksLikeHermesAgentEntrypoint(_ argument: String) -> Bool {
argument
.replacingOccurrences(of: "\\", with: "/")
.range(of: "hermes-agent/hermes", options: [.caseInsensitive, .literal]) != nil
}
private static func hermesTopLevelOptionConsumesValue(_ option: String) -> Bool {
switch option {
case "-z", "--oneshot", "-m", "--model", "--provider", "--reasoning",
"-t", "--toolsets", "-r", "--resume", "-c", "--continue",
"-s", "--skills", "--usage-file", "-p", "--profile":
return true
default:
return false
}
}
static func argumentLooksLikeOpenCode(_ argument: String) -> Bool {
let normalized = argument.lowercased()
let pathComponents = (normalized as NSString).pathComponents