feat(cli): opt-in git_branch status-bar field (⎇ current branch)

MiniMax Code CLI 0.3.1 added a status-line segment showing the git branch
for the current workspace. Hermes' status bar had no repo-awareness field.

Adds `git_branch` to display.status_bar.fields (opt-in only — the default
set never probes the filesystem). Reads .git/HEAD directly with a 5s
per-directory TTL cache (no subprocess per repaint); follows gitdir:
pointer files so worktrees/submodules resolve their private HEAD; a
detached HEAD renders the abbreviated commit.

Inspired by MiniMax Code CLI 0.3.1 changelog (agent.minimax.io/docs/changelog).
This commit is contained in:
Teknium
2026-09-09 02:44:23 -07:00
parent bef5b7c2c3
commit 7b037f0efa
5 changed files with 157 additions and 6 deletions
+18 -3
View File
@@ -208,6 +208,7 @@ class CLIStatusBarMixin:
"battery_label": "",
"battery_category": "dim",
"focus_label": "", # /focus badge: the reduced-output mode is never invisible.
"git_branch": "",
"goal_active": False,
"goal_turns_used": 0,
"goal_max_turns": 0}
@@ -220,6 +221,17 @@ class CLIStatusBarMixin:
except Exception:
pass
# Git branch (⎇) — opt-in via display.status_bar.fields, so the filesystem probe
# (TTL-cached in status_bar_git) only runs when the user asked for the segment.
try:
_fields = self._get_status_bar_field_set()
if _fields is not None and "git_branch" in _fields:
from hermes_cli.status_bar_git import current_git_branch
snapshot["git_branch"] = current_git_branch()
except Exception:
pass
# Battery reads are memoised inside agent.battery, so per-repaint polling is cheap.
if getattr(self, "_battery_visible", False):
try:
@@ -955,9 +967,9 @@ class CLIStatusBarMixin:
``CLI_CONFIG``; no per-render YAML parse). ``None`` = not customized, show everything.
Fields: model, context_detail, context_pct, cache_hit, latency, tps, compressions,
bg_tasks, bg_processes, bg_subagents, goal, duration, prompt_elapsed, idle_since,
focus, yolo, stash, battery, title, total_tokens (opt-in only). Order is fixed; the
config controls visibility only.
bg_tasks, bg_processes, bg_subagents, goal, git_branch (opt-in only), duration,
prompt_elapsed, idle_since, focus, yolo, stash, battery, title, total_tokens
(opt-in only). Order is fixed; the config controls visibility only.
"""
from cli import CLI_CONFIG
if hasattr(self, "_status_bar_field_set_cache"):
@@ -1043,6 +1055,9 @@ class CLIStatusBarMixin:
add_count("bg_subagents", "active_background_subagents", "⛓")
if goal_segment:
add("goal", _STRONG, goal_segment)
git_branch = snapshot.get("git_branch") or ""
if git_branch:
add("git_branch", _DIM, f"⎇ {git_branch}")
if not narrow:
add("duration", _DIM, duration_label)
if wide:
+2 -1
View File
@@ -896,7 +896,8 @@ DEFAULT_CONFIG = {
# CLI/TUI status bar fields. Non-empty = only listed fields show (built-in order kept,
# config controls visibility not ordering); empty = default set. Available: model,
# context_detail, context_pct, cache_hit, latency, tps, compressions, bg_tasks,
# bg_processes, bg_subagents, goal, duration, prompt_elapsed, idle_since, focus, yolo,
# bg_processes, bg_subagents, goal, git_branch (⎇ current branch, opt-in only), duration,
# prompt_elapsed, idle_since, focus, yolo,
# stash, battery, title, total_tokens (session Σ, opt-in only). Narrow terminals still drop
# context_detail/prompt_elapsed/idle_since.
"status_bar": {
+65
View File
@@ -0,0 +1,65 @@
"""Current git branch for the CLI status bar.
Reads ``.git/HEAD`` directly (no subprocess) so status-bar repaints stay cheap, with a
short per-directory TTL cache. Worktrees and submodules (``.git`` as a ``gitdir:``
pointer file) resolve to their private git dir, whose HEAD is per-worktree.
"""
from __future__ import annotations
import os
import time
from pathlib import Path
from typing import Optional
_TTL_SECONDS = 5.0
_cache: dict = {}
def _resolve_git_dir(start: Path) -> Optional[Path]:
"""Nearest enclosing git dir for ``start``, following worktree pointer files."""
for parent in (start, *start.parents):
dotgit = parent / ".git"
if dotgit.is_dir():
return dotgit
if dotgit.is_file():
try:
line = dotgit.read_text(encoding="utf-8", errors="replace").strip()
except OSError:
return None
if line.startswith("gitdir:"):
target = (parent / line.split(":", 1)[1].strip()).resolve()
return target if target.is_dir() else None
return None
return None
def current_git_branch(cwd: Optional[str] = None) -> str:
"""Branch name for ``cwd`` (defaults to the process cwd); ``""`` outside a repo.
A detached HEAD renders as the abbreviated commit (``a1b2c3d…``). Results are
cached ~5s per directory so per-repaint calls never re-walk the tree.
"""
try:
base = Path(cwd or os.getcwd()).resolve()
except OSError:
return ""
key = str(base)
now = time.monotonic()
hit = _cache.get(key)
if hit and now - hit[0] < _TTL_SECONDS:
return hit[1]
label = ""
git_dir = _resolve_git_dir(base)
if git_dir is not None:
try:
head = (git_dir / "HEAD").read_text(encoding="utf-8", errors="replace").strip()
except OSError:
head = ""
if head.startswith("ref:"):
ref = head.split(":", 1)[1].strip()
label = ref[len("refs/heads/"):] if ref.startswith("refs/heads/") else ref.rsplit("/", 1)[-1]
elif head:
label = f"{head[:8]}…"
_cache[key] = (now, label)
return label
+70
View File
@@ -0,0 +1,70 @@
"""Invariant tests for the opt-in git-branch status-bar field."""
from datetime import datetime, timedelta
from cli import HermesCLI
from hermes_cli import status_bar_git
from hermes_cli.status_bar_git import current_git_branch
def _make_cli():
cli_obj = HermesCLI.__new__(HermesCLI)
cli_obj.model = "anthropic/claude-sonnet-4-20250514"
cli_obj.session_start = datetime.now() - timedelta(minutes=3)
cli_obj.conversation_history = [{"role": "user", "content": "hi"}]
cli_obj.agent = None
return cli_obj
def test_current_git_branch_reads_head_and_worktree_pointer(tmp_path):
"""Branch resolves from .git/HEAD in a plain repo AND through a gitdir: pointer
(worktree layout); a non-repo dir yields ''."""
repo = tmp_path / "repo"
(repo / ".git").mkdir(parents=True)
(repo / ".git" / "HEAD").write_text("ref: refs/heads/minimax-inspired/status-bar-git-branch\n")
status_bar_git._cache.clear()
assert current_git_branch(str(repo)) == "minimax-inspired/status-bar-git-branch"
# Worktree: .git is a file pointing at a private git dir with its own HEAD.
private = tmp_path / "gitdir-store"
private.mkdir()
(private / "HEAD").write_text("ref: refs/heads/feature-x\n")
wt = tmp_path / "wt"
wt.mkdir()
(wt / ".git").write_text(f"gitdir: {private}\n")
status_bar_git._cache.clear()
assert current_git_branch(str(wt)) == "feature-x"
plain = tmp_path / "plain"
plain.mkdir()
status_bar_git._cache.clear()
assert current_git_branch(str(plain)) == ""
def test_git_branch_segment_is_opt_in(monkeypatch, tmp_path):
"""The ⎇ segment renders only when 'git_branch' is in the configured field list —
default field set (None) never probes or shows it."""
repo = tmp_path / "repo"
(repo / ".git").mkdir(parents=True)
(repo / ".git" / "HEAD").write_text("ref: refs/heads/main\n")
monkeypatch.chdir(repo)
status_bar_git._cache.clear()
cli_obj = _make_cli()
cli_obj._status_bar_field_set_cache = None # default set
snapshot = cli_obj._get_status_bar_snapshot()
assert snapshot["git_branch"] == ""
text = "".join(
t for seg in cli_obj._status_bar_segments(
snapshot, 120, None, False, styled=False) for _, t in seg)
assert "⎇" not in text
cli_obj2 = _make_cli()
fields = frozenset({"model", "git_branch"})
cli_obj2._status_bar_field_set_cache = fields
snapshot2 = cli_obj2._get_status_bar_snapshot()
assert snapshot2["git_branch"] == "main"
text2 = "".join(
t for seg in cli_obj2._status_bar_segments(
snapshot2, 120, fields, False, styled=False) for _, t in seg)
assert "⎇ main" in text2
+2 -2
View File
@@ -2115,11 +2115,11 @@ display:
fields: ["model", "duration", "total_tokens"] # visibility only; built-in order is preserved
```
Supported fields: `model`, `context_detail` (used/total tokens), `context_pct` (percent + meter), `cache_hit` (prompt cache hit ratio — resets on model switch and compression), `latency` (rolling mean API latency, last 10 calls), `tps` (rolling output tokens/sec, last 10 calls), `compressions`, `bg_tasks`, `bg_processes`, `bg_subagents`, `goal`, `duration`, `prompt_elapsed`, `idle_since`, `focus`, `yolo`, `stash`, `battery`, `title` (right-aligned session badge), and `total_tokens` (session Σ — opt-in only, never shown by default).
Supported fields: `model`, `context_detail` (used/total tokens), `context_pct` (percent + meter), `cache_hit` (prompt cache hit ratio — resets on model switch and compression), `latency` (rolling mean API latency, last 10 calls), `tps` (rolling output tokens/sec, last 10 calls), `compressions`, `bg_tasks`, `bg_processes`, `bg_subagents`, `goal`, `git_branch` (⎇ current git branch of the working directory — opt-in only, never shown by default; detached HEAD shows the abbreviated commit), `duration`, `prompt_elapsed`, `idle_since`, `focus`, `yolo`, `stash`, `battery`, `title` (right-aligned session badge), and `total_tokens` (session Σ — opt-in only, never shown by default).
Notes:
- An empty list (the default) keeps the standard set — everything except `total_tokens`.
- An empty list (the default) keeps the standard set — everything except `total_tokens` and `git_branch`.
- The config controls **visibility, not order**; fields render in their built-in positions.
- Narrow terminals still drop wide-mode-only fields (`context_detail`, `cache_hit`, `latency`, `tps`, `prompt_elapsed`, `idle_since`) regardless of config (`cache_hit` also shows in the medium ≥52-col tier).
- `latency`/`tps` stay hidden until API calls have been recorded (e.g. the Codex app-server backend reports no latency).