Files
agent-framework/python/samples/02-agents/harness/console/components/scroll_panel.py
T
westey bad05a2bdc Python: Harness console for python (#6312)
* Add initial harness console for python

* Add textual to project

* Add planning and approval flows with list selector

* Address PR comments

* Fix list selection bug

* Fix PR #6312 round 2 review comments

- Escape untrusted agent text with rich.markup.escape() in observers
  (text_output, planning_output, reasoning_display) to prevent markup injection
- Remove non-functional 'Always approve' choices from tool_approval.py
  (framework lacks CreateAlwaysApproveToolResponse support)
- Remove textual from root pyproject.toml dev deps (sample-specific)
- Add PEP 723 inline script metadata to harness_research.py
- Narrow except Exception to except NoMatches in list_selection.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix build error

* Fix build errors

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-09 05:48:35 +00:00

128 lines
4.2 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Scrolling panel for conversation history display."""
from __future__ import annotations
from typing import TYPE_CHECKING
from textual.widgets import RichLog
if TYPE_CHECKING:
from ..app_state import OutputEntry
class HarnessScrollPanel(RichLog):
"""Scrolling panel for displaying conversation history.
Uses Textual's RichLog widget for efficient append-only rendering with
Rich text formatting support. Automatically scrolls to the bottom when
new entries are added.
For streaming text, the panel uses a truncate-and-rewrite strategy: it
tracks where streaming began in the RichLog lines list, and on each update
truncates back to that point and rewrites the full accumulated text as a
single write. This ensures consistent rendering without line-break artifacts
between streamed chunks.
"""
def __init__(self, **kwargs) -> None:
"""Initialize the scroll panel.
Args:
**kwargs: Additional arguments passed to RichLog.
"""
super().__init__(
**kwargs,
auto_scroll=True, # Automatically scroll to bottom
wrap=True, # Wrap long lines instead of horizontal scroll
markup=True, # Enable Rich markup
highlight=True, # Enable syntax highlighting
)
self._entries: list[OutputEntry] = []
self._is_streaming = False
self._streaming_line_start: int = 0
def append_entry(self, entry: OutputEntry) -> None:
"""Append a new output entry to the conversation history.
Args:
entry: The output entry to append.
"""
self._entries.append(entry)
text = self._format_entry(entry)
self.write(text)
def set_streaming_entry(self, entry: OutputEntry) -> None:
"""Set or update the current streaming entry.
On each update, truncates the RichLog back to where streaming
started, then rewrites the full streaming text as a single block.
This ensures no spurious line breaks between chunks while avoiding
a full rewrite of all entries.
Args:
entry: The streaming entry (will be mutated externally).
"""
if not self._is_streaming:
# First streaming chunk — record where streaming lines begin
self._is_streaming = True
self._entries.append(entry)
self._streaming_line_start = len(self.lines)
# Truncate lines back to where streaming started
if len(self.lines) > self._streaming_line_start:
del self.lines[self._streaming_line_start:]
from textual.geometry import Size
self.virtual_size = Size(self._widest_line_width, len(self.lines))
# Write full streaming text as a single renderable
formatted = self._format_text(entry.text, entry.color)
self.write(formatted)
def end_streaming(self) -> None:
"""End the current streaming mode."""
if self._is_streaming:
self._is_streaming = False
self._streaming_line_start = 0
def _rewrite_all(self) -> None:
"""Clear and rewrite all entries from scratch."""
self.clear()
for entry in self._entries:
self.write(self._format_entry(entry))
def _format_entry(self, entry: OutputEntry) -> str:
"""Format an output entry with Rich markup.
Args:
entry: The entry to format.
Returns:
Formatted string with Rich markup for color and styling.
"""
return self._format_text(entry.text, entry.color)
@staticmethod
def _format_text(text: str, color: str | None) -> str:
"""Format text with optional Rich color markup.
Args:
text: The text to format.
color: Optional Rich color name.
Returns:
Formatted string.
"""
if color:
return f"[{color}]{text}[/{color}]"
return text
def clear_history(self) -> None:
"""Clear all conversation history from the panel."""
self._entries.clear()
self._is_streaming = False
self._streaming_line_start = 0
self.clear()