From 47721209fde4aaf1211f3cc36797d82db6a6ee2c Mon Sep 17 00:00:00 2001 From: estefano viteri Date: Sat, 7 Nov 2026 07:50:55 -0500 Subject: [PATCH] fix(cli): enable arrow-key history and line editing in adk run input() only supports arrow keys and history once the readline module is imported, and the CLI never imported it. Pressing arrow up in `adk run` printed `^[[A` instead of recalling the previous message. Import readline when the interactive session starts. Skip it where readline is unavailable, such as Windows. --- src/google/adk/cli/cli.py | 14 ++++++++ tests/unittests/cli/utils/test_cli.py | 47 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 17371da5e91..e4372b6ff40 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -192,6 +192,19 @@ def _collect_pending_function_calls( return pending +def _enable_line_editing() -> None: + """Enables arrow-key editing and history for input() when available. + + input() only supports line editing once the readline module is imported. + readline is not available on Windows, where the console already provides + history. + """ + try: + import readline # noqa: F401 + except ImportError: + pass + + def _is_positive_response(s: str) -> bool: """Returns True if the string is a positive response.""" return s.strip().lower() in ('y', 'yes', 'true', 'confirm') @@ -262,6 +275,7 @@ async def run_interactively( agent_loader: Optional[AgentLoader] = None, agent_folder_name: Optional[str] = None, ) -> None: + _enable_line_editing() app = _to_app(root_agent_or_app, session.app_name) runner = Runner( app=app, diff --git a/tests/unittests/cli/utils/test_cli.py b/tests/unittests/cli/utils/test_cli.py index 5105ee60a9c..1370241b8b0 100644 --- a/tests/unittests/cli/utils/test_cli.py +++ b/tests/unittests/cli/utils/test_cli.py @@ -642,6 +642,53 @@ async def test_run_interactively_whitespace_and_exit( assert any("echo:hello" in m for m in echoed) +def test_enable_line_editing_imports_readline( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """_enable_line_editing should import readline so input() gets history.""" + pytest.importorskip("readline") + monkeypatch.delitem(sys.modules, "readline", raising=False) + + cli._enable_line_editing() + + assert "readline" in sys.modules + + +def test_enable_line_editing_without_readline( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """_enable_line_editing should not fail where readline is missing.""" + # A None entry in sys.modules makes the import raise ImportError, as on + # Windows. + monkeypatch.setitem(sys.modules, "readline", None) + + cli._enable_line_editing() + + +@pytest.mark.asyncio +async def test_run_interactively_enables_line_editing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """run_interactively should enable line editing before reading input.""" + session_service = InMemorySessionService() + sess = await session_service.create_session(app_name="dummy", user_id="u") + calls: list[str] = [] + monkeypatch.setattr( + cli, "_enable_line_editing", lambda: calls.append("enabled") + ) + monkeypatch.setattr("builtins.input", lambda *_a, **_k: "exit") + + await cli.run_interactively( + BaseAgent(name="root"), + InMemoryArtifactService(), + sess, + session_service, + InMemoryCredentialService(), + ) + + assert calls == ["enabled"] + + def test_print_event_omits_internal_metadata_in_jsonl( monkeypatch: pytest.MonkeyPatch, ) -> None: