mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
3 Commits
self-hoste
...
clean-mark
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0de6f9699 | ||
|
|
cc4b663cf7 | ||
|
|
7f9a43e217 |
@@ -5,7 +5,9 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@@ -28,6 +30,8 @@ from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from prompt_toolkit.shortcuts import print_container
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
|
||||
from openhands import __version__
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
@@ -36,6 +40,7 @@ from openhands.events import EventSource, EventStream
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
AgentFinishAction,
|
||||
ChangeAgentStateAction,
|
||||
CmdRunAction,
|
||||
MCPAction,
|
||||
@@ -65,10 +70,12 @@ MAX_RECENT_THOUGHTS = 5
|
||||
# Color and styling constants
|
||||
COLOR_GOLD = '#FFD700'
|
||||
COLOR_GREY = '#808080'
|
||||
COLOR_AGENT_BLUE = '#5FAFFF' # Soft blue for all agent outputs
|
||||
DEFAULT_STYLE = Style.from_dict(
|
||||
{
|
||||
'gold': COLOR_GOLD,
|
||||
'grey': COLOR_GREY,
|
||||
'agent-blue': COLOR_AGENT_BLUE,
|
||||
'prompt': f'{COLOR_GOLD} bold',
|
||||
}
|
||||
)
|
||||
@@ -252,7 +259,19 @@ def display_thought_if_new(thought: str) -> None:
|
||||
def display_event(event: Event, config: OpenHandsConfig) -> None:
|
||||
global streaming_output_text_area
|
||||
with print_lock:
|
||||
if isinstance(event, CmdRunAction):
|
||||
if isinstance(event, AgentFinishAction):
|
||||
# Handle agent finish actions with special styling
|
||||
# Determine the message to display
|
||||
if event.final_thought:
|
||||
message = event.final_thought
|
||||
elif event.thought:
|
||||
message = event.thought
|
||||
else:
|
||||
message = "All done! What's next on the agenda?"
|
||||
|
||||
# Display with finish styling
|
||||
display_agent_message(message, is_finish=True)
|
||||
elif isinstance(event, CmdRunAction):
|
||||
# For CmdRunAction, display thought first, then command
|
||||
if hasattr(event, 'thought') and event.thought:
|
||||
display_message(event.thought)
|
||||
@@ -275,8 +294,8 @@ def display_event(event: Event, config: OpenHandsConfig) -> None:
|
||||
|
||||
if isinstance(event, MessageAction):
|
||||
if event.source == EventSource.AGENT:
|
||||
# Check if this message content is a duplicate thought
|
||||
display_thought_if_new(event.content)
|
||||
# Display agent messages with distinctive styling
|
||||
display_agent_message(event.content)
|
||||
elif isinstance(event, CmdOutputObservation):
|
||||
display_command_output(event.content)
|
||||
elif isinstance(event, FileEditObservation):
|
||||
@@ -291,6 +310,24 @@ def display_event(event: Event, config: OpenHandsConfig) -> None:
|
||||
display_error(event.content)
|
||||
|
||||
|
||||
def process_markdown_for_terminal(text: str) -> str:
|
||||
"""
|
||||
Process markdown syntax for terminal display using Rich.
|
||||
This function renders markdown as formatted text for the terminal.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Use Rich to render the markdown without width constraints
|
||||
console = Console(file=io.StringIO(), highlight=False, width=None)
|
||||
console.print(Markdown(text))
|
||||
|
||||
# Get the rendered output
|
||||
rendered_text = console.file.getvalue() # type: ignore
|
||||
|
||||
return rendered_text.strip()
|
||||
|
||||
|
||||
def display_message(message: str) -> None:
|
||||
message = message.strip()
|
||||
|
||||
@@ -298,6 +335,38 @@ def display_message(message: str) -> None:
|
||||
print_formatted_text(f'\n{message}')
|
||||
|
||||
|
||||
def display_agent_message(message: str, is_finish: bool = False) -> None:
|
||||
"""
|
||||
Display a message from the agent with distinctive styling and markdown rendering.
|
||||
|
||||
Args:
|
||||
message: The message content to display
|
||||
is_finish: Whether this is a finish message (changes the icon)
|
||||
"""
|
||||
message = message.strip()
|
||||
|
||||
if message:
|
||||
# Process markdown in the message
|
||||
try:
|
||||
# Process markdown for terminal display
|
||||
processed_message = process_markdown_for_terminal(message)
|
||||
except Exception:
|
||||
# If markdown processing fails, use the original message
|
||||
processed_message = message
|
||||
|
||||
# Choose the appropriate icon based on message type
|
||||
icon = '🎯' if is_finish else '🔹'
|
||||
header_text = 'Agent Finished' if is_finish else 'Agent Message'
|
||||
|
||||
# Print a simple header
|
||||
print_formatted_text(FormattedText([('fg:' + COLOR_AGENT_BLUE, f'\n{icon} {header_text}')]))
|
||||
print_formatted_text('')
|
||||
|
||||
# Print the message content directly without any wrapping constraints
|
||||
print_formatted_text(FormattedText([('fg:' + COLOR_AGENT_BLUE, processed_message)]))
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
def display_error(error: str) -> None:
|
||||
error = error.strip()
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ numpy = "*"
|
||||
json-repair = "*"
|
||||
browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface
|
||||
html2text = "*"
|
||||
rich = "*" # For terminal formatting and markdown rendering
|
||||
deprecated = "*"
|
||||
pexpect = "*"
|
||||
jinja2 = "^3.1.3"
|
||||
|
||||
@@ -145,8 +145,8 @@ class TestThoughtDisplayOrder:
|
||||
# Verify that final thought is displayed
|
||||
mock_display_message.assert_called_once_with('This is a final thought.')
|
||||
|
||||
@patch('openhands.cli.tui.display_message')
|
||||
def test_message_action_from_agent(self, mock_display_message):
|
||||
@patch('openhands.cli.tui.display_agent_message')
|
||||
def test_message_action_from_agent(self, mock_display_agent_message):
|
||||
"""Test that MessageAction from agent is displayed."""
|
||||
config = MagicMock(spec=OpenHandsConfig)
|
||||
|
||||
@@ -156,8 +156,8 @@ class TestThoughtDisplayOrder:
|
||||
|
||||
display_event(message_action, config)
|
||||
|
||||
# Verify that message is displayed
|
||||
mock_display_message.assert_called_once_with('Hello from agent')
|
||||
# Verify that agent message is displayed
|
||||
mock_display_agent_message.assert_called_once_with('Hello from agent')
|
||||
|
||||
@patch('openhands.cli.tui.display_message')
|
||||
def test_message_action_from_user_not_displayed(self, mock_display_message):
|
||||
|
||||
@@ -6,6 +6,7 @@ from openhands.cli.tui import (
|
||||
CustomDiffLexer,
|
||||
UsageMetrics,
|
||||
UserCancelledError,
|
||||
display_agent_message,
|
||||
display_banner,
|
||||
display_command,
|
||||
display_event,
|
||||
@@ -26,6 +27,7 @@ from openhands.events import EventSource
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
AgentFinishAction,
|
||||
CmdRunAction,
|
||||
MCPAction,
|
||||
MessageAction,
|
||||
@@ -107,15 +109,15 @@ class TestDisplayFunctions:
|
||||
assert 'What do you want to build?' in message_text
|
||||
assert 'Type /help for help' in message_text
|
||||
|
||||
@patch('openhands.cli.tui.display_message')
|
||||
def test_display_event_message_action(self, mock_display_message):
|
||||
@patch('openhands.cli.tui.display_agent_message')
|
||||
def test_display_event_message_action(self, mock_display_agent_message):
|
||||
config = MagicMock(spec=OpenHandsConfig)
|
||||
message = MessageAction(content='Test message')
|
||||
message._source = EventSource.AGENT
|
||||
|
||||
display_event(message, config)
|
||||
|
||||
mock_display_message.assert_called_once_with('Test message')
|
||||
mock_display_agent_message.assert_called_once_with('Test message')
|
||||
|
||||
@patch('openhands.cli.tui.display_command')
|
||||
def test_display_event_cmd_action(self, mock_display_command):
|
||||
@@ -181,6 +183,15 @@ class TestDisplayFunctions:
|
||||
display_event(action, config)
|
||||
|
||||
mock_display_message.assert_called_once_with('Thinking about this...')
|
||||
|
||||
@patch('openhands.cli.tui.display_agent_message')
|
||||
def test_display_event_agent_finish(self, mock_display_agent_message):
|
||||
config = MagicMock(spec=OpenHandsConfig)
|
||||
finish_action = AgentFinishAction(final_thought='Task completed')
|
||||
|
||||
display_event(finish_action, config)
|
||||
|
||||
mock_display_agent_message.assert_called_once_with('Task completed', is_finish=True)
|
||||
|
||||
@patch('openhands.cli.tui.display_mcp_action')
|
||||
def test_display_event_mcp_action(self, mock_display_mcp_action):
|
||||
@@ -255,6 +266,37 @@ class TestDisplayFunctions:
|
||||
mock_print.assert_called_once()
|
||||
args, kwargs = mock_print.call_args
|
||||
assert message in str(args[0])
|
||||
|
||||
@patch('openhands.cli.tui.shutil.get_terminal_size')
|
||||
@patch('openhands.cli.tui.print_formatted_text')
|
||||
def test_display_agent_message(self, mock_print_formatted, mock_terminal_size):
|
||||
from collections import namedtuple
|
||||
|
||||
# Mock terminal size
|
||||
Size = namedtuple('Size', ['columns', 'lines'])
|
||||
mock_terminal_size.return_value = Size(columns=80, lines=24)
|
||||
|
||||
message = 'Agent message'
|
||||
display_agent_message(message)
|
||||
|
||||
# Should be called multiple times now (header, separator, content)
|
||||
assert mock_print_formatted.call_count >= 3
|
||||
|
||||
@patch('openhands.cli.tui.shutil.get_terminal_size')
|
||||
@patch('openhands.cli.tui.print_formatted_text')
|
||||
def test_display_agent_message_with_markdown(self, mock_print_formatted, mock_terminal_size):
|
||||
from collections import namedtuple
|
||||
|
||||
# Mock terminal size
|
||||
Size = namedtuple('Size', ['columns', 'lines'])
|
||||
mock_terminal_size.return_value = Size(columns=80, lines=24)
|
||||
|
||||
# Test with markdown content
|
||||
message = '# Heading\n\nThis is **bold** text.'
|
||||
display_agent_message(message)
|
||||
|
||||
# Should be called multiple times now (header, separator, content)
|
||||
assert mock_print_formatted.call_count >= 3
|
||||
|
||||
@patch('openhands.cli.tui.print_container')
|
||||
def test_display_command_awaiting_confirmation(self, mock_print_container):
|
||||
|
||||
Reference in New Issue
Block a user