#!/usr/bin/env python3
"""
Agent chat functionality for OpenHands CLI.
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 (
Message,
TextContent,
)
from openhands.sdk.conversation.state import AgentExecutionStatus
from openhands_cli.runner import ConversationRunner
from openhands_cli.setup import MissingAgentSpec, setup_conversation
from openhands_cli.tui.settings.mcp_screen import MCPScreen
from openhands_cli.tui.settings.settings_screen import SettingsScreen
from openhands_cli.tui.tui import (
display_help,
display_welcome,
)
from openhands_cli.user_actions import UserConfirmation, exit_session_confirmation
from openhands_cli.user_actions.utils import get_session_prompter
def _restore_tty() -> None:
"""
Ensure terminal modes are reset in case prompt_toolkit cleanup didn't run.
- Turn off application cursor keys (DECCKM): ESC[?1l
- Turn off bracketed paste: ESC[?2004l
"""
try:
sys.stdout.write('\x1b[?1l\x1b[?2004l')
sys.stdout.flush()
except Exception:
pass
def _print_exit_hint(conversation_id: str) -> None:
"""Print a resume hint with the current conversation ID."""
print_formatted_text(
HTML(f'Conversation ID: {conversation_id}')
)
print_formatted_text(
HTML(
f'Hint: run openhands --resume {conversation_id} '
'to resume this conversation.'
)
)
def run_cli_entry(resume_conversation_id: str | None = None) -> None:
"""Run the agent chat session using the agent SDK.
Raises:
AgentSetupError: If agent setup fails
KeyboardInterrupt: If user interrupts the session
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)
display_welcome(conversation.id, bool(resume_conversation_id))
# Create conversation runner to handle state machine logic
runner = ConversationRunner(conversation)
session = get_session_prompter()
# Main chat loop
while True:
try:
# Get user input
user_input = session.prompt(
HTML('> '),
multiline=False,
)
if not user_input.strip():
continue
# Handle commands
command = user_input.strip().lower()
message = Message(
role='user',
content=[TextContent(text=user_input)],
)
if command == '/exit':
exit_confirmation = exit_session_confirmation()
if exit_confirmation == UserConfirmation.ACCEPT:
print_formatted_text(HTML('\nGoodbye! 👋'))
_print_exit_hint(conversation.id)
break
elif command == '/settings':
settings_screen = SettingsScreen(conversation)
settings_screen.display_settings()
continue
elif command == '/mcp':
mcp_screen = MCPScreen()
mcp_screen.display_mcp_info(conversation.agent)
continue
elif command == '/clear':
display_welcome(conversation.id)
continue
elif command == '/help':
display_help()
continue
elif command == '/status':
print_formatted_text(
HTML(f'Conversation ID: {conversation.id}')
)
print_formatted_text(HTML('Status: Active'))
confirmation_status = (
'enabled' if conversation.state.confirmation_mode else 'disabled'
)
print_formatted_text(
HTML(f'Confirmation mode: {confirmation_status}')
)
continue
elif command == '/confirm':
runner.toggle_confirmation_mode()
new_status = (
'enabled' if runner.is_confirmation_mode_enabled else 'disabled'
)
print_formatted_text(
HTML(f'Confirmation mode {new_status}')
)
continue
elif command == '/resume':
if not (
conversation.state.agent_status == AgentExecutionStatus.PAUSED
or conversation.state.agent_status
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
):
print_formatted_text(
HTML('No paused conversation to resume...')
)
continue
# Resume without new message
message = None
runner.process_message(message)
print() # Add spacing
except KeyboardInterrupt:
exit_confirmation = exit_session_confirmation()
if exit_confirmation == UserConfirmation.ACCEPT:
print_formatted_text(HTML('\nGoodbye! 👋'))
_print_exit_hint(conversation.id)
break
# Clean up terminal state
_restore_tty()