mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-12 05:09:01 +08:00
Compare commits
4 Commits
docs/execu
...
vbrunet/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64d9755a79 | ||
|
|
52be0f2336 | ||
|
|
53cca7e50b | ||
|
|
d441527af9 |
100
cli.py
100
cli.py
@@ -77,6 +77,71 @@ from hermes_cli.banner import _format_context_length, format_banner_version_labe
|
||||
_COMMAND_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Terminal Tab Title
|
||||
# =============================================================================
|
||||
|
||||
_original_terminal_title: str | None = None # Saved on first _set_terminal_title call
|
||||
_last_written_title: str = "" # Cache last written title to skip redundant writes
|
||||
|
||||
|
||||
def _set_terminal_title(title: str) -> None:
|
||||
"""Set the terminal tab/window title using OSC 0 escape sequence.
|
||||
|
||||
Saves the original title on first call so it can be restored on exit.
|
||||
Only writes to real TTYs — redirected output would leak escape codes.
|
||||
Skips writes when the title hasn't changed (deduplication).
|
||||
|
||||
Writes to sys.__stdout__ (not sys.stdout) to bypass prompt_toolkit's
|
||||
patch_stdout StdoutProxy, which mangles raw escape sequences.
|
||||
"""
|
||||
global _original_terminal_title, _last_written_title
|
||||
# Use __stdout__ to bypass patch_stdout's StdoutProxy (#terminal-title)
|
||||
_out = sys.__stdout__
|
||||
if not _out.isatty():
|
||||
return
|
||||
# Skip if title hasn't changed
|
||||
if title == _last_written_title:
|
||||
return
|
||||
try:
|
||||
if _original_terminal_title is None:
|
||||
_original_terminal_title = os.environ.get("TERM_TITLE", "")
|
||||
|
||||
# OSC 0 ; <title> BEL — sets icon name and window title
|
||||
_out.write(f"\033]0;{title}\007")
|
||||
_out.flush()
|
||||
_last_written_title = title
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _restore_terminal_title() -> None:
|
||||
"""Restore the original terminal title (if we saved one)."""
|
||||
global _original_terminal_title, _last_written_title
|
||||
_out = sys.__stdout__
|
||||
if not _out.isatty():
|
||||
return
|
||||
try:
|
||||
if _original_terminal_title is not None:
|
||||
_out.write(f"\033]0;{_original_terminal_title}\007")
|
||||
_out.flush()
|
||||
_last_written_title = "" # Invalidate cache so next _set_terminal_title writes
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _terminal_title_cwd() -> str:
|
||||
"""Return a short cwd label for terminal title use."""
|
||||
try:
|
||||
cwd = os.getcwd()
|
||||
except (FileNotFoundError, PermissionError, OSError):
|
||||
return "?"
|
||||
home = os.path.expanduser("~")
|
||||
if cwd.startswith(home):
|
||||
cwd = "~" + cwd[len(home):]
|
||||
return cwd
|
||||
|
||||
|
||||
# Load .env from ~/.hermes/.env first, then project root as dev fallback.
|
||||
# User-managed env files should override stale shell exports on restart.
|
||||
from hermes_constants import get_hermes_home, display_hermes_home
|
||||
@@ -679,6 +744,10 @@ def _run_cleanup():
|
||||
if _cleanup_done:
|
||||
return
|
||||
_cleanup_done = True
|
||||
try:
|
||||
_restore_terminal_title()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_cleanup_all_terminals()
|
||||
except Exception:
|
||||
@@ -1897,6 +1966,9 @@ class HermesCLI:
|
||||
else:
|
||||
self.busy_input_mode = "interrupt"
|
||||
|
||||
# terminal_title: update terminal tab/window title with agent state
|
||||
self._terminal_title_enabled = CLI_CONFIG["display"].get("terminal_title", True)
|
||||
|
||||
self.verbose = verbose if verbose is not None else (self.tool_progress_mode == "verbose")
|
||||
|
||||
# streaming: stream tokens to the terminal as they arrive (display.streaming in config.yaml)
|
||||
@@ -2161,6 +2233,11 @@ class HermesCLI:
|
||||
self._background_tasks: Dict[str, threading.Thread] = {}
|
||||
self._background_task_counter = 0
|
||||
|
||||
def _set_title(self, title: str) -> None:
|
||||
"""Update terminal tab title if enabled."""
|
||||
if self._terminal_title_enabled:
|
||||
_set_terminal_title(f"Hermes: {title}")
|
||||
|
||||
def _invalidate(self, min_interval: float = 0.25) -> None:
|
||||
"""Throttled UI repaint — prevents terminal blinking on slow/SSH connections."""
|
||||
now = time.monotonic()
|
||||
@@ -2653,11 +2730,21 @@ class HermesCLI:
|
||||
return changed
|
||||
|
||||
def _on_thinking(self, text: str) -> None:
|
||||
"""Called by agent when thinking starts/stops. Updates TUI spinner."""
|
||||
"""Called by agent when thinking starts/stops. Updates TUI spinner + terminal title."""
|
||||
if not text:
|
||||
self._flush_reasoning_preview(force=True)
|
||||
self._spinner_text = text or ""
|
||||
self._spinner_text = ""
|
||||
self._tool_start_time = 0.0
|
||||
if getattr(self, "_agent_running", False):
|
||||
self._set_title("Thinking...")
|
||||
self._invalidate()
|
||||
return
|
||||
self._spinner_text = text
|
||||
self._tool_start_time = 0.0 # clear tool timer when switching to thinking
|
||||
# Update terminal title with the thinking verb (strip kawaii face prefix)
|
||||
# text is like "(◕‿◕) brainstorming..." — extract just the verb
|
||||
verb = text.split(" ", 1)[-1].strip() # drop face token
|
||||
self._set_title(verb.capitalize() if verb else "Thinking...")
|
||||
self._invalidate()
|
||||
|
||||
# ── Streaming display ────────────────────────────────────────────────
|
||||
@@ -7688,6 +7775,9 @@ class HermesCLI:
|
||||
"""
|
||||
if event_type == "tool.completed":
|
||||
self._tool_start_time = 0.0
|
||||
# Title: agent is still running, will think next
|
||||
if getattr(self, "_agent_running", False):
|
||||
self._set_title("Thinking...")
|
||||
# Print stacked scrollback line for "all" / "new" modes
|
||||
if function_name and self.tool_progress_mode in ("all", "new"):
|
||||
duration = kwargs.get("duration", 0.0)
|
||||
@@ -7749,6 +7839,7 @@ class HermesCLI:
|
||||
label = label[:_pl - 3] + "..."
|
||||
self._spinner_text = f"{emoji} {label}"
|
||||
self._tool_start_time = time.monotonic()
|
||||
self._set_title(f"Running {function_name}")
|
||||
# Store args for stacked scrollback line on completion
|
||||
self._pending_tool_info.setdefault(function_name, []).append(
|
||||
function_args if function_args is not None else {}
|
||||
@@ -9531,6 +9622,9 @@ class HermesCLI:
|
||||
self._startup_skills_line_shown = True
|
||||
self._console_print()
|
||||
|
||||
# Set initial terminal tab title
|
||||
self._set_title(_terminal_title_cwd())
|
||||
|
||||
# State for async operation
|
||||
self._agent_running = False
|
||||
self._pending_input = queue.Queue() # For normal input (commands + new queries)
|
||||
@@ -11142,12 +11236,14 @@ class HermesCLI:
|
||||
|
||||
# Regular chat - run agent
|
||||
self._agent_running = True
|
||||
self._set_title("Thinking...")
|
||||
app.invalidate() # Refresh status line
|
||||
|
||||
try:
|
||||
self.chat(user_input, images=submit_images or None)
|
||||
finally:
|
||||
self._agent_running = False
|
||||
self._set_title(_terminal_title_cwd())
|
||||
self._spinner_text = ""
|
||||
self._tool_start_time = 0.0
|
||||
self._pending_tool_info.clear()
|
||||
|
||||
@@ -727,6 +727,7 @@ DEFAULT_CONFIG = {
|
||||
"final_response_markdown": "strip", # render | strip | raw
|
||||
"inline_diffs": True, # Show inline diff previews for write actions (write_file, patch, skill_manage)
|
||||
"show_cost": False, # Show $ cost in the status bar (off by default)
|
||||
"terminal_title": True, # Update terminal tab/window title with agent state (thinking, tool, idle+cwd)
|
||||
"skin": "default",
|
||||
# TUI busy indicator style: kaomoji (default), emoji, unicode (braille
|
||||
# spinner), or ascii. Live-swappable via `/indicator <style>`.
|
||||
|
||||
@@ -351,6 +351,7 @@ AUTHOR_MAP = {
|
||||
"aptx4561@gmail.com": "cokemine",
|
||||
"arilotter@gmail.com": "ethernet8023",
|
||||
"ben@nousresearch.com": "benbarclay",
|
||||
"vincent.brunet.us@gmail.com": "crypto-vincent",
|
||||
"birdiegyal@gmail.com": "yyovil",
|
||||
"boschi1997@gmail.com": "nicoloboschi",
|
||||
"chef.ya@gmail.com": "cherifya",
|
||||
|
||||
Reference in New Issue
Block a user