From 7ad10183aee3ec1f30b43192fcc87c1773fd3965 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sat, 14 Mar 2026 19:39:31 -0700 Subject: [PATCH] feat: show workspace status in cli banner --- hermes_cli/banner.py | 50 +++++++++++++++ tests/hermes_cli/test_banner_workspace.py | 77 +++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 tests/hermes_cli/test_banner_workspace.py diff --git a/hermes_cli/banner.py b/hermes_cli/banner.py index f1925651cd6..aaf9b5df44f 100644 --- a/hermes_cli/banner.py +++ b/hermes_cli/banner.py @@ -15,6 +15,8 @@ from rich.console import Console from rich.panel import Panel from rich.table import Table +from hermes_cli.config import load_config + from prompt_toolkit import print_formatted_text as _pt_print from prompt_toolkit.formatted_text import ANSI as _PT_ANSI @@ -124,6 +126,48 @@ def get_available_skills() -> Dict[str, List[str]]: return skills_by_category +def _workspace_root_labels(config: Dict[str, Any]) -> list[str]: + workspace_cfg = config.get("workspace", {}) or {} + kb_cfg = config.get("knowledgebase", {}) or {} + if not workspace_cfg.get("enabled", True) or not kb_cfg.get("enabled", True): + return [] + + workspace_path = workspace_cfg.get("path") or str(Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "workspace") + workspace_root = Path(os.path.expandvars(os.path.expanduser(workspace_path))).resolve() + roots = kb_cfg.get("roots") or [str(workspace_root)] + + labels: list[str] = [] + seen: set[str] = set() + for raw_root in roots: + expanded = Path(os.path.expandvars(os.path.expanduser(str(raw_root)))).resolve() + if expanded == workspace_root: + label = "workspace" + else: + label = expanded.name or str(expanded) + if label == workspace_root.name: + label = str(expanded) + if label in seen: + continue + seen.add(label) + labels.append(label) + return labels + + +def _get_workspace_banner_line() -> Optional[str]: + try: + config = load_config() + except Exception: + return None + labels = _workspace_root_labels(config) + if not labels: + return None + if len(labels) > 3: + display = ", ".join(labels[:3]) + f" +{len(labels) - 3} more" + else: + display = ", ".join(labels) + return f"Activated Workspace(s): {display}" + + # ========================================================================= # Update check # ========================================================================= @@ -352,6 +396,12 @@ def build_welcome_banner(console: Console, model: str, cwd: str, else: right_lines.append(f"[dim {dim}]No skills installed[/]") + workspace_line = _get_workspace_banner_line() + if workspace_line: + right_lines.append("") + right_lines.append(f"[bold {accent}]Workspace[/]") + right_lines.append(f"[{text}]{workspace_line}[/]") + right_lines.append("") mcp_connected = sum(1 for s in mcp_status if s["connected"]) if mcp_status else 0 summary_parts = [f"{len(tools)} tools", f"{total_skills} skills"] diff --git a/tests/hermes_cli/test_banner_workspace.py b/tests/hermes_cli/test_banner_workspace.py new file mode 100644 index 00000000000..8a91a851e14 --- /dev/null +++ b/tests/hermes_cli/test_banner_workspace.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from io import StringIO + +from rich.console import Console + + +def test_workspace_banner_line_uses_default_workspace_when_enabled(monkeypatch, tmp_path): + from hermes_cli import banner + + cfg = { + "workspace": {"enabled": True, "path": str(tmp_path / "workspace")}, + "knowledgebase": {"enabled": True, "roots": []}, + } + monkeypatch.setattr(banner, "load_config", lambda: cfg) + + line = banner._get_workspace_banner_line() + + assert line == "Activated Workspace(s): workspace" + + +def test_workspace_banner_line_lists_multiple_roots(monkeypatch, tmp_path): + from hermes_cli import banner + + cfg = { + "workspace": {"enabled": True, "path": str(tmp_path / "workspace")}, + "knowledgebase": { + "enabled": True, + "roots": [ + str(tmp_path / "workspace"), + str(tmp_path / "notes"), + str(tmp_path / "project-docs"), + ], + }, + } + monkeypatch.setattr(banner, "load_config", lambda: cfg) + + line = banner._get_workspace_banner_line() + + assert line == "Activated Workspace(s): workspace, notes, project-docs" + + +def test_workspace_banner_line_omits_when_disabled(monkeypatch): + from hermes_cli import banner + + cfg = { + "workspace": {"enabled": False, "path": ""}, + "knowledgebase": {"enabled": False, "roots": []}, + } + monkeypatch.setattr(banner, "load_config", lambda: cfg) + + assert banner._get_workspace_banner_line() is None + + +def test_build_welcome_banner_renders_workspace_line(monkeypatch): + from hermes_cli import banner + + monkeypatch.setattr(banner, "check_for_updates", lambda: 0) + monkeypatch.setattr(banner, "get_available_skills", lambda: {}) + monkeypatch.setattr(banner, "_get_workspace_banner_line", lambda: "Activated Workspace(s): workspace, notes") + + buf = StringIO() + console = Console(file=buf, force_terminal=False, width=140, color_system=None) + + banner.build_welcome_banner( + console=console, + model="anthropic/claude-sonnet-4.5", + cwd="/tmp/project", + tools=[], + enabled_toolsets=["hermes-cli"], + session_id="sess-1", + get_toolset_for_tool=lambda _: "other", + context_length=200000, + ) + + rendered = buf.getvalue() + assert "Activated Workspace(s): workspace, notes" in rendered