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
3 changes: 2 additions & 1 deletion openhands_cli/shared/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from openhands_cli.shared.conversation_summary import extract_conversation_summary
from openhands_cli.shared.slash_commands import parse_slash_command
from openhands_cli.shared.text_utils import truncate_text


__all__ = ["extract_conversation_summary", "parse_slash_command"]
__all__ = ["extract_conversation_summary", "parse_slash_command", "truncate_text"]
35 changes: 35 additions & 0 deletions openhands_cli/shared/text_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Text utilities shared across the codebase."""

ELLIPSIS = "..."


def truncate_text(
text: str,
max_length: int,
*,
from_start: bool = True,
collapse_whitespace: bool = True,
) -> str:
"""Truncate text with ellipsis if it exceeds max_length.

Args:
text: The text to truncate.
max_length: Maximum length before truncation.
from_start: If True, keep the start and add ellipsis at end.
If False, keep the end and add ellipsis at start (useful for paths).
collapse_whitespace: If True, replace newlines and carriage returns with spaces.

Returns:
The truncated text, or the original text if it's within the limit.
"""
if collapse_whitespace:
text = text.replace("\n", " ").replace("\r", " ")

if len(text) <= max_length:
return text

ellipsis_len = len(ELLIPSIS)
if from_start:
return text[: max_length - ellipsis_len] + ELLIPSIS
else:
return ELLIPSIS + text[-(max_length - ellipsis_len) :]
13 changes: 3 additions & 10 deletions openhands_cli/tui/panels/history_side_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from openhands_cli.conversations.models import ConversationMetadata
from openhands_cli.conversations.store.local import LocalFileStore
from openhands_cli.shared.text_utils import truncate_text
from openhands_cli.theme import OPENHANDS_THEME
from openhands_cli.tui.panels.history_panel_style import HISTORY_PANEL_STYLE

Expand Down Expand Up @@ -58,7 +59,7 @@ def __init__(
# Use title if available, otherwise use ID
has_title = bool(conversation.title)
if conversation.title:
title = _escape_rich_markup(_truncate(conversation.title, 100))
title = _escape_rich_markup(truncate_text(conversation.title, 100))
content = f"{title}\n[dim]{conv_id} • {time_str}[/dim]"
else:
content = f"[dim]New conversation[/dim]\n[dim]{conv_id} • {time_str}[/dim]"
Expand All @@ -82,7 +83,7 @@ def has_title(self) -> bool:
def set_title(self, title: str) -> None:
"""Update the displayed title for this history item."""
time_str = _format_time(self._created_at)
title_text = _escape_rich_markup(_truncate(title, 100))
title_text = _escape_rich_markup(truncate_text(title, 100))
conv_id = _escape_rich_markup(self.conversation_id)
self.update(f"{title_text}\n[dim]{conv_id} • {time_str}[/dim]")
self._has_title = True
Expand Down Expand Up @@ -460,11 +461,3 @@ def _format_time(dt: datetime) -> str:
return f"{diff.days}d ago"
else:
return dt.strftime("%Y-%m-%d")


def _truncate(text: str, max_length: int) -> str:
"""Truncate text for display."""
text = text.replace("\n", " ").replace("\r", " ")
if len(text) <= max_length:
return text
return text[: max_length - 3] + "..."
15 changes: 6 additions & 9 deletions openhands_cli/tui/widgets/richlog_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from openhands.tools.task_tracker.definition import TaskTrackerObservation
from openhands.tools.terminal.definition import TerminalAction
from openhands_cli.shared.delegate_formatter import format_delegate_title
from openhands_cli.shared.text_utils import truncate_text
from openhands_cli.stores import CliSettings
from openhands_cli.theme import OPENHANDS_THEME
from openhands_cli.tui.widgets.collapsible import (
Expand All @@ -46,7 +47,6 @@

# Maximum line length for truncating titles/commands in collapsed view
MAX_LINE_LENGTH = 70
ELLIPSIS = "..."

# Default agent name - don't show prefix for this agent
DEFAULT_AGENT_NAME = "OpenHands Agent"
Expand Down Expand Up @@ -544,17 +544,14 @@ def _truncate_for_display(
from_start: If True, keep the start and add ellipsis at end.
If False, keep the end and add ellipsis at start (for paths).
"""
if len(text) > max_length:
if from_start:
return text[: max_length - len(ELLIPSIS)] + ELLIPSIS
else:
return ELLIPSIS + text[-(max_length - len(ELLIPSIS)) :]
return text
return truncate_text(
text, max_length, from_start=from_start, collapse_whitespace=False
)

def _clean_and_truncate(self, text: str, *, from_start: bool = True) -> str:
"""Strip, collapse newlines, truncate, and escape Rich markup for display."""
text = str(text).strip().replace("\n", " ")
text = self._truncate_for_display(text, from_start=from_start)
text = str(text).strip()
text = truncate_text(text, MAX_LINE_LENGTH, from_start=from_start)
return self._escape_rich_markup(text)

def _extract_meaningful_title(self, event, fallback_title: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion tests/tui/widgets/test_richlog_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from openhands.sdk.event.conversation_error import ConversationErrorEvent
from openhands.sdk.llm import MessageToolCall
from openhands.tools.terminal.definition import TerminalAction
from openhands_cli.shared.text_utils import ELLIPSIS
from openhands_cli.stores import CliSettings
from openhands_cli.tui.textual_app import OpenHandsApp
from openhands_cli.tui.widgets.richlog_visualizer import (
ELLIPSIS,
MAX_LINE_LENGTH,
ConversationVisualizer,
)
Expand Down
Loading