Files
hermes-agent/hermes_cli/cli_footer_split.py
teknium1 7f21c7dcc7 fix: keep cli_footer_split importable under the prompt_toolkit test stubs
tests/hermes_cli/test_prompt_stash_cli.py builds HermesCLI with a fixed
list of prompt_toolkit submodules replaced by MagicMocks; importing
prompt_toolkit.layout.screen for a type annotation fell outside that list
and raised ModuleNotFoundError at cli_tui_mixin import time.
2026-09-20 13:35:09 -07:00

40 lines
2.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Root container of the classic CLI footer: clips a late row instead of giving up.
prompt_toolkit renders a non-full-screen layout into exactly the height it measured a moment
earlier (``Renderer.render`` → ``HSplit.preferred_height``), then ``HSplit._divide_heights``
measures every child again while painting. Each footer row here has a callable height that the
agent thread flips mid-turn (``_agent_running`` → spacer, ``_spinner_text`` → spinner,
``_command_running`` → hint), so a row appearing between the two passes makes the summed minimum
exceed the budget by one and stock ``HSplit`` replaces the WHOLE footer with its
" Window too small... " placeholder (#57393) — on any terminal size, including a maximised 200×50
pane. Hermes's throttled ``_invalidate`` can then drop the healing repaint for the length of a
tool call, so the placeholder sits there looking like a geometry error.
"""
from __future__ import annotations
from prompt_toolkit.application import get_app
from prompt_toolkit.layout import HSplit
class FooterSplit(HSplit):
"""``HSplit`` whose overflow path keeps the composer on screen and repaints right away.
When the base class would paint the placeholder, allocate the children's minimum heights
bottom-up (composer, rules and status bar first) so only the row that appeared late — always
above them — loses its row for this one frame, and schedule an unthrottled redraw so the
next frame measures and paints against the same state.
"""
def _divide_heights(self, write_position) -> list[int] | None:
sizes = super()._divide_heights(write_position)
if sizes is not None:
return sizes
budget = write_position.height
clipped: list[int] = []
for child in reversed(self._all_children):
rows = min(child.preferred_height(write_position.width, budget).min, budget)
budget -= rows
clipped.append(rows)
get_app().invalidate()
return clipped[::-1]