mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-12 08:51:53 +00:00
feat(cli): persist resolved approval/clarify prompts in scrollback (#44702)
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix Lockfile Fix / auto-fix-main (push) Waiting to run
Nix Lockfile Fix / fix (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
OSV-Scanner / Scan lockfiles (push) Waiting to run
Build Skills Index / build-index (push) Waiting to run
Build Skills Index / trigger-deploy (push) Blocked by required conditions
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Typecheck / typecheck (apps/bootstrap-installer) (push) Waiting to run
Typecheck / typecheck (apps/desktop) (push) Waiting to run
Typecheck / typecheck (apps/shared) (push) Waiting to run
Typecheck / typecheck (ui-tui) (push) Waiting to run
Typecheck / typecheck (web) (push) Waiting to run
uv.lock check / uv lock --check (push) Waiting to run
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix Lockfile Fix / auto-fix-main (push) Waiting to run
Nix Lockfile Fix / fix (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
OSV-Scanner / Scan lockfiles (push) Waiting to run
Build Skills Index / build-index (push) Waiting to run
Build Skills Index / trigger-deploy (push) Blocked by required conditions
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Typecheck / typecheck (apps/bootstrap-installer) (push) Waiting to run
Typecheck / typecheck (apps/desktop) (push) Waiting to run
Typecheck / typecheck (apps/shared) (push) Waiting to run
Typecheck / typecheck (ui-tui) (push) Waiting to run
Typecheck / typecheck (web) (push) Waiting to run
uv.lock check / uv lock --check (push) Waiting to run
Modal prompt panels (dangerous-command approval, clarify questions) live in the prompt_toolkit layout and vanish on the next repaint, leaving no trace of the question or the decision in chat history. Emit a dim one-line summary after each prompt resolves: ⚠ Approval: <command> → allowed for session ? Clarify: <question> → <answer> Gated on display.persist_prompts (default true). Detail and outcome are whitespace-collapsed and capped at 120 chars.
This commit is contained in:
parent
8e5b7592f8
commit
4474873d2c
3 changed files with 114 additions and 0 deletions
33
cli.py
33
cli.py
|
|
@ -456,6 +456,9 @@ def load_cli_config() -> Dict[str, Any]:
|
|||
"busy_input_mode": "interrupt",
|
||||
"persistent_output": True,
|
||||
"persistent_output_max_lines": 200,
|
||||
# Print a one-line summary of resolved modal prompts (approval /
|
||||
# clarify) into scrollback so the decision survives the repaint.
|
||||
"persist_prompts": True,
|
||||
|
||||
"skin": "default",
|
||||
},
|
||||
|
|
@ -9361,6 +9364,25 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
for line in reqs["details"].split("\n"):
|
||||
_cprint(f" {line}")
|
||||
|
||||
def _persist_prompt_summary(self, icon: str, label: str, detail: str, outcome: str) -> None:
|
||||
"""Print a one-line scrollback summary of a resolved modal prompt.
|
||||
|
||||
Modal panels (approval / clarify) live in the prompt_toolkit layout and
|
||||
vanish on the next repaint, so the question and the decision leave no
|
||||
trace in the terminal scrollback. When display.persist_prompts is on
|
||||
(default), emit a dim single line after the prompt resolves so the
|
||||
decision survives in chat history.
|
||||
"""
|
||||
if not CLI_CONFIG.get("display", {}).get("persist_prompts", True):
|
||||
return
|
||||
detail = " ".join(detail.split())
|
||||
if len(detail) > 120:
|
||||
detail = detail[:119] + "…"
|
||||
outcome = " ".join(outcome.split())
|
||||
if len(outcome) > 120:
|
||||
outcome = outcome[:119] + "…"
|
||||
_cprint(f"\n{_DIM}{icon} {label}: {detail} → {outcome}{_RST}")
|
||||
|
||||
def _clarify_callback(self, question, choices):
|
||||
"""
|
||||
Platform callback for the clarify tool. Called from the agent thread.
|
||||
|
|
@ -9400,6 +9422,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
try:
|
||||
result = response_queue.get(timeout=1)
|
||||
self._clarify_deadline = 0
|
||||
self._persist_prompt_summary("?", "Clarify", question, str(result))
|
||||
return result
|
||||
except queue.Empty:
|
||||
remaining = self._clarify_deadline - _time.monotonic()
|
||||
|
|
@ -9513,6 +9536,16 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
self._approval_state = None
|
||||
self._approval_deadline = 0
|
||||
self._paint_now()
|
||||
_outcome_labels = {
|
||||
"once": "allowed once",
|
||||
"session": "allowed for session",
|
||||
"always": "added to allowlist",
|
||||
"deny": "denied",
|
||||
}
|
||||
self._persist_prompt_summary(
|
||||
"⚠", "Approval", command,
|
||||
_outcome_labels.get(result, str(result)),
|
||||
)
|
||||
return result
|
||||
except queue.Empty:
|
||||
remaining = self._approval_deadline - _time.monotonic()
|
||||
|
|
|
|||
|
|
@ -1429,6 +1429,10 @@ DEFAULT_CONFIG = {
|
|||
# behaves badly with replayed scrollback.
|
||||
"persistent_output": True,
|
||||
"persistent_output_max_lines": 200,
|
||||
# Print a one-line summary of resolved modal prompts (approval /
|
||||
# clarify) into scrollback so the question and decision survive the
|
||||
# panel repaint. Set false to keep scrollback untouched.
|
||||
"persist_prompts": True,
|
||||
"inline_diffs": True, # Show inline diff previews for write actions (write_file, patch, skill_manage)
|
||||
# File-mutation verifier footer. When true (default), the agent
|
||||
# appends a one-line advisory to its final response whenever a
|
||||
|
|
|
|||
|
|
@ -565,3 +565,80 @@ class TestApprovalCallbackThreadLocalWiring:
|
|||
# would hold a stale reference to a disposed CLI instance.
|
||||
assert seen["approval_after"] is None
|
||||
assert seen["sudo_after"] is None
|
||||
|
||||
|
||||
class TestPersistPromptSummary:
|
||||
"""display.persist_prompts — one-line scrollback record of resolved modals."""
|
||||
|
||||
def _resolve_approval(self, cli, answer, command="rm -rf /tmp/scratch"):
|
||||
result = {}
|
||||
|
||||
def _run():
|
||||
result["value"] = cli._approval_callback(command, "danger")
|
||||
|
||||
t = threading.Thread(target=_run, daemon=True)
|
||||
t.start()
|
||||
deadline = time.time() + 2
|
||||
while cli._approval_state is None and time.time() < deadline:
|
||||
time.sleep(0.01)
|
||||
cli._approval_state["response_queue"].put(answer)
|
||||
t.join(timeout=2)
|
||||
return result["value"]
|
||||
|
||||
def test_approval_resolution_prints_summary_line(self):
|
||||
cli = _make_cli_stub()
|
||||
printed = []
|
||||
with patch.object(cli_module, "_cprint", printed.append):
|
||||
verdict = self._resolve_approval(cli, "session")
|
||||
assert verdict == "session"
|
||||
summary = "\n".join(printed)
|
||||
assert "Approval" in summary
|
||||
assert "rm -rf /tmp/scratch" in summary
|
||||
assert "allowed for session" in summary
|
||||
|
||||
def test_approval_summary_truncates_long_command(self):
|
||||
cli = _make_cli_stub()
|
||||
printed = []
|
||||
long_cmd = "sudo " + ("x" * 300)
|
||||
with patch.object(cli_module, "_cprint", printed.append):
|
||||
self._resolve_approval(cli, "deny", command=long_cmd)
|
||||
summary = "\n".join(printed)
|
||||
assert "denied" in summary
|
||||
assert "…" in summary
|
||||
# The raw 300-char tail must not be dumped wholesale.
|
||||
assert "x" * 200 not in summary
|
||||
|
||||
def test_persist_prompts_false_suppresses_summary(self):
|
||||
cli = _make_cli_stub()
|
||||
printed = []
|
||||
with patch.dict(cli_module.CLI_CONFIG.get("display", {}), {"persist_prompts": False}), \
|
||||
patch.object(cli_module, "_cprint", printed.append):
|
||||
verdict = self._resolve_approval(cli, "once")
|
||||
assert verdict == "once"
|
||||
assert not any("Approval" in p for p in printed)
|
||||
|
||||
def test_clarify_resolution_prints_summary_line(self):
|
||||
cli = _make_cli_stub()
|
||||
cli._clarify_state = None
|
||||
cli._clarify_freetext = False
|
||||
cli._clarify_deadline = 0
|
||||
printed = []
|
||||
result = {}
|
||||
|
||||
def _run():
|
||||
result["value"] = cli._clarify_callback("Pick a path?", ["A", "B"])
|
||||
|
||||
with patch.object(cli_module, "_cprint", printed.append):
|
||||
t = threading.Thread(target=_run, daemon=True)
|
||||
t.start()
|
||||
deadline = time.time() + 2
|
||||
while cli._clarify_state is None and time.time() < deadline:
|
||||
time.sleep(0.01)
|
||||
cli._clarify_state["response_queue"].put("B")
|
||||
t.join(timeout=2)
|
||||
|
||||
assert result["value"] == "B"
|
||||
summary = "\n".join(printed)
|
||||
assert "Clarify" in summary
|
||||
assert "Pick a path?" in summary
|
||||
assert "B" in summary
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue