Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class RunConfig(ConfigBase):
async_trace: If True, collect per-task tracing data. Default is False.
display_tui: If True, display the terminal throughput TUI instead of periodic
log lines during generation. Requires a TTY; falls back to log lines in
non-TTY environments. Default is True.
non-TTY environments. Default is False.
progress_interval: How often (in seconds) the async progress reporter emits a
consolidated log block. Must be > 0. Default is 5.0.
preserve_dropped_columns: If True, write columns removed by drop processors to
Expand Down Expand Up @@ -185,7 +185,7 @@ class RunConfig(ConfigBase):
max_conversation_restarts: int = Field(default=5, ge=0)
max_conversation_correction_steps: int = Field(default=0, ge=0)
async_trace: bool = False
display_tui: bool = True
display_tui: bool = False
progress_interval: float = Field(default=5.0, gt=0.0)
preserve_dropped_columns: bool = Field(
default=True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def test_run_config_accepts_native_renderer() -> None:
assert JinjaRenderingEngine(run_config.jinja_rendering_engine) == JinjaRenderingEngine.NATIVE


def test_run_config_defaults_to_display_tui_enabled() -> None:
assert RunConfig().display_tui is True
def test_run_config_defaults_to_display_tui_disabled() -> None:
assert RunConfig().display_tui is False


def test_run_config_accepts_display_tui() -> None:
Expand Down
1 change: 1 addition & 0 deletions packages/data-designer-engine/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ dependencies = [
"sqlfluff>=4.1.0,<5",
"starlette>=1.2.0,<2", # 1.2.0 fixes security advisory pulled in by mcp
"tiktoken>=0.8.0,<1",
"wcwidth>=0.2.13,<1",
]

[tool.hatch.build.targets.wheel]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import Sequence, TextIO

import asciichartpy
from wcwidth import wcswidth

_ANSI_RE = re.compile(r"\033\[[0-9;?]*[a-zA-Z]")
_CONTROL_RE = re.compile(r"[\x00-\x1f\x7f-\x9f]")
Expand Down Expand Up @@ -67,13 +68,24 @@


def _visible_len(text: str) -> int:
return len(_ANSI_RE.sub("", text))
visible = _ANSI_RE.sub("", text)
width = wcswidth(visible)
return width if width >= 0 else len(visible)


def _truncate_to_width(text: str, width: int) -> str:
for index in range(1, len(text) + 1):
prefix_width = wcswidth(text[:index])
if prefix_width < 0 or prefix_width > width:
return text[: index - 1]
return text


def _fit_ansi(text: str, width: int) -> str:
visible = _visible_len(text)
if visible > width:
return _ANSI_RE.sub("", text)[:width] + _RESET
fitted = _truncate_to_width(_ANSI_RE.sub("", text), width)
return fitted + (" " * (width - _visible_len(fitted))) + _RESET
return text + (" " * (width - visible))


Expand All @@ -87,7 +99,8 @@ def sanitize_terminal_text(label: str) -> str:

def _fit_plain(text: str, width: int) -> str:
clean = sanitize_terminal_text(text)
return clean[:width].ljust(width)
fitted = _truncate_to_width(clean, width)
return fitted + (" " * (width - _visible_len(fitted)))


def _average(values: Sequence[float]) -> float:
Expand Down Expand Up @@ -294,6 +307,7 @@ def __init__(self, stream: TextIO | None = None, *, panel_height: int = _DEFAULT
self._feedback_markers: list[_FeedbackMarker] = []
self._lock = Lock()
self._drawn_lines = 0
self._drawn_terminal_size: tuple[int, int] | None = None
self._active = False
self._owns_stream = False
self._wrapped_handlers: list[tuple[logging.StreamHandler, object]] = []
Expand Down Expand Up @@ -344,6 +358,7 @@ def __exit__(self, *args: object) -> None:
self._unwrap_handlers()
self._active = False
self._drawn_lines = 0
self._drawn_terminal_size = None
finally:
self._release_stream()

Expand Down Expand Up @@ -489,12 +504,16 @@ def _unwrap_handlers(self) -> None:

def _clear_bars(self) -> None:
"""Clear drawn panel lines from the terminal. Caller must hold the lock."""
terminal_size = shutil.get_terminal_size()
if self._drawn_lines > 0:
clear_count = min(self._drawn_lines, max(0, shutil.get_terminal_size().lines - 1))
clear_count = min(self._drawn_lines, max(0, terminal_size.lines - 1))
for _ in range(clear_count):
self._write("\033[A\033[2K")
self._write("\r\033[2K")
self._drawn_lines = 0
if self._drawn_terminal_size is not None and self._drawn_terminal_size != tuple(terminal_size):
self._write("\033[J")
self._drawn_terminal_size = None

def _redraw_if_due(self, now: float, *, force: bool = False) -> None:
if force or self._drawn_lines == 0 or now - self._last_redraw_time >= _MIN_REDRAW_INTERVAL_SECONDS:
Expand All @@ -504,7 +523,7 @@ def _redraw(self, *, force: bool = False, now: float | None = None) -> None:
"""Redraw the chart panel. Caller must hold the lock."""
terminal_size = shutil.get_terminal_size()
if terminal_size.columns < _MIN_TERMINAL_WIDTH or terminal_size.lines < _MIN_TERMINAL_HEIGHT:
self._drawn_lines = 0
self._clear_bars()
self._write("\033[?25h")
self._unwrap_handlers()
self._active = False
Expand All @@ -521,6 +540,7 @@ def _redraw(self, *, force: bool = False, now: float | None = None) -> None:
for line in lines:
self._write(line + "\n")
self._drawn_lines = len(lines)
self._drawn_terminal_size = tuple(terminal_size)
self._last_redraw_time = time.perf_counter() if now is None else now

def _format_panel(self) -> list[str]:
Expand Down Expand Up @@ -736,7 +756,7 @@ def _column_table_widths(
fixed_width_without_label_or_progress = (
2 + (separator_count * _LEGEND_COLUMN_GAP) + done_width + (rate_width * 2) + status_width
)
content_label_width = max(len("column"), *(len(sanitize_terminal_text(bar.label)) for bar in bars))
content_label_width = max(_visible_len("column"), *(_visible_len(bar.label) for bar in bars))
desired_label_width = max(_MIN_LEGEND_LABEL_WIDTH, content_label_width)
available_width = inner_width - fixed_width_without_label_or_progress

Expand Down Expand Up @@ -826,13 +846,13 @@ def _model_table_widths(
available_text_width = inner_width - fixed_width_without_text
desired_alias_width = max(
_MIN_MODEL_ALIAS_WIDTH,
len("model alias"),
*(len(state.model_alias) for state in model_usage),
_visible_len("model alias"),
*(_visible_len(state.model_alias) for state in model_usage),
)
desired_model_width = max(
_MIN_MODEL_NAME_WIDTH,
len("model name"),
*(len(state.model_name) for state in model_usage),
_visible_len("model name"),
*(_visible_len(state.model_name) for state in model_usage),
)

if available_text_width >= desired_alias_width + desired_model_width:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from unittest.mock import patch

import pytest
from wcwidth import wcswidth

from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event
from data_designer.engine.observability import (
Expand All @@ -36,6 +37,7 @@
CURSOR_UP_CLEAR = "\033[A\033[2K"
HIDE_CURSOR = "\033[?25l"
SHOW_CURSOR = "\033[?25h"
ERASE_TO_END = "\033[J"
_ALL_ANSI_RE = re.compile(r"\033\[[0-9;?]*[a-zA-Z]")


Expand Down Expand Up @@ -140,11 +142,16 @@ def test_only_one_panel_owns_a_stream(tty_stream: FakeTTY) -> None:
def test_active_panel_falls_back_when_terminal_shrinks(tty_stream: FakeTTY) -> None:
with TerminalThroughputPanel(stream=tty_stream) as bar:
bar.add_bar("a", "column 'a'", 100)
snapshot = tty_stream.getvalue()
with patch.object(shutil, "get_terminal_size", return_value=os.terminal_size((20, 5))):
bar.update("a", completed=1, success=1, force=True)
assert bar.is_active is False
assert bar.drawn_lines == 0

resize_output = tty_stream.getvalue()[len(snapshot) :]
assert ERASE_TO_END in resize_output
assert resize_output.index(ERASE_TO_END) < resize_output.index(SHOW_CURSOR)

assert tty_stream.getvalue().count(HIDE_CURSOR) == 1
assert tty_stream.getvalue().count(SHOW_CURSOR) == 1

Expand Down Expand Up @@ -400,6 +407,53 @@ def test_narrow_terminal_keeps_panel_within_width(tty_stream: FakeTTY) -> None:
assert len(line) <= 35


@pytest.mark.parametrize(
("terminal_width", "label", "model_alias", "model_name"),
[
# Exercise double-width CJK/emoji, combining marks, and ZWJ sequences whose
# code-point counts differ from their displayed terminal cell widths.
(30, "εˆ—πŸš€" * 20, "ζ¨‘εž‹" * 20, "提供者/ζ¨‘εž‹" * 20),
(36, "e\u0301" * 20, "πŸ‘©πŸ½β€πŸ’»" * 20, "ζ¨‘εž‹/e\u0301" * 20),
(50, "εˆ—πŸš€" * 20, "ζ¨‘εž‹" * 20, "提供者/ζ¨‘εž‹" * 20),
(80, "e\u0301" * 20, "πŸ‘©πŸ½β€πŸ’»" * 20, "ζ¨‘εž‹/e\u0301" * 20),
],
)
def test_wide_unicode_labels_stay_within_terminal_width(
tty_stream: FakeTTY,
terminal_width: int,
label: str,
model_alias: str,
model_name: str,
) -> None:
with patch.object(shutil, "get_terminal_size", return_value=os.terminal_size((terminal_width, 24))):
with TerminalThroughputPanel(stream=tty_stream) as bar:
bar.add_bar("a", label, 100)
bar.record_model_usage(
model_alias=model_alias,
model_name=model_name,
input_tokens=100,
output_tokens=25,
force=True,
)

assert all(wcswidth(line) == terminal_width - 1 for line in _last_panel_lines(tty_stream.getvalue()))


def test_active_panel_clears_visible_artifacts_when_terminal_resizes(tty_stream: FakeTTY) -> None:
terminal_size = os.terminal_size((80, 24))

with patch.object(shutil, "get_terminal_size", side_effect=lambda: terminal_size):
with TerminalThroughputPanel(stream=tty_stream) as bar:
bar.add_bar("a", "column 'a'", 100)
snapshot = tty_stream.getvalue()
terminal_size = os.terminal_size((50, 12))

bar.update("a", completed=1, success=1, force=True)

assert bar.is_active is True
assert ERASE_TO_END in tty_stream.getvalue()[len(snapshot) :]


def test_update_many_single_redraw(tty_stream: FakeTTY) -> None:
with TerminalThroughputPanel(stream=tty_stream) as bar:
bar.add_bar("a", "col_a", 100)
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading