From 709b79f26a6151dcebaa79686bdb51fc02a801b8 Mon Sep 17 00:00:00 2001 From: Jathin Sreenivas Date: Mon, 13 Apr 2026 21:02:07 +0200 Subject: [PATCH 1/2] fix(tui): preserve Rich Text styling in collapsible content for diff highlighting --- .../tui/widgets/richlog_visualizer.py | 25 ++++---- tests/tui/widgets/test_richlog_visualizer.py | 64 +++++++++++++++++++ 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/openhands_cli/tui/widgets/richlog_visualizer.py b/openhands_cli/tui/widgets/richlog_visualizer.py index 594d975df..735bff2d6 100644 --- a/openhands_cli/tui/widgets/richlog_visualizer.py +++ b/openhands_cli/tui/widgets/richlog_visualizer.py @@ -407,7 +407,7 @@ def render_refinement_message(self, content: str) -> None: self._render_message_widget(content) def _update_widget_in_ui( - self, collapsible: Collapsible, new_title: str, new_content: str + self, collapsible: Collapsible, new_title: str | Text, new_content: str | Text ) -> None: """Update an existing widget in the UI (must be called from main thread).""" collapsible.update_title(new_title) @@ -505,14 +505,13 @@ def _build_action_title(self, event: ActionEvent) -> str: def _build_observation_content( self, event: ObservationEvent | UserRejectObservation | AgentErrorEvent - ) -> str: - """Build content string from an observation event. + ) -> str | Text: + """Build content from an observation event. - Returns the Rich-formatted content to preserve colors and styling. + Returns the Rich Text object directly to preserve colors and styling + (e.g., red/green diff highlighting from FileEditorObservation). """ - # Return the visualize content directly (Rich Text object) - # The Collapsible widget can handle Rich renderables - return str(event.visualize) + return event.visualize def _escape_rich_markup(self, text: str) -> str: """Escape Rich markup characters in text to prevent markup errors. @@ -761,9 +760,9 @@ def _create_titled_collapsible( ) -> Collapsible: """Create a standard titled collapsible for non-action events.""" title = self._extract_meaningful_title(event, fallback_title) - content_string = self._escape_rich_markup(str(event.visualize)) + content = event.visualize return self._make_collapsible( - content_string, + content, f"{self._get_agent_prefix()}{title}", event, ) @@ -807,7 +806,7 @@ def _create_event_collapsible(self, event: Event) -> Collapsible | None: if isinstance(event, ActionEvent): title = self._build_action_title(event) collapsible = self._make_collapsible( - self._escape_rich_markup(str(content)), + content, title, event, ) @@ -829,11 +828,9 @@ def _create_event_collapsible(self, event: Event) -> Collapsible | None: title = self._extract_meaningful_title( event, f"UNKNOWN Event: {event.__class__.__name__}" ) - content_string = ( - f"{self._escape_rich_markup(str(content))}\n\nSource: {event.source}" - ) + full_content = Text.assemble(content, f"\n\nSource: {event.source}") return self._make_collapsible( - content_string, + full_content, f"{self._get_agent_prefix()}{title}", event, ) diff --git a/tests/tui/widgets/test_richlog_visualizer.py b/tests/tui/widgets/test_richlog_visualizer.py index 9ca787048..7e80120fe 100644 --- a/tests/tui/widgets/test_richlog_visualizer.py +++ b/tests/tui/widgets/test_richlog_visualizer.py @@ -1441,3 +1441,67 @@ def test_action_title_has_prefix_for_non_default_agent(self, mock_cli_settings): assert "(Code Reviewer Agent)" in title # Should contain the command assert "git diff" in title + + +class TestRichTextPreservation: + """Tests that Rich Text styling is preserved through the rendering pipeline. + + Relates to: https://github.com/OpenHands/OpenHands-CLI/issues/308 + """ + + def test_build_observation_content_returns_text_object(self, visualizer): + """_build_observation_content should return Text, not str.""" + event = AgentErrorEvent( + tool_name="terminal", + tool_call_id="call_1", + error="test error", + ) + result = visualizer._build_observation_content(event) + assert isinstance(result, Text) + + def test_build_observation_content_preserves_styles(self, visualizer): + """_build_observation_content should preserve Rich Text spans/styles.""" + event = AgentErrorEvent( + tool_name="terminal", + tool_call_id="call_1", + error="test error", + ) + result = visualizer._build_observation_content(event) + # The Text object should have style spans (bold "Tool:", etc.) + assert isinstance(result, Text) + assert len(result._spans) > 0, "Text object should have style spans" + + def test_create_titled_collapsible_passes_text_object( + self, visualizer, mock_cli_settings + ): + """_create_titled_collapsible should pass Text to collapsible, not str.""" + event = ConversationErrorEvent( + source="agent", + code="test_error", + detail="Test error", + ) + with mock_cli_settings(visualizer=visualizer): + collapsible = visualizer._create_titled_collapsible(event, "Test Error") + # Content widget should have received a Text object, not plain str + content = collapsible._content_widget._Static__content + assert isinstance(content, Text) + + def test_create_event_collapsible_action_preserves_text( + self, visualizer, mock_cli_settings + ): + """Action collapsible content should be Text, not escaped str.""" + action = RichLogMockAction(command="test") + tool_call = create_tool_call("call_1", "test") + event = ActionEvent( + thought=[TextContent(text="testing")], + action=action, + tool_name="test", + tool_call_id="call_1", + tool_call=tool_call, + llm_response_id="resp_1", + ) + with mock_cli_settings(visualizer=visualizer): + collapsible = visualizer._create_event_collapsible(event) + assert collapsible is not None + # Verify it was stored in pending actions + assert "call_1" in visualizer._pending_actions From 89cbe4b856dd041631cf1a6bb300b5f26c7251bc Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Sun, 17 May 2026 17:35:21 +0200 Subject: [PATCH 2/2] Update tests/tui/widgets/test_richlog_visualizer.py Co-authored-by: OpenHands Bot --- tests/tui/widgets/test_richlog_visualizer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/tui/widgets/test_richlog_visualizer.py b/tests/tui/widgets/test_richlog_visualizer.py index 05b95f7d3..426b89269 100644 --- a/tests/tui/widgets/test_richlog_visualizer.py +++ b/tests/tui/widgets/test_richlog_visualizer.py @@ -1562,5 +1562,8 @@ def test_create_event_collapsible_action_preserves_text( with mock_cli_settings(visualizer=visualizer): collapsible = visualizer._create_event_collapsible(event) assert collapsible is not None + # Verify content is a Text object, not plain str + content = collapsible._content_widget._Static__content + assert isinstance(content, Text) # Verify it was stored in pending actions assert "call_1" in visualizer._pending_actions