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
+13
View File
@@ -1602,6 +1602,19 @@ DEFAULT_CONFIG = {
"enabled": False,
"fields": ["model", "context_pct", "cwd"], # Order shown; drop any to hide
},
# CLI/TUI interactive status bar field customization (mirrors the
# runtime_footer.fields pattern above). When the list is non-empty,
# only the listed fields appear; the built-in order is preserved
# (the config controls visibility, not ordering). Empty = show the
# default set. Available: model, context_detail, context_pct,
# compressions, bg_tasks, bg_processes, bg_subagents, goal,
# duration, prompt_elapsed, idle_since, focus, yolo, total_tokens.
# total_tokens (session Σ) is opt-in only — it never shows unless
# listed here. Narrow terminals still drop wide-mode-only fields
# (context_detail, prompt_elapsed, idle_since) regardless of config.
"status_bar": {
"fields": [], # empty = built-in defaults (all fields)
},
"copy_shortcut": "auto", # "auto" (platform default) | "ctrl_c" | "ctrl_shift_c" | "disabled"
# Petdex animated mascot (https://github.com/crafter-station/petdex).
# A purely cosmetic sprite that reacts to agent activity across the
+138
View File
@@ -370,3 +370,141 @@ class TestIdleSinceLastTurn:
assert snapshot["idle_since"].startswith("✓ ")
class TestStatusBarFieldConfig:
"""Tests for display.status_bar.fields config customization (#41909)."""
def _cli_with_fields(self, fields, width=120):
cli_obj = _attach_agent(
_make_cli(),
prompt_tokens=10_230,
completion_tokens=2_220,
total_tokens=12_450,
api_calls=7,
context_tokens=12_450,
context_length=200_000,
compressions=7,
)
with patch.object(cli_mod, "CLI_CONFIG", {"display": {"status_bar": {"fields": fields}}}):
text = cli_obj._build_status_bar_text(width=width)
return text
def test_default_fields_show_all(self):
"""With no config, all default fields appear."""
cli_obj = _attach_agent(
_make_cli(),
prompt_tokens=10_230,
completion_tokens=2_220,
total_tokens=12_450,
api_calls=7,
context_tokens=12_450,
context_length=200_000,
compressions=7,
)
with patch.object(cli_mod, "CLI_CONFIG", {}):
text = cli_obj._build_status_bar_text(width=120)
assert "claude-sonnet-4-20250514" in text
assert "12.4K/200K" in text
assert "🗜️" in text
assert "15m" in text
def test_only_model_and_duration(self):
text = self._cli_with_fields(["model", "duration"])
assert "claude-sonnet-4-20250514" in text
assert "15m" in text
assert "12.4K/200K" not in text
assert "🗜️" not in text
assert "%" not in text
def test_only_model(self):
text = self._cli_with_fields(["model"])
assert "claude-sonnet-4-20250514" in text
assert "15m" not in text
assert "12.4K/200K" not in text
def test_context_pct_only(self):
text = self._cli_with_fields(["context_pct"])
assert "%" in text
assert "claude-sonnet-4-20250514" not in text
def test_compressions_only(self):
text = self._cli_with_fields(["compressions"])
assert "🗜️ 7" in text
assert "claude-sonnet-4-20250514" not in text
def test_total_tokens_when_explicitly_requested(self):
text = self._cli_with_fields(["model", "total_tokens"])
assert "Σ12.4K" in text
assert "claude-sonnet-4-20250514" in text
def test_total_tokens_hidden_by_default(self):
cli_obj = _attach_agent(
_make_cli(),
prompt_tokens=10_230,
completion_tokens=2_220,
total_tokens=12_450,
api_calls=7,
context_tokens=12_450,
context_length=200_000,
compressions=7,
)
with patch.object(cli_mod, "CLI_CONFIG", {}):
text = cli_obj._build_status_bar_text(width=120)
assert "Σ" not in text
def test_narrow_terminal_drops_context_detail(self):
"""Narrow terminal (<76) ignores context_detail even if configured."""
text = self._cli_with_fields(["model", "context_detail", "duration"], width=60)
assert "claude-sonnet-4-20250514" in text
assert "15m" in text
assert "12.4K/200K" not in text
def test_field_config_never_empties_the_bar(self):
"""A fields list matching nothing still anchors on the model name."""
text = self._cli_with_fields(["nonexistent_field"])
assert "claude-sonnet-4-20250514" in text
def test_fragments_respect_field_config(self):
cli_obj = _attach_agent(
_make_cli(),
prompt_tokens=10_230,
completion_tokens=2_220,
total_tokens=12_450,
api_calls=7,
context_tokens=12_450,
context_length=200_000,
compressions=7,
)
cli_obj._status_bar_visible = True
with patch.object(cli_mod, "CLI_CONFIG", {"display": {"status_bar": {"fields": ["model", "duration"]}}}), \
patch.object(cli_obj, "_get_tui_terminal_width", return_value=120):
frags = cli_obj._get_status_bar_fragments()
frag_texts = [text for _, text in frags]
assert any("claude-sonnet-4-20250514" in t for t in frag_texts)
assert any("15m" in t for t in frag_texts)
assert not any("🗜️" in t for t in frag_texts)
assert not any("12.4K" in t for t in frag_texts)
def test_field_order_is_fixed(self):
"""Config controls visibility, not ordering — model stays first."""
text = self._cli_with_fields(["duration", "model", "compressions"])
model_pos = text.find("claude-sonnet-4-20250514")
comp_pos = text.find("🗜️")
dur_pos = text.find("15m")
assert 0 <= model_pos < comp_pos < dur_pos
def test_empty_fields_list_uses_defaults(self):
text = self._cli_with_fields([])
assert "claude-sonnet-4-20250514" in text
assert "12.4K/200K" in text
assert "🗜️" in text
def test_field_set_is_cached_per_instance(self):
cli_obj = _make_cli()
with patch.object(cli_mod, "CLI_CONFIG", {"display": {"status_bar": {"fields": ["model"]}}}):
first = cli_obj._get_status_bar_field_set()
# Cache holds even if config object changes afterwards (per-session semantics).
with patch.object(cli_mod, "CLI_CONFIG", {"display": {"status_bar": {"fields": ["duration"]}}}):
second = cli_obj._get_status_bar_field_set()
assert first == second == frozenset({"model"})
+22
View File
@@ -1868,6 +1868,8 @@ display:
runtime_footer: # Gateway: append a runtime-context footer to final replies
enabled: false
fields: ["model", "context_pct", "cwd"]
status_bar: # CLI/TUI: choose which status-bar fields are visible
fields: [] # empty = show the default set; see below
file_mutation_verifier: true # Append an advisory footer when write_file/patch calls failed this turn
credits_notices: true # Nous credits status-bar notices (usage bands, grant-spent, depleted). false = silence them; /usage still works
cli_rebuild_scrollback_on_redraw: false # Classic CLI: also wipe terminal scrollback (CSI 3J) on /redraw / Ctrl+L / width-change resize recovery. Enable when a terminal/tmux stack stamps stale prompt chrome into scrollback on maximize/restore.
@@ -1968,6 +1970,26 @@ Tool progress requires a gateway adapter that can display progress updates safel
Focus view is **display-only**. It never edits conversation history, the system prompt, tool schemas, or any request payload — hidden detail is suppressed on screen, never discarded, and prompt caching is completely unaffected.
### Status-bar field selection (CLI/TUI)
The interactive status bar at the bottom of the CLI/TUI shows the model, context usage, compression count, background-activity counters, timers, and mode badges. `display.status_bar.fields` chooses which of those are visible — useful for a minimal bar (just model + duration) or for surfacing the opt-in session token total:
```yaml
display:
status_bar:
fields: ["model", "duration", "total_tokens"] # visibility only; built-in order is preserved
```
Supported fields: `model`, `context_detail` (used/total tokens), `context_pct` (percent + meter), `compressions`, `bg_tasks`, `bg_processes`, `bg_subagents`, `goal`, `duration`, `prompt_elapsed`, `idle_since`, `focus`, `yolo`, 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`.
- The config controls **visibility, not order**; fields render in their built-in positions.
- Narrow terminals still drop wide-mode-only fields (`context_detail`, `prompt_elapsed`, `idle_since`) regardless of config.
- The battery indicator, session title, and prompt-stash indicator have their own toggles (`/battery`, `/title`) and are not governed by this list.
- Display-only: no effect on prompt caching or request payloads. Changes take effect on the next session start.
### Runtime-metadata footer (gateway only)
When `display.runtime_footer.enabled: true`, Hermes appends a small runtime-context footer to the **final** message of each gateway turn. The current footer can show the model, context-window percentage, and current working directory. Off by default; opt in per-gateway if your team wants every reply to include this provenance.