feat(cli): add display.status_bar.fields config for customizing status bar

Allow users to control which fields appear in the interactive CLI status
bar via display.status_bar.fields in config.yaml.

Available fields: model, context_pct, context_detail, compressions,
bg_tasks, bg_processes, duration, prompt_elapsed, yolo, total_tokens.

When the list is empty (default), all fields are shown as before.
The field order is fixed (model always first); the config controls
visibility only. Narrow terminals (<76 cols) automatically drop
context_detail regardless of config.

total_tokens is opt-in only (not shown by default) to avoid width
overflow in the prompt_toolkit fragment renderer.

Closes #41909
This commit is contained in:
liuhao1024
2026-06-08 17:48:27 +08:00
committed by Teknium
parent b4403a942a
commit fb786d2f5b
4 changed files with 367 additions and 117 deletions
+194 -117
View File
@@ -7170,6 +7170,35 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
return f"⊙ goal {used}/{max_turns}"
return "⊙ goal"
def _get_status_bar_field_set(self) -> Optional[frozenset]:
"""Return the set of visible status-bar fields from config.
Reads ``display.status_bar.fields`` from the module-level
``CLI_CONFIG`` (no per-render YAML parse — the status bar repaints
every frame). Returns ``None`` when the user has not customized the
bar (use built-in defaults, i.e. show everything), or a
``frozenset`` of field names when the list is non-empty.
Available fields: model, context_detail, context_pct, compressions,
bg_tasks, bg_processes, bg_subagents, goal, duration,
prompt_elapsed, idle_since, focus, yolo, total_tokens.
``total_tokens`` is opt-in only (never shown by default).
The field order is fixed; the config controls visibility only.
"""
if hasattr(self, "_status_bar_field_set_cache"):
return self._status_bar_field_set_cache
result = None
try:
display = CLI_CONFIG.get("display") if isinstance(CLI_CONFIG, dict) else None
status_bar = (display or {}).get("status_bar") if isinstance(display, dict) else None
fields = status_bar.get("fields") if isinstance(status_bar, dict) else None
if isinstance(fields, list) and fields:
result = frozenset(str(f) for f in fields)
except Exception:
result = None
self._status_bar_field_set_cache = result
return result
def _build_status_bar_text(self, width: Optional[int] = None) -> str:
"""Return a compact one-line session status string for the TUI footer."""
try:
@@ -7186,75 +7215,109 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
yolo_active = self._is_session_yolo_active()
goal_segment = self._status_bar_goal_segment(snapshot)
field_set = self._get_status_bar_field_set()
def _ok(name: str) -> bool:
return field_set is None or name in field_set
if not _ok("goal"):
goal_segment = ""
if not _ok("focus"):
focus_label = ""
if width < 52:
text = f"{battery_prefix}⚕ {snapshot['model_short']} · {duration_label}"
segs = []
if _ok("model"):
segs.append(f"⚕ {snapshot['model_short']}")
if _ok("duration"):
segs.append(duration_label)
if goal_segment:
text += f" · {goal_segment}"
segs.append(goal_segment)
if focus_label:
text += f" · {focus_label}"
if yolo_active:
text += " · ⚠ YOLO"
segs.append(focus_label)
if yolo_active and _ok("yolo"):
segs.append("⚠ YOLO")
text = battery_prefix + " · ".join(segs) if segs else f"{battery_prefix}⚕ {snapshot['model_short']}"
return self._right_align_status_title(text, session_title, width)
if width < 76:
parts = [f"⚕ {snapshot['model_short']}", percent_label]
parts = []
if _ok("model"):
parts.append(f"⚕ {snapshot['model_short']}")
if _ok("context_pct"):
parts.append(percent_label)
if battery_label:
parts.insert(0, battery_label)
compressions = snapshot.get("compressions", 0)
if compressions:
if compressions and _ok("compressions"):
parts.append(f"🗜️ {compressions}")
bg_count = snapshot.get("active_background_tasks", 0)
if bg_count:
if bg_count and _ok("bg_tasks"):
parts.append(f"▶ {bg_count}")
bg_proc_count = snapshot.get("active_background_processes", 0)
if bg_proc_count:
if bg_proc_count and _ok("bg_processes"):
parts.append(f"⚙ {bg_proc_count}")
bg_subagent_count = snapshot.get("active_background_subagents", 0)
if bg_subagent_count:
if bg_subagent_count and _ok("bg_subagents"):
parts.append(f"⛓ {bg_subagent_count}")
if goal_segment:
parts.append(goal_segment)
parts.append(duration_label)
if _ok("duration"):
parts.append(duration_label)
if focus_label:
parts.append(focus_label)
if yolo_active:
if yolo_active and _ok("yolo"):
parts.append("⚠ YOLO")
if not parts:
parts = [f"⚕ {snapshot['model_short']}"]
return self._right_align_status_title(" · ".join(parts), session_title, width)
if snapshot["context_length"]:
ctx_total = _format_context_length(snapshot["context_length"])
ctx_used = format_token_count_compact(snapshot["context_tokens"])
context_label = f"{ctx_used}/{ctx_total}"
else:
context_label = "ctx --"
compressions = snapshot.get("compressions", 0)
parts = [f"⚕ {snapshot['model_short']}", context_label, percent_label]
parts = []
if _ok("model"):
parts.append(f"⚕ {snapshot['model_short']}")
if _ok("context_detail"):
if snapshot["context_length"]:
ctx_total = _format_context_length(snapshot["context_length"])
ctx_used = format_token_count_compact(snapshot["context_tokens"])
context_label = f"{ctx_used}/{ctx_total}"
else:
context_label = "ctx --"
parts.append(context_label)
if _ok("context_pct"):
parts.append(percent_label)
if battery_label:
parts.insert(0, battery_label)
if compressions:
compressions = snapshot.get("compressions", 0)
if compressions and _ok("compressions"):
parts.append(f"🗜️ {compressions}")
bg_count = snapshot.get("active_background_tasks", 0)
if bg_count:
if bg_count and _ok("bg_tasks"):
parts.append(f"▶ {bg_count}")
bg_proc_count = snapshot.get("active_background_processes", 0)
if bg_proc_count:
if bg_proc_count and _ok("bg_processes"):
parts.append(f"⚙ {bg_proc_count}")
bg_subagent_count = snapshot.get("active_background_subagents", 0)
if bg_subagent_count:
if bg_subagent_count and _ok("bg_subagents"):
parts.append(f"⛓ {bg_subagent_count}")
if goal_segment:
parts.append(goal_segment)
parts.append(duration_label)
if _ok("duration"):
parts.append(duration_label)
prompt_elapsed = snapshot.get("prompt_elapsed")
if prompt_elapsed:
if prompt_elapsed and _ok("prompt_elapsed"):
parts.append(prompt_elapsed)
idle_since = snapshot.get("idle_since")
if idle_since:
if idle_since and _ok("idle_since"):
parts.append(idle_since)
if focus_label:
parts.append(focus_label)
if yolo_active:
if yolo_active and _ok("yolo"):
parts.append("⚠ YOLO")
# Session token total (Σ) — opt-in only via an explicit fields
# list, so default bars never widen.
total_tokens = snapshot.get("session_total_tokens", 0)
if total_tokens and field_set is not None and "total_tokens" in field_set:
parts.append(f"Σ{format_token_count_compact(total_tokens)}")
if not parts:
parts = [f"⚕ {snapshot['model_short']}"]
return self._right_align_status_title(" │ ".join(parts), session_title, width)
except Exception:
return f"⚕ {self.model if getattr(self, 'model', None) else 'Hermes'}"
@@ -7277,23 +7340,39 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
battery_style = self._battery_status_style(snapshot.get("battery_category", "dim"))
focus_label = snapshot.get("focus_label") or ""
session_title = snapshot.get("session_title") or ""
field_set = self._get_status_bar_field_set()
def _ok(name: str) -> bool:
return field_set is None or name in field_set
if not _ok("goal"):
goal_segment = ""
if not _ok("focus"):
focus_label = ""
def _append(frag_list, sep, *pieces):
if frag_list:
frag_list.append(("class:status-bar-dim", sep))
frag_list.extend(pieces)
if width < 52:
frags = [
("class:status-bar", " ⚕ "),
("class:status-bar-strong", snapshot["model_short"]),
("class:status-bar-dim", " · "),
("class:status-bar-dim", duration_label),
]
frags = []
if _ok("model"):
frags.append(("class:status-bar", " ⚕ "))
frags.append(("class:status-bar-strong", snapshot["model_short"]))
if _ok("duration"):
_append(frags, " · ", ("class:status-bar-dim", duration_label))
if goal_segment:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", goal_segment))
_append(frags, " · ", ("class:status-bar-strong", goal_segment))
if focus_label:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", focus_label))
if yolo_active:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-yolo", "⚠ YOLO"))
_append(frags, " · ", ("class:status-bar-strong", focus_label))
if yolo_active and _ok("yolo"):
_append(frags, " · ", ("class:status-bar-yolo", "⚠ YOLO"))
if not frags:
frags = [
("class:status-bar", " ⚕ "),
("class:status-bar-strong", snapshot["model_short"]),
]
frags.append(("class:status-bar", " "))
else:
percent = snapshot["context_percent"]
@@ -7303,98 +7382,96 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
bg_count = snapshot.get("active_background_tasks", 0)
bg_proc_count = snapshot.get("active_background_processes", 0)
bg_subagent_count = snapshot.get("active_background_subagents", 0)
frags = [
("class:status-bar", " ⚕ "),
("class:status-bar-strong", snapshot["model_short"]),
("class:status-bar-dim", " · "),
(self._status_bar_context_style(percent), percent_label),
]
if compressions:
frags.append(("class:status-bar-dim", " · "))
frags.append((self._compression_count_style(compressions), f"🗜️ {compressions}"))
if bg_count:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", f"▶ {bg_count}"))
if bg_proc_count:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", f"⚙ {bg_proc_count}"))
if bg_subagent_count:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", f"⛓ {bg_subagent_count}"))
frags = []
if _ok("model"):
frags.append(("class:status-bar", " ⚕ "))
frags.append(("class:status-bar-strong", snapshot["model_short"]))
if _ok("context_pct"):
_append(frags, " · ", (self._status_bar_context_style(percent), percent_label))
if compressions and _ok("compressions"):
_append(frags, " · ", (self._compression_count_style(compressions), f"🗜️ {compressions}"))
if bg_count and _ok("bg_tasks"):
_append(frags, " · ", ("class:status-bar-strong", f"▶ {bg_count}"))
if bg_proc_count and _ok("bg_processes"):
_append(frags, " · ", ("class:status-bar-strong", f"⚙ {bg_proc_count}"))
if bg_subagent_count and _ok("bg_subagents"):
_append(frags, " · ", ("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),
])
_append(frags, " · ", ("class:status-bar-strong", goal_segment))
if _ok("duration"):
_append(frags, " · ", ("class:status-bar-dim", duration_label))
if focus_label:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-strong", focus_label))
if yolo_active:
frags.append(("class:status-bar-dim", " · "))
frags.append(("class:status-bar-yolo", "⚠ YOLO"))
_append(frags, " · ", ("class:status-bar-strong", focus_label))
if yolo_active and _ok("yolo"):
_append(frags, " · ", ("class:status-bar-yolo", "⚠ YOLO"))
if not frags:
frags = [
("class:status-bar", " ⚕ "),
("class:status-bar-strong", snapshot["model_short"]),
]
frags.append(("class:status-bar", " "))
else:
if snapshot["context_length"]:
ctx_total = _format_context_length(snapshot["context_length"])
ctx_used = format_token_count_compact(snapshot["context_tokens"])
context_label = f"{ctx_used}/{ctx_total}"
else:
context_label = "ctx --"
bar_style = self._status_bar_context_style(percent)
compressions = snapshot.get("compressions", 0)
bg_count = snapshot.get("active_background_tasks", 0)
bg_proc_count = snapshot.get("active_background_processes", 0)
bg_subagent_count = snapshot.get("active_background_subagents", 0)
frags = [
("class:status-bar", " ⚕ "),
("class:status-bar-strong", snapshot["model_short"]),
("class:status-bar-dim", " │ "),
("class:status-bar-dim", context_label),
("class:status-bar-dim", " │ "),
(bar_style, self._build_context_bar(percent)),
("class:status-bar-dim", " "),
(bar_style, percent_label),
]
if compressions:
frags.append(("class:status-bar-dim", " │ "))
frags.append((self._compression_count_style(compressions), f"🗜️ {compressions}"))
if bg_count:
frags.append(("class:status-bar-dim", " │ "))
frags.append(("class:status-bar-strong", f"▶ {bg_count}"))
if bg_proc_count:
frags.append(("class:status-bar-dim", " │ "))
frags.append(("class:status-bar-strong", f"⚙ {bg_proc_count}"))
if bg_subagent_count:
frags.append(("class:status-bar-dim", " │ "))
frags.append(("class:status-bar-strong", f"⛓ {bg_subagent_count}"))
frags = []
if _ok("model"):
frags.append(("class:status-bar", " ⚕ "))
frags.append(("class:status-bar-strong", snapshot["model_short"]))
if _ok("context_detail"):
if snapshot["context_length"]:
ctx_total = _format_context_length(snapshot["context_length"])
ctx_used = format_token_count_compact(snapshot["context_tokens"])
context_label = f"{ctx_used}/{ctx_total}"
else:
context_label = "ctx --"
_append(frags, " │ ", ("class:status-bar-dim", context_label))
if _ok("context_pct"):
_append(
frags,
" │ ",
(bar_style, self._build_context_bar(percent)),
("class:status-bar-dim", " "),
(bar_style, percent_label),
)
if compressions and _ok("compressions"):
_append(frags, " │ ", (self._compression_count_style(compressions), f"🗜️ {compressions}"))
if bg_count and _ok("bg_tasks"):
_append(frags, " │ ", ("class:status-bar-strong", f"▶ {bg_count}"))
if bg_proc_count and _ok("bg_processes"):
_append(frags, " │ ", ("class:status-bar-strong", f"⚙ {bg_proc_count}"))
if bg_subagent_count and _ok("bg_subagents"):
_append(frags, " │ ", ("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),
])
_append(frags, " │ ", ("class:status-bar-strong", goal_segment))
if _ok("duration"):
_append(frags, " │ ", ("class:status-bar-dim", duration_label))
# Position 7: per-prompt elapsed timer (live or frozen)
prompt_elapsed = snapshot.get("prompt_elapsed")
if prompt_elapsed:
frags.append(("class:status-bar-dim", " │ "))
frags.append(("class:status-bar-dim", prompt_elapsed))
if prompt_elapsed and _ok("prompt_elapsed"):
_append(frags, " │ ", ("class:status-bar-dim", prompt_elapsed))
# Position 8: idle time since the last final agent response
idle_since = snapshot.get("idle_since")
if idle_since:
frags.append(("class:status-bar-dim", " │ "))
frags.append(("class:status-bar-dim", idle_since))
if idle_since and _ok("idle_since"):
_append(frags, " │ ", ("class:status-bar-dim", idle_since))
# Persistent focus-view badge — so the reduced-output mode
# is never invisible (mirrors the YOLO badge convention).
if focus_label:
frags.append(("class:status-bar-dim", " │ "))
frags.append(("class:status-bar-strong", focus_label))
if yolo_active:
frags.append(("class:status-bar-dim", " │ "))
frags.append(("class:status-bar-yolo", "⚠ YOLO"))
_append(frags, " │ ", ("class:status-bar-strong", focus_label))
if yolo_active and _ok("yolo"):
_append(frags, " │ ", ("class:status-bar-yolo", "⚠ YOLO"))
# Session token total (Σ) — opt-in only via an explicit
# fields list, so default bars never widen.
total_tokens = snapshot.get("session_total_tokens", 0)
if total_tokens and field_set is not None and "total_tokens" in field_set:
_append(frags, " │ ", ("class:status-bar-dim", f"Σ{format_token_count_compact(total_tokens)}"))
if not frags:
frags = [
("class:status-bar", " ⚕ "),
("class:status-bar-strong", snapshot["model_short"]),
]
frags.append(("class:status-bar", " "))
# Stash indicator (📌 N) — appended after all width tiers so the