mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
feat(cli): add /update slash command to CLI and TUI (#23854)
* feat: add /update slash command to CLI and TUI * test(cli): add Python tests for /update slash command Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): address Copilot review for /update slash command Route classic CLI /update through prompt_toolkit modal confirmation and defer relaunch to the main-thread cleanup path after app.exit(). Tighten Y/n semantics, add Python wrapper and catalog coverage tests, and assert /update stays visible in the TUI command catalog. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): address review feedback on /update command - Replace raw input() with _prompt_text_input_modal in _handle_update_command to avoid EOF/hang/keystroke-leak races with prompt_toolkit's stdin ownership - Fix confirmation logic: only proceed on recognized affirmative aliases (y/yes/1/ok); cancel on everything else including empty string, typos, and unrecognized input — matches all other [Y/n] prompts in the codebase - Route relaunch through main-thread shutdown path: set _pending_relaunch and return False from process_command so process_loop triggers app.exit(); run() then calls relaunch() after prompt_toolkit has restored terminal modes and after cleanup — safe on both POSIX (execvp) and Windows (subprocess+exit) - Fix misleading docstring in test_update_command.py: the Vitest only covers the TypeScript slash handler that emits code 42, not the Python wrapper branch that acts on it - Rewrite tests to use SimpleNamespace pattern (like test_destructive_slash_confirm) so _prompt_text_input_modal can be stubbed directly - Add Python test for _launch_tui exit-code-42 → relaunch branch in main.py Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com> * fix(cli): polish test fixtures for /update command - Remove unused _prompt_text_input from SimpleNamespace stub - Use pytest.fail sentinel in managed-install guard test to catch unexpected modal invocations Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com> * chore: re-trigger CI after Copilot review fixes Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>
This commit is contained in:
parent
378bca1d2f
commit
2ef501e1f5
11 changed files with 289 additions and 3 deletions
69
cli.py
69
cli.py
|
|
@ -2830,6 +2830,11 @@ class HermesCLI:
|
|||
# process_command() when the user runs /exit --delete or /quit --delete.
|
||||
# Ported from google-gemini/gemini-cli#19332.
|
||||
self._delete_session_on_exit = False
|
||||
# /update: when set, run() executes relaunch() after prompt_toolkit
|
||||
# has fully exited and cleaned up terminal modes. Set by
|
||||
# _handle_update_command() so the relaunch happens on the main thread,
|
||||
# not the background process_loop thread.
|
||||
self._pending_relaunch: list[str] | None = None
|
||||
self._last_ctrl_c_time = 0
|
||||
self._clarify_state = None
|
||||
self._clarify_freetext = False
|
||||
|
|
@ -7948,6 +7953,9 @@ class HermesCLI:
|
|||
self._handle_copy_command(cmd_original)
|
||||
elif canonical == "debug":
|
||||
self._handle_debug_command()
|
||||
elif canonical == "update":
|
||||
if self._handle_update_command():
|
||||
return False
|
||||
elif canonical == "paste":
|
||||
self._handle_paste_command()
|
||||
elif canonical == "image":
|
||||
|
|
@ -9230,6 +9238,58 @@ class HermesCLI:
|
|||
args = SimpleNamespace(lines=200, expire=7, local=False)
|
||||
run_debug_share(args)
|
||||
|
||||
def _handle_update_command(self) -> bool:
|
||||
"""Handle /update — update Hermes Agent to the latest version.
|
||||
|
||||
In the classic CLI this exits the session and relaunches as
|
||||
``hermes update`` so the user sees update output directly and gets
|
||||
the new version on next launch.
|
||||
|
||||
Returns ``True`` when the update was confirmed (caller should trigger
|
||||
app exit so the relaunch is deferred to the main thread after
|
||||
prompt_toolkit cleans up terminal modes). Returns ``False`` / falsy
|
||||
when cancelled.
|
||||
"""
|
||||
from hermes_cli.config import is_managed, format_managed_message
|
||||
|
||||
if is_managed():
|
||||
print(f" ✗ {format_managed_message('update Hermes Agent')}")
|
||||
return False
|
||||
|
||||
# Use the prompt_toolkit-native modal so the confirmation panel
|
||||
# renders properly above the composer and avoids raw input() races
|
||||
# with the prompt_toolkit event loop (same pattern as
|
||||
# _confirm_destructive_slash).
|
||||
choices = [
|
||||
("once", "Update Now", "exit the current session and update Hermes Agent"),
|
||||
("cancel", "Cancel", "keep the current session"),
|
||||
]
|
||||
raw = self._prompt_text_input_modal(
|
||||
title="⚕ Update Hermes Agent",
|
||||
detail="This will exit the current session and run `hermes update`.",
|
||||
choices=choices,
|
||||
)
|
||||
if raw is None:
|
||||
print(" 🟡 /update cancelled.")
|
||||
return False
|
||||
choice = self._normalize_slash_confirm_choice(raw, choices)
|
||||
if choice != "once":
|
||||
print(" 🟡 /update cancelled.")
|
||||
return False
|
||||
|
||||
print()
|
||||
print(" ⚕ Launching update...")
|
||||
print()
|
||||
|
||||
# Store the relaunch args so run() can exec them from the main thread
|
||||
# after prompt_toolkit exits and restores terminal modes. Calling
|
||||
# relaunch() directly here (from the process_loop daemon thread) would
|
||||
# skip terminal cleanup on POSIX (execvp replaces the process mid-TUI)
|
||||
# and only exit the worker thread on Windows (subprocess.run +
|
||||
# sys.exit inside a non-main thread does not exit the process).
|
||||
self._pending_relaunch = ["update"]
|
||||
return True
|
||||
|
||||
def _show_usage(self):
|
||||
"""Show rate limits (if available) and session token usage."""
|
||||
if not self.agent:
|
||||
|
|
@ -13937,6 +13997,15 @@ class HermesCLI:
|
|||
_run_cleanup()
|
||||
self._print_exit_summary()
|
||||
|
||||
# Deferred relaunch: /update sets _pending_relaunch so the exec
|
||||
# happens here — after prompt_toolkit has exited and fully restored
|
||||
# terminal modes — rather than from the background process_loop
|
||||
# thread (which would skip terminal cleanup on POSIX and only exit
|
||||
# the worker thread on Windows).
|
||||
if getattr(self, '_pending_relaunch', None):
|
||||
from hermes_cli.relaunch import relaunch
|
||||
relaunch(self._pending_relaunch, preserve_inherited=False)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue