diff --git a/gateway/run.py b/gateway/run.py index d4f2ba8d25..f023b0d349 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -5138,6 +5138,28 @@ class GatewayRunner: _cmd_def = _resolve_cmd(command) if command else None canonical = _cmd_def.name if _cmd_def else command + # Expand alias quick commands before built-in dispatch so targets like + # /model openai/gpt-5.5 --provider openrouter reach the /model handler. + # Preserve built-in precedence; aliases only need early handling when + # the typed command is not already known. + if command and _cmd_def is None: + if isinstance(self.config, dict): + quick_commands = self.config.get("quick_commands", {}) or {} + else: + quick_commands = getattr(self.config, "quick_commands", {}) or {} + if isinstance(quick_commands, dict) and command in quick_commands: + qcmd = quick_commands[command] + if qcmd.get("type") == "alias": + target = qcmd.get("target", "").strip() + if target: + target = target if target.startswith("/") else f"/{target}" + target_command = target.lstrip("/") + user_args = event.get_command_args().strip() + event.text = f"{target} {user_args}".strip() + command = target_command.split()[0] if target_command else target_command + _cmd_def = _resolve_cmd(command) if command else None + canonical = _cmd_def.name if _cmd_def else command + # Fire the ``command:`` hook for any recognized slash # command — built-in OR plugin-registered. Handlers can return a # dict with ``{"decision": "deny" | "handled" | "rewrite", ...}`` @@ -5351,7 +5373,7 @@ class GatewayRunner: target_command = target.lstrip("/") user_args = event.get_command_args().strip() event.text = f"{target} {user_args}".strip() - command = target_command + command = target_command.split()[0] if target_command else target_command # Fall through to normal command dispatch below else: return f"Quick command '/{command}' has no target defined." diff --git a/tests/e2e/test_platform_commands.py b/tests/e2e/test_platform_commands.py index b891ea7372..4924eed6a9 100644 --- a/tests/e2e/test_platform_commands.py +++ b/tests/e2e/test_platform_commands.py @@ -138,6 +138,29 @@ class TestSlashCommands: response_text = send.call_args[1].get("content") or send.call_args[0][1] assert "compress" in response_text.lower() or "context" in response_text.lower() + @pytest.mark.asyncio + async def test_quick_command_alias_targets_builtin_command_with_args( + self, adapter, runner, platform + ): + """Alias targets with args must reach the built-in command handler.""" + runner.config.quick_commands = { + "s": {"type": "alias", "target": "/status extra-arg"} + } + async def _handle_status(event): + assert event.get_command_args() == "extra-arg" + return "status via alias" + + runner._handle_status_command = AsyncMock(side_effect=_handle_status) + + send = await send_and_capture(adapter, "/s", platform) + + send.assert_called_once() + response_text = send.call_args[1].get("content") or send.call_args[0][1] + assert response_text == "status via alias" + runner._handle_status_command.assert_awaited_once() + runner._handle_message_with_agent.assert_not_awaited() + + class TestSessionLifecycle: """Verify session state changes across command sequences."""