feat(cli): show active /goal segment in the TUI status bar

Append a "⊙ goal 3/20" segment (turns used / turn budget) to the CLI
status bar whenever a standing /goal is active. Mirrors the desktop
composer goal indicator: active-goal-only — paused/done goals stay out
of the bar since they already print their own glyph lines in-thread.

- Snapshot: goal_active / goal_turns_used / goal_max_turns from the
  cached GoalManager (in-memory attribute read, no DB hit per repaint).
- Rendered in all three width tiers of both _build_status_bar_text and
  _get_status_bar_fragments, and it respects the /statusbar toggle for
  free (the toggle gates _get_status_bar_fragments as a whole).
- Tests: segment composition, active-only contract, all width tiers.

Status-bar goal indicator concept from #43020.

Co-authored-by: Akshan Krithick <akshankrithick305@gmail.com>

Assisted-by: Claude Fable 5 via Hermes Agent
This commit is contained in:
teknium1 2026-07-26 14:23:34 -07:00 committed by Teknium
parent e5d21e87cb
commit e769560c76
2 changed files with 145 additions and 0 deletions

49
cli.py
View file

@ -5122,6 +5122,23 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
except Exception:
pass
# Standing /goal state (Ralph loop). GoalManager is cached on self and
# keeps its state in memory, so this is a cheap attribute read — no DB
# hit per repaint. Only an *active* goal earns a segment; paused/done
# goals stay out of the bar (matching the desktop's active-first row).
snapshot["goal_active"] = False
snapshot["goal_turns_used"] = 0
snapshot["goal_max_turns"] = 0
try:
goal_mgr = self._get_goal_manager()
if goal_mgr is not None and goal_mgr.is_active():
goal_state = goal_mgr.state
snapshot["goal_active"] = True
snapshot["goal_turns_used"] = int(getattr(goal_state, "turns_used", 0) or 0)
snapshot["goal_max_turns"] = int(getattr(goal_state, "max_turns", 0) or 0)
except Exception:
pass
if not agent:
return snapshot
@ -5564,6 +5581,21 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
cont = " | Continuous" if self._voice_continuous else ""
return [("class:voice-status", f" 🎤 Voice mode{tts}{cont}{label} to record ")]
@staticmethod
def _status_bar_goal_segment(snapshot: Dict[str, Any]) -> str:
"""Return the ``⊙ goal 3/20`` segment, or ``""`` when no goal is active.
Active-goal-only by design: paused/done goals don't occupy status-bar
real estate (they already print their own glyph lines in the thread).
"""
if not snapshot.get("goal_active"):
return ""
used = snapshot.get("goal_turns_used") or 0
max_turns = snapshot.get("goal_max_turns") or 0
if max_turns:
return f"⊙ goal {used}/{max_turns}"
return "⊙ goal"
def _build_status_bar_text(self, width: Optional[int] = None) -> str:
"""Return a compact one-line session status string for the TUI footer."""
try:
@ -5577,8 +5609,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
battery_prefix = f"{battery_label}" if battery_label else ""
yolo_active = self._is_session_yolo_active()
goal_segment = self._status_bar_goal_segment(snapshot)
if width < 52:
text = f"{battery_prefix}{snapshot['model_short']} · {duration_label}"
if goal_segment:
text += f" · {goal_segment}"
if yolo_active:
text += " · ⚠ YOLO"
return self._trim_status_bar_text(text, width)
@ -5598,6 +5633,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
bg_subagent_count = snapshot.get("active_background_subagents", 0)
if bg_subagent_count:
parts.append(f"{bg_subagent_count}")
if goal_segment:
parts.append(goal_segment)
parts.append(duration_label)
if yolo_active:
parts.append("⚠ YOLO")
@ -5625,6 +5662,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
bg_subagent_count = snapshot.get("active_background_subagents", 0)
if bg_subagent_count:
parts.append(f"{bg_subagent_count}")
if goal_segment:
parts.append(goal_segment)
parts.append(duration_label)
prompt_elapsed = snapshot.get("prompt_elapsed")
if prompt_elapsed:
@ -5651,6 +5690,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
width = self._get_tui_terminal_width()
duration_label = snapshot["duration"]
yolo_active = self._is_session_yolo_active()
goal_segment = self._status_bar_goal_segment(snapshot)
battery_label = snapshot.get("battery_label") or ""
battery_style = self._battery_status_style(snapshot.get("battery_category", "dim"))
@ -5661,6 +5701,9 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
("class:status-bar-dim", " · "),
("class:status-bar-dim", duration_label),
]
if goal_segment:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", goal_segment))
if yolo_active:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-yolo", "⚠ YOLO"))
@ -5691,6 +5734,9 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
if bg_subagent_count:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", f"{bg_subagent_count}"))
if goal_segment:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", goal_segment))
frags.extend([
("class:status-bar-dim", " · "),
("class:status-bar-dim", duration_label),
@ -5734,6 +5780,9 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
if bg_subagent_count:
frags.append(("class:status-bar-dim", ""))
frags.append(("class:status-bar-strong", f"{bg_subagent_count}"))
if goal_segment:
frags.append(("class:status-bar-dim", ""))
frags.append(("class:status-bar-strong", goal_segment))
frags.extend([
("class:status-bar-dim", ""),
("class:status-bar-dim", duration_label),

View file

@ -0,0 +1,96 @@
"""Status-bar goal segment (⊙ goal N/M) — active-goal-only rendering.
The segment mirrors the desktop composer goal indicator: it appears only
while a /goal is ACTIVE, shows turns used vs the turn budget, and stays out
of the bar entirely for paused/done/absent goals (those already print their
own glyph lines in the conversation thread).
"""
from datetime import datetime, timedelta
from types import SimpleNamespace
from cli import HermesCLI
def _make_cli(model: str = "anthropic/claude-sonnet-4-20250514"):
cli_obj = HermesCLI.__new__(HermesCLI)
cli_obj.model = model
cli_obj.session_start = datetime.now() - timedelta(minutes=14, seconds=32)
cli_obj.conversation_history = [{"role": "user", "content": "hi"}]
cli_obj.agent = None
return cli_obj
def _attach_goal(cli_obj, *, active: bool, turns_used: int = 3, max_turns: int = 20):
"""Bind a fake GoalManager the way _get_goal_manager caches one."""
cli_obj.session_id = "sess-goal-test"
cli_obj._goal_manager = SimpleNamespace(
session_id="sess-goal-test",
is_active=lambda: active,
state=SimpleNamespace(turns_used=turns_used, max_turns=max_turns),
)
return cli_obj
class TestStatusBarGoalSegment:
def test_goal_segment_composition(self):
cli_obj = _attach_goal(_make_cli(), active=True, turns_used=3, max_turns=20)
snapshot = cli_obj._get_status_bar_snapshot()
assert snapshot["goal_active"] is True
assert snapshot["goal_turns_used"] == 3
assert snapshot["goal_max_turns"] == 20
assert cli_obj._status_bar_goal_segment(snapshot) == "⊙ goal 3/20"
def test_goal_segment_absent_without_goal(self):
cli_obj = _make_cli() # no session_id → no goal manager
snapshot = cli_obj._get_status_bar_snapshot()
assert snapshot["goal_active"] is False
assert cli_obj._status_bar_goal_segment(snapshot) == ""
def test_goal_segment_absent_when_paused(self):
# Paused goals must NOT occupy the status bar (active-only contract).
cli_obj = _attach_goal(_make_cli(), active=False)
snapshot = cli_obj._get_status_bar_snapshot()
assert snapshot["goal_active"] is False
assert cli_obj._status_bar_goal_segment(snapshot) == ""
def test_goal_segment_without_budget_omits_counter(self):
segment = HermesCLI._status_bar_goal_segment(
{"goal_active": True, "goal_turns_used": 0, "goal_max_turns": 0}
)
assert segment == "⊙ goal"
def test_active_goal_rendered_in_wide_status_bar(self):
cli_obj = _attach_goal(_make_cli(), active=True, turns_used=5, max_turns=20)
text = cli_obj._build_status_bar_text(width=120)
assert "⊙ goal 5/20" in text
def test_active_goal_rendered_in_medium_status_bar(self):
cli_obj = _attach_goal(_make_cli(), active=True, turns_used=1, max_turns=20)
text = cli_obj._build_status_bar_text(width=60)
assert "⊙ goal 1/20" in text
def test_active_goal_rendered_in_narrow_status_bar(self):
cli_obj = _attach_goal(_make_cli(), active=True, turns_used=2, max_turns=20)
text = cli_obj._build_status_bar_text(width=50)
assert "⊙ goal" in text
def test_no_goal_segment_in_status_bar_without_goal(self):
cli_obj = _make_cli()
text = cli_obj._build_status_bar_text(width=120)
assert "⊙ goal" not in text