mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(banner): hyperlink startup banner title to latest GitHub release (#14945)
Wrap the existing version label in the welcome-banner panel title
('Hermes Agent v… · upstream … · local …') with an OSC-8 terminal
hyperlink pointing at the latest git tag's GitHub release page
(https://github.com/NousResearch/hermes-agent/releases/tag/<tag>).
Clickable in modern terminals (iTerm2, WezTerm, Windows Terminal,
GNOME Terminal, Kitty, etc.); degrades to plain text on terminals
without OSC-8 support. No new line added to the banner.
New get_latest_release_tag() helper runs 'git describe --tags
--abbrev=0' in the Hermes checkout (3s timeout, per-process cache,
silent fallback for non-git/pip installs and forks without tags).
This commit is contained in:
parent
2acc8783d1
commit
6051fba9dc
2 changed files with 119 additions and 1 deletions
|
|
@ -238,6 +238,52 @@ def get_git_banner_state(repo_dir: Optional[Path] = None) -> Optional[dict]:
|
||||||
return {"upstream": upstream, "local": local, "ahead": max(ahead, 0)}
|
return {"upstream": upstream, "local": local, "ahead": max(ahead, 0)}
|
||||||
|
|
||||||
|
|
||||||
|
_RELEASE_URL_BASE = "https://github.com/NousResearch/hermes-agent/releases/tag"
|
||||||
|
_latest_release_cache: Optional[tuple] = None # (tag, url) once resolved
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_release_tag(repo_dir: Optional[Path] = None) -> Optional[tuple]:
|
||||||
|
"""Return ``(tag, release_url)`` for the latest git tag, or None.
|
||||||
|
|
||||||
|
Local-only — runs ``git describe --tags --abbrev=0`` against the
|
||||||
|
Hermes checkout. Cached per-process. Release URL always points at the
|
||||||
|
canonical NousResearch/hermes-agent repo (forks don't get a link).
|
||||||
|
"""
|
||||||
|
global _latest_release_cache
|
||||||
|
if _latest_release_cache is not None:
|
||||||
|
return _latest_release_cache or None
|
||||||
|
|
||||||
|
repo_dir = repo_dir or _resolve_repo_dir()
|
||||||
|
if repo_dir is None:
|
||||||
|
_latest_release_cache = () # falsy sentinel — skip future lookups
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "describe", "--tags", "--abbrev=0"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=3,
|
||||||
|
cwd=str(repo_dir),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
_latest_release_cache = ()
|
||||||
|
return None
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
_latest_release_cache = ()
|
||||||
|
return None
|
||||||
|
|
||||||
|
tag = (result.stdout or "").strip()
|
||||||
|
if not tag:
|
||||||
|
_latest_release_cache = ()
|
||||||
|
return None
|
||||||
|
|
||||||
|
url = f"{_RELEASE_URL_BASE}/{tag}"
|
||||||
|
_latest_release_cache = (tag, url)
|
||||||
|
return _latest_release_cache
|
||||||
|
|
||||||
|
|
||||||
def format_banner_version_label() -> str:
|
def format_banner_version_label() -> str:
|
||||||
"""Return the version label shown in the startup banner title."""
|
"""Return the version label shown in the startup banner title."""
|
||||||
base = f"Hermes Agent v{VERSION} ({RELEASE_DATE})"
|
base = f"Hermes Agent v{VERSION} ({RELEASE_DATE})"
|
||||||
|
|
@ -519,9 +565,16 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
|
||||||
agent_name = _skin_branding("agent_name", "Hermes Agent")
|
agent_name = _skin_branding("agent_name", "Hermes Agent")
|
||||||
title_color = _skin_color("banner_title", "#FFD700")
|
title_color = _skin_color("banner_title", "#FFD700")
|
||||||
border_color = _skin_color("banner_border", "#CD7F32")
|
border_color = _skin_color("banner_border", "#CD7F32")
|
||||||
|
version_label = format_banner_version_label()
|
||||||
|
release_info = get_latest_release_tag()
|
||||||
|
if release_info:
|
||||||
|
_tag, _url = release_info
|
||||||
|
title_markup = f"[bold {title_color}][link={_url}]{version_label}[/link][/]"
|
||||||
|
else:
|
||||||
|
title_markup = f"[bold {title_color}]{version_label}[/]"
|
||||||
outer_panel = Panel(
|
outer_panel = Panel(
|
||||||
layout_table,
|
layout_table,
|
||||||
title=f"[bold {title_color}]{format_banner_version_label()}[/]",
|
title=title_markup,
|
||||||
border_style=border_color,
|
border_style=border_color,
|
||||||
padding=(0, 2),
|
padding=(0, 2),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -68,3 +68,68 @@ def test_build_welcome_banner_uses_normalized_toolset_names():
|
||||||
assert "homeassistant_tools:" not in output
|
assert "homeassistant_tools:" not in output
|
||||||
assert "honcho_tools:" not in output
|
assert "honcho_tools:" not in output
|
||||||
assert "web_tools:" not in output
|
assert "web_tools:" not in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_welcome_banner_title_is_hyperlinked_to_release():
|
||||||
|
"""Panel title (version label) is wrapped in an OSC-8 hyperlink to the GitHub release."""
|
||||||
|
import io
|
||||||
|
from unittest.mock import patch as _patch
|
||||||
|
import hermes_cli.banner as _banner
|
||||||
|
import model_tools as _mt
|
||||||
|
import tools.mcp_tool as _mcp
|
||||||
|
|
||||||
|
_banner._latest_release_cache = None
|
||||||
|
tag_url = ("v2026.4.23", "https://github.com/NousResearch/hermes-agent/releases/tag/v2026.4.23")
|
||||||
|
|
||||||
|
buf = io.StringIO()
|
||||||
|
with (
|
||||||
|
_patch.object(_mt, "check_tool_availability", return_value=(["web"], [])),
|
||||||
|
_patch.object(_banner, "get_available_skills", return_value={}),
|
||||||
|
_patch.object(_banner, "get_update_result", return_value=None),
|
||||||
|
_patch.object(_mcp, "get_mcp_status", return_value=[]),
|
||||||
|
_patch.object(_banner, "get_latest_release_tag", return_value=tag_url),
|
||||||
|
):
|
||||||
|
console = Console(file=buf, force_terminal=True, color_system="truecolor", width=160)
|
||||||
|
_banner.build_welcome_banner(
|
||||||
|
console=console, model="x", cwd="/tmp",
|
||||||
|
session_id="abc123",
|
||||||
|
tools=[{"function": {"name": "read_file"}}],
|
||||||
|
get_toolset_for_tool=lambda n: "file",
|
||||||
|
)
|
||||||
|
|
||||||
|
raw = buf.getvalue()
|
||||||
|
# The existing version label must still be present in the title
|
||||||
|
assert "Hermes Agent v" in raw, "Version label missing from title"
|
||||||
|
# OSC-8 hyperlink escape sequence present with the release URL
|
||||||
|
assert "\x1b]8;" in raw, "OSC-8 hyperlink not emitted"
|
||||||
|
assert "releases/tag/v2026.4.23" in raw, "Release URL missing from banner output"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_welcome_banner_title_falls_back_when_no_tag():
|
||||||
|
"""Without a resolvable tag, the panel title renders as plain text (no hyperlink escape)."""
|
||||||
|
import io
|
||||||
|
from unittest.mock import patch as _patch
|
||||||
|
import hermes_cli.banner as _banner
|
||||||
|
import model_tools as _mt
|
||||||
|
import tools.mcp_tool as _mcp
|
||||||
|
|
||||||
|
_banner._latest_release_cache = None
|
||||||
|
buf = io.StringIO()
|
||||||
|
with (
|
||||||
|
_patch.object(_mt, "check_tool_availability", return_value=(["web"], [])),
|
||||||
|
_patch.object(_banner, "get_available_skills", return_value={}),
|
||||||
|
_patch.object(_banner, "get_update_result", return_value=None),
|
||||||
|
_patch.object(_mcp, "get_mcp_status", return_value=[]),
|
||||||
|
_patch.object(_banner, "get_latest_release_tag", return_value=None),
|
||||||
|
):
|
||||||
|
console = Console(file=buf, force_terminal=True, color_system="truecolor", width=160)
|
||||||
|
_banner.build_welcome_banner(
|
||||||
|
console=console, model="x", cwd="/tmp",
|
||||||
|
session_id="abc123",
|
||||||
|
tools=[{"function": {"name": "read_file"}}],
|
||||||
|
get_toolset_for_tool=lambda n: "file",
|
||||||
|
)
|
||||||
|
|
||||||
|
raw = buf.getvalue()
|
||||||
|
assert "Hermes Agent v" in raw, "Version label missing from title"
|
||||||
|
assert "\x1b]8;" not in raw, "OSC-8 hyperlink should not be emitted without a tag"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue