diff --git a/llm/cli.py b/llm/cli.py index 3123a387f..45ed17872 100644 --- a/llm/cli.py +++ b/llm/cli.py @@ -1267,13 +1267,13 @@ def chat( """ Hold an ongoing chat with a model. """ - # Left and right arrow keys to move cursor: - if sys.platform != "win32": + # Left and right arrow keys to move cursor. + # pyreadline3 on Windows does not implement Bash `bind -x`. + try: readline.parse_and_bind("\\e[D: backward-char") readline.parse_and_bind("\\e[C: forward-char") - else: - readline.parse_and_bind("bind -x '\\e[D: backward-char'") - readline.parse_and_bind("bind -x '\\e[C: forward-char'") + except Exception: + pass log_path = pathlib.Path(database) if database else logs_db_path() (log_path.parent).mkdir(parents=True, exist_ok=True) db = sqlite_utils.Database(log_path) diff --git a/tests/test_chat.py b/tests/test_chat.py index 120f757f7..d8410f374 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -355,3 +355,35 @@ def test_chat_fragments(tmpdir): ).output assert '"prompt": "one' in output assert '"prompt": "two"' in output + + +def test_chat_readline_does_not_use_bash_bind_x(mock_model, logs_db, monkeypatch): + calls = [] + monkeypatch.setattr( + llm.cli.readline, "parse_and_bind", lambda spec: calls.append(spec) + ) + mock_model.enqueue(["ok"]) + result = CliRunner().invoke( + llm.cli.cli, + ["chat", "-m", "mock"], + input="Hi\nquit\n", + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert calls == ["\\e[D: backward-char", "\\e[C: forward-char"] + + +def test_chat_survives_readline_bind_errors(mock_model, logs_db, monkeypatch): + def boom(_spec): + raise ValueError("pyreadline3 does not support this bind") + + monkeypatch.setattr(llm.cli.readline, "parse_and_bind", boom) + mock_model.enqueue(["ok"]) + result = CliRunner().invoke( + llm.cli.cli, + ["chat", "-m", "mock"], + input="Hi\nquit\n", + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert "ok" in result.output