Fix project isolation: Make loadChatHistory respect active project sessions

- Modified loadChatHistory() to check for active project before fetching all sessions
- When active project exists, use project.sessions instead of fetching from API
- Added detailed console logging to debug session filtering
- This prevents ALL sessions from appearing in every project's sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-22 14:43:05 +00:00
Unverified
parent b82837aa5f
commit 55aafbae9a
6463 changed files with 1115462 additions and 4486 deletions

View File

@@ -0,0 +1,121 @@
# ABOUTME: Output formatter module initialization
# ABOUTME: Exports base classes, formatter implementations, and legacy console classes
"""Output formatting module for Claude adapter responses.
This module provides:
1. Legacy output utilities (backward compatible):
- DiffStats, DiffFormatter, RalphConsole - Rich terminal utilities
- RICH_AVAILABLE - Rich library availability flag
2. New formatter classes for structured output:
- PlainTextFormatter: Basic text output without colors
- RichTerminalFormatter: Rich terminal output with colors and panels
- JsonFormatter: Structured JSON output for programmatic consumption
Example usage (new formatters):
from ralph_orchestrator.output import (
RichTerminalFormatter,
VerbosityLevel,
ToolCallInfo,
)
formatter = RichTerminalFormatter(verbosity=VerbosityLevel.VERBOSE)
tool_info = ToolCallInfo(tool_name="Read", tool_id="abc123", input_params={"path": "test.py"})
output = formatter.format_tool_call(tool_info, iteration=1)
formatter.print(output)
Example usage (legacy console):
from ralph_orchestrator.output import RalphConsole
console = RalphConsole()
console.print_status("Processing...")
console.print_success("Done!")
"""
# Import legacy classes from console module (backward compatibility)
from .console import (
RICH_AVAILABLE,
DiffFormatter,
DiffStats,
RalphConsole,
)
# Import new formatter base classes
from .base import (
FormatContext,
MessageType,
OutputFormatter,
TokenUsage,
ToolCallInfo,
VerbosityLevel,
)
# Import content detection
from .content_detector import ContentDetector, ContentType
# Import new formatter implementations
from .json_formatter import JsonFormatter
from .plain import PlainTextFormatter
from .rich_formatter import RichTerminalFormatter
__all__ = [
# Legacy exports (backward compatibility)
"RICH_AVAILABLE",
"DiffStats",
"DiffFormatter",
"RalphConsole",
# New base classes
"OutputFormatter",
"VerbosityLevel",
"MessageType",
"TokenUsage",
"ToolCallInfo",
"FormatContext",
# Content detection
"ContentDetector",
"ContentType",
# New formatters
"PlainTextFormatter",
"RichTerminalFormatter",
"JsonFormatter",
# Factory function
"create_formatter",
]
def create_formatter(
format_type: str = "rich",
verbosity: VerbosityLevel = VerbosityLevel.NORMAL,
**kwargs,
) -> OutputFormatter:
"""Factory function to create appropriate formatter.
Args:
format_type: Type of formatter ("plain", "rich", "json")
verbosity: Verbosity level for output
**kwargs: Additional arguments passed to formatter constructor
Returns:
Configured OutputFormatter instance
Raises:
ValueError: If format_type is not recognized
"""
formatters = {
"plain": PlainTextFormatter,
"text": PlainTextFormatter,
"rich": RichTerminalFormatter,
"terminal": RichTerminalFormatter,
"json": JsonFormatter,
}
if format_type.lower() not in formatters:
raise ValueError(
f"Unknown format type: {format_type}. "
f"Valid options: {', '.join(formatters.keys())}"
)
formatter_class = formatters[format_type.lower()]
return formatter_class(verbosity=verbosity, **kwargs)

View File

@@ -0,0 +1,404 @@
# ABOUTME: Base classes and interfaces for output formatting
# ABOUTME: Defines OutputFormatter ABC with verbosity levels, event types, and token tracking
"""Base classes for Claude adapter output formatting."""
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Dict, List, Optional
_logger = logging.getLogger(__name__)
class VerbosityLevel(Enum):
"""Verbosity levels for output formatting."""
QUIET = 0 # Only errors and final results
NORMAL = 1 # Tool calls, assistant messages (no details)
VERBOSE = 2 # Full tool inputs/outputs, detailed messages
DEBUG = 3 # Everything including internal state
class MessageType(Enum):
"""Types of messages that can be formatted."""
SYSTEM = "system"
ASSISTANT = "assistant"
USER = "user"
TOOL_CALL = "tool_call"
TOOL_RESULT = "tool_result"
ERROR = "error"
INFO = "info"
PROGRESS = "progress"
@dataclass
class TokenUsage:
"""Tracks token usage and costs."""
input_tokens: int = 0
output_tokens: int = 0
total_tokens: int = 0
cost: float = 0.0
model: str = ""
# Running totals across session
session_input_tokens: int = 0
session_output_tokens: int = 0
session_total_tokens: int = 0
session_cost: float = 0.0
def add(
self,
input_tokens: int = 0,
output_tokens: int = 0,
cost: float = 0.0,
model: str = "",
) -> None:
"""Add tokens to current and session totals."""
self.input_tokens = input_tokens
self.output_tokens = output_tokens
self.total_tokens = input_tokens + output_tokens
self.cost = cost
if model:
self.model = model
self.session_input_tokens += input_tokens
self.session_output_tokens += output_tokens
self.session_total_tokens += input_tokens + output_tokens
self.session_cost += cost
def reset_current(self) -> None:
"""Reset current iteration tokens (keep session totals)."""
self.input_tokens = 0
self.output_tokens = 0
self.total_tokens = 0
self.cost = 0.0
@dataclass
class ToolCallInfo:
"""Information about a tool call."""
tool_name: str
tool_id: str
input_params: Dict[str, Any] = field(default_factory=dict)
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
result: Optional[Any] = None
is_error: bool = False
duration_ms: Optional[int] = None
@dataclass
class FormatContext:
"""Context information for formatting operations."""
iteration: int = 0
verbosity: VerbosityLevel = VerbosityLevel.NORMAL
timestamp: Optional[datetime] = None
token_usage: Optional[TokenUsage] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.timestamp is None:
self.timestamp = datetime.now()
if self.token_usage is None:
self.token_usage = TokenUsage()
class OutputFormatter(ABC):
"""Abstract base class for output formatters.
Formatters handle rendering of Claude adapter events to different output
formats (plain text, rich terminal, JSON, etc.). They support verbosity
levels and consistent token usage tracking.
"""
def __init__(self, verbosity: VerbosityLevel = VerbosityLevel.NORMAL) -> None:
"""Initialize formatter with verbosity level.
Args:
verbosity: Output verbosity level
"""
self._verbosity = verbosity
self._token_usage = TokenUsage()
self._start_time = datetime.now()
self._callbacks: List[Callable[[MessageType, Any, FormatContext], None]] = []
@property
def verbosity(self) -> VerbosityLevel:
"""Get current verbosity level."""
return self._verbosity
@verbosity.setter
def verbosity(self, level: VerbosityLevel) -> None:
"""Set verbosity level."""
self._verbosity = level
@property
def token_usage(self) -> TokenUsage:
"""Get current token usage."""
return self._token_usage
def should_display(self, message_type: MessageType) -> bool:
"""Check if message type should be displayed at current verbosity.
Args:
message_type: Type of message to check
Returns:
True if message should be displayed
"""
# Always show errors
if message_type == MessageType.ERROR:
return True
if self._verbosity == VerbosityLevel.QUIET:
return False
if self._verbosity == VerbosityLevel.NORMAL:
return message_type in (
MessageType.ASSISTANT,
MessageType.TOOL_CALL,
MessageType.PROGRESS,
MessageType.INFO,
)
# VERBOSE and DEBUG show everything
return True
def register_callback(
self, callback: Callable[[MessageType, Any, FormatContext], None]
) -> None:
"""Register a callback for format events.
Args:
callback: Function to call with (message_type, content, context)
"""
self._callbacks.append(callback)
def _notify_callbacks(
self, message_type: MessageType, content: Any, context: FormatContext
) -> None:
"""Notify all registered callbacks."""
for callback in self._callbacks:
try:
callback(message_type, content, context)
except Exception as e:
# Log but don't let callback errors break formatting
callback_name = getattr(callback, "__name__", repr(callback))
_logger.debug(
"Callback %s failed for %s: %s: %s",
callback_name,
message_type,
type(e).__name__,
e,
)
def _create_context(
self, iteration: int = 0, metadata: Optional[Dict[str, Any]] = None
) -> FormatContext:
"""Create a format context with current state.
Args:
iteration: Current iteration number
metadata: Additional metadata
Returns:
FormatContext instance
"""
return FormatContext(
iteration=iteration,
verbosity=self._verbosity,
timestamp=datetime.now(),
token_usage=self._token_usage,
metadata=metadata or {},
)
def update_tokens(
self,
input_tokens: int = 0,
output_tokens: int = 0,
cost: float = 0.0,
model: str = "",
) -> None:
"""Update token usage tracking.
Args:
input_tokens: Number of input tokens used
output_tokens: Number of output tokens used
cost: Cost in USD
model: Model name
"""
self._token_usage.add(input_tokens, output_tokens, cost, model)
@abstractmethod
def format_tool_call(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool call for display.
Args:
tool_info: Tool call information
iteration: Current iteration number
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_tool_result(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool result for display.
Args:
tool_info: Tool call info with result
iteration: Current iteration number
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_assistant_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format an assistant message for display.
Args:
message: Assistant message text
iteration: Current iteration number
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_system_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format a system message for display.
Args:
message: System message text
iteration: Current iteration number
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_error(
self,
error: str,
exception: Optional[Exception] = None,
iteration: int = 0,
) -> str:
"""Format an error for display.
Args:
error: Error message
exception: Optional exception object
iteration: Current iteration number
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_progress(
self,
message: str,
current: int = 0,
total: int = 0,
iteration: int = 0,
) -> str:
"""Format progress information for display.
Args:
message: Progress message
current: Current progress value
total: Total progress value
iteration: Current iteration number
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_token_usage(self, show_session: bool = True) -> str:
"""Format token usage summary for display.
Args:
show_session: Include session totals
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_section_header(self, title: str, iteration: int = 0) -> str:
"""Format a section header for display.
Args:
title: Section title
iteration: Current iteration number
Returns:
Formatted string representation
"""
pass
@abstractmethod
def format_section_footer(self) -> str:
"""Format a section footer for display.
Returns:
Formatted string representation
"""
pass
def summarize_content(self, content: str, max_length: int = 500) -> str:
"""Summarize long content for display.
Args:
content: Content to summarize
max_length: Maximum length before truncation
Returns:
Summarized content
"""
if len(content) <= max_length:
return content
# Truncate with indicator
half = (max_length - 20) // 2
return f"{content[:half]}\n... [{len(content)} chars truncated] ...\n{content[-half:]}"
def get_elapsed_time(self) -> float:
"""Get elapsed time since formatter creation.
Returns:
Elapsed time in seconds
"""
return (datetime.now() - self._start_time).total_seconds()

View File

@@ -0,0 +1,915 @@
# ABOUTME: Colored terminal output utilities using Rich
# ABOUTME: Provides DiffFormatter, DiffStats, and RalphConsole for enhanced CLI output
"""Colored terminal output utilities using Rich."""
import logging
import re
from dataclasses import dataclass, field
from typing import Optional
_logger = logging.getLogger(__name__)
# Try to import Rich components with fallback
try:
from rich.console import Console
from rich.markdown import Markdown
from rich.markup import escape
from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
Console = None # type: ignore
Markdown = None # type: ignore
Panel = None # type: ignore
Syntax = None # type: ignore
Table = None # type: ignore
def escape(x: str) -> str:
"""Fallback escape function."""
return str(x).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@dataclass
class DiffStats:
"""Statistics for diff content."""
additions: int = 0
deletions: int = 0
files: int = 0
files_changed: dict[str, tuple[int, int]] = field(
default_factory=dict
) # filename -> (additions, deletions)
class DiffFormatter:
"""Formatter for enhanced diff visualization."""
# Diff display constants
MAX_CONTEXT_LINES = 3 # Maximum context lines to show before/after changes
LARGE_DIFF_THRESHOLD = 100 # Lines count for "large diff" detection
SEPARATOR_WIDTH = 60 # Width of visual separators
LINE_NUM_WIDTH = 6 # Width for line number display
# Binary file patterns
BINARY_EXTENSIONS = {
".png",
".jpg",
".jpeg",
".gif",
".pdf",
".zip",
".tar",
".gz",
".so",
".pyc",
".exe",
".dll",
}
def __init__(self, console: "Console") -> None:
"""
Initialize diff formatter.
Args:
console: Rich console for output
"""
self.console = console
def format_and_print(self, text: str) -> None:
"""
Print diff with enhanced visualization and file path highlighting.
Features:
- Color-coded diff lines (additions, deletions, context)
- File path highlighting with improved contrast
- Diff statistics summary (+X/-Y lines) with per-file breakdown
- Visual separation between file changes with subtle styling
- Enhanced hunk headers with line range info and context highlighting
- Smart context line limiting for large diffs
- Improved spacing for better readability
- Binary file detection and special handling
- Empty diff detection with clear messaging
Args:
text: Diff text to render
"""
if not RICH_AVAILABLE:
print(text)
return
lines = text.split("\n")
# Calculate diff statistics
stats = self._calculate_stats(lines)
# Handle empty diffs
if stats.additions == 0 and stats.deletions == 0 and stats.files == 0:
self.console.print("[dim italic]No changes detected[/dim italic]")
return
# Print summary if we have changes
self._print_summary(stats)
current_file = None
current_file_name = None
context_line_count = 0
in_change_section = False
for line in lines:
# File headers - highlight with bold cyan
if line.startswith("diff --git"):
# Add visual separator between files (except first)
if current_file is not None:
# Print per-file stats before separator
if current_file_name is not None:
self._print_file_stats(current_file_name, stats)
self.console.print()
self.console.print(f"[dim]{'' * self.SEPARATOR_WIDTH}[/dim]")
self.console.print()
current_file = line
# Extract filename for stats tracking
current_file_name = self._extract_filename(line)
# Check for binary files
if self._is_binary_file(line):
self.console.print(
f"[bold magenta]{line} [dim](binary)[/dim][/bold magenta]"
)
else:
self.console.print(f"[bold cyan]{line}[/bold cyan]")
context_line_count = 0
elif line.startswith("Binary files"):
# Binary file indicator
self.console.print(f"[yellow]📦 {line}[/yellow]")
continue
elif line.startswith("---") or line.startswith("+++"):
# File paths - extract and highlight
if line.startswith("---"):
self.console.print(f"[bold red]{line}[/bold red]")
else: # +++
self.console.print(f"[bold green]{line}[/bold green]")
# Hunk headers - enhanced with context
elif line.startswith("@@"):
# Add subtle spacing before hunk for better visual separation
if context_line_count > 0:
self.console.print()
# Extract line ranges for better readability
hunk_info = self._format_hunk_header(line)
self.console.print(f"[bold magenta]{hunk_info}[/bold magenta]")
context_line_count = 0
in_change_section = False
# Added lines - enhanced with bold for better contrast
elif line.startswith("+"):
self.console.print(f"[bold green]{line}[/bold green]")
in_change_section = True
context_line_count = 0
# Removed lines - enhanced with bold for better contrast
elif line.startswith("-"):
self.console.print(f"[bold red]{line}[/bold red]")
in_change_section = True
context_line_count = 0
# Context lines
else:
# Only show limited context lines for large diffs
if stats.additions + stats.deletions > self.LARGE_DIFF_THRESHOLD:
# Show context around changes only
if in_change_section:
if context_line_count < self.MAX_CONTEXT_LINES:
self.console.print(f"[dim]{line}[/dim]")
context_line_count += 1
elif context_line_count == self.MAX_CONTEXT_LINES:
self.console.print(
"[dim italic] ⋮ (context lines omitted for readability)[/dim italic]"
)
context_line_count += 1
else:
# Leading context - always show up to limit
if context_line_count < self.MAX_CONTEXT_LINES:
self.console.print(f"[dim]{line}[/dim]")
context_line_count += 1
else:
# Small diff - show all context
self.console.print(f"[dim]{line}[/dim]")
# Print final file stats
if current_file_name:
self._print_file_stats(current_file_name, stats)
# Add spacing after diff for better separation from next content
self.console.print()
def _calculate_stats(self, lines: list[str]) -> DiffStats:
"""
Calculate statistics from diff lines including per-file breakdown.
Args:
lines: List of diff lines
Returns:
DiffStats with additions, deletions, files count, and per-file breakdown
"""
stats = DiffStats()
current_file = None
for line in lines:
if line.startswith("diff --git"):
stats.files += 1
current_file = self._extract_filename(line)
if current_file and current_file not in stats.files_changed:
stats.files_changed[current_file] = (0, 0)
elif line.startswith("+") and not line.startswith("+++"):
stats.additions += 1
if current_file and current_file in stats.files_changed:
adds, dels = stats.files_changed[current_file]
stats.files_changed[current_file] = (adds + 1, dels)
elif line.startswith("-") and not line.startswith("---"):
stats.deletions += 1
if current_file and current_file in stats.files_changed:
adds, dels = stats.files_changed[current_file]
stats.files_changed[current_file] = (adds, dels + 1)
return stats
def _print_summary(self, stats: DiffStats) -> None:
"""
Print diff statistics summary.
Args:
stats: Diff statistics
"""
if stats.additions == 0 and stats.deletions == 0:
return
summary = "[bold cyan]📊 Changes:[/bold cyan] "
if stats.additions > 0:
summary += f"[green]+{stats.additions}[/green]"
if stats.additions > 0 and stats.deletions > 0:
summary += " "
if stats.deletions > 0:
summary += f"[red]-{stats.deletions}[/red]"
if stats.files > 1:
summary += f" [dim]({stats.files} files)[/dim]"
self.console.print(summary)
self.console.print()
def _is_binary_file(self, diff_header: str) -> bool:
"""
Check if diff is for a binary file based on extension.
Args:
diff_header: Diff header line (e.g., "diff --git a/file.png b/file.png")
Returns:
True if file appears to be binary
"""
from pathlib import Path
# Extract file path from diff header
parts = diff_header.split()
if len(parts) >= 3:
file_path = parts[2] # e.g., "a/file.png"
ext = Path(file_path).suffix.lower()
return ext in self.BINARY_EXTENSIONS
return False
def _extract_filename(self, diff_header: str) -> Optional[str]:
"""
Extract filename from diff header line.
Args:
diff_header: Diff header line (e.g., "diff --git a/file.py b/file.py")
Returns:
Filename or None if not found
"""
parts = diff_header.split()
if len(parts) >= 3:
# Extract from "a/file.py" or "b/file.py"
file_path = parts[2]
if file_path.startswith("a/") or file_path.startswith("b/"):
return file_path[2:]
return file_path
return None
def _print_file_stats(self, filename: str, stats: DiffStats) -> None:
"""
Print per-file statistics with visual bar.
Args:
filename: Name of the file
stats: DiffStats containing per-file breakdown
"""
if filename and filename in stats.files_changed:
adds, dels = stats.files_changed[filename]
if adds > 0 or dels > 0:
# Calculate visual bar proportions (max 30 chars)
total_changes = adds + dels
bar_width = min(30, total_changes)
if total_changes > 0:
add_width = int((adds / total_changes) * bar_width)
del_width = bar_width - add_width
# Create visual bar
bar = ""
if add_width > 0:
bar += f"[bold green]{'' * add_width}[/bold green]"
if del_width > 0:
bar += f"[bold red]{'' * del_width}[/bold red]"
# Print stats with bar
summary = f" {bar} "
if adds > 0:
summary += f"[bold green]+{adds}[/bold green]"
if adds > 0 and dels > 0:
summary += " "
if dels > 0:
summary += f"[bold red]-{dels}[/bold red]"
self.console.print(summary)
def _format_hunk_header(self, hunk: str) -> str:
"""
Format hunk header with enhanced readability and context highlighting.
Transforms: @@ -140,7 +140,7 @@ class RalphConsole:
Into: @@ Lines 140-147 → 140-147 @@ class RalphConsole:
With context (function/class name) highlighted in cyan.
Args:
hunk: Original hunk header line
Returns:
Formatted hunk header with improved readability
"""
# Extract line ranges using regex
pattern = r"@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$"
match = re.search(pattern, hunk)
if not match:
return hunk
old_start = int(match.group(1))
old_count = int(match.group(2)) if match.group(2) else 1
new_start = int(match.group(3))
new_count = int(match.group(4)) if match.group(4) else 1
context = match.group(5).strip()
# Calculate end lines
old_end = old_start + old_count - 1
new_end = new_start + new_count - 1
# Format with readable line ranges
header = f"@@ Lines {old_start}-{old_end}{new_start}-{new_end} @@"
# Highlight context (function/class name) if present
if context:
# Highlight the context in cyan for better visibility
header += f" [cyan]{context}[/cyan]"
return header
class RalphConsole:
"""Rich console wrapper for Ralph output."""
# Display constants
CLEAR_LINE_WIDTH = 80 # Characters to clear when clearing a line
PROGRESS_BAR_WIDTH = 30 # Width of progress bar in characters
COUNTDOWN_COLOR_CHANGE_THRESHOLD_HIGH = 5 # Seconds remaining for yellow
COUNTDOWN_COLOR_CHANGE_THRESHOLD_LOW = 2 # Seconds remaining for red
MARKDOWN_INDICATOR_THRESHOLD = 2 # Minimum markdown patterns to consider as markdown
DIFF_SCAN_LINE_LIMIT = 5 # Number of lines to scan for diff indicators
DIFF_HUNK_SCAN_CHARS = 100 # Characters to scan for diff hunk markers
# Regex patterns for content detection and formatting
CODE_BLOCK_PATTERN = r"```(\w+)?\n(.*?)\n```"
FILE_REF_PATTERN = r"(\S+\.[a-zA-Z0-9]+):(\d+)"
INLINE_CODE_PATTERN = r"`([^`\n]+)`"
HUNK_HEADER_PATTERN = r"@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$"
TABLE_SEPARATOR_PATTERN = r"^\s*\|[\s\-:|]+\|\s*$"
MARKDOWN_HEADING_PATTERN = r"^#{1,6}\s+.+"
MARKDOWN_UNORDERED_LIST_PATTERN = r"^[\*\-]\s+.+"
MARKDOWN_ORDERED_LIST_PATTERN = r"^\d+\.\s+.+"
MARKDOWN_BOLD_PATTERN = r"\*\*.+?\*\*"
MARKDOWN_ITALIC_PATTERN = r"\*.+?\*"
MARKDOWN_BLOCKQUOTE_PATTERN = r"^>\s+.+"
MARKDOWN_TASK_LIST_PATTERN = r"^[\*\-]\s+\[([ xX])\]\s+.+"
MARKDOWN_HORIZONTAL_RULE_PATTERN = r"^(\-{3,}|\*{3,}|_{3,})\s*$"
def __init__(self) -> None:
"""Initialize Rich console."""
if RICH_AVAILABLE:
self.console = Console()
self.diff_formatter = DiffFormatter(self.console)
else:
self.console = None
self.diff_formatter = None
def print_status(self, message: str, style: str = "cyan") -> None:
"""Print status message."""
if self.console:
# Use markup escaping to prevent Rich from parsing brackets in the icon
self.console.print(f"[{style}][[*]] {message}[/{style}]")
else:
print(f"[*] {message}")
def print_success(self, message: str) -> None:
"""Print success message."""
if self.console:
self.console.print(f"[green]✓[/green] {message}")
else:
print(f"{message}")
def print_error(self, message: str, severity: str = "error") -> None:
"""
Print error message with severity-based formatting.
Args:
message: Error message to print
severity: Error severity level ("critical", "error", "warning")
"""
severity_styles = {
"critical": ("[red bold]⛔[/red bold]", "red bold"),
"error": ("[red]✗[/red]", "red"),
"warning": ("[yellow]⚠[/yellow]", "yellow"),
}
icon, style = severity_styles.get(severity, severity_styles["error"])
if self.console:
self.console.print(f"{icon} [{style}]{message}[/{style}]")
else:
print(f"{message}")
def print_warning(self, message: str) -> None:
"""Print warning message."""
if self.console:
self.console.print(f"[yellow]⚠[/yellow] {message}")
else:
print(f"{message}")
def print_info(self, message: str) -> None:
"""Print info message."""
if self.console:
self.console.print(f"[blue][/blue] {message}")
else:
print(f" {message}")
def print_header(self, title: str) -> None:
"""Print section header."""
if self.console and Panel:
self.console.print(
Panel(title, style="green bold", border_style="green"),
justify="left",
)
else:
print(f"\n=== {title} ===\n")
def print_iteration_header(self, iteration: int) -> None:
"""Print iteration header."""
if self.console:
self.console.print(
f"\n[cyan bold]=== RALPH ITERATION {iteration} ===[/cyan bold]\n"
)
else:
print(f"\n=== RALPH ITERATION {iteration} ===\n")
def print_stats(
self,
iteration: int,
success_count: int,
error_count: int,
start_time: str,
prompt_file: str,
recent_lines: list[str],
) -> None:
"""
Print statistics table.
Args:
iteration: Current iteration number
success_count: Number of successful iterations
error_count: Number of failed iterations
start_time: Start time string
prompt_file: Prompt file name
recent_lines: Recent log entries
"""
if not self.console or not Table:
# Plain text fallback
print("\nRALPH STATISTICS")
print(f" Iteration: {iteration}")
print(f" Successful: {success_count}")
print(f" Failed: {error_count}")
print(f" Started: {start_time}")
print(f" Prompt: {prompt_file}")
return
# Create stats table with better formatting
table = Table(
title="🤖 RALPH STATISTICS",
show_header=True,
header_style="bold yellow",
border_style="cyan",
)
table.add_column("Metric", style="cyan bold", no_wrap=True, width=20)
table.add_column("Value", style="white", width=40)
# Calculate success rate
total = success_count + error_count
success_rate = (success_count / total * 100) if total > 0 else 0
table.add_row("🔄 Current Iteration", str(iteration))
table.add_row("✅ Successful", f"[green bold]{success_count}[/green bold]")
table.add_row("❌ Failed", f"[red bold]{error_count}[/red bold]")
# Determine success rate color based on percentage
if success_rate > 80:
rate_color = "green"
elif success_rate > 50:
rate_color = "yellow"
else:
rate_color = "red"
table.add_row("📊 Success Rate", f"[{rate_color}]{success_rate:.1f}%[/]")
table.add_row("🕐 Started", start_time or "Unknown")
table.add_row("📝 Prompt", prompt_file)
self.console.print(table)
# Show recent activity with better formatting
if recent_lines:
self.console.print("\n[yellow bold]📋 RECENT ACTIVITY[/yellow bold]")
for line in recent_lines:
# Clean up log lines for display and escape Rich markup
clean_line = escape(line.strip())
if "[SUCCESS]" in clean_line:
self.console.print(f" [green]▸[/green] {clean_line}")
elif "[ERROR]" in clean_line:
self.console.print(f" [red]▸[/red] {clean_line}")
elif "[WARNING]" in clean_line:
self.console.print(f" [yellow]▸[/yellow] {clean_line}")
else:
self.console.print(f" [blue]▸[/blue] {clean_line}")
self.console.print()
def print_countdown(self, remaining: int, total: int) -> None:
"""
Print countdown timer with progress bar.
Args:
remaining: Seconds remaining
total: Total delay seconds
"""
# Guard against division by zero
if total <= 0:
return
# Calculate progress
progress = (total - remaining) / total
filled = int(self.PROGRESS_BAR_WIDTH * progress)
bar = "" * filled + "" * (self.PROGRESS_BAR_WIDTH - filled)
# Color based on time remaining (using constants)
if remaining > self.COUNTDOWN_COLOR_CHANGE_THRESHOLD_HIGH:
color = "green"
elif remaining > self.COUNTDOWN_COLOR_CHANGE_THRESHOLD_LOW:
color = "yellow"
else:
color = "red"
if self.console:
self.console.print(
f"\r[{color}]⏳ [{bar}] {remaining}s / {total}s remaining[/{color}]",
end="",
)
else:
print(f"\r⏳ [{bar}] {remaining}s / {total}s remaining", end="")
def clear_line(self) -> None:
"""Clear current line."""
if self.console:
self.console.print("\r" + " " * self.CLEAR_LINE_WIDTH + "\r", end="")
else:
print("\r" + " " * self.CLEAR_LINE_WIDTH + "\r", end="")
def print_separator(self) -> None:
"""Print visual separator."""
if self.console:
self.console.print("\n[cyan]---[/cyan]\n")
else:
print("\n---\n")
def clear_screen(self) -> None:
"""Clear screen."""
if self.console:
self.console.clear()
else:
print("\033[2J\033[H", end="")
def print_message(self, text: str) -> None:
"""
Print message with intelligent formatting and improved visual hierarchy.
Detects and formats:
- Code blocks (```language) with syntax highlighting
- Diffs (lines starting with +, -, @@) with enhanced visualization
- Markdown tables with proper rendering
- Markdown headings, lists, emphasis with spacing
- Inline code (`code`) with highlighting
- Plain text with file path detection
Args:
text: Message text to print
"""
if not self.console:
print(text)
return
# Check if text contains code blocks
if "```" in text:
# Split text by code blocks and process each part
parts = re.split(self.CODE_BLOCK_PATTERN, text, flags=re.DOTALL)
for i, part in enumerate(parts):
if i % 3 == 0: # Regular text between code blocks
if part.strip():
self._print_formatted_text(part)
# Add subtle spacing after text before code block
if i + 1 < len(parts):
self.console.print()
elif i % 3 == 1: # Language identifier
language = part or "text"
code = parts[i + 1] if i + 1 < len(parts) else ""
if code.strip() and Syntax:
# Use syntax highlighting for code blocks with enhanced features
syntax = Syntax(
code,
language,
theme="monokai",
line_numbers=True,
word_wrap=True,
indent_guides=True,
padding=(1, 2),
)
self.console.print(syntax)
# Add spacing after code block if more content follows
if i + 2 < len(parts) and parts[i + 2].strip():
self.console.print()
elif self._is_diff_content(text):
# Format as diff with enhanced visualization
if self.diff_formatter:
self.diff_formatter.format_and_print(text)
else:
print(text)
elif self._is_markdown_table(text):
# Render markdown tables nicely
self._print_markdown_table(text)
# Add spacing after table
self.console.print()
elif self._is_markdown_content(text):
# Render rich markdown with headings, lists, emphasis
self._print_markdown(text)
else:
# Regular text - check for inline code and format accordingly
self._print_formatted_text(text)
def _is_diff_content(self, text: str) -> bool:
"""
Check if text appears to be diff content.
Args:
text: Text to check
Returns:
True if text looks like diff output
"""
diff_indicators = [
text.startswith("diff --git"),
text.startswith("--- "),
text.startswith("+++ "),
"@@" in text[: self.DIFF_HUNK_SCAN_CHARS], # Diff hunk markers
any(
line.startswith(("+", "-", "@@"))
for line in text.split("\n")[: self.DIFF_SCAN_LINE_LIMIT]
),
]
return any(diff_indicators)
def _is_markdown_table(self, text: str) -> bool:
"""
Check if text appears to be a markdown table.
Args:
text: Text to check
Returns:
True if text looks like a markdown table
"""
lines = text.strip().split("\n")
if len(lines) < 2:
return False
# Check for table separator line (e.g., |---|---|)
for line in lines[:3]:
if re.match(self.TABLE_SEPARATOR_PATTERN, line):
return True
return False
def _print_markdown_table(self, text: str) -> None:
"""
Print markdown table with Rich formatting.
Args:
text: Markdown table text
"""
if Markdown:
# Use Rich's Markdown renderer for tables
md = Markdown(text)
self.console.print(md)
else:
print(text)
def _is_markdown_content(self, text: str) -> bool:
"""
Check if text appears to contain rich markdown (headings, lists, etc.).
Args:
text: Text to check
Returns:
True if text looks like markdown with formatting
"""
markdown_indicators = [
re.search(self.MARKDOWN_HEADING_PATTERN, text, re.MULTILINE), # Headings
re.search(
self.MARKDOWN_UNORDERED_LIST_PATTERN, text, re.MULTILINE
), # Unordered lists
re.search(
self.MARKDOWN_ORDERED_LIST_PATTERN, text, re.MULTILINE
), # Ordered lists
re.search(self.MARKDOWN_BOLD_PATTERN, text), # Bold
re.search(self.MARKDOWN_ITALIC_PATTERN, text), # Italic
re.search(
self.MARKDOWN_BLOCKQUOTE_PATTERN, text, re.MULTILINE
), # Blockquotes
re.search(
self.MARKDOWN_TASK_LIST_PATTERN, text, re.MULTILINE
), # Task lists
re.search(
self.MARKDOWN_HORIZONTAL_RULE_PATTERN, text, re.MULTILINE
), # Horizontal rules
]
# Return true if at least MARKDOWN_INDICATOR_THRESHOLD markdown indicators present
threshold = self.MARKDOWN_INDICATOR_THRESHOLD
return sum(bool(indicator) for indicator in markdown_indicators) >= threshold
def _preprocess_markdown(self, text: str) -> str:
"""
Preprocess markdown text for better rendering.
Handles:
- Task lists with checkboxes (- [ ] and - [x])
- Horizontal rules with visual enhancement
- Code blocks with language hints
Args:
text: Raw markdown text
Returns:
Preprocessed markdown text
"""
lines = text.split("\n")
processed_lines = []
for line in lines:
# Enhanced task lists with visual indicators
if re.match(self.MARKDOWN_TASK_LIST_PATTERN, line):
# Replace [ ] with ☐ and [x]/[X] with ☑
line = re.sub(r"\[\s\]", "", line)
line = re.sub(r"\[[xX]\]", "", line)
# Enhanced horizontal rules - make them more visible
if re.match(self.MARKDOWN_HORIZONTAL_RULE_PATTERN, line):
line = f"\n{'' * 60}\n"
processed_lines.append(line)
return "\n".join(processed_lines)
def _print_markdown(self, text: str) -> None:
"""
Print markdown content with Rich formatting and improved spacing.
Args:
text: Markdown text to render
"""
if not Markdown:
print(text)
return
# Add subtle spacing before markdown for visual separation
has_heading = re.search(self.MARKDOWN_HEADING_PATTERN, text, re.MULTILINE)
if has_heading:
self.console.print()
# Preprocess markdown for enhanced features
processed_text = self._preprocess_markdown(text)
md = Markdown(processed_text)
self.console.print(md)
# Add spacing after markdown blocks for better separation from next content
self.console.print()
def _print_formatted_text(self, text: str) -> None:
"""
Print text with basic formatting, inline code, file path highlighting, and error detection.
Args:
text: Text to print
"""
if not self.console:
print(text)
return
# Check for error/exception patterns and apply special formatting
if self._is_error_traceback(text):
self._print_error_traceback(text)
return
# First, highlight file paths with line numbers (e.g., "file.py:123")
text = re.sub(
self.FILE_REF_PATTERN,
lambda m: (
f"[bold yellow]{m.group(1)}[/bold yellow]:"
f"[bold blue]{m.group(2)}[/bold blue]"
),
text,
)
# Check for inline code (single backticks)
if "`" in text and "```" not in text:
# Replace inline code with Rich markup - improved visibility
formatted_text = re.sub(
self.INLINE_CODE_PATTERN,
lambda m: f"[cyan on grey23]{m.group(1)}[/cyan on grey23]",
text,
)
self.console.print(formatted_text, highlight=True)
else:
# Enable markup for file paths and highlighting for URLs
self.console.print(text, markup=True, highlight=True)
def _is_error_traceback(self, text: str) -> bool:
"""
Check if text appears to be an error traceback.
Args:
text: Text to check
Returns:
True if text looks like an error traceback
"""
error_indicators = [
"Traceback (most recent call last):" in text,
re.search(r'^\s*File ".*", line \d+', text, re.MULTILINE),
re.search(
r"^(Error|Exception|ValueError|TypeError|RuntimeError):",
text,
re.MULTILINE,
),
]
return any(error_indicators)
def _print_error_traceback(self, text: str) -> None:
"""
Print error traceback with enhanced formatting.
Args:
text: Error traceback text
"""
if not Syntax:
print(text)
return
# Use Python syntax highlighting for tracebacks
try:
syntax = Syntax(
text,
"python",
theme="monokai",
line_numbers=False,
word_wrap=True,
background_color="grey11",
)
self.console.print("\n[red bold]⚠ Error Traceback:[/red bold]")
self.console.print(syntax)
self.console.print()
except Exception as e:
# Fallback to simple red text if syntax highlighting fails
_logger.warning("Syntax highlighting failed for traceback: %s: %s", type(e).__name__, e)
self.console.print(f"[red]{text}[/red]")

View File

@@ -0,0 +1,247 @@
# ABOUTME: Content type detection for smart output formatting
# ABOUTME: Detects diffs, code blocks, markdown, tables, and tracebacks
"""Content type detection for intelligent output formatting."""
import re
from enum import Enum
from typing import Optional
class ContentType(Enum):
"""Types of content that can be detected."""
PLAIN_TEXT = "plain_text"
DIFF = "diff"
CODE_BLOCK = "code_block"
MARKDOWN = "markdown"
MARKDOWN_TABLE = "markdown_table"
ERROR_TRACEBACK = "error_traceback"
class ContentDetector:
"""Detects content types for smart formatting.
Analyzes text content to determine the most appropriate rendering method.
Detection priority: code_block > diff > traceback > table > markdown > plain
"""
# Detection constants
DIFF_HUNK_SCAN_CHARS = 100
DIFF_SCAN_LINE_LIMIT = 5
MARKDOWN_INDICATOR_THRESHOLD = 2
# Regex patterns
CODE_BLOCK_PATTERN = re.compile(r"```(\w+)?\n.*?\n```", re.DOTALL)
MARKDOWN_HEADING_PATTERN = re.compile(r"^#{1,6}\s+.+", re.MULTILINE)
MARKDOWN_UNORDERED_LIST_PATTERN = re.compile(r"^[\*\-]\s+.+", re.MULTILINE)
MARKDOWN_ORDERED_LIST_PATTERN = re.compile(r"^\d+\.\s+.+", re.MULTILINE)
MARKDOWN_BOLD_PATTERN = re.compile(r"\*\*.+?\*\*")
MARKDOWN_ITALIC_PATTERN = re.compile(r"(?<!\*)\*(?!\*)[^*\n]+\*(?!\*)")
MARKDOWN_BLOCKQUOTE_PATTERN = re.compile(r"^>\s+.+", re.MULTILINE)
MARKDOWN_TASK_LIST_PATTERN = re.compile(r"^[\*\-]\s+\[([ xX])\]\s+.+", re.MULTILINE)
MARKDOWN_HORIZONTAL_RULE_PATTERN = re.compile(r"^(\-{3,}|\*{3,}|_{3,})\s*$", re.MULTILINE)
TABLE_SEPARATOR_PATTERN = re.compile(r"^\s*\|[\s\-:|]+\|\s*$", re.MULTILINE)
TRACEBACK_FILE_LINE_PATTERN = re.compile(r'^\s*File ".*", line \d+', re.MULTILINE)
TRACEBACK_ERROR_PATTERN = re.compile(
r"^(Error|Exception|ValueError|TypeError|RuntimeError|KeyError|AttributeError|"
r"IndexError|ImportError|FileNotFoundError|NameError|ZeroDivisionError):",
re.MULTILINE,
)
def detect(self, text: str) -> ContentType:
"""Detect the primary content type of text.
Detection priority ensures more specific types are matched first:
1. Code blocks (```...```) - highest priority
2. Diffs (git diff format)
3. Error tracebacks (Python exceptions)
4. Markdown tables (|...|)
5. Rich markdown (headings, lists, etc.)
6. Plain text (fallback)
Args:
text: Text content to analyze
Returns:
The detected ContentType
"""
if not text or not text.strip():
return ContentType.PLAIN_TEXT
# Check in priority order
if self.is_code_block(text):
return ContentType.CODE_BLOCK
if self.is_diff(text):
return ContentType.DIFF
if self.is_error_traceback(text):
return ContentType.ERROR_TRACEBACK
if self.is_markdown_table(text):
return ContentType.MARKDOWN_TABLE
if self.is_markdown(text):
return ContentType.MARKDOWN
return ContentType.PLAIN_TEXT
def is_diff(self, text: str) -> bool:
"""Check if text is diff content.
Detects git diff format including:
- diff --git headers
- --- and +++ file markers (as diff markers, not markdown hr)
- @@ hunk markers
Note: We avoid matching lines that merely start with + or - as those
could be markdown list items. Diff detection requires more specific
markers like @@ hunks or diff --git headers.
Args:
text: Text to check
Returns:
True if text appears to be diff content
"""
if not text:
return False
# Check for definitive diff markers
diff_indicators = [
text.startswith("diff --git"),
"@@" in text[: self.DIFF_HUNK_SCAN_CHARS],
]
if any(diff_indicators):
return True
# Check for --- a/ and +++ b/ patterns (file markers in unified diff)
# These are more specific than just --- or +++ which could be markdown hr
lines = text.split("\n")[: self.DIFF_SCAN_LINE_LIMIT]
has_file_markers = (
any(line.startswith("--- a/") or line.startswith("--- /") for line in lines)
and any(line.startswith("+++ b/") or line.startswith("+++ /") for line in lines)
)
if has_file_markers:
return True
# Check for @@ hunk pattern specifically
return any(line.startswith("@@") for line in lines)
def is_code_block(self, text: str) -> bool:
"""Check if text contains fenced code blocks.
Detects markdown-style code blocks with triple backticks.
Args:
text: Text to check
Returns:
True if text contains code blocks
"""
if not text:
return False
return "```" in text and self.CODE_BLOCK_PATTERN.search(text) is not None
def is_markdown(self, text: str) -> bool:
"""Check if text contains rich markdown formatting.
Requires at least MARKDOWN_INDICATOR_THRESHOLD indicators to avoid
false positives on text that happens to contain a single markdown element.
Detected elements:
- Headings (# Title)
- Lists (- item, 1. item)
- Emphasis (**bold**, *italic*)
- Blockquotes (> quote)
- Task lists (- [ ] task)
- Horizontal rules (---)
Args:
text: Text to check
Returns:
True if text appears to be markdown content
"""
if not text:
return False
markdown_indicators = [
self.MARKDOWN_HEADING_PATTERN.search(text),
self.MARKDOWN_UNORDERED_LIST_PATTERN.search(text),
self.MARKDOWN_ORDERED_LIST_PATTERN.search(text),
self.MARKDOWN_BOLD_PATTERN.search(text),
self.MARKDOWN_ITALIC_PATTERN.search(text),
self.MARKDOWN_BLOCKQUOTE_PATTERN.search(text),
self.MARKDOWN_TASK_LIST_PATTERN.search(text),
self.MARKDOWN_HORIZONTAL_RULE_PATTERN.search(text),
]
return sum(bool(indicator) for indicator in markdown_indicators) >= self.MARKDOWN_INDICATOR_THRESHOLD
def is_markdown_table(self, text: str) -> bool:
"""Check if text is a markdown table.
Detects tables with pipe separators and header dividers.
Args:
text: Text to check
Returns:
True if text appears to be a markdown table
"""
if not text:
return False
lines = text.strip().split("\n")
if len(lines) < 2:
return False
# Check for table separator line (|---|---|) in first few lines
for line in lines[:3]:
if self.TABLE_SEPARATOR_PATTERN.match(line):
return True
return False
def is_error_traceback(self, text: str) -> bool:
"""Check if text is an error traceback.
Detects Python exception tracebacks.
Args:
text: Text to check
Returns:
True if text appears to be an error traceback
"""
if not text:
return False
error_indicators = [
"Traceback (most recent call last):" in text,
self.TRACEBACK_FILE_LINE_PATTERN.search(text),
self.TRACEBACK_ERROR_PATTERN.search(text),
]
return any(error_indicators)
def extract_code_blocks(self, text: str) -> list[tuple[Optional[str], str]]:
"""Extract code blocks from text.
Args:
text: Text containing code blocks
Returns:
List of (language, code) tuples. Language may be None.
"""
if not text:
return []
blocks = []
pattern = re.compile(r"```(\w+)?\n(.*?)\n```", re.DOTALL)
for match in pattern.finditer(text):
language = match.group(1)
code = match.group(2)
blocks.append((language, code))
return blocks

View File

@@ -0,0 +1,416 @@
# ABOUTME: JSON output formatter for programmatic consumption
# ABOUTME: Produces structured JSON output for parsing by other tools
"""JSON output formatter for Claude adapter."""
import json
from datetime import datetime
from typing import Any, Dict, List, Optional
from .base import (
MessageType,
OutputFormatter,
ToolCallInfo,
VerbosityLevel,
)
class JsonFormatter(OutputFormatter):
"""JSON formatter for programmatic output consumption.
Produces structured JSON output suitable for parsing by other tools,
logging systems, or downstream processing pipelines.
"""
def __init__(
self,
verbosity: VerbosityLevel = VerbosityLevel.NORMAL,
pretty: bool = True,
include_timestamps: bool = True,
) -> None:
"""Initialize JSON formatter.
Args:
verbosity: Output verbosity level
pretty: Pretty-print JSON with indentation
include_timestamps: Include timestamps in output
"""
super().__init__(verbosity)
self._pretty = pretty
self._include_timestamps = include_timestamps
self._events: List[Dict[str, Any]] = []
def _to_json(self, obj: Dict[str, Any]) -> str:
"""Convert object to JSON string.
Args:
obj: Dictionary to serialize
Returns:
JSON string
"""
if self._pretty:
return json.dumps(obj, indent=2, default=str, ensure_ascii=False)
return json.dumps(obj, default=str, ensure_ascii=False)
def _create_event(
self,
event_type: str,
data: Dict[str, Any],
iteration: int = 0,
) -> Dict[str, Any]:
"""Create a structured event object.
Args:
event_type: Type of event
data: Event data
iteration: Current iteration number
Returns:
Event dictionary
"""
event = {
"type": event_type,
"iteration": iteration,
"data": data,
}
if self._include_timestamps:
event["timestamp"] = datetime.now().isoformat()
return event
def _record_event(self, event: Dict[str, Any]) -> None:
"""Record event for later retrieval.
Args:
event: Event dictionary to record
"""
self._events.append(event)
def get_events(self) -> List[Dict[str, Any]]:
"""Get all recorded events.
Returns:
List of event dictionaries
"""
return self._events.copy()
def clear_events(self) -> None:
"""Clear recorded events."""
self._events.clear()
def format_tool_call(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool call as JSON.
Args:
tool_info: Tool call information
iteration: Current iteration number
Returns:
JSON string representation
"""
if not self.should_display(MessageType.TOOL_CALL):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.TOOL_CALL, tool_info, context)
data: Dict[str, Any] = {
"tool_name": tool_info.tool_name,
"tool_id": tool_info.tool_id,
}
if self._verbosity.value >= VerbosityLevel.VERBOSE.value:
data["input_params"] = tool_info.input_params
if tool_info.start_time:
data["start_time"] = tool_info.start_time.isoformat()
event = self._create_event("tool_call", data, iteration)
self._record_event(event)
return self._to_json(event)
def format_tool_result(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool result as JSON.
Args:
tool_info: Tool call info with result
iteration: Current iteration number
Returns:
JSON string representation
"""
if not self.should_display(MessageType.TOOL_RESULT):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.TOOL_RESULT, tool_info, context)
data: Dict[str, Any] = {
"tool_name": tool_info.tool_name,
"tool_id": tool_info.tool_id,
"is_error": tool_info.is_error,
}
if tool_info.duration_ms is not None:
data["duration_ms"] = tool_info.duration_ms
if self._verbosity.value >= VerbosityLevel.VERBOSE.value:
result = tool_info.result
if isinstance(result, str) and len(result) > 1000:
data["result"] = self.summarize_content(result, 1000)
data["result_truncated"] = True
data["result_full_length"] = len(result)
else:
data["result"] = result
if tool_info.end_time:
data["end_time"] = tool_info.end_time.isoformat()
event = self._create_event("tool_result", data, iteration)
self._record_event(event)
return self._to_json(event)
def format_assistant_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format an assistant message as JSON.
Args:
message: Assistant message text
iteration: Current iteration number
Returns:
JSON string representation
"""
if not self.should_display(MessageType.ASSISTANT):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.ASSISTANT, message, context)
data: Dict[str, Any] = {}
if self._verbosity == VerbosityLevel.NORMAL and len(message) > 1000:
data["message"] = self.summarize_content(message, 1000)
data["message_truncated"] = True
data["message_full_length"] = len(message)
else:
data["message"] = message
data["message_truncated"] = False
data["message_length"] = len(message)
event = self._create_event("assistant_message", data, iteration)
self._record_event(event)
return self._to_json(event)
def format_system_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format a system message as JSON.
Args:
message: System message text
iteration: Current iteration number
Returns:
JSON string representation
"""
if not self.should_display(MessageType.SYSTEM):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.SYSTEM, message, context)
data = {
"message": message,
}
event = self._create_event("system_message", data, iteration)
self._record_event(event)
return self._to_json(event)
def format_error(
self,
error: str,
exception: Optional[Exception] = None,
iteration: int = 0,
) -> str:
"""Format an error as JSON.
Args:
error: Error message
exception: Optional exception object
iteration: Current iteration number
Returns:
JSON string representation
"""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.ERROR, error, context)
data: Dict[str, Any] = {
"error": error,
}
if exception:
data["exception_type"] = type(exception).__name__
data["exception_str"] = str(exception)
if self._verbosity.value >= VerbosityLevel.VERBOSE.value:
import traceback
data["traceback"] = traceback.format_exception(
type(exception), exception, exception.__traceback__
)
event = self._create_event("error", data, iteration)
self._record_event(event)
return self._to_json(event)
def format_progress(
self,
message: str,
current: int = 0,
total: int = 0,
iteration: int = 0,
) -> str:
"""Format progress information as JSON.
Args:
message: Progress message
current: Current progress value
total: Total progress value
iteration: Current iteration number
Returns:
JSON string representation
"""
if not self.should_display(MessageType.PROGRESS):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.PROGRESS, message, context)
data: Dict[str, Any] = {
"message": message,
"current": current,
"total": total,
}
if total > 0:
data["percentage"] = round((current / total) * 100, 1)
event = self._create_event("progress", data, iteration)
self._record_event(event)
return self._to_json(event)
def format_token_usage(self, show_session: bool = True) -> str:
"""Format token usage summary as JSON.
Args:
show_session: Include session totals
Returns:
JSON string representation
"""
usage = self._token_usage
data: Dict[str, Any] = {
"current": {
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
"total_tokens": usage.total_tokens,
"cost": usage.cost,
},
}
if show_session:
data["session"] = {
"input_tokens": usage.session_input_tokens,
"output_tokens": usage.session_output_tokens,
"total_tokens": usage.session_total_tokens,
"cost": usage.session_cost,
}
if usage.model:
data["model"] = usage.model
event = self._create_event("token_usage", data, 0)
return self._to_json(event)
def format_section_header(self, title: str, iteration: int = 0) -> str:
"""Format a section header as JSON.
Args:
title: Section title
iteration: Current iteration number
Returns:
JSON string representation
"""
data = {
"title": title,
"elapsed_seconds": self.get_elapsed_time(),
}
event = self._create_event("section_start", data, iteration)
self._record_event(event)
return self._to_json(event)
def format_section_footer(self) -> str:
"""Format a section footer as JSON.
Returns:
JSON string representation
"""
data = {
"elapsed_seconds": self.get_elapsed_time(),
}
event = self._create_event("section_end", data, 0)
self._record_event(event)
return self._to_json(event)
def get_summary(self) -> Dict[str, Any]:
"""Get a summary of all recorded events.
Returns:
Summary dictionary with counts and totals
"""
event_counts: Dict[str, int] = {}
for event in self._events:
event_type = event.get("type", "unknown")
event_counts[event_type] = event_counts.get(event_type, 0) + 1
return {
"total_events": len(self._events),
"event_counts": event_counts,
"token_usage": {
"total_tokens": self._token_usage.session_total_tokens,
"total_cost": self._token_usage.session_cost,
},
"elapsed_seconds": self.get_elapsed_time(),
}
def export_events(self) -> str:
"""Export all recorded events as a JSON array.
Returns:
JSON string with all events
"""
return self._to_json({"events": self._events, "summary": self.get_summary()})

View File

@@ -0,0 +1,298 @@
# ABOUTME: Plain text output formatter for non-terminal environments
# ABOUTME: Provides basic text formatting without colors or special characters
"""Plain text output formatter for Claude adapter."""
from datetime import datetime
from typing import Optional
from .base import (
MessageType,
OutputFormatter,
ToolCallInfo,
VerbosityLevel,
)
class PlainTextFormatter(OutputFormatter):
"""Plain text formatter for environments without rich terminal support.
Produces readable output without ANSI codes, colors, or special characters.
Suitable for logging to files or basic terminal output.
"""
# Formatting constants
SEPARATOR_WIDTH = 60
HEADER_CHAR = "="
SUBHEADER_CHAR = "-"
SECTION_CHAR = "#"
def __init__(self, verbosity: VerbosityLevel = VerbosityLevel.NORMAL) -> None:
"""Initialize plain text formatter.
Args:
verbosity: Output verbosity level
"""
super().__init__(verbosity)
def _timestamp(self) -> str:
"""Get formatted timestamp string."""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def _separator(self, char: str = "-", width: int = None) -> str:
"""Create a separator line."""
return char * (width or self.SEPARATOR_WIDTH)
def format_tool_call(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool call for plain text display.
Args:
tool_info: Tool call information
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.TOOL_CALL):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.TOOL_CALL, tool_info, context)
lines = [
self._separator(),
f"[{self._timestamp()}] TOOL CALL: {tool_info.tool_name}",
f" ID: {tool_info.tool_id[:12]}...",
]
if self._verbosity.value >= VerbosityLevel.VERBOSE.value:
if tool_info.input_params:
lines.append(" Input Parameters:")
for key, value in tool_info.input_params.items():
value_str = str(value)
if len(value_str) > 100:
value_str = value_str[:97] + "..."
lines.append(f" {key}: {value_str}")
lines.append(self._separator())
return "\n".join(lines)
def format_tool_result(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool result for plain text display.
Args:
tool_info: Tool call info with result
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.TOOL_RESULT):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.TOOL_RESULT, tool_info, context)
status = "ERROR" if tool_info.is_error else "Success"
duration = f" ({tool_info.duration_ms}ms)" if tool_info.duration_ms else ""
lines = [
f"[TOOL RESULT] {tool_info.tool_name}{duration}",
f" ID: {tool_info.tool_id[:12]}...",
f" Status: {status}",
]
if self._verbosity.value >= VerbosityLevel.VERBOSE.value and tool_info.result:
result_str = str(tool_info.result)
if len(result_str) > 500:
result_str = self.summarize_content(result_str, 500)
lines.append(" Output:")
for line in result_str.split("\n"):
lines.append(f" {line}")
lines.append(self._separator())
return "\n".join(lines)
def format_assistant_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format an assistant message for plain text display.
Args:
message: Assistant message text
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.ASSISTANT):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.ASSISTANT, message, context)
if self._verbosity == VerbosityLevel.QUIET:
return ""
# Summarize if too long and not verbose
if self._verbosity == VerbosityLevel.NORMAL and len(message) > 1000:
message = self.summarize_content(message, 1000)
return f"[{self._timestamp()}] ASSISTANT:\n{message}\n"
def format_system_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format a system message for plain text display.
Args:
message: System message text
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.SYSTEM):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.SYSTEM, message, context)
return f"[{self._timestamp()}] SYSTEM: {message}\n"
def format_error(
self,
error: str,
exception: Optional[Exception] = None,
iteration: int = 0,
) -> str:
"""Format an error for plain text display.
Args:
error: Error message
exception: Optional exception object
iteration: Current iteration number
Returns:
Formatted string representation
"""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.ERROR, error, context)
lines = [
self._separator(self.HEADER_CHAR),
f"[{self._timestamp()}] ERROR (Iteration {iteration})",
f" Message: {error}",
]
if exception and self._verbosity.value >= VerbosityLevel.VERBOSE.value:
lines.append(f" Type: {type(exception).__name__}")
import traceback
tb = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
lines.append(" Traceback:")
for line in tb.split("\n"):
lines.append(f" {line}")
lines.append(self._separator(self.HEADER_CHAR))
return "\n".join(lines)
def format_progress(
self,
message: str,
current: int = 0,
total: int = 0,
iteration: int = 0,
) -> str:
"""Format progress information for plain text display.
Args:
message: Progress message
current: Current progress value
total: Total progress value
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.PROGRESS):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.PROGRESS, message, context)
if total > 0:
pct = (current / total) * 100
bar_width = 30
filled = int(bar_width * current / total)
bar = "#" * filled + "-" * (bar_width - filled)
return f"[{bar}] {pct:.1f}% - {message}"
return f"[...] {message}"
def format_token_usage(self, show_session: bool = True) -> str:
"""Format token usage summary for plain text display.
Args:
show_session: Include session totals
Returns:
Formatted string representation
"""
usage = self._token_usage
lines = [
self._separator(self.SUBHEADER_CHAR),
"TOKEN USAGE:",
f" Current: {usage.total_tokens:,} tokens (${usage.cost:.4f})",
f" Input: {usage.input_tokens:,} | Output: {usage.output_tokens:,}",
]
if show_session:
lines.extend([
f" Session: {usage.session_total_tokens:,} tokens (${usage.session_cost:.4f})",
f" Input: {usage.session_input_tokens:,} | Output: {usage.session_output_tokens:,}",
])
if usage.model:
lines.append(f" Model: {usage.model}")
lines.append(self._separator(self.SUBHEADER_CHAR))
return "\n".join(lines)
def format_section_header(self, title: str, iteration: int = 0) -> str:
"""Format a section header for plain text display.
Args:
title: Section title
iteration: Current iteration number
Returns:
Formatted string representation
"""
lines = [
"",
self._separator(self.HEADER_CHAR),
f"{title} (Iteration {iteration})" if iteration else title,
self._separator(self.HEADER_CHAR),
]
return "\n".join(lines)
def format_section_footer(self) -> str:
"""Format a section footer for plain text display.
Returns:
Formatted string representation
"""
elapsed = self.get_elapsed_time()
return f"\n{self._separator(self.SUBHEADER_CHAR)}\nElapsed: {elapsed:.1f}s\n"

View File

@@ -0,0 +1,737 @@
# ABOUTME: Rich terminal formatter with colors, panels, and progress indicators
# ABOUTME: Provides visually enhanced output using the Rich library with smart content detection
"""Rich terminal output formatter for Claude adapter.
This formatter provides intelligent content detection and rendering:
- Diffs are rendered with color-coded additions/deletions
- Code blocks get syntax highlighting
- Markdown is rendered with proper formatting
- Error tracebacks are highlighted for readability
"""
import logging
import re
from datetime import datetime
from io import StringIO
from typing import Optional
from .base import (
MessageType,
OutputFormatter,
ToolCallInfo,
VerbosityLevel,
)
from .content_detector import ContentDetector, ContentType
_logger = logging.getLogger(__name__)
# Try to import Rich components with fallback
try:
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from rich.syntax import Syntax
from rich.markup import escape
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
Console = None # type: ignore
Panel = None # type: ignore
class RichTerminalFormatter(OutputFormatter):
"""Rich terminal formatter with colors, panels, and progress indicators.
Provides visually enhanced output using the Rich library for terminal
display. Falls back to plain text if Rich is not available.
"""
# Color scheme
COLORS = {
"tool_name": "bold cyan",
"tool_id": "dim",
"success": "bold green",
"error": "bold red",
"warning": "yellow",
"info": "blue",
"timestamp": "dim white",
"header": "bold magenta",
"assistant": "white",
"system": "dim cyan",
"token_input": "green",
"token_output": "yellow",
"cost": "bold yellow",
}
# Icons
ICONS = {
"tool": "",
"success": "",
"error": "",
"warning": "",
"info": "",
"assistant": "",
"system": "",
"token": "",
"clock": "",
"progress": "",
}
def __init__(
self,
verbosity: VerbosityLevel = VerbosityLevel.NORMAL,
console: Optional["Console"] = None,
smart_detection: bool = True,
) -> None:
"""Initialize rich terminal formatter.
Args:
verbosity: Output verbosity level
console: Optional Rich console instance (creates new if None)
smart_detection: Enable smart content detection (diff, code, markdown)
"""
super().__init__(verbosity)
self._rich_available = RICH_AVAILABLE
self._smart_detection = smart_detection
self._content_detector = ContentDetector() if smart_detection else None
if RICH_AVAILABLE:
self._console = console or Console()
# Import DiffFormatter for diff rendering
from .console import DiffFormatter
self._diff_formatter = DiffFormatter(self._console)
else:
self._console = None
self._diff_formatter = None
@property
def console(self) -> Optional["Console"]:
"""Get the Rich console instance."""
return self._console
def _timestamp(self) -> str:
"""Get formatted timestamp string with Rich markup."""
ts = datetime.now().strftime("%H:%M:%S")
if self._rich_available:
return f"[{self.COLORS['timestamp']}]{ts}[/]"
return ts
def _full_timestamp(self) -> str:
"""Get full timestamp with date."""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if self._rich_available:
return f"[{self.COLORS['timestamp']}]{ts}[/]"
return ts
def format_tool_call(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool call for rich terminal display.
Args:
tool_info: Tool call information
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.TOOL_CALL):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.TOOL_CALL, tool_info, context)
if not self._rich_available:
return self._format_tool_call_plain(tool_info)
# Build rich formatted output
icon = self.ICONS["tool"]
name_color = self.COLORS["tool_name"]
id_color = self.COLORS["tool_id"]
lines = [
f"{icon} [{name_color}]TOOL CALL: {tool_info.tool_name}[/]",
f" [{id_color}]ID: {tool_info.tool_id[:12]}...[/]",
]
if self._verbosity.value >= VerbosityLevel.VERBOSE.value:
if tool_info.input_params:
lines.append(f" [{self.COLORS['info']}]Input Parameters:[/]")
for key, value in tool_info.input_params.items():
value_str = str(value)
if len(value_str) > 100:
value_str = value_str[:97] + "..."
# Escape Rich markup in values
if self._rich_available:
value_str = escape(value_str)
lines.append(f" - {key}: {value_str}")
return "\n".join(lines)
def _format_tool_call_plain(self, tool_info: ToolCallInfo) -> str:
"""Plain fallback for tool call formatting."""
lines = [
f"TOOL CALL: {tool_info.tool_name}",
f" ID: {tool_info.tool_id[:12]}...",
]
if tool_info.input_params:
for key, value in tool_info.input_params.items():
value_str = str(value)[:100]
lines.append(f" {key}: {value_str}")
return "\n".join(lines)
def format_tool_result(
self,
tool_info: ToolCallInfo,
iteration: int = 0,
) -> str:
"""Format a tool result for rich terminal display.
Args:
tool_info: Tool call info with result
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.TOOL_RESULT):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.TOOL_RESULT, tool_info, context)
if not self._rich_available:
return self._format_tool_result_plain(tool_info)
# Determine status styling
if tool_info.is_error:
status_icon = self.ICONS["error"]
status_color = self.COLORS["error"]
status_text = "ERROR"
else:
status_icon = self.ICONS["success"]
status_color = self.COLORS["success"]
status_text = "Success"
duration = f" ({tool_info.duration_ms}ms)" if tool_info.duration_ms else ""
lines = [
f"{status_icon} [{status_color}]TOOL RESULT: {tool_info.tool_name}{duration}[/]",
f" [{self.COLORS['tool_id']}]ID: {tool_info.tool_id[:12]}...[/]",
f" Status: [{status_color}]{status_text}[/]",
]
if self._verbosity.value >= VerbosityLevel.VERBOSE.value and tool_info.result:
result_str = str(tool_info.result)
if len(result_str) > 500:
result_str = self.summarize_content(result_str, 500)
# Escape Rich markup in result
if self._rich_available:
result_str = escape(result_str)
lines.append(f" [{self.COLORS['info']}]Output:[/]")
for line in result_str.split("\n")[:20]: # Limit lines
lines.append(f" {line}")
if result_str.count("\n") > 20:
lines.append(f" [{self.COLORS['timestamp']}]... ({result_str.count(chr(10)) - 20} more lines)[/]")
return "\n".join(lines)
def _format_tool_result_plain(self, tool_info: ToolCallInfo) -> str:
"""Plain fallback for tool result formatting."""
status = "ERROR" if tool_info.is_error else "Success"
lines = [
f"TOOL RESULT: {tool_info.tool_name}",
f" Status: {status}",
]
if tool_info.result:
lines.append(f" Output: {str(tool_info.result)[:200]}")
return "\n".join(lines)
def format_assistant_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format an assistant message for rich terminal display.
With smart_detection enabled, detects and renders:
- Diffs with color-coded additions/deletions
- Code blocks with syntax highlighting
- Markdown with proper formatting
- Error tracebacks with special highlighting
Args:
message: Assistant message text
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.ASSISTANT):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.ASSISTANT, message, context)
if self._verbosity == VerbosityLevel.QUIET:
return ""
# Summarize if needed (only for normal verbosity)
display_message = message
if self._verbosity == VerbosityLevel.NORMAL and len(message) > 1000:
display_message = self.summarize_content(message, 1000)
if not self._rich_available:
return f"ASSISTANT: {display_message}"
# Use smart detection if enabled
if self._smart_detection and self._content_detector:
content_type = self._content_detector.detect(display_message)
return self._render_smart_content(display_message, content_type)
# Fallback to simple formatting
icon = self.ICONS["assistant"]
return f"{icon} [{self.COLORS['assistant']}]{escape(display_message)}[/]"
def _render_smart_content(self, text: str, content_type: ContentType) -> str:
"""Render content based on detected type.
Args:
text: Text content to render
content_type: Detected content type
Returns:
Formatted string (may include Rich markup)
"""
if content_type == ContentType.DIFF:
return self._render_diff(text)
elif content_type == ContentType.CODE_BLOCK:
return self._render_code_blocks(text)
elif content_type == ContentType.MARKDOWN:
return self._render_markdown(text)
elif content_type == ContentType.MARKDOWN_TABLE:
return self._render_markdown(text) # Tables use markdown renderer
elif content_type == ContentType.ERROR_TRACEBACK:
return self._render_traceback(text)
else:
# Plain text - escape and format
icon = self.ICONS["assistant"]
return f"{icon} [{self.COLORS['assistant']}]{escape(text)}[/]"
def _render_diff(self, text: str) -> str:
"""Render diff content with colors.
Uses the DiffFormatter for enhanced diff visualization with
color-coded additions/deletions and file statistics.
Args:
text: Diff text to render
Returns:
Empty string (diff is printed directly to console)
"""
if self._diff_formatter and self._console:
# DiffFormatter prints directly, so capture would require buffer
# For now, print directly and return marker
self._diff_formatter.format_and_print(text)
return "" # Already printed
return f"[dim]{text}[/dim]"
def _render_code_blocks(self, text: str) -> str:
"""Render text with code blocks using syntax highlighting.
Extracts code blocks, renders them with Rich Syntax, and
formats the surrounding text.
Args:
text: Text containing code blocks
Returns:
Formatted string with code block markers for print_smart()
"""
if not self._console or not self._content_detector:
return f"[dim]{text}[/dim]"
# Split by code blocks and render each part
parts = []
pattern = r"```(\w+)?\n(.*?)\n```"
last_end = 0
for match in re.finditer(pattern, text, re.DOTALL):
# Text before code block
before = text[last_end:match.start()].strip()
if before:
parts.append(("text", before))
# Code block
language = match.group(1) or "text"
code = match.group(2)
parts.append(("code", language, code))
last_end = match.end()
# Text after last code block
after = text[last_end:].strip()
if after:
parts.append(("text", after))
# Render to string buffer using console
buffer = StringIO()
temp_console = Console(file=buffer, force_terminal=True, width=100)
for part in parts:
if part[0] == "text":
temp_console.print(part[1], markup=True, highlight=True)
else: # code
_, language, code = part
syntax = Syntax(
code,
language,
theme="monokai",
line_numbers=True,
word_wrap=True,
)
temp_console.print(syntax)
return buffer.getvalue()
def _render_markdown(self, text: str) -> str:
"""Render markdown content with Rich formatting.
Uses Rich's Markdown renderer for headings, lists, emphasis, etc.
Args:
text: Markdown text to render
Returns:
Formatted markdown string
"""
if not self._console:
return text
try:
from rich.markdown import Markdown
# Preprocess for task lists
processed = self._preprocess_markdown(text)
buffer = StringIO()
temp_console = Console(file=buffer, force_terminal=True, width=100)
temp_console.print(Markdown(processed))
return buffer.getvalue()
except ImportError:
return text
def _preprocess_markdown(self, text: str) -> str:
"""Preprocess markdown for enhanced rendering.
Converts task list checkboxes to visual indicators.
Args:
text: Raw markdown
Returns:
Preprocessed markdown
"""
# Convert task lists: [ ] -> ☐, [x] -> ☑
text = re.sub(r"\[\s\]", "", text)
text = re.sub(r"\[[xX]\]", "", text)
return text
def _render_traceback(self, text: str) -> str:
"""Render error traceback with syntax highlighting.
Uses Python syntax highlighting for better readability.
Args:
text: Traceback text to render
Returns:
Formatted traceback string
"""
if not self._console:
return f"[red]{text}[/red]"
try:
buffer = StringIO()
temp_console = Console(file=buffer, force_terminal=True, width=100)
temp_console.print("[red bold]⚠ Error Traceback:[/red bold]")
syntax = Syntax(
text,
"python",
theme="monokai",
line_numbers=False,
word_wrap=True,
background_color="grey11",
)
temp_console.print(syntax)
return buffer.getvalue()
except Exception as e:
_logger.warning("Rich traceback rendering failed: %s: %s", type(e).__name__, e)
return f"[red]{escape(text)}[/red]"
def print_smart(self, message: str, iteration: int = 0) -> None:
"""Print message with smart content detection directly to console.
This is the preferred method for displaying assistant messages
as it handles all content types appropriately and prints directly.
Args:
message: Message text to print
iteration: Current iteration number
"""
if not self.should_display(MessageType.ASSISTANT):
return
if self._verbosity == VerbosityLevel.QUIET:
return
if not self._console:
print(f"ASSISTANT: {message}")
return
# Use smart detection
if self._smart_detection and self._content_detector:
content_type = self._content_detector.detect(message)
if content_type == ContentType.DIFF:
# DiffFormatter prints directly
if self._diff_formatter:
self._diff_formatter.format_and_print(message)
return
# For other content types, use format and print
formatted = self.format_assistant_message(message, iteration)
if formatted:
self._console.print(formatted, markup=True)
def format_system_message(
self,
message: str,
iteration: int = 0,
) -> str:
"""Format a system message for rich terminal display.
Args:
message: System message text
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.SYSTEM):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.SYSTEM, message, context)
if not self._rich_available:
return f"SYSTEM: {message}"
icon = self.ICONS["system"]
return f"{icon} [{self.COLORS['system']}]SYSTEM: {message}[/]"
def format_error(
self,
error: str,
exception: Optional[Exception] = None,
iteration: int = 0,
) -> str:
"""Format an error for rich terminal display.
Args:
error: Error message
exception: Optional exception object
iteration: Current iteration number
Returns:
Formatted string representation
"""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.ERROR, error, context)
if not self._rich_available:
return f"ERROR: {error}"
icon = self.ICONS["error"]
color = self.COLORS["error"]
lines = [
f"\n{icon} [{color}]ERROR (Iteration {iteration})[/]",
f" [{color}]{error}[/]",
]
if exception and self._verbosity.value >= VerbosityLevel.VERBOSE.value:
lines.append(f" [{self.COLORS['warning']}]Type: {type(exception).__name__}[/]")
import traceback
tb = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
lines.append(f" [{self.COLORS['timestamp']}]Traceback:[/]")
for line in tb.split("\n")[:15]: # Limit traceback lines
if line.strip():
lines.append(f" {escape(line)}" if self._rich_available else f" {line}")
return "\n".join(lines)
def format_progress(
self,
message: str,
current: int = 0,
total: int = 0,
iteration: int = 0,
) -> str:
"""Format progress information for rich terminal display.
Args:
message: Progress message
current: Current progress value
total: Total progress value
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self.should_display(MessageType.PROGRESS):
return ""
context = self._create_context(iteration)
self._notify_callbacks(MessageType.PROGRESS, message, context)
if not self._rich_available:
if total > 0:
pct = (current / total) * 100
return f"[{pct:.0f}%] {message}"
return f"[...] {message}"
icon = self.ICONS["progress"]
if total > 0:
pct = (current / total) * 100
bar_width = 20
filled = int(bar_width * current / total)
bar = "" * filled + "" * (bar_width - filled)
return f"{icon} [{self.COLORS['info']}][{bar}] {pct:.0f}%[/] {message}"
return f"{icon} [{self.COLORS['info']}][...][/] {message}"
def format_token_usage(self, show_session: bool = True) -> str:
"""Format token usage summary for rich terminal display.
Args:
show_session: Include session totals
Returns:
Formatted string representation
"""
usage = self._token_usage
if not self._rich_available:
lines = [
f"TOKEN USAGE: {usage.total_tokens:,} (${usage.cost:.4f})",
]
if show_session:
lines.append(f" Session: {usage.session_total_tokens:,} (${usage.session_cost:.4f})")
return "\n".join(lines)
icon = self.ICONS["token"]
input_color = self.COLORS["token_input"]
output_color = self.COLORS["token_output"]
cost_color = self.COLORS["cost"]
lines = [
f"\n{icon} [{self.COLORS['header']}]TOKEN USAGE[/]",
f" Current: [{input_color}]{usage.input_tokens:,} in[/] | [{output_color}]{usage.output_tokens:,} out[/] | [{cost_color}]${usage.cost:.4f}[/]",
]
if show_session:
lines.append(
f" Session: [{input_color}]{usage.session_input_tokens:,} in[/] | [{output_color}]{usage.session_output_tokens:,} out[/] | [{cost_color}]${usage.session_cost:.4f}[/]"
)
if usage.model:
lines.append(f" [{self.COLORS['timestamp']}]Model: {usage.model}[/]")
return "\n".join(lines)
def format_section_header(self, title: str, iteration: int = 0) -> str:
"""Format a section header for rich terminal display.
Args:
title: Section title
iteration: Current iteration number
Returns:
Formatted string representation
"""
if not self._rich_available:
sep = "=" * 60
header_title = f"{title} (Iteration {iteration})" if iteration else title
return f"\n{sep}\n{header_title}\n{sep}"
header_title = f"{title} (Iteration {iteration})" if iteration else title
sep = "" * 50
return f"\n[{self.COLORS['header']}]{sep}\n{header_title}\n{sep}[/]"
def format_section_footer(self) -> str:
"""Format a section footer for rich terminal display.
Returns:
Formatted string representation
"""
elapsed = self.get_elapsed_time()
if not self._rich_available:
return f"\n{'=' * 50}\nElapsed: {elapsed:.1f}s\n"
icon = self.ICONS["clock"]
return f"\n[{self.COLORS['timestamp']}]{icon} Elapsed: {elapsed:.1f}s[/]\n"
def print(self, text: str) -> None:
"""Print formatted text to console.
Args:
text: Rich-formatted text to print
"""
if self._console:
self._console.print(text, markup=True)
else:
# Strip markup for plain output
import re
plain = re.sub(r"\[/?[^\]]+\]", "", text)
print(plain)
def print_panel(self, content: str, title: str = "", border_style: str = "blue") -> None:
"""Print content in a Rich panel.
Args:
content: Content to display
title: Panel title
border_style: Panel border color
"""
if self._console and self._rich_available and Panel:
panel = Panel(content, title=title, border_style=border_style)
self._console.print(panel)
else:
if title:
print(f"\n=== {title} ===")
print(content)
print()
def create_progress_bar(self) -> Optional["Progress"]:
"""Create a Rich progress bar instance.
Returns:
Progress instance or None if Rich not available
"""
if not self._rich_available or not Progress:
return None
return Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
console=self._console,
)