mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 15:31:38 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e5cd76a29 |
25
cli.py
25
cli.py
@@ -1329,7 +1329,12 @@ class HermesCLI:
|
|||||||
def _build_status_bar_text(self, width: Optional[int] = None) -> str:
|
def _build_status_bar_text(self, width: Optional[int] = None) -> str:
|
||||||
try:
|
try:
|
||||||
snapshot = self._get_status_bar_snapshot()
|
snapshot = self._get_status_bar_snapshot()
|
||||||
width = width or shutil.get_terminal_size((80, 24)).columns
|
if width is None:
|
||||||
|
try:
|
||||||
|
from prompt_toolkit.application import get_app
|
||||||
|
width = get_app().output.get_size().columns
|
||||||
|
except Exception:
|
||||||
|
width = 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 "--"
|
||||||
duration_label = snapshot["duration"]
|
duration_label = snapshot["duration"]
|
||||||
@@ -1359,6 +1364,15 @@ class HermesCLI:
|
|||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
snapshot = self._get_status_bar_snapshot()
|
snapshot = self._get_status_bar_snapshot()
|
||||||
|
# Use prompt_toolkit's own terminal width when running inside the
|
||||||
|
# TUI — shutil.get_terminal_size() can return stale or fallback
|
||||||
|
# values (especially on SSH) that differ from what prompt_toolkit
|
||||||
|
# actually renders, causing the fragments to overflow to a second
|
||||||
|
# line and produce duplicated status bar rows over long sessions.
|
||||||
|
try:
|
||||||
|
from prompt_toolkit.application import get_app
|
||||||
|
width = get_app().output.get_size().columns
|
||||||
|
except Exception:
|
||||||
width = shutil.get_terminal_size((80, 24)).columns
|
width = shutil.get_terminal_size((80, 24)).columns
|
||||||
duration_label = snapshot["duration"]
|
duration_label = snapshot["duration"]
|
||||||
|
|
||||||
@@ -6894,6 +6908,15 @@ class HermesCLI:
|
|||||||
Window(
|
Window(
|
||||||
content=FormattedTextControl(lambda: cli_ref._get_status_bar_fragments()),
|
content=FormattedTextControl(lambda: cli_ref._get_status_bar_fragments()),
|
||||||
height=1,
|
height=1,
|
||||||
|
# Prevent fragments that overflow the terminal width from
|
||||||
|
# wrapping onto a second line, which causes the status bar to
|
||||||
|
# appear duplicated (one full + one partial row) during long
|
||||||
|
# sessions, especially on SSH where shutil.get_terminal_size
|
||||||
|
# may return stale values. _get_status_bar_fragments now reads
|
||||||
|
# width from prompt_toolkit's own output object, so fragments
|
||||||
|
# will always fit; wrap_lines=False is the belt-and-suspenders
|
||||||
|
# guard against any future width mismatch.
|
||||||
|
wrap_lines=False,
|
||||||
),
|
),
|
||||||
filter=Condition(lambda: cli_ref._status_bar_visible),
|
filter=Condition(lambda: cli_ref._status_bar_visible),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -182,3 +182,94 @@ class TestCLIUsageReport:
|
|||||||
assert "Total cost:" in output
|
assert "Total cost:" in output
|
||||||
assert "n/a" in output
|
assert "n/a" in output
|
||||||
assert "Pricing unknown for glm-5" in output
|
assert "Pricing unknown for glm-5" in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatusBarWidthSource:
|
||||||
|
"""Ensure status bar fragments don't overflow the terminal width."""
|
||||||
|
|
||||||
|
def _make_wide_cli(self):
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
cli_obj = _attach_agent(
|
||||||
|
_make_cli(),
|
||||||
|
prompt_tokens=100_000,
|
||||||
|
completion_tokens=5_000,
|
||||||
|
total_tokens=105_000,
|
||||||
|
api_calls=20,
|
||||||
|
context_tokens=100_000,
|
||||||
|
context_length=200_000,
|
||||||
|
)
|
||||||
|
cli_obj._status_bar_visible = True
|
||||||
|
return cli_obj
|
||||||
|
|
||||||
|
def test_fragments_fit_within_announced_width(self):
|
||||||
|
"""Total fragment text length must not exceed the width used to build them."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
cli_obj = self._make_wide_cli()
|
||||||
|
|
||||||
|
for width in (40, 52, 76, 80, 120, 200):
|
||||||
|
mock_app = MagicMock()
|
||||||
|
mock_app.output.get_size.return_value = MagicMock(columns=width)
|
||||||
|
|
||||||
|
with patch("prompt_toolkit.application.get_app", return_value=mock_app):
|
||||||
|
frags = cli_obj._get_status_bar_fragments()
|
||||||
|
|
||||||
|
total_text = "".join(text for _, text in frags)
|
||||||
|
assert len(total_text) <= width + 4, ( # +4 for minor padding chars
|
||||||
|
f"At width={width}, fragment total {len(total_text)} chars overflows "
|
||||||
|
f"({total_text!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_fragments_use_pt_width_over_shutil(self):
|
||||||
|
"""When prompt_toolkit reports a width, shutil.get_terminal_size must not be used."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
cli_obj = self._make_wide_cli()
|
||||||
|
|
||||||
|
mock_app = MagicMock()
|
||||||
|
mock_app.output.get_size.return_value = MagicMock(columns=120)
|
||||||
|
|
||||||
|
with patch("prompt_toolkit.application.get_app", return_value=mock_app) as mock_get_app, \
|
||||||
|
patch("shutil.get_terminal_size") as mock_shutil:
|
||||||
|
cli_obj._get_status_bar_fragments()
|
||||||
|
|
||||||
|
mock_shutil.assert_not_called()
|
||||||
|
|
||||||
|
def test_fragments_fall_back_to_shutil_when_no_app(self):
|
||||||
|
"""Outside a TUI context (no running app), shutil must be used as fallback."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
cli_obj = self._make_wide_cli()
|
||||||
|
|
||||||
|
with patch("prompt_toolkit.application.get_app", side_effect=Exception("no app")), \
|
||||||
|
patch("shutil.get_terminal_size", return_value=MagicMock(columns=100)) as mock_shutil:
|
||||||
|
frags = cli_obj._get_status_bar_fragments()
|
||||||
|
|
||||||
|
mock_shutil.assert_called()
|
||||||
|
assert len(frags) > 0
|
||||||
|
|
||||||
|
def test_build_status_bar_text_uses_pt_width(self):
|
||||||
|
"""_build_status_bar_text() must also prefer prompt_toolkit width."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
cli_obj = self._make_wide_cli()
|
||||||
|
|
||||||
|
mock_app = MagicMock()
|
||||||
|
mock_app.output.get_size.return_value = MagicMock(columns=80)
|
||||||
|
|
||||||
|
with patch("prompt_toolkit.application.get_app", return_value=mock_app), \
|
||||||
|
patch("shutil.get_terminal_size") as mock_shutil:
|
||||||
|
text = cli_obj._build_status_bar_text() # no explicit width
|
||||||
|
|
||||||
|
mock_shutil.assert_not_called()
|
||||||
|
assert isinstance(text, str)
|
||||||
|
assert len(text) > 0
|
||||||
|
|
||||||
|
def test_explicit_width_skips_pt_lookup(self):
|
||||||
|
"""An explicit width= argument must bypass both PT and shutil lookups."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
cli_obj = self._make_wide_cli()
|
||||||
|
|
||||||
|
with patch("prompt_toolkit.application.get_app") as mock_get_app, \
|
||||||
|
patch("shutil.get_terminal_size") as mock_shutil:
|
||||||
|
text = cli_obj._build_status_bar_text(width=100)
|
||||||
|
|
||||||
|
mock_get_app.assert_not_called()
|
||||||
|
mock_shutil.assert_not_called()
|
||||||
|
assert len(text) > 0
|
||||||
|
|||||||
Reference in New Issue
Block a user