From 3eb73de924b8bd5865eba56a91973f2523ba75f6 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Mon, 3 Nov 2025 11:30:08 -0500 Subject: [PATCH] CLI: lazy load conversation for `/new` command (#11601) Co-authored-by: openhands --- openhands-cli/openhands_cli/agent_chat.py | 7 +-- .../tests/commands/test_new_command.py | 52 +++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/openhands-cli/openhands_cli/agent_chat.py b/openhands-cli/openhands_cli/agent_chat.py index 6e3aef21e4..c86081598d 100644 --- a/openhands-cli/openhands_cli/agent_chat.py +++ b/openhands-cli/openhands_cli/agent_chat.py @@ -143,8 +143,9 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None: elif command == '/new': try: # Start a fresh conversation (no resume ID = new conversation) - conversation = setup_conversation(conversation_id) - runner = ConversationRunner(conversation) + conversation_id = uuid.uuid4() + runner = None + conversation = None display_welcome(conversation_id, resume=False) print_formatted_text( HTML('✓ Started fresh conversation') @@ -195,7 +196,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None: # Resume without new message message = None - if not runner: + if not runner or not conversation: conversation = setup_conversation(conversation_id) runner = ConversationRunner(conversation) runner.process_message(message) diff --git a/openhands-cli/tests/commands/test_new_command.py b/openhands-cli/tests/commands/test_new_command.py index 759c4b4918..a02f69f49b 100644 --- a/openhands-cli/tests/commands/test_new_command.py +++ b/openhands-cli/tests/commands/test_new_command.py @@ -2,12 +2,18 @@ from unittest.mock import MagicMock, patch from uuid import UUID + from prompt_toolkit.input.defaults import create_pipe_input from prompt_toolkit.output.defaults import DummyOutput -from openhands_cli.setup import MissingAgentSpec, verify_agent_exists_or_setup_agent, setup_conversation + +from openhands_cli.setup import ( + MissingAgentSpec, + verify_agent_exists_or_setup_agent, +) from openhands_cli.user_actions import UserConfirmation -@patch('openhands_cli.setup.load_agent_specs') + +@patch("openhands_cli.setup.load_agent_specs") def test_verify_agent_exists_or_setup_agent_success(mock_load_agent_specs): """Test that verify_agent_exists_or_setup_agent returns agent successfully.""" # Mock the agent object @@ -22,11 +28,10 @@ def test_verify_agent_exists_or_setup_agent_success(mock_load_agent_specs): mock_load_agent_specs.assert_called_once_with() -@patch('openhands_cli.setup.SettingsScreen') -@patch('openhands_cli.setup.load_agent_specs') +@patch("openhands_cli.setup.SettingsScreen") +@patch("openhands_cli.setup.load_agent_specs") def test_verify_agent_exists_or_setup_agent_missing_agent_spec( - mock_load_agent_specs, - mock_settings_screen_class + mock_load_agent_specs, mock_settings_screen_class ): """Test that verify_agent_exists_or_setup_agent handles MissingAgentSpec exception.""" # Mock the SettingsScreen instance @@ -37,7 +42,7 @@ def test_verify_agent_exists_or_setup_agent_missing_agent_spec( mock_agent = MagicMock() mock_load_agent_specs.side_effect = [ MissingAgentSpec("Agent spec missing"), - mock_agent + mock_agent, ] # Call the function @@ -51,14 +56,11 @@ def test_verify_agent_exists_or_setup_agent_missing_agent_spec( mock_settings_screen.configure_settings.assert_called_once_with(first_time=True) - - - -@patch('openhands_cli.agent_chat.exit_session_confirmation') -@patch('openhands_cli.agent_chat.get_session_prompter') -@patch('openhands_cli.agent_chat.setup_conversation') -@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent') -@patch('openhands_cli.agent_chat.ConversationRunner') +@patch("openhands_cli.agent_chat.exit_session_confirmation") +@patch("openhands_cli.agent_chat.get_session_prompter") +@patch("openhands_cli.agent_chat.setup_conversation") +@patch("openhands_cli.agent_chat.verify_agent_exists_or_setup_agent") +@patch("openhands_cli.agent_chat.ConversationRunner") def test_new_command_resets_confirmation_mode( mock_runner_cls, mock_verify_agent, @@ -74,27 +76,35 @@ def test_new_command_resets_confirmation_mode( mock_verify_agent.return_value = mock_agent # Mock conversation - only one is created when /new is called - conv1 = MagicMock(); conv1.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') + conv1 = MagicMock() + conv1.id = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") mock_setup_conversation.return_value = conv1 # One runner instance for the conversation - runner1 = MagicMock(); runner1.is_confirmation_mode_active = True + runner1 = MagicMock() + runner1.is_confirmation_mode_active = True mock_runner_cls.return_value = runner1 # Real session fed by a pipe (no interactive confirmation now) - from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter + from openhands_cli.user_actions.utils import ( + get_session_prompter as real_get_session_prompter, + ) + with create_pipe_input() as pipe: output = DummyOutput() session = real_get_session_prompter(input=pipe, output=output) mock_get_session_prompter.return_value = session from openhands_cli.agent_chat import run_cli_entry - # Trigger /new, then /exit (exit will be auto-accepted) - for ch in "/new\r/exit\r": + + # Trigger /new + # First user message should trigger runner creation + # Then /exit (exit will be auto-accepted) + for ch in "/new\rhello\r/exit\r": pipe.send_text(ch) run_cli_entry(None) - # Assert we created one runner for the conversation when /new was called + # Assert we created one runner for the conversation when a message was processed after /new assert mock_runner_cls.call_count == 1 assert mock_runner_cls.call_args_list[0].args[0] is conv1