diff --git a/src/plyngent/cli/readline_setup.py b/src/plyngent/cli/readline_setup.py index bbe0bad..75a9f79 100644 --- a/src/plyngent/cli/readline_setup.py +++ b/src/plyngent/cli/readline_setup.py @@ -60,17 +60,20 @@ def build_completer(state: ReplState) -> Callable[[str, int], str | None]: def _argument_options(state: ReplState, command: str, text: str) -> list[str]: - if command == "/provider": - return filter_prefix(text, sorted(state.config.providers.keys())) - if command == "/model": - return filter_prefix(text, sorted(state.provider.models.keys())) - if command in {"/tools", "/stream", "/verbose"}: - return filter_prefix(text, list(_ON_OFF_ARGS)) - if command == "/export": - return filter_prefix(text, list(_EXPORT_ARGS)) - if command == "/resume": - return [] - return [] + candidates: list[str] = [] + if command == "/help": + # Second token: command name without leading slash (also accept /name). + candidates = [n.removeprefix("/") for n in slash_commands()] + text = text.lstrip("/") + elif command == "/provider": + candidates = sorted(state.config.providers.keys()) + elif command == "/model": + candidates = sorted(state.provider.models.keys()) + elif command in {"/tools", "/stream", "/verbose"}: + candidates = list(_ON_OFF_ARGS) + elif command == "/export": + candidates = list(_EXPORT_ARGS) + return filter_prefix(text, candidates) def bind_tab_complete(readline_mod: object) -> None: diff --git a/src/plyngent/cli/slash.py b/src/plyngent/cli/slash.py index dc96a84..4188eef 100644 --- a/src/plyngent/cli/slash.py +++ b/src/plyngent/cli/slash.py @@ -112,8 +112,8 @@ def help_cmd(ctx: click.Context, command: str | None) -> None: if cmd is None: click.echo(f"unknown command /{name}; try /help") return - # Build a child context so get_help includes usage. - with click.Context(cmd, info_name=name, parent=ctx) as sub: + # Standalone context (no parent=help) so usage is "/compact …" not "help … compact". + with click.Context(cmd, info_name=f"/{name}") as sub: click.echo(cmd.get_help(sub)) return click.echo(slash.get_help(ctx)) diff --git a/tests/test_cli/test_readline_setup.py b/tests/test_cli/test_readline_setup.py index 4b493a9..9af4e2f 100644 --- a/tests/test_cli/test_readline_setup.py +++ b/tests/test_cli/test_readline_setup.py @@ -116,3 +116,29 @@ def test_completer_provider_args(tmp_path: object, monkeypatch: object) -> None: first = completer("r", 0) assert first == "remote" + + +def test_completer_help_commands(tmp_path: object, monkeypatch: object) -> None: + import readline + from pathlib import Path + + import pytest + + assert isinstance(tmp_path, Path) + assert isinstance(monkeypatch, pytest.MonkeyPatch) + + state = _minimal_state(tmp_path) + completer = build_completer(state) + monkeypatch.setattr(readline, "get_line_buffer", lambda: "/help ") + monkeypatch.setattr(readline, "get_begidx", lambda: len("/help ")) + + found: list[str] = [] + index = 0 + while True: + item = completer("c", index) + if item is None: + break + found.append(item) + index += 1 + assert "compact" in found + assert "clear" in found diff --git a/tests/test_cli/test_repl_commands.py b/tests/test_cli/test_repl_commands.py index b55d342..e1fcabe 100644 --- a/tests/test_cli/test_repl_commands.py +++ b/tests/test_cli/test_repl_commands.py @@ -96,6 +96,14 @@ async def test_help_and_clear(state: ReplState) -> None: assert state.agent.messages == [] +async def test_help_command_usage_line(state: ReplState, capsys: pytest.CaptureFixture[str]) -> None: + assert await handle_slash(state, "/help compact") is True + out = capsys.readouterr().out + assert "Usage: /compact" in out + assert "help [COMMAND] compact" not in out + assert "Soft-compact" in out + + async def test_quit(state: ReplState) -> None: assert await handle_slash(state, "/quit") is False