Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 46 additions & 22 deletions src/mcp_cli/chat/streaming_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,33 +741,57 @@ def _create_display_content(self):
if self._interrupted:
status_text.append(" • INTERRUPTED", style="red bold")

# Response content with typing cursor
# Response content with typing cursor - handle ANSI content properly
# Initialize is_ascii_art to avoid scope issues
is_ascii_art = False

if self.current_response:
try:
# Progressive markdown rendering with cursor
display_text = self.current_response
if not self._interrupted:
display_text += " ▌" # Add typing cursor
response_content = Markdown(markup=display_text)
except Exception as e:
# Fallback to plain text if markdown fails
logger.debug(f"Markdown rendering failed: {e}")
display_text = self.current_response
if not self._interrupted:
display_text += " ▌"
response_content = Text(display_text)
display_text = self.current_response
if not self._interrupted:
display_text += " ▌" # Add typing cursor

# Check if content contains ANSI or ASCII art that needs special handling
from mcp_cli.tools.formatting import format_content_with_ansi_support
box_chars = {'┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', '│', '─', '█', '▄', '▀', '▌', '▐'}
has_ansi = '\033[' in display_text or '\x1b[' in display_text
is_ascii_art = any(char in display_text for char in box_chars) or has_ansi

if is_ascii_art:
# For ASCII art/ANSI content, use our specialized formatter
response_content = format_content_with_ansi_support(display_text, no_wrap=True)
else:
# For regular content, try markdown first
try:
response_content = Markdown(markup=display_text)
except Exception as e:
# Fallback to proper text wrapping without ellipses
logger.debug(f"Markdown rendering failed: {e}")
response_content = Text(display_text, overflow="fold", no_wrap=False)
else:
# Show just cursor when no content yet
cursor_style = "dim" if not self._interrupted else "red"
response_content = Text("▌", style=cursor_style)

# Create panel with dynamic styling
border_style = "blue" if not self._interrupted else "red"

return Panel(
response_content,
title=status_text,
title_align="left",
border_style=border_style,
padding=(0, 1),
)

# Use optimized settings for ASCII art/ANSI content
if is_ascii_art and self.current_response:
return Panel(
response_content,
title=status_text,
title_align="left",
border_style=border_style,
width=None,
padding=(0, 0),
expand=True,
safe_box=False,
)
else:
return Panel(
response_content,
title=status_text,
title_align="left",
border_style=border_style,
padding=(0, 1),
)
55 changes: 39 additions & 16 deletions src/mcp_cli/chat/ui_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,25 +620,33 @@ def print_assistant_response(self, content: str, elapsed: float):
f"[yellow]Warning: Error restoring signal handler: {sig_exc}[/yellow]"
)

# Display the assistant's response
# Display the assistant's response with proper ANSI support
try:
# Check if content might contain problematic markup characters
content = content or "[No Response]"

# Check if content contains ANSI or ASCII art that needs special handling
from mcp_cli.tools.formatting import format_content_with_ansi_support
box_chars = {'┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', '│', '─', '█', '▄', '▀', '▌', '▐'}
has_ansi = '\033[' in content or '\x1b[' in content
is_ascii_art = any(char in content for char in box_chars) or has_ansi
needs_text_object = "[/" in content or "\\[" in content

if needs_text_object:
# Use Text object to prevent markup parsing issues
response_content = Text(content or "[No Response]")
# response_content = Text(text=(content or "[No Response]"), overflow="fold")
if is_ascii_art:
# For ASCII art/ANSI content, use our specialized formatter
response_content = format_content_with_ansi_support(content, no_wrap=True)
elif needs_text_object:
# Use Text object to prevent markup parsing issues - no ellipses
response_content = Text(content, overflow="fold", no_wrap=False)
else:
# Otherwise use Markdown as normal
try:
response_content = Markdown(content or "[No Response]")
response_content = Markdown(content)
except Exception as md_exc:
# Fallback to Text if Markdown parsing fails
# Fallback to proper text wrapping without ellipses
log.warning(
f"Markdown parsing failed, using Text object: {md_exc}"
)
response_content = Text(content or "[No Response]")
response_content = Text(content, overflow="fold", no_wrap=False)

def remove_think_tags(text):
print(f"Removing thinking tags from content: {text}")
Expand All @@ -649,14 +657,29 @@ def remove_think_tags(text):
if suppress_thinking:
response_content = remove_think_tags(response_content)

print(
Panel(
response_content,
style="bold blue",
title="Assistant",
subtitle=f"Response time: {elapsed:.2f}s",
# Use optimized panel settings for ASCII art
if is_ascii_art:
print(
Panel(
response_content,
style="bold blue",
title="Assistant",
subtitle=f"Response time: {elapsed:.2f}s",
width=None,
padding=(0, 1),
expand=True,
safe_box=False
)
)
else:
print(
Panel(
response_content,
style="bold blue",
title="Assistant",
subtitle=f"Response time: {elapsed:.2f}s",
)
)
)
except Exception as panel_exc:
log.error(f"Error creating response panel: {panel_exc}")
# Fallback to plain text if rich formatting fails
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_cli/commands/tools_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async def tools_call_action(tm: ToolManager) -> None: # noqa: D401

try:
result: ToolCallResult = await tm.execute_tool(fq_name, args)
display_tool_call_result(None, result, console)
display_tool_call_result(result, console)
except Exception as exc: # noqa: BLE001
logger.exception("Error executing tool")
cprint(f"[red]Error: {exc}[/red]")
Expand Down
68 changes: 64 additions & 4 deletions src/mcp_cli/tools/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,40 @@ def patched_add_row(*args, **kwargs):
return table


def format_content_with_ansi_support(content: str, no_wrap: bool = False):
"""
Helper function to format content with proper ANSI escape sequence handling.

Args:
content: The content to format
no_wrap: If True, disable wrapping (for ASCII art/charts)

Returns:
Rich Text object with proper formatting
"""
from rich.text import Text

# Check if content looks like ASCII art/chart (contains box drawing characters or ANSI codes)
box_chars = {'┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', '│', '─', '█', '▄', '▀', '▌', '▐'}
has_ansi = '\033[' in content or '\x1b[' in content # Check for ANSI escape sequences
is_ascii_art = any(char in content for char in box_chars) or has_ansi

if is_ascii_art or no_wrap:
# For ASCII art, need special handling to preserve formatting
# First, clean up any invisible characters that might interfere
clean_content = content.replace('\u200b', '').replace('\u200c', '').replace('\u200d', '') # Remove zero-width spaces

# Handle ANSI content by using Text.from_ansi() to properly interpret escape sequences
if has_ansi:
return Text.from_ansi(clean_content, no_wrap=True)
else:
return Text(clean_content, no_wrap=True, overflow="ignore")
else:
# For regular content, use Text object with normal wrapping behavior
# Use "ellipsis" instead of "fold" to ensure proper line breaking
return Text(content)


def display_tool_call_result(result, console=None):
"""Display the result of a tool call."""
import json
Expand All @@ -125,10 +159,36 @@ def display_tool_call_result(result, console=None):
title += f" ({result.execution_time:.2f}s)"
title += "[/green]"

# Use Text object to prevent markup parsing issues
text_content = Text(content)

print_func(Panel(text_content, title=title, style="green"))
# Check if we need special handling for ASCII art
box_chars = {'┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', '│', '─', '█', '▄', '▀', '▌', '▐'}
has_ansi = '\033[' in content or '\x1b[' in content
is_ascii_art = any(char in content for char in box_chars) or has_ansi

if is_ascii_art:
# For ASCII art, use the helper function and optimized panel settings
text_content = format_content_with_ansi_support(content, no_wrap=True)
print_func(Panel(
text_content,
title=title,
style="green",
width=None,
padding=(0, 0),
expand=True,
safe_box=False
))
else:
# For regular content, force proper text wrapping without ellipses
from rich.text import Text
# Create Text object with forced wrapping - NO ellipses mode
text_content = Text(content, overflow="fold", no_wrap=False)
print_func(Panel(
text_content,
title=title,
style="green",
expand=False,
width=None, # Let it size naturally
padding=(0, 1) # Small padding for readability
))
else:
# Format error result
error_msg = result.error or "Unknown error"
Expand Down
Loading