feat: first-class plugin architecture + hide status bar cost by default (#1544)
The persistent status bar now shows context %, token counts, and
duration but NOT $ cost by default. Cost display is opt-in via:
display:
show_cost: true
in config.yaml, or: hermes config set display.show_cost true
The /usage command still shows full cost breakdown since the user
explicitly asked for it — this only affects the always-visible bar.
Status bar without cost:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ 15m
Status bar with show_cost: true:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ $0.06 │ 15m
This commit is contained in:
parent
57be18c026
commit
447594be28
3 changed files with 67 additions and 18 deletions
57
cli.py
57
cli.py
|
|
@ -204,6 +204,7 @@ def load_cli_config() -> Dict[str, Any]:
|
||||||
"compact": False,
|
"compact": False,
|
||||||
"resume_display": "full",
|
"resume_display": "full",
|
||||||
"show_reasoning": False,
|
"show_reasoning": False,
|
||||||
|
"show_cost": False,
|
||||||
"skin": "default",
|
"skin": "default",
|
||||||
},
|
},
|
||||||
"clarify": {
|
"clarify": {
|
||||||
|
|
@ -1023,6 +1024,8 @@ class HermesCLI:
|
||||||
self.bell_on_complete = CLI_CONFIG["display"].get("bell_on_complete", False)
|
self.bell_on_complete = CLI_CONFIG["display"].get("bell_on_complete", False)
|
||||||
# show_reasoning: display model thinking/reasoning before the response
|
# show_reasoning: display model thinking/reasoning before the response
|
||||||
self.show_reasoning = CLI_CONFIG["display"].get("show_reasoning", False)
|
self.show_reasoning = CLI_CONFIG["display"].get("show_reasoning", False)
|
||||||
|
# show_cost: display $ cost in the status bar (off by default)
|
||||||
|
self.show_cost = CLI_CONFIG["display"].get("show_cost", False)
|
||||||
self.verbose = verbose if verbose is not None else (self.tool_progress_mode == "verbose")
|
self.verbose = verbose if verbose is not None else (self.tool_progress_mode == "verbose")
|
||||||
|
|
||||||
# Configuration - priority: CLI args > env vars > config file
|
# Configuration - priority: CLI args > env vars > config file
|
||||||
|
|
@ -1276,13 +1279,22 @@ class HermesCLI:
|
||||||
width = width or shutil.get_terminal_size((80, 24)).columns
|
width = width or shutil.get_terminal_size((80, 24)).columns
|
||||||
percent = snapshot["context_percent"]
|
percent = snapshot["context_percent"]
|
||||||
percent_label = f"{percent}%" if percent is not None else "--"
|
percent_label = f"{percent}%" if percent is not None else "--"
|
||||||
cost_label = f"${snapshot['session_cost']:.2f}" if snapshot["pricing_known"] else "cost n/a"
|
|
||||||
duration_label = snapshot["duration"]
|
duration_label = snapshot["duration"]
|
||||||
|
show_cost = getattr(self, "show_cost", False)
|
||||||
|
|
||||||
|
if show_cost:
|
||||||
|
cost_label = f"${snapshot['session_cost']:.2f}" if snapshot["pricing_known"] else "cost n/a"
|
||||||
|
else:
|
||||||
|
cost_label = None
|
||||||
|
|
||||||
if width < 52:
|
if width < 52:
|
||||||
return f"⚕ {snapshot['model_short']} · {duration_label}"
|
return f"⚕ {snapshot['model_short']} · {duration_label}"
|
||||||
if width < 76:
|
if width < 76:
|
||||||
return f"⚕ {snapshot['model_short']} · {percent_label} · {cost_label} · {duration_label}"
|
parts = [f"⚕ {snapshot['model_short']}", percent_label]
|
||||||
|
if cost_label:
|
||||||
|
parts.append(cost_label)
|
||||||
|
parts.append(duration_label)
|
||||||
|
return " · ".join(parts)
|
||||||
|
|
||||||
if snapshot["context_length"]:
|
if snapshot["context_length"]:
|
||||||
ctx_total = _format_context_length(snapshot["context_length"])
|
ctx_total = _format_context_length(snapshot["context_length"])
|
||||||
|
|
@ -1291,7 +1303,11 @@ class HermesCLI:
|
||||||
else:
|
else:
|
||||||
context_label = "ctx --"
|
context_label = "ctx --"
|
||||||
|
|
||||||
return f"⚕ {snapshot['model_short']} │ {context_label} │ {percent_label} │ {cost_label} │ {duration_label}"
|
parts = [f"⚕ {snapshot['model_short']}", context_label, percent_label]
|
||||||
|
if cost_label:
|
||||||
|
parts.append(cost_label)
|
||||||
|
parts.append(duration_label)
|
||||||
|
return " │ ".join(parts)
|
||||||
except Exception:
|
except Exception:
|
||||||
return f"⚕ {self.model if getattr(self, 'model', None) else 'Hermes'}"
|
return f"⚕ {self.model if getattr(self, 'model', None) else 'Hermes'}"
|
||||||
|
|
||||||
|
|
@ -1299,8 +1315,13 @@ class HermesCLI:
|
||||||
try:
|
try:
|
||||||
snapshot = self._get_status_bar_snapshot()
|
snapshot = self._get_status_bar_snapshot()
|
||||||
width = shutil.get_terminal_size((80, 24)).columns
|
width = shutil.get_terminal_size((80, 24)).columns
|
||||||
cost_label = f"${snapshot['session_cost']:.2f}" if snapshot["pricing_known"] else "cost n/a"
|
|
||||||
duration_label = snapshot["duration"]
|
duration_label = snapshot["duration"]
|
||||||
|
show_cost = getattr(self, "show_cost", False)
|
||||||
|
|
||||||
|
if show_cost:
|
||||||
|
cost_label = f"${snapshot['session_cost']:.2f}" if snapshot["pricing_known"] else "cost n/a"
|
||||||
|
else:
|
||||||
|
cost_label = None
|
||||||
|
|
||||||
if width < 52:
|
if width < 52:
|
||||||
return [
|
return [
|
||||||
|
|
@ -1314,17 +1335,23 @@ class HermesCLI:
|
||||||
percent = snapshot["context_percent"]
|
percent = snapshot["context_percent"]
|
||||||
percent_label = f"{percent}%" if percent is not None else "--"
|
percent_label = f"{percent}%" if percent is not None else "--"
|
||||||
if width < 76:
|
if width < 76:
|
||||||
return [
|
frags = [
|
||||||
("class:status-bar", " ⚕ "),
|
("class:status-bar", " ⚕ "),
|
||||||
("class:status-bar-strong", snapshot["model_short"]),
|
("class:status-bar-strong", snapshot["model_short"]),
|
||||||
("class:status-bar-dim", " · "),
|
("class:status-bar-dim", " · "),
|
||||||
(self._status_bar_context_style(percent), percent_label),
|
(self._status_bar_context_style(percent), percent_label),
|
||||||
("class:status-bar-dim", " · "),
|
]
|
||||||
("class:status-bar-dim", cost_label),
|
if cost_label:
|
||||||
|
frags.extend([
|
||||||
|
("class:status-bar-dim", " · "),
|
||||||
|
("class:status-bar-dim", cost_label),
|
||||||
|
])
|
||||||
|
frags.extend([
|
||||||
("class:status-bar-dim", " · "),
|
("class:status-bar-dim", " · "),
|
||||||
("class:status-bar-dim", duration_label),
|
("class:status-bar-dim", duration_label),
|
||||||
("class:status-bar", " "),
|
("class:status-bar", " "),
|
||||||
]
|
])
|
||||||
|
return frags
|
||||||
|
|
||||||
if snapshot["context_length"]:
|
if snapshot["context_length"]:
|
||||||
ctx_total = _format_context_length(snapshot["context_length"])
|
ctx_total = _format_context_length(snapshot["context_length"])
|
||||||
|
|
@ -1334,7 +1361,7 @@ class HermesCLI:
|
||||||
context_label = "ctx --"
|
context_label = "ctx --"
|
||||||
|
|
||||||
bar_style = self._status_bar_context_style(percent)
|
bar_style = self._status_bar_context_style(percent)
|
||||||
return [
|
frags = [
|
||||||
("class:status-bar", " ⚕ "),
|
("class:status-bar", " ⚕ "),
|
||||||
("class:status-bar-strong", snapshot["model_short"]),
|
("class:status-bar-strong", snapshot["model_short"]),
|
||||||
("class:status-bar-dim", " │ "),
|
("class:status-bar-dim", " │ "),
|
||||||
|
|
@ -1343,12 +1370,18 @@ class HermesCLI:
|
||||||
(bar_style, self._build_context_bar(percent)),
|
(bar_style, self._build_context_bar(percent)),
|
||||||
("class:status-bar-dim", " "),
|
("class:status-bar-dim", " "),
|
||||||
(bar_style, percent_label),
|
(bar_style, percent_label),
|
||||||
("class:status-bar-dim", " │ "),
|
]
|
||||||
("class:status-bar-dim", cost_label),
|
if cost_label:
|
||||||
|
frags.extend([
|
||||||
|
("class:status-bar-dim", " │ "),
|
||||||
|
("class:status-bar-dim", cost_label),
|
||||||
|
])
|
||||||
|
frags.extend([
|
||||||
("class:status-bar-dim", " │ "),
|
("class:status-bar-dim", " │ "),
|
||||||
("class:status-bar-dim", duration_label),
|
("class:status-bar-dim", duration_label),
|
||||||
("class:status-bar", " "),
|
("class:status-bar", " "),
|
||||||
]
|
])
|
||||||
|
return frags
|
||||||
except Exception:
|
except Exception:
|
||||||
return [("class:status-bar", f" {self._build_status_bar_text()} ")]
|
return [("class:status-bar", f" {self._build_status_bar_text()} ")]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,7 @@ DEFAULT_CONFIG = {
|
||||||
"resume_display": "full",
|
"resume_display": "full",
|
||||||
"bell_on_complete": False,
|
"bell_on_complete": False,
|
||||||
"show_reasoning": False,
|
"show_reasoning": False,
|
||||||
|
"show_cost": False, # Show $ cost in the status bar (off by default)
|
||||||
"skin": "default",
|
"skin": "default",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,24 +65,39 @@ class TestCLIStatusBar:
|
||||||
assert "claude-sonnet-4-20250514" in text
|
assert "claude-sonnet-4-20250514" in text
|
||||||
assert "12.4K/200K" in text
|
assert "12.4K/200K" in text
|
||||||
assert "6%" in text
|
assert "6%" in text
|
||||||
assert "$0.06" in text
|
assert "$0.06" not in text # cost hidden by default
|
||||||
assert "15m" in text
|
assert "15m" in text
|
||||||
|
|
||||||
|
def test_build_status_bar_text_shows_cost_when_enabled(self):
|
||||||
|
cli_obj = _attach_agent(
|
||||||
|
_make_cli(),
|
||||||
|
prompt_tokens=10000,
|
||||||
|
completion_tokens=2400,
|
||||||
|
total_tokens=12400,
|
||||||
|
api_calls=7,
|
||||||
|
context_tokens=12400,
|
||||||
|
context_length=200_000,
|
||||||
|
)
|
||||||
|
cli_obj.show_cost = True
|
||||||
|
|
||||||
|
text = cli_obj._build_status_bar_text(width=120)
|
||||||
|
assert "$" in text # cost is shown when enabled
|
||||||
|
|
||||||
def test_build_status_bar_text_collapses_for_narrow_terminal(self):
|
def test_build_status_bar_text_collapses_for_narrow_terminal(self):
|
||||||
cli_obj = _attach_agent(
|
cli_obj = _attach_agent(
|
||||||
_make_cli(),
|
_make_cli(),
|
||||||
prompt_tokens=10_230,
|
prompt_tokens=10000,
|
||||||
completion_tokens=2_220,
|
completion_tokens=2400,
|
||||||
total_tokens=12_450,
|
total_tokens=12400,
|
||||||
api_calls=7,
|
api_calls=7,
|
||||||
context_tokens=12_450,
|
context_tokens=12400,
|
||||||
context_length=200_000,
|
context_length=200_000,
|
||||||
)
|
)
|
||||||
|
|
||||||
text = cli_obj._build_status_bar_text(width=60)
|
text = cli_obj._build_status_bar_text(width=60)
|
||||||
|
|
||||||
assert "⚕" in text
|
assert "⚕" in text
|
||||||
assert "$0.06" in text
|
assert "$0.06" not in text # cost hidden by default
|
||||||
assert "15m" in text
|
assert "15m" in text
|
||||||
assert "200K" not in text
|
assert "200K" not in text
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue