mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
/clear, /new, /reset, and /undo now ask the user to confirm before discarding conversation state — three-option prompt routed through the existing tools.slash_confirm primitive. Native yes/no buttons render on Telegram, Discord, and Slack (their adapters already implement send_slash_confirm); other platforms get a text-fallback prompt and reply with /approve, /always, or /cancel. The classic prompt_toolkit CLI uses the same three-option flow via the established _prompt_text_input pattern (see _confirm_and_reload_mcp). TUI keeps its existing modal overlay (#12312). Gated by new config key approvals.destructive_slash_confirm (default true). Picking 'Always Approve' flips the gate to false so subsequent destructive commands run silently — matches the established mcp_reload_confirm UX. Out of scope: /cron remove (separate domain — scheduled jobs, not session history). Existing TUI overlay env-var (HERMES_TUI_NO_CONFIRM) left unchanged; cosmetic unification can come later. Closes #4069.
This commit is contained in:
parent
0cafe7d50d
commit
b9c001116e
9 changed files with 730 additions and 3 deletions
89
cli.py
89
cli.py
|
|
@ -6751,6 +6751,12 @@ class HermesCLI:
|
|||
self._force_full_redraw()
|
||||
_cprint(f" {_DIM}✓ UI redrawn{_RST}")
|
||||
elif canonical == "clear":
|
||||
if self._confirm_destructive_slash(
|
||||
"clear",
|
||||
"This clears the screen and starts a new session.\n"
|
||||
"The current conversation history will be discarded.",
|
||||
) is None:
|
||||
return
|
||||
self.new_session(silent=True)
|
||||
_clear_output_history()
|
||||
# Clear terminal screen. Inside the TUI, Rich's console.clear()
|
||||
|
|
@ -6873,6 +6879,12 @@ class HermesCLI:
|
|||
elif canonical == "new":
|
||||
parts = cmd_original.split(maxsplit=1)
|
||||
title = parts[1].strip() if len(parts) > 1 else None
|
||||
if self._confirm_destructive_slash(
|
||||
"new",
|
||||
"This starts a fresh session.\n"
|
||||
"The current conversation history will be discarded.",
|
||||
) is None:
|
||||
return
|
||||
self.new_session(title=title)
|
||||
elif canonical == "resume":
|
||||
self._handle_resume_command(cmd_original)
|
||||
|
|
@ -6890,6 +6902,11 @@ class HermesCLI:
|
|||
# Re-queue the message so process_loop sends it to the agent
|
||||
self._pending_input.put(retry_msg)
|
||||
elif canonical == "undo":
|
||||
if self._confirm_destructive_slash(
|
||||
"undo",
|
||||
"This removes the last user/assistant exchange from history.",
|
||||
) is None:
|
||||
return
|
||||
self.undo_last()
|
||||
elif canonical == "branch":
|
||||
self._handle_branch_command(cmd_original)
|
||||
|
|
@ -8307,6 +8324,78 @@ class HermesCLI:
|
|||
if _reload_thread.is_alive():
|
||||
print(" ⚠️ MCP reload timed out (30s). Some servers may not have reconnected.")
|
||||
|
||||
def _confirm_destructive_slash(self, command: str, detail: str) -> Optional[str]:
|
||||
"""Prompt the user to confirm a destructive session slash command.
|
||||
|
||||
Used by ``/clear``, ``/new``/``/reset``, and ``/undo`` before they
|
||||
discard conversation state. Three-option prompt:
|
||||
|
||||
1. Approve Once — proceed this time only
|
||||
2. Always Approve — proceed and persist
|
||||
``approvals.destructive_slash_confirm: false`` so future
|
||||
destructive commands run without confirmation
|
||||
3. Cancel — abort
|
||||
|
||||
Gated by ``approvals.destructive_slash_confirm`` (default on). If the
|
||||
gate is off the function returns ``"once"`` immediately without
|
||||
prompting.
|
||||
|
||||
Returns ``"once"``, ``"always"``, or ``None`` (cancelled). Callers
|
||||
proceed with the destructive action when the result is non-None.
|
||||
"""
|
||||
# Gate check — respects prior "Always Approve" clicks.
|
||||
try:
|
||||
cfg = load_cli_config()
|
||||
approvals = cfg.get("approvals") if isinstance(cfg, dict) else None
|
||||
confirm_required = True
|
||||
if isinstance(approvals, dict):
|
||||
confirm_required = bool(approvals.get("destructive_slash_confirm", True))
|
||||
except Exception:
|
||||
confirm_required = True
|
||||
|
||||
if not confirm_required:
|
||||
return "once"
|
||||
|
||||
# Render warning + prompt — single-line composer prompt, mirrors
|
||||
# ``_confirm_and_reload_mcp``.
|
||||
print()
|
||||
print(f"⚠️ /{command} — destroys conversation state")
|
||||
print()
|
||||
for line in detail.splitlines():
|
||||
print(f" {line}")
|
||||
print()
|
||||
print(" [1] Approve Once — proceed this time only")
|
||||
print(" [2] Always Approve — proceed and silence this prompt permanently")
|
||||
print(" [3] Cancel — keep current conversation")
|
||||
print()
|
||||
raw = self._prompt_text_input("Choice [1/2/3]: ")
|
||||
if raw is None:
|
||||
print(f"🟡 /{command} cancelled (no input).")
|
||||
return None
|
||||
choice_raw = raw.strip().lower()
|
||||
if choice_raw in ("1", "once", "approve", "yes", "y", "ok"):
|
||||
choice = "once"
|
||||
elif choice_raw in ("2", "always", "remember"):
|
||||
choice = "always"
|
||||
elif choice_raw in ("3", "cancel", "nevermind", "no", "n", ""):
|
||||
choice = "cancel"
|
||||
else:
|
||||
print(f"🟡 Unrecognized choice '{raw}'. /{command} cancelled.")
|
||||
return None
|
||||
|
||||
if choice == "cancel":
|
||||
print(f"🟡 /{command} cancelled. Conversation unchanged.")
|
||||
return None
|
||||
|
||||
if choice == "always":
|
||||
if save_config_value("approvals.destructive_slash_confirm", False):
|
||||
print("🔒 Future /clear, /new, /reset, and /undo will run without confirmation.")
|
||||
print(" Re-enable via `approvals.destructive_slash_confirm: true` in config.yaml.")
|
||||
else:
|
||||
print("⚠️ Couldn't persist opt-out — proceeding once.")
|
||||
|
||||
return choice
|
||||
|
||||
def _confirm_and_reload_mcp(self, cmd_original: str = "") -> None:
|
||||
"""Interactive /reload-mcp — confirm with the user, then reload.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue