fix(cli): add inline --yes/now skip for destructive slash commands (#30768)

Issue #30768 reports that on native Windows PowerShell the destructive-slash
confirmation modal renders but never registers keypresses, leaving the user
unable to confirm or cancel /reset, /new, /clear, or /undo. The modal works
on macOS, Linux, and WSL; PR #23907 (merged May 11) replaced the
daemon-thread input() pattern with a prompt_toolkit-native keybinding modal
but the win32 input pipeline apparently doesn't dispatch keys to the
filter-conditioned handlers. The modal investigation is ongoing.

This change ships the immediate escape hatch: append `now`, `--yes`, or `-y`
to any destructive slash command to bypass the modal and run the action
immediately. Works on every platform without touching the broken Windows
code path.

  /reset now            -> reset, no modal
  /new --yes my-session -> new session titled "my-session", no modal
  /clear -y             -> clear, no modal
  /undo -y              -> undo, no modal

The default behavior (modal prompts when approvals.destructive_slash_confirm
is True) is unchanged for users who don't pass a skip token.

Implementation:

- New classmethod HermesCLI._split_destructive_skip(text) -> (remainder, skip)
  parses a destructive-slash command string, strips the leading "/cmd" word
  and any recognized skip tokens (case-insensitive exact match, not substring),
  and reports whether a skip was requested.
- HermesCLI._confirm_destructive_slash gains an optional cmd_original= arg.
  When the arg contains a skip token, it returns "once" immediately —
  before the gate check and before any modal rendering.
- The /clear, /new, /undo handlers in process_command pass cmd_original
  through. /new additionally uses _split_destructive_skip to strip skip
  tokens from the remaining text before deriving the session title, so
  "/new now My Session" yields title="My Session" (not "now My Session").

Tests:

- 7 new unit tests in tests/cli/test_destructive_slash_confirm.py covering
  the helper (recognized tokens, command-word stripping, case-insensitive
  exact match, None/empty input) and the modal bypass (now and --yes both
  skip; no-skip-token still consults the modal).
- 3 new integration tests in tests/cli/test_destructive_slash_inline_skip_e2e.py
  driving HermesCLI.process_command end-to-end and asserting (a) new_session
  is invoked, (b) the modal is never reached, (c) the skip token does not
  leak into the session title, and (d) the no-skip-token path still reaches
  the modal as a sanity check that we haven't accidentally short-circuited
  the normal flow.

All 31 tests across the destructive-slash test surface pass.

Docs:

- website/docs/reference/slash-commands.md documents the new flags both in
  the destructive-commands table and the dedicated approval section, with a
  link back to issue #30768 explaining why the escape hatch exists.
This commit is contained in:
Teknium 2026-05-24 15:28:15 -07:00
parent 99a7ecc335
commit 8e68426981
4 changed files with 318 additions and 4 deletions

69
cli.py
View file

@ -8115,6 +8115,7 @@ class HermesCLI:
"clear",
"This clears the screen and starts a new session.\n"
"The current conversation history will be discarded.",
cmd_original=cmd_original,
) is None:
return
self.new_session(silent=True)
@ -8239,12 +8240,16 @@ class HermesCLI:
if not self._handle_handoff_command(cmd_original):
return False
elif canonical == "new":
parts = cmd_original.split(maxsplit=1)
title = parts[1].strip() if len(parts) > 1 else None
# Strip inline-skip tokens (now/--yes/-y) before deriving the title
# so "/new now My Session" yields title="My Session" instead of
# title="now My Session". See _split_destructive_skip.
_new_args, _ = self._split_destructive_skip(cmd_original)
title = _new_args.strip() or None
if self._confirm_destructive_slash(
"new",
"This starts a fresh session.\n"
"The current conversation history will be discarded.",
cmd_original=cmd_original,
) is None:
return
self.new_session(title=title)
@ -8271,6 +8276,7 @@ class HermesCLI:
if self._confirm_destructive_slash(
"undo",
"This removes the last user/assistant exchange from history.",
cmd_original=cmd_original,
) is None:
return
self.undo_last()
@ -9922,7 +9928,49 @@ 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]:
# Inline-skip tokens that bypass the destructive-slash confirmation modal.
# Matches the escape-hatch pattern users on broken modal platforms
# (currently native Windows PowerShell — issue #30768) need to self-serve
# without having to flip approvals.destructive_slash_confirm in config.
_DESTRUCTIVE_SKIP_TOKENS = frozenset({"now", "--yes", "-y"})
@classmethod
def _split_destructive_skip(cls, cmd_text: Optional[str]) -> tuple[str, bool]:
"""Split inline-skip tokens out of a destructive slash command.
Returns ``(remainder, skip)`` where ``remainder`` is the original
text with the command word and any recognized skip tokens removed,
and ``skip`` is True iff at least one skip token was found.
Examples:
"/reset now" -> ("", True)
"/reset --yes My title" -> ("My title", True)
"/new My title" -> ("My title", False)
"/clear" -> ("", False)
"""
if not cmd_text:
return "", False
tokens = cmd_text.strip().split()
if not tokens:
return "", False
# Drop leading "/cmd" word — callers pass the full command text.
if tokens[0].startswith("/"):
tokens = tokens[1:]
skip = False
kept: list[str] = []
for tok in tokens:
if tok.lower() in cls._DESTRUCTIVE_SKIP_TOKENS:
skip = True
continue
kept.append(tok)
return " ".join(kept), skip
def _confirm_destructive_slash(
self,
command: str,
detail: str,
cmd_original: Optional[str] = None,
) -> Optional[str]:
"""Prompt the user to confirm a destructive session slash command.
Used by ``/clear``, ``/new``/``/reset``, and ``/undo`` before they
@ -9938,9 +9986,24 @@ class HermesCLI:
gate is off the function returns ``"once"`` immediately without
prompting.
Inline-skip: if ``cmd_original`` contains ``now``, ``--yes``, or
``-y`` as an argument (e.g. ``/reset now``, ``/new --yes My title``),
the modal is bypassed and ``"once"`` is returned immediately. This is
an escape hatch for platforms where the prompt_toolkit modal hangs
(issue #30768 — native Windows PowerShell). Callers are responsible
for stripping the skip tokens from any remaining argument parsing
(see :meth:`_split_destructive_skip`).
Returns ``"once"``, ``"always"``, or ``None`` (cancelled). Callers
proceed with the destructive action when the result is non-None.
"""
# Inline-skip escape hatch — works regardless of platform/modal state.
# See class-level _DESTRUCTIVE_SKIP_TOKENS for the accepted tokens.
if cmd_original:
_, _skip = self._split_destructive_skip(cmd_original)
if _skip:
return "once"
# Gate check — respects prior "Always Approve" clicks.
try:
cfg = load_cli_config()