diff --git a/openhands-cli/openhands_cli/agent_chat.py b/openhands-cli/openhands_cli/agent_chat.py
index 6278c7d96a..2e6216359e 100644
--- a/openhands-cli/openhands_cli/agent_chat.py
+++ b/openhands-cli/openhands_cli/agent_chat.py
@@ -6,14 +6,15 @@ Provides a conversation interface with an AI agent using OpenHands patterns.
import sys
-from prompt_toolkit import print_formatted_text
-from prompt_toolkit.formatted_text import HTML
-
from openhands.sdk import (
+ BaseConversation,
Message,
TextContent,
)
from openhands.sdk.conversation.state import AgentExecutionStatus
+from prompt_toolkit import print_formatted_text
+from prompt_toolkit.formatted_text import HTML
+
from openhands_cli.runner import ConversationRunner
from openhands_cli.setup import MissingAgentSpec, setup_conversation
from openhands_cli.tui.settings.mcp_screen import MCPScreen
@@ -26,6 +27,30 @@ from openhands_cli.user_actions import UserConfirmation, exit_session_confirmati
from openhands_cli.user_actions.utils import get_session_prompter
+def _start_fresh_conversation(resume_conversation_id: str | None = None) -> BaseConversation:
+ """Start a fresh conversation by creating a new conversation instance.
+
+ Handles the complete conversation setup process including settings screen
+ if agent configuration is missing.
+
+ Args:
+ resume_conversation_id: Optional conversation ID to resume
+
+ Returns:
+ BaseConversation: A new conversation instance
+ """
+ conversation = None
+ settings_screen = SettingsScreen()
+
+ while not conversation:
+ try:
+ conversation = setup_conversation(resume_conversation_id)
+ except MissingAgentSpec:
+ settings_screen.handle_basic_settings(escapable=False)
+
+ return conversation
+
+
def _restore_tty() -> None:
"""
Ensure terminal modes are reset in case prompt_toolkit cleanup didn't run.
@@ -62,15 +87,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
EOFError: If EOF is encountered
"""
- conversation = None
- settings_screen = SettingsScreen()
-
- while not conversation:
- try:
- conversation = setup_conversation(resume_conversation_id)
- except MissingAgentSpec:
- settings_screen.handle_basic_settings(escapable=False)
-
+ conversation = _start_fresh_conversation(resume_conversation_id)
display_welcome(conversation.id, bool(resume_conversation_id))
# Create conversation runner to handle state machine logic
@@ -118,6 +135,22 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
display_welcome(conversation.id)
continue
+ elif command == '/new':
+ try:
+ # Start a fresh conversation (no resume ID = new conversation)
+ conversation = _start_fresh_conversation()
+ runner = ConversationRunner(conversation)
+ display_welcome(conversation.id, resume=False)
+ print_formatted_text(
+ HTML('✓ Started fresh conversation')
+ )
+ continue
+ except Exception as e:
+ print_formatted_text(
+ HTML(f'Error starting fresh conversation: {e}')
+ )
+ continue
+
elif command == '/help':
display_help()
continue
diff --git a/openhands-cli/openhands_cli/tui/tui.py b/openhands-cli/openhands_cli/tui/tui.py
index 08c581edbc..2ead091f5e 100644
--- a/openhands-cli/openhands_cli/tui/tui.py
+++ b/openhands-cli/openhands_cli/tui/tui.py
@@ -17,6 +17,7 @@ COMMANDS = {
'/exit': 'Exit the application',
'/help': 'Display available commands',
'/clear': 'Clear the screen',
+ '/new': 'Start a fresh conversation',
'/status': 'Display conversation details',
'/confirm': 'Toggle confirmation mode on/off',
'/resume': 'Resume a paused conversation',
diff --git a/openhands-cli/tests/test_new_command.py b/openhands-cli/tests/test_new_command.py
new file mode 100644
index 0000000000..4f7031153c
--- /dev/null
+++ b/openhands-cli/tests/test_new_command.py
@@ -0,0 +1,100 @@
+"""Tests for the /new command functionality."""
+
+from unittest.mock import MagicMock, patch
+from uuid import UUID
+from openhands_cli.agent_chat import _start_fresh_conversation
+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.setup import MissingAgentSpec
+from openhands_cli.user_actions import UserConfirmation
+
+@patch('openhands_cli.agent_chat.setup_conversation')
+def test_start_fresh_conversation_success(mock_setup_conversation):
+ """Test that _start_fresh_conversation creates a new conversation successfully."""
+ # Mock the conversation object
+ mock_conversation = MagicMock()
+ mock_conversation.id = UUID('12345678-1234-5678-9abc-123456789abc')
+ mock_setup_conversation.return_value = mock_conversation
+
+ # Call the function
+ result = _start_fresh_conversation()
+
+ # Verify the result
+ assert result == mock_conversation
+ mock_setup_conversation.assert_called_once_with(None)
+
+
+@patch('openhands_cli.agent_chat.SettingsScreen')
+@patch('openhands_cli.agent_chat.setup_conversation')
+def test_start_fresh_conversation_missing_agent_spec(
+ mock_setup_conversation,
+ mock_settings_screen_class
+):
+ """Test that _start_fresh_conversation handles MissingAgentSpec exception."""
+ # Mock the SettingsScreen instance
+ mock_settings_screen = MagicMock()
+ mock_settings_screen_class.return_value = mock_settings_screen
+
+ # Mock setup_conversation to raise MissingAgentSpec on first call, then succeed
+ mock_conversation = MagicMock()
+ mock_conversation.id = UUID('12345678-1234-5678-9abc-123456789abc')
+ mock_setup_conversation.side_effect = [
+ MissingAgentSpec("Agent spec missing"),
+ mock_conversation
+ ]
+
+ # Call the function
+ result = _start_fresh_conversation()
+
+ # Verify the result
+ assert result == mock_conversation
+ # Should be called twice: first fails, second succeeds
+ assert mock_setup_conversation.call_count == 2
+ # Settings screen should be called once
+ mock_settings_screen.handle_basic_settings.assert_called_once_with(escapable=False)
+
+
+
+
+
+@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.ConversationRunner')
+def test_new_command_resets_confirmation_mode(
+ mock_runner_cls,
+ mock_setup_conversation,
+ mock_get_session_prompter,
+ mock_exit_confirm,
+):
+ # Auto-accept the exit prompt to avoid interactive UI and EOFError
+ mock_exit_confirm.return_value = UserConfirmation.ACCEPT
+
+ conv1 = MagicMock(); conv1.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
+ conv2 = MagicMock(); conv2.id = UUID('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')
+ mock_setup_conversation.side_effect = [conv1, conv2]
+
+ # Distinct runner instances for each conversation
+ runner1 = MagicMock(); runner1.is_confirmation_mode_enabled = True
+ runner2 = MagicMock(); runner2.is_confirmation_mode_enabled = False
+ mock_runner_cls.side_effect = [runner1, runner2]
+
+ # 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
+ 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 /status, then /exit (exit will be auto-accepted)
+ for ch in "/new\r/status\r/exit\r":
+ pipe.send_text(ch)
+
+ run_cli_entry(None)
+
+ # Assert we switched to a new runner for conv2
+ assert mock_runner_cls.call_count == 2
+ assert mock_runner_cls.call_args_list[0].args[0] is conv1
+ assert mock_runner_cls.call_args_list[1].args[0] is conv2
diff --git a/openhands-cli/tests/test_tui.py b/openhands-cli/tests/test_tui.py
index 1ffd7d887d..067bef177c 100644
--- a/openhands-cli/tests/test_tui.py
+++ b/openhands-cli/tests/test_tui.py
@@ -77,6 +77,7 @@ def test_commands_dict() -> None:
'/exit',
'/help',
'/clear',
+ '/new',
'/status',
'/confirm',
'/resume',