Skip to content

[Bug]: /clear, /new, /reset, /undo confirmation prompt cannot be answered — keystrokes leak into chat composer #22958

@shermanwsmith

Description

@shermanwsmith

Bug Description

Version: 0.13.0
Platform: Linux

The destructive-slash confirmation prompt added to cli.py (_confirm_destructive_slash, around line 8362) renders correctly but its input intercept never fires. Typing the answer (1, 2, or 3) sends that character to the agent as a chat message instead of resolving the prompt.

Steps to Reproduce

  1. Start Hermes CLI in TUI mode (hermes chat).
  2. Type /clear (or /new, /reset, /undo).
  3. Confirmation prompt prints:
⚠   /clear — destroys conversation state

  This clears the screen and starts a new session.
  The current conversation history will be discarded.

  [1] Approve Once   — proceed this time only
  [2] Always Approve — proceed and silence this prompt permanently
  [3] Cancel         — keep current conversation
  1. Type 2 and press Enter.

Expected Behavior

prompt resolves, "Always Approve" path runs, approvals.destructive_slash_confirm is persisted to false.

Actual Behavior

the 2 is delivered to the agent as a user message ("2"). The prompt remains unresolved. The destructive command never runs and never cancels — it's just abandoned.

Affected Component

CLI (interactive chat)

Messaging Platform (if gateway-related)

No response

Debug Report

N/A, contains identifiable personal information due to machine setup

Operating System

Pop!OS 24.04

Python Version

3.11.15

Hermes Version

0.13.0

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

Most likely cause:
_prompt_text_input (cli.py:5860) calls Python's built-in input() from inside prompt_toolkit.application.run_in_terminal:

def _prompt_text_input(self, prompt_text: str) -> str | None:
    result = [None]
    def _ask():
        try:
            result[0] = input(prompt_text).strip() or None
        except (KeyboardInterrupt, EOFError):
            pass
    if self._app:
        from prompt_toolkit.application import run_in_terminal
        was_visible = self._status_bar_visible
        self._status_bar_visible = False
        self._app.invalidate()
        try:
            run_in_terminal(_ask)
        finally:
            self._status_bar_visible = was_visible
            self._app.invalidate()
    else:
        _ask()
    return result[0]

When the TUI Application is active, run_in_terminal is supposed to suspend the app's screen and hand stdin back to the wrapped callable. In this code path that handoff doesn't appear to happen — the prompt prints, but keystrokes continue to be consumed by prompt_toolkit's main input handler and end up in _pending_input, which process_loop then forwards to the agent as a normal user message. The result [None] is never populated, the prompt returns None, and _confirm_destructive_slash treats that as "cancelled (no input)", except the cancel message is also never printed because the function is sitting on input() that will never return.

May be related — same session as a broken-prompt repro shows these in ~/.hermes/logs/errors.log:

WARNING cli: process_loop unhandled error (msg may be lost):
There is no current event loop in thread 'Thread-3 (process_loop)'.

process_loop is the thread that drains _pending_input, so an asyncio-context bug there could plausibly interfere with run_in_terminal's stdin handoff. Not confirmed, just a correlation worth checking.

Proposed Fix (optional)

Replace the built-in input() in _prompt_text_input with prompt_toolkit's native prompt(), which cooperates with the active Application instead of competing for stdin:

def _prompt_text_input(self, prompt_text: str) -> str | None:
    if self._app:
        from prompt_toolkit.application import run_in_terminal
        from prompt_toolkit.shortcuts import prompt as pt_prompt
        result = [None]
        def _ask():
            try:
                raw = pt_prompt(prompt_text)
                result[0] = raw.strip() or None
            except (KeyboardInterrupt, EOFError):
                pass
        was_visible = self._status_bar_visible
        self._status_bar_visible = False
        self._app.invalidate()
        try:
            run_in_terminal(_ask)
        finally:
            self._status_bar_visible = was_visible
            self._app.invalidate()
        return result[0]
    # Non-TUI fallback unchanged
    try:
        return input(prompt_text).strip() or None
    except (KeyboardInterrupt, EOFError):
        return None

A more invasive but cleaner alternative: render the choice as a prompt_toolkit modal, similar to the /model picker at cli.py:5884 (_open_model_picker). That reuses the existing input-capture machinery and gets you arrow-key navigation for free, but it's a bigger change.

Workaround for affected users

Add to ~/.hermes/config.yaml:

  approvals:
    destructive_slash_confirm: false

This bypasses the prompt entirely (cli.py:8391–8392). Confirmed working on 0.13.0.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — major feature broken, no workaroundcomp/cliCLI entry point, hermes_cli/, setup wizardsweeper:implemented-on-mainSweeper: behavior already present on current maintype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions