2026-02-21 23:17:18 -08:00
|
|
|
"""Interactive prompt callbacks for terminal_tool integration.
|
|
|
|
|
|
|
|
|
|
These bridge terminal_tool's interactive prompts (clarify, sudo, approval)
|
|
|
|
|
into prompt_toolkit's event loop. Each function takes the HermesCLI instance
|
|
|
|
|
as its first argument and uses its state (queues, app reference) to coordinate
|
|
|
|
|
with the TUI.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import queue
|
|
|
|
|
import time as _time
|
|
|
|
|
|
|
|
|
|
from hermes_cli.banner import cprint, _DIM, _RST
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clarify_callback(cli, question, choices):
|
|
|
|
|
"""Prompt for clarifying question through the TUI.
|
|
|
|
|
|
|
|
|
|
Sets up the interactive selection UI, then blocks until the user
|
|
|
|
|
responds. Returns the user's choice or a timeout message.
|
|
|
|
|
"""
|
|
|
|
|
from cli import CLI_CONFIG
|
|
|
|
|
|
|
|
|
|
timeout = CLI_CONFIG.get("clarify", {}).get("timeout", 120)
|
|
|
|
|
response_queue = queue.Queue()
|
|
|
|
|
is_open_ended = not choices or len(choices) == 0
|
|
|
|
|
|
|
|
|
|
cli._clarify_state = {
|
|
|
|
|
"question": question,
|
|
|
|
|
"choices": choices if not is_open_ended else [],
|
|
|
|
|
"selected": 0,
|
|
|
|
|
"response_queue": response_queue,
|
|
|
|
|
}
|
|
|
|
|
cli._clarify_deadline = _time.monotonic() + timeout
|
|
|
|
|
cli._clarify_freetext = is_open_ended
|
|
|
|
|
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
result = response_queue.get(timeout=1)
|
|
|
|
|
cli._clarify_deadline = 0
|
|
|
|
|
return result
|
|
|
|
|
except queue.Empty:
|
|
|
|
|
remaining = cli._clarify_deadline - _time.monotonic()
|
|
|
|
|
if remaining <= 0:
|
|
|
|
|
break
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
|
|
|
|
|
cli._clarify_state = None
|
|
|
|
|
cli._clarify_freetext = False
|
|
|
|
|
cli._clarify_deadline = 0
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
cprint(f"\n{_DIM}(clarify timed out after {timeout}s — agent will decide){_RST}")
|
|
|
|
|
return (
|
|
|
|
|
"The user did not provide a response within the time limit. "
|
|
|
|
|
"Use your best judgement to make the choice and proceed."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sudo_password_callback(cli) -> str:
|
|
|
|
|
"""Prompt for sudo password through the TUI.
|
|
|
|
|
|
|
|
|
|
Sets up a password input area and blocks until the user responds.
|
|
|
|
|
"""
|
|
|
|
|
timeout = 45
|
|
|
|
|
response_queue = queue.Queue()
|
|
|
|
|
|
|
|
|
|
cli._sudo_state = {"response_queue": response_queue}
|
|
|
|
|
cli._sudo_deadline = _time.monotonic() + timeout
|
|
|
|
|
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
result = response_queue.get(timeout=1)
|
|
|
|
|
cli._sudo_state = None
|
|
|
|
|
cli._sudo_deadline = 0
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
if result:
|
|
|
|
|
cprint(f"\n{_DIM} ✓ Password received (cached for session){_RST}")
|
|
|
|
|
else:
|
|
|
|
|
cprint(f"\n{_DIM} ⏭ Skipped{_RST}")
|
|
|
|
|
return result
|
|
|
|
|
except queue.Empty:
|
|
|
|
|
remaining = cli._sudo_deadline - _time.monotonic()
|
|
|
|
|
if remaining <= 0:
|
|
|
|
|
break
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
|
|
|
|
|
cli._sudo_state = None
|
|
|
|
|
cli._sudo_deadline = 0
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
cprint(f"\n{_DIM} ⏱ Timeout — continuing without sudo{_RST}")
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def approval_callback(cli, command: str, description: str) -> str:
|
|
|
|
|
"""Prompt for dangerous command approval through the TUI.
|
|
|
|
|
|
|
|
|
|
Shows a selection UI with choices: once / session / always / deny.
|
2026-03-12 06:27:21 -07:00
|
|
|
When the command is longer than 70 characters, a "view" option is
|
|
|
|
|
included so the user can reveal the full text before deciding.
|
2026-02-21 23:17:18 -08:00
|
|
|
"""
|
|
|
|
|
timeout = 60
|
|
|
|
|
response_queue = queue.Queue()
|
|
|
|
|
choices = ["once", "session", "always", "deny"]
|
2026-03-12 06:27:21 -07:00
|
|
|
if len(command) > 70:
|
|
|
|
|
choices.append("view")
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
cli._approval_state = {
|
|
|
|
|
"command": command,
|
|
|
|
|
"description": description,
|
|
|
|
|
"choices": choices,
|
|
|
|
|
"selected": 0,
|
|
|
|
|
"response_queue": response_queue,
|
|
|
|
|
}
|
|
|
|
|
cli._approval_deadline = _time.monotonic() + timeout
|
|
|
|
|
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
result = response_queue.get(timeout=1)
|
|
|
|
|
cli._approval_state = None
|
|
|
|
|
cli._approval_deadline = 0
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
return result
|
|
|
|
|
except queue.Empty:
|
|
|
|
|
remaining = cli._approval_deadline - _time.monotonic()
|
|
|
|
|
if remaining <= 0:
|
|
|
|
|
break
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
|
|
|
|
|
cli._approval_state = None
|
|
|
|
|
cli._approval_deadline = 0
|
|
|
|
|
if hasattr(cli, '_app') and cli._app:
|
|
|
|
|
cli._app.invalidate()
|
|
|
|
|
cprint(f"\n{_DIM} ⏱ Timeout — denying command{_RST}")
|
|
|
|
|
return "deny"
|