Merge pull request #19338 from asheriif/fix/tui-plugin-slash-exec-live

fix(tui): run plugin slash commands live
This commit is contained in:
brooklyn! 2026-05-04 09:57:45 -07:00 committed by GitHub
commit d9c090fe36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 121 additions and 3 deletions

View file

@ -391,6 +391,99 @@ def test_slash_exec_rejects_skill_commands(server):
assert "skill command" in resp["error"]["message"]
def test_slash_exec_handles_plugin_commands_in_live_gateway(server):
"""Plugin slash commands return normal slash.exec output without using the worker."""
sid = "test-session"
class Worker:
def __init__(self):
self.calls = []
def run(self, cmd):
self.calls.append(cmd)
return f"worker:{cmd}"
worker = Worker()
server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker}
with patch(
"hermes_cli.plugins.get_plugin_command_handler",
lambda name: (lambda arg: f"plugin:{arg}") if name == "plugin-cmd" else None,
):
resp = server.handle_request({
"id": "r-plugin-slash",
"method": "slash.exec",
"params": {"command": "plugin-cmd hello", "session_id": sid},
})
assert "error" not in resp
assert resp["result"] == {"output": "plugin:hello"}
assert worker.calls == []
def test_slash_exec_plugin_lookup_failure_falls_back_to_worker(server):
"""Plugin discovery failures must not break ordinary slash-worker commands."""
sid = "test-session"
class Worker:
def __init__(self):
self.calls = []
def run(self, cmd):
self.calls.append(cmd)
return f"worker:{cmd}"
worker = Worker()
server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker}
with patch(
"hermes_cli.plugins.get_plugin_command_handler",
side_effect=RuntimeError("discovery boom"),
):
resp = server.handle_request({
"id": "r-plugin-lookup-failure",
"method": "slash.exec",
"params": {"command": "help", "session_id": sid},
})
assert "error" not in resp
assert resp["result"] == {"output": "worker:help"}
assert worker.calls == ["help"]
def test_slash_exec_plugin_handler_error_returns_output(server):
"""Plugin handler failures return slash output so the TUI does not redispatch."""
sid = "test-session"
class Worker:
def __init__(self):
self.calls = []
def run(self, cmd):
self.calls.append(cmd)
return f"worker:{cmd}"
def handler(arg):
raise RuntimeError(f"handler boom: {arg}")
worker = Worker()
server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker}
with patch(
"hermes_cli.plugins.get_plugin_command_handler",
lambda name: handler if name == "plugin-cmd" else None,
):
resp = server.handle_request({
"id": "r-plugin-handler-error",
"method": "slash.exec",
"params": {"command": "plugin-cmd hello", "session_id": sid},
})
assert "error" not in resp
assert resp["result"] == {"output": "Plugin command error: handler boom: hello"}
assert worker.calls == []
@pytest.mark.parametrize("cmd", ["retry", "queue hello", "q hello", "steer fix the test", "plan"])
def test_slash_exec_rejects_pending_input_commands(server, cmd):
"""slash.exec must reject commands that use _pending_input in the CLI."""

View file

@ -5171,9 +5171,13 @@ def _(rid, params: dict) -> dict:
return _err(rid, 4004, "empty command")
# Skill slash commands and _pending_input commands must NOT go through the
# slash worker — see _PENDING_INPUT_COMMANDS definition above.
_cmd_parts = cmd.split() if not cmd.startswith("/") else cmd.lstrip("/").split()
_cmd_base = _cmd_parts[0] if _cmd_parts else ""
# slash worker — see _PENDING_INPUT_COMMANDS definition above. Plugin
# commands must also avoid the worker, but unlike skills/pending-input they
# still return normal slash.exec output so the TUI keeps the pager path.
_cmd_text = cmd.lstrip("/") if cmd.startswith("/") else cmd
_cmd_parts = _cmd_text.split(maxsplit=1)
_cmd_base = (_cmd_parts[0] if _cmd_parts else "").lower()
_cmd_arg = _cmd_parts[1] if len(_cmd_parts) > 1 else ""
if _cmd_base in _PENDING_INPUT_COMMANDS:
return _err(
@ -5191,6 +5195,27 @@ def _(rid, params: dict) -> dict:
except Exception:
pass
plugin_handler = None
resolve_plugin_command_result = None
if _cmd_base:
try:
from hermes_cli.plugins import (
get_plugin_command_handler,
resolve_plugin_command_result,
)
plugin_handler = get_plugin_command_handler(_cmd_base)
except Exception:
plugin_handler = None
resolve_plugin_command_result = None
if plugin_handler and resolve_plugin_command_result:
try:
result = resolve_plugin_command_result(plugin_handler(_cmd_arg))
return _ok(rid, {"output": str(result or "(no output)")})
except Exception as e:
return _ok(rid, {"output": f"Plugin command error: {e}"})
worker = session.get("slash_worker")
if not worker:
try: