# NousResearch/hermes-agent · TUI /goal command dispatch and empty-command input handling

## Open now

- Nothing open on GitHub.

The members appear to center on the TUI /goal slash command, including a bug report of an "empty command" message with swallowed input text in the desktop app. Several titles describe changes to route pending-input commands through command.dispatch, but the titles alone do not show their outcome.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #48953. Verify it addresses your case.
- **Recommended:** read PR #48953 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-48953`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #48848: Bug: /goal command returns "empty command" and swallows all input text in TUI desktop app

## Prior & competing PRs

- **#48953: fix(tui): route pending-input commands via command.dispatch (#48848)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `tests/tui_gateway/test_goal_command.py`, `tests/tui_gateway/test_protocol.py`, `tui_gateway/server.py`
  - fix hunks:
  - `tests/tui_gateway/test_goal_command.py`

    ```diff
    @@ -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):
    ```
  - `tests/tui_gateway/test_protocol.py`

    ```diff
    @@ -1121,20 +1121,45 @@ def handler(arg):
     
     
     @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."""
    +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".
    +
    +    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"
    -    server._sessions[sid] = {"session_key": sid, "agent": None}
     
    -    resp = server.handle_request({
    +    # 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):
    ```
  - `tui_gateway/server.py`

    ```diff
    @@ -8462,7 +8462,9 @@ def _(rid, params: dict) -> dict:
     
     # 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",
    ```

    ```diff
    @@ -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:
    ```
- **#48856: Fix TUI /goal slash dispatch**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `1699525638ed4feba3fd35f0be5c6d4d2d326a49`, `#48848`
- **#48944: fix(tui): route pending-input commands via command.dispatch (#48848)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#48953`

## Fix status

- Merged to main in PR #48953 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-19`.
- Subsystem: `gateway`
