diff --git a/openhands-cli/openhands_cli/agent_chat.py b/openhands-cli/openhands_cli/agent_chat.py index 882400e65e..6e3aef21e4 100644 --- a/openhands-cli/openhands_cli/agent_chat.py +++ b/openhands-cli/openhands_cli/agent_chat.py @@ -127,7 +127,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None: break elif command == '/settings': - settings_screen = SettingsScreen(conversation) + settings_screen = SettingsScreen(runner.conversation if runner else None) settings_screen.display_settings() continue diff --git a/openhands-cli/openhands_cli/tui/settings/settings_screen.py b/openhands-cli/openhands_cli/tui/settings/settings_screen.py index 0f6799ed3d..6adfc821af 100644 --- a/openhands-cli/openhands_cli/tui/settings/settings_screen.py +++ b/openhands-cli/openhands_cli/tui/settings/settings_screen.py @@ -33,9 +33,6 @@ class SettingsScreen: agent_spec = self.agent_store.load() if not agent_spec: return - assert self.conversation is not None, ( - 'Conversation must be set to display settings.' - ) llm = agent_spec.llm advanced_llm_settings = True if llm.base_url else False @@ -62,12 +59,20 @@ class SettingsScreen: labels_and_values.extend( [ (' API Key', '********' if llm.api_key else 'Not Set'), + ] + ) + + if self.conversation: + labels_and_values.extend([ ( ' Confirmation Mode', 'Enabled' if self.conversation.is_confirmation_mode_active else 'Disabled', - ), + ) + ]) + + labels_and_values.extend([ ( ' Memory Condensation', 'Enabled' if agent_spec.condenser else 'Disabled', @@ -153,7 +158,7 @@ class SettingsScreen: api_key = prompt_api_key( step_counter, custom_model.split('/')[0] if len(custom_model.split('/')) > 1 else '', - self.conversation.agent.llm.api_key if self.conversation else None, + self.conversation.state.agent.llm.api_key if self.conversation else None, escapable=escapable, ) memory_condensation = choose_memory_condensation(step_counter) diff --git a/openhands-cli/tests/commands/test_settings_command.py b/openhands-cli/tests/commands/test_settings_command.py new file mode 100644 index 0000000000..b822242517 --- /dev/null +++ b/openhands-cli/tests/commands/test_settings_command.py @@ -0,0 +1,57 @@ +"""Test for the /settings command functionality.""" + +from unittest.mock import MagicMock, patch +from prompt_toolkit.input.defaults import create_pipe_input +from prompt_toolkit.output.defaults import DummyOutput + +from openhands_cli.agent_chat import run_cli_entry +from openhands_cli.user_actions import UserConfirmation + + +@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.SettingsScreen') +def test_settings_command_works_without_conversation( + mock_settings_screen_class, + mock_runner_cls, + mock_verify_agent, + mock_setup_conversation, + mock_get_session_prompter, + mock_exit_confirm, +): + """Test that /settings command works when no conversation is active (bug fix scenario).""" + # Auto-accept the exit prompt to avoid interactive UI + mock_exit_confirm.return_value = UserConfirmation.ACCEPT + + # Mock agent verification to succeed + mock_agent = MagicMock() + mock_verify_agent.return_value = mock_agent + + # Mock the SettingsScreen instance + mock_settings_screen = MagicMock() + mock_settings_screen_class.return_value = mock_settings_screen + + # No runner initially (simulates starting CLI without a conversation) + mock_runner_cls.return_value = None + + # Real session fed by a pipe + 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 + + # Trigger /settings, then /exit (exit will be auto-accepted) + for ch in "/settings\r/exit\r": + pipe.send_text(ch) + + run_cli_entry(None) + + # Assert SettingsScreen was created with None conversation (the bug fix) + mock_settings_screen_class.assert_called_once_with(None) + + # Assert display_settings was called (settings screen was shown) + mock_settings_screen.display_settings.assert_called_once() \ No newline at end of file