Merge pull request #48953 from kshitijk4poor/salvage/issue-48848
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
OSV-Scanner / Scan lockfiles (push) Waiting to run
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Typecheck / typecheck (apps/bootstrap-installer) (push) Waiting to run
Typecheck / typecheck (apps/desktop) (push) Waiting to run
Typecheck / typecheck (apps/shared) (push) Waiting to run
Typecheck / typecheck (ui-tui) (push) Waiting to run
Typecheck / typecheck (web) (push) Waiting to run
Typecheck / desktop-build (push) Waiting to run
uv.lock check / uv lock --check (push) Waiting to run

fix(tui): route pending-input commands via command.dispatch (#48848)
This commit is contained in:
kshitij 2026-06-19 14:59:17 +05:30 committed by GitHub
commit df4ca2c5ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 18 deletions

View file

@ -185,15 +185,17 @@ def test_goal_requires_session(server):
# ── slash.exec /goal routing ──────────────────────────────────────────
def test_slash_exec_rejects_goal_routes_to_command_dispatch(server, session):
"""slash.exec must reject /goal with 4018 so the TUI client falls through
to command.dispatch. Without this, the HermesCLI slash-worker subprocess
would set the goal but silently drop the kickoff the queue is in-proc."""
def test_slash_exec_routes_goal_to_command_dispatch(server, session):
"""slash.exec must route /goal directly to command.dispatch internally
instead of returning an error. Previously the 4018 error required the
TUI client to retry via command.dispatch, but some clients failed the
fallback, leaving the command empty ("empty command")."""
sid, _, _ = session
r = _call(server, "slash.exec", command="goal status", session_id=sid)
assert "error" in r
assert r["error"]["code"] == 4018
assert "command.dispatch" in r["error"]["message"]
# Should succeed by routing to command.dispatch internally
assert "result" in r
assert r["result"]["type"] == "exec"
assert "No active goal" in r["result"]["output"]
def test_pending_input_commands_includes_goal(server):

View file

@ -1121,20 +1121,45 @@ def test_slash_exec_plugin_handler_error_returns_output(server):
@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."""
sid = "test-session"
server._sessions[sid] = {"session_key": sid, "agent": None}
def test_slash_exec_routes_pending_input_commands_to_dispatch(server, cmd):
"""slash.exec must route _pending_input commands to command.dispatch
internally instead of returning the old 4018 "use command.dispatch"
fallback error (#48848). Some TUI clients failed that client-side
fallback, dropping the input and surfacing "empty command".
resp = server.handle_request({
The contract is that slash.exec produces exactly the response
command.dispatch would for the same command no fragile retry hop.
"""
base, _, arg = cmd.partition(" ")
def fresh_session():
return {"session_key": "test-session", "agent": None}
sid = "test-session"
# Response from the (new) internal routing in slash.exec.
server._sessions[sid] = fresh_session()
routed = server.handle_request({
"id": "r1",
"method": "slash.exec",
"params": {"command": cmd, "session_id": sid},
})
assert "error" in resp
assert resp["error"]["code"] == 4018
assert "pending-input command" in resp["error"]["message"]
# Response from calling command.dispatch directly with the parsed parts.
server._sessions[sid] = fresh_session()
direct = server.handle_request({
"id": "r1",
"method": "command.dispatch",
"params": {"name": base, "arg": arg, "session_id": sid},
})
# slash.exec must no longer emit the old client-fallback rejection.
if "error" in routed:
assert "pending-input command" not in routed["error"]["message"]
# Internal routing must yield the same payload as command.dispatch.
assert routed.get("result") == direct.get("result")
assert routed.get("error") == direct.get("error")
def test_command_dispatch_queue_sends_message(server):

View file

@ -8462,7 +8462,9 @@ _TUI_EXTRA: list[tuple[str, str, str]] = [
# Commands that queue messages onto _pending_input in the CLI.
# In the TUI the slash worker subprocess has no reader for that queue,
# so slash.exec rejects them → TUI falls through to command.dispatch.
# so slash.exec routes them to command.dispatch internally (which handles
# them and returns a structured payload) instead of erroring out and
# relying on a client-side fallback. See #48848.
_PENDING_INPUT_COMMANDS: frozenset[str] = frozenset(
{
"retry",
@ -9729,8 +9731,16 @@ def _(rid, params: dict) -> dict:
_cmd_arg = _cmd_parts[1] if len(_cmd_parts) > 1 else ""
if _cmd_base in _PENDING_INPUT_COMMANDS:
return _err(
rid, 4018, f"pending-input command: use command.dispatch for /{_cmd_base}"
# Route directly to command.dispatch instead of returning an error
# that requires the frontend to retry. Some TUI clients fail the
# fallback, leaving the command empty and showing "empty command".
return _methods["command.dispatch"](
rid,
{
"name": _cmd_base,
"arg": _cmd_arg,
"session_id": params.get("session_id", ""),
},
)
if _cmd_base in _WORKER_BLOCKED_COMMANDS: