Files
hermes-agent/hermes_cli/cli_session_dock.py
teknium1 43455f5670 feat(cli): live dock shows the standing /goal and queued prompts
The classic CLI dock above the status bar painted subagents and background
processes only. A standing /goal (and whether it is active, parked or
paused) was visible only as a "⊙ goal N/M" status-bar segment, and prompts
waiting in /queue could not be seen at all while the agent worked.

The dock now paints the goal's status line on its top row and a Queue block
(count, first three previews, +N more) on its bottom rows, closest to the
input they will be sent from; F7's summary line carries "goal active|parked|
paused" and "N queued". Goal state comes only from the GoalManager the CLI
already holds for the current session, so the 1 Hz dock refresh never opens
state.db and never shows a previous session's goal after /new.

/queue now dispatches inline while the agent runs, honouring its
busy_policy="dispatch": typed mid-run it was queued as the raw command, so
"/queue X" only re-enqueued X after the turn and /queue list|rm|edit could
not inspect the queue until it had drained.
2026-09-23 10:05:40 -07:00

41 lines
1.6 KiB
Python

"""Goal + queued-prompt rows for the classic CLI live-work dock.
Sibling of ``cli_process_dock``: the dock paints an active ``/goal`` and the prompts waiting in
``/queue`` under the subagent and process blocks, so both are visible while the agent works
instead of only through ``/goal status`` and ``/queue list``.
"""
from __future__ import annotations
# Queued prompts drain one per turn; past this many the dock shows a ``+N more`` line.
QUEUE_ROWS = 3
def goal_line(cli) -> str:
"""``GoalManager.status_line()`` for an active or paused goal, else ``''``.
Reads only the manager the CLI already holds for its current session: the dock refreshes on
the spinner thread every second and must never be the thing that opens state.db.
"""
mgr = getattr(cli, "_goal_manager", None)
if mgr is None or mgr.session_id != getattr(cli, "session_id", None) or not mgr.has_goal():
return ""
return mgr.status_line()
def _prompt_text(item) -> str:
if isinstance(item, tuple): # (text, images) from an image-attached submit
item = item[0]
# TimelineNotification carries a compact title; voice/seeded sentinels wrap their text.
text = getattr(item, "display_text", None) or getattr(item, "text", None) or str(item)
return " ".join(str(text).split())
def queued_prompts(cli) -> list[str]:
"""One-line previews of the turns waiting in ``_pending_input``, oldest first."""
pending = getattr(cli, "_pending_input", None)
if pending is None:
return []
with pending.mutex:
items = list(pending.queue)
return [text for text in map(_prompt_text, items) if text]