From a4fa1481e281b1d35ccc3fc9d9f1a59d235525f4 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:48:50 -0700 Subject: [PATCH] fix(tui): route /learn through command.dispatch so the prompt fires (#52232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Desktop GUI (tui_gateway) slash worker subprocess has no reader for the CLI's _pending_input queue. /learn's CLI handler prints the ack and puts the built prompt onto that queue, so in the TUI the prompt was silently dropped — ack shown, no LLM turn, no skill created (#51829). command.dispatch already handles 'learn' correctly (returns {type: send, message: build_learn_prompt(arg)}), but 'learn' was missing from _PENDING_INPUT_COMMANDS, so slash.exec fell through to the worker instead of routing to command.dispatch. Add it to the frozenset, matching the existing goal/queue/steer/plan pattern. --- tests/tui_gateway/test_protocol.py | 34 +++++++++++++++++++++++++++++- tui_gateway/server.py | 1 + 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index 054fc4df09f..3b385bf825a 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -1214,7 +1214,7 @@ def test_slash_exec_plugin_handler_error_returns_output(server): assert worker.calls == [] -@pytest.mark.parametrize("cmd", ["retry", "queue hello", "q hello", "steer fix the test", "plan"]) +@pytest.mark.parametrize("cmd", ["retry", "queue hello", "q hello", "steer fix the test", "plan", "learn create a skill from https://example.com/docs"]) 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" @@ -1288,6 +1288,38 @@ def test_command_dispatch_queue_requires_arg(server): assert resp["error"]["code"] == 4004 +def test_command_dispatch_learn_sends_built_prompt(server): + """command.dispatch /learn returns {type: 'send', message: } + so the TUI fires a real agent turn (#51829). The CLI handler queues onto + _pending_input — a queue the TUI slash worker has no reader for — so the + prompt was silently dropped after the ack. Routing through command.dispatch + injects the standards-guided prompt as a normal turn instead. + """ + from agent.learn_prompt import build_learn_prompt + + sid = "test-session" + server._sessions[sid] = {"session_key": sid} + + arg = "create a skill from https://example.com/docs" + resp = server.handle_request({ + "id": "r-learn", + "method": "command.dispatch", + "params": {"name": "learn", "arg": arg, "session_id": sid}, + }) + + assert "error" not in resp + result = resp["result"] + assert result["type"] == "send" + assert result["message"] == build_learn_prompt(arg) + + +def test_pending_input_commands_includes_learn(server): + """Guard: _PENDING_INPUT_COMMANDS must list 'learn' — without it slash.exec + routes /learn to the slash worker, which only prints the ack and drops the + prompt onto the dead _pending_input queue (#51829).""" + assert "learn" in server._PENDING_INPUT_COMMANDS + + def test_skills_manage_search_uses_tools_hub_sources(server): result = type("Result", (), { "description": "Build better terminal demos", diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 726a143d87e..b6afc03d4eb 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -9885,6 +9885,7 @@ _PENDING_INPUT_COMMANDS: frozenset[str] = frozenset( "plan", "goal", "undo", + "learn", } )