feat(cli): /focus — reduced-output view with hidden-line recovery and status indicator

Display-only port of Claude Code /focus; composes with existing /verbose tool-progress modes.
This commit is contained in:
teknium1 2026-07-26 14:36:09 -07:00 committed by Teknium
parent e1ace0ac98
commit d6fa2709de
16 changed files with 1199 additions and 1 deletions

86
cli.py
View file

@ -4113,6 +4113,25 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
# YAML 1.1 parses bare `off` as boolean False — normalise to string.
_raw_tp = CLI_CONFIG["display"].get("tool_progress", "all")
self.tool_progress_mode = "off" if _raw_tp is False else str(_raw_tp)
# focus_view: display-only reduced-output mode (/focus). When on, the
# tool-progress mode is snapped to "off" so the EXISTING suppression
# path hides per-tool lines, and the pre-focus mode is stashed so
# /focus off restores it. Purely cosmetic — never changes what is sent
# to the model. See hermes_cli/focus_view.py.
self._focus_view_enabled = bool(CLI_CONFIG["display"].get("focus_view", False))
self._focus_saved_tool_progress = None
self._focus_hidden_lines = 0
self._focus_last_counted_tool = None
if self._focus_view_enabled:
from hermes_cli.focus_view import (
FOCUS_TOOL_PROGRESS_MODE,
normalize_tool_progress_mode,
)
self._focus_saved_tool_progress = normalize_tool_progress_mode(
self.tool_progress_mode
)
self.tool_progress_mode = FOCUS_TOOL_PROGRESS_MODE
# resume_display: "full" (show history) | "minimal" (one-liner only)
self.resume_display = CLI_CONFIG["display"].get("resume_display", "full")
# bell_on_complete: play terminal bell (\a) when agent finishes a response
@ -5091,8 +5110,20 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
"active_background_subagents": 0,
"battery_label": "",
"battery_category": "dim",
# Focus view badge (/focus). Persistent indicator so the reduced
# output mode is never invisible. Display-only.
"focus_label": "",
}
try:
from hermes_cli.focus_view import focus_statusbar_segment
snapshot["focus_label"] = focus_statusbar_segment(
bool(getattr(self, "_focus_view_enabled", False))
)
except Exception:
pass
# Battery read-out (first status-bar element when enabled). Reads are
# memoised for a few seconds inside agent.battery, so polling it on
# every status-bar repaint is cheap.
@ -5715,6 +5746,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
duration_label = snapshot["duration"]
battery_label = snapshot.get("battery_label") or ""
battery_prefix = f"{battery_label}" if battery_label else ""
focus_label = snapshot.get("focus_label") or ""
yolo_active = self._is_session_yolo_active()
goal_segment = self._status_bar_goal_segment(snapshot)
@ -5722,6 +5754,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
text = f"{battery_prefix}{snapshot['model_short']} · {duration_label}"
if goal_segment:
text += f" · {goal_segment}"
if focus_label:
text += f" · {focus_label}"
if yolo_active:
text += " · ⚠ YOLO"
return self._trim_status_bar_text(text, width)
@ -5744,6 +5778,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
if goal_segment:
parts.append(goal_segment)
parts.append(duration_label)
if focus_label:
parts.append(focus_label)
if yolo_active:
parts.append("⚠ YOLO")
return self._trim_status_bar_text(" · ".join(parts), width)
@ -5779,6 +5815,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
idle_since = snapshot.get("idle_since")
if idle_since:
parts.append(idle_since)
if focus_label:
parts.append(focus_label)
if yolo_active:
parts.append("⚠ YOLO")
return self._trim_status_bar_text("".join(parts), width)
@ -5801,6 +5839,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
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"))
focus_label = snapshot.get("focus_label") or ""
if width < 52:
frags = [
@ -5812,6 +5851,9 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
if goal_segment:
frags.append(("class:status-bar-dim", " · "))
frags.append(("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"))
@ -5849,6 +5891,9 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
("class:status-bar-dim", " · "),
("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"))
@ -5905,6 +5950,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
if idle_since:
frags.append(("class:status-bar-dim", ""))
frags.append(("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"))
@ -9644,6 +9694,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self._handle_timestamps_command(cmd_original)
elif canonical == "verbose":
self._toggle_verbose()
elif canonical == "focus":
self._handle_focus_command(cmd_original)
elif canonical == "footer":
self._handle_footer_command(cmd_original)
elif canonical == "yolo":
@ -10302,6 +10354,22 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
idx = 2 # default to "all"
self.tool_progress_mode = cycle[(idx + 1) % len(cycle)]
# /verbose is the explicit tool-progress control, so cycling it takes
# ownership of the mode back from focus view. Leaving _focus_view_enabled
# set would show a "focus" status-bar badge and hidden-line counts while
# tool lines were visibly printing. Display-only state change.
if getattr(self, "_focus_view_enabled", False):
self._focus_view_enabled = False
self._focus_saved_tool_progress = None
self._focus_hidden_lines = 0
self._focus_last_counted_tool = None
try:
from hermes_cli.focus_view import FOCUS_CONFIG_KEY
save_config_value(FOCUS_CONFIG_KEY, False)
except Exception:
pass
if self.agent:
self.agent.reasoning_callback = self._current_reasoning_callback()
# Keep the live agent's tool_progress_mode in sync so the
@ -11465,6 +11533,15 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self._turn_summary_record(
function_name, kwargs.get("result"), kwargs.get("is_error", False)
)
# Focus view: count the scrollback line we are NOT printing, so the
# post-turn recovery line can report how much was hidden. Counted
# against the pre-focus tool-progress mode, so a user who already
# had /verbose off is never told focus hid something it didn't.
if getattr(self, "_focus_view_enabled", False):
try:
self._note_focus_hidden_line(function_name or "")
except Exception:
pass
# Print stacked scrollback line for "new" / "all" / "verbose" modes.
# "verbose" was previously omitted here, so non-streaming model
# calls (MoA aggregator, copilot-acp) rendered each tool only into
@ -13346,6 +13423,15 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
pass
# Focus view: dim recovery line reporting what was hidden this turn
# (and how to reveal it). Printed after the response so the turn
# reads prompt → answer → "⋯ N tool lines hidden". Display-only;
# resets the counter for the next turn.
try:
self._emit_focus_recovery_line()
except Exception:
pass
# Play terminal bell when agent finishes (if enabled).
# Works over SSH — the bell propagates to the user's terminal.
if self.bell_on_complete: