Source code for ralph.display.content_condenser

"""Predictable head+tail condensation for oversized content lines."""

from __future__ import annotations

import os
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Mapping

from rich.cells import cell_len

from ralph.display.long_content_summary import (
    build_ai_summary,
    build_headline_or_placeholder,
    should_summarize,
)

_SOFT_LIMIT = 400
_HARD_LIMIT = 4000

_CondensedResult = tuple[str, bool] | tuple[str, bool, str | None, str | None]


def _slice_to_cells(text: str, max_cells: int) -> str:
    """Return the longest prefix of *text* that fits within *max_cells* display cells."""
    result: list[str] = []
    used = 0
    for char in text:
        w = cell_len(char)
        if used + w > max_cells:
            break
        result.append(char)
        used += w
    return "".join(result)


[docs] @dataclass class CondenseOptions: """Options for content condensation. Attributes: soft_limit: Display cell width before showing truncation hint. hard_limit: Display cell width before switching to head+tail mode. overflow_ref: Reference string embedded in truncation suffix. summary: Whether to generate summary lines. env: Environment variables mapping for AI summary hooks. """ soft_limit: int = _SOFT_LIMIT hard_limit: int = _HARD_LIMIT overflow_ref: str | None = None summary: bool = False env: Mapping[str, str] | None = None
def _build_summaries(text: str, env: Mapping[str, str] | None) -> tuple[str | None, str | None]: """Build summary lines if content meets criteria, else return (None, None).""" _env = env if env is not None else os.environ if should_summarize(text, _env): return ( build_headline_or_placeholder(text, max_chars=200), build_ai_summary(text, _env), ) return (None, None) def _truncation_suffix(overflow_ref: str | None) -> str: """Build truncation suffix string.""" if overflow_ref is not None: return f" … (truncated, see {overflow_ref})" return " … (truncated)" def _elision_suffix(omitted: int, overflow_ref: str | None) -> str: """Build middle elision suffix string.""" if overflow_ref is not None: return f" … (+{omitted} chars, see {overflow_ref}) … " return f" … (+{omitted} chars truncated) … " def _extract_tail(text: str, tail_cells: int) -> str: """Extract tail portion of *text* up to *tail_cells* display cells.""" tail_chars: list[str] = [] used = 0 for char in reversed(text): w = cell_len(char) if used + w > tail_cells: break tail_chars.append(char) used += w return "".join(reversed(tail_chars)) def _return_result( visible: str, condensed: bool, options: CondenseOptions, original: str ) -> _CondensedResult: """Return result tuple, optionally including summary lines.""" if options.summary: summary_line, ai_summary_line = _build_summaries(original, options.env) return (visible, condensed, summary_line, ai_summary_line) return (visible, condensed) def _condense_head_only(text: str, options: CondenseOptions) -> _CondensedResult: """Condense using head-only truncation.""" head = _slice_to_cells(text, options.soft_limit) visible = head + _truncation_suffix(options.overflow_ref) return _return_result(visible, True, options, text) def _condense_head_and_tail(text: str, total: int, options: CondenseOptions) -> _CondensedResult: """Condense using head + tail with middle elision.""" head_cells = options.hard_limit // 2 tail_cells = options.hard_limit - head_cells head = _slice_to_cells(text, head_cells) tail = _extract_tail(text, tail_cells) omitted = total - cell_len(head) - cell_len(tail) visible = head + _elision_suffix(omitted, options.overflow_ref) + tail return _return_result(visible, True, options, text)
[docs] def condense_content( text: str, *, options: CondenseOptions | None = None, ) -> tuple[str, bool] | tuple[str, bool, str | None, str | None]: """Condense *text* so it fits within display limits. Pass a :class:`CondenseOptions` to configure limits, overflow ref, and summarization. Omit to use defaults. Returns ``(visible, condensed_flag)`` when ``options.summary`` is False. Returns ``(visible, condensed_flag, summary_line, ai_summary_line)`` when ``options.summary`` is True. Rules: - If ``cell_len(text) <= soft_limit``: return ``(text, False[, None, None])`` - If ``cell_len(text) <= hard_limit``: head-only truncation with suffix - If ``cell_len(text) > hard_limit``: head + tail with middle elided """ opts = options if options is not None else CondenseOptions() if not text: return _return_result("", False, opts, "") try: total = cell_len(text) except Exception: return _return_result(text, False, opts, text) if total <= opts.soft_limit: return _return_result(text, False, opts, text) if total <= opts.hard_limit: return _condense_head_only(text, opts) return _condense_head_and_tail(text, total, opts)
__all__ = ["CondenseOptions", "condense_content"]