"""
TUI screen management for Pulka.

This module manages the sheet stack, viewer state, and dialog handling
within the terminal user interface.
"""

from __future__ import annotations

import os
import subprocess
import threading
from collections.abc import Callable, Iterator, Sequence
from contextlib import nullcontext, suppress
from dataclasses import dataclass, field
from io import StringIO
from pathlib import Path
from time import perf_counter_ns
from types import MethodType, SimpleNamespace
from typing import TYPE_CHECKING, Any

import polars as pl
from prompt_toolkit import Application
from prompt_toolkit.application import run_in_terminal
from prompt_toolkit.application.current import get_app_or_none
from prompt_toolkit.formatted_text import ANSI, StyleAndTextTuples
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.containers import HSplit
from prompt_toolkit.widgets import Box, Button, Dialog, TextArea
from rich.console import Console
from rich.pretty import Pretty

from .. import __version__ as pulka_version
from .. import theme
from ..clipboard import copy_to_clipboard
from ..command.parser import CommandDispatchResult
from ..command.registry import CommandContext
from ..command.runtime import CommandRuntimeResult
from ..config import use_prompt_toolkit_table
from ..core.engine.contracts import TableSlice
from ..core.engine.polars_adapter import table_slice_from_dataframe
from ..core.sheet_actions import SheetEnterAction, resolve_enter_action
from ..core.sheet_traits import resolve_display_path, resolve_sheet_traits
from ..core.viewer import Viewer, build_filter_expr_for_values, viewer_public_state
from ..core.viewer.ui_hooks import NullViewerUIHooks
from ..core.viewer.ui_state import resolve_insight_state, set_insight_state
from ..data.filter_lang import FilterError
from ..data.transform_lang import TransformError
from ..logging import Recorder, frame_hash, viewer_state_snapshot
from ..sheets.file_browser_sheet import FileDeletionResult

if TYPE_CHECKING:  # pragma: no cover - import for type checking only
    from ..api.session import Session

from . import modals as tui_modals
from .completions import ColumnNameCompleter, FilesystemPathCompleter
from .controllers.column_insight import ColumnInsightController
from .controllers.dataset_reload import DatasetReloadController
from .controllers.file_browser import FileBrowserController
from .controllers.file_ops import FileOpsController
from .controllers.file_watch import FileSnapshot, FileWatchController
from .job_pump import JobPump
from .keymap import build_key_bindings

_CLIPBOARD_RUN_IN_TERMINAL_THRESHOLD = 70 * 1024
from .modal_manager import ModalManager
from .presenters import StatusPresenter
from .screen_layout import build_screen_layout
from .ui_hooks import PromptToolkitViewerUIHooks

# Constants
_STACK_MIN_SIZE = 2  # Minimum stack size for frequency view filters
_HISTORY_MAX_SIZE = 20  # Maximum size for search/filter history
_CELL_MODAL_CHROME_HEIGHT = 8  # Non-text area rows needed by the cell modal
_INSIGHT_MIN_COLS_DEFAULT = 120  # Hide insight when the terminal is narrower than this


def _format_expr_filters_for_modal(filter_clauses: Sequence[object]) -> str:
    """Return a joined expression filter string for the expression modal."""

    expr_texts: list[str] = []
    for clause in filter_clauses:
        kind = getattr(clause, "kind", None)
        text = getattr(clause, "text", None)
        if kind != "expr" or not text:
            continue
        expr_texts.append(text.strip())

    if not expr_texts:
        return ""

    wrapped = [f"({text})" for text in expr_texts]
    return " & ".join(wrapped)


def _ordered_freq_values(freq_viewer: Viewer, selected_ids: set[object]) -> list[object]:
    """Return selected frequency values ordered by current display rows."""

    if not selected_ids:
        return []

    freq_column = getattr(freq_viewer, "freq_source_col", None)
    if not freq_column:
        freq_column = freq_viewer.columns[0] if freq_viewer.columns else None

    ordered: list[object] = []
    display_df = getattr(getattr(freq_viewer, "sheet", None), "_display_df", None)
    if display_df is not None and freq_column in getattr(display_df, "columns", ()):
        with suppress(Exception):
            for value in display_df.get_column(freq_column).to_list():
                if value in selected_ids and value not in ordered:
                    ordered.append(value)

    for value in selected_ids:
        if value not in ordered:
            ordered.append(value)

    return ordered


def _resolve_full_path(path: Path) -> Path:
    try:
        candidate = path.expanduser()
    except Exception:
        candidate = path
    try:
        return candidate.resolve()
    except Exception:
        return candidate.absolute()


def _last_open_dataset_path(session: Session | None) -> str | None:
    if session is None:
        return None
    path = session.dataset_path
    if path is None:
        return None
    return str(_resolve_full_path(path))


@dataclass
class _ColumnSearchState:
    """Mutable state tracked while the column search feature is active."""

    query: str | None = None
    matches: list[int] = field(default_factory=list)
    position: int | None = None

    def clear(self) -> None:
        """Reset the stored query and matches."""

        self.query = None
        self.matches.clear()
        self.position = None

    def set(self, query: str, matches: list[int], *, current_col: int) -> None:
        """Store a fresh query and its matches."""

        self.query = query
        self._apply_matches(matches, current_col=current_col, preserve_position=False)

    def recompute(self, matches: list[int], *, current_col: int) -> None:
        """Refresh matches for an existing query while keeping position when possible."""

        self._apply_matches(matches, current_col=current_col, preserve_position=True)

    def _apply_matches(
        self, matches: list[int], *, current_col: int, preserve_position: bool
    ) -> None:
        previous_position = self.position if preserve_position else None
        self.matches = list(matches)
        if not self.matches:
            self.position = None
            return

        if current_col in self.matches:
            self.position = self.matches.index(current_col)
        elif previous_position is not None and previous_position < len(self.matches):
            self.position = previous_position
        else:
            self.position = 0


@dataclass
class _ClipboardRegionState:
    """Track the starting cell for a clipboard region selection."""

    row_id: int | None = None
    col_index: int | None = None
    awaiting_start: bool = True
    format: str = "tsv"


class Screen:
    def __init__(
        self,
        viewer: Viewer,
        recorder: Recorder | None = None,
        *,
        on_shutdown: Callable[[Session], None] | None = None,
    ):
        self.viewer = viewer
        self.session = viewer.session
        if self.session is None:
            raise RuntimeError("Screen requires a session-bound viewer")
        self._last_terminal_size: tuple[int, int] | None = None
        self._terminal_resize_handler_installed = False
        self._on_shutdown = on_shutdown
        self.commands = self.session.commands
        self._runtime = self.session.command_runtime
        self._recorder = recorder or self.session.recorder
        self._runtime.prepare_viewer(self.viewer)
        self.view_stack = self.session.view_stack
        self._insight_controller: ColumnInsightController | None = None
        self._insight_base_default = self._initial_insight_enabled()
        initial_insight_state = resolve_insight_state(
            self.viewer, fallback_enabled=self._insight_base_default
        )
        self._insight_enabled = initial_insight_state.enabled
        self._insight_user_enabled = initial_insight_state.user_enabled
        self._insight_allowed = initial_insight_state.allowed
        self._insight_terminal_allowed = True
        self._transform_signature: tuple[object, ...] | None = None
        self._transforms_active = False
        self._return_to_column_after_transforms = False
        self._transform_autoclose_timer: threading.Timer | None = None
        self._transforms_insight_panel = None
        self._jobs: dict[Viewer, object] = {}  # Jobs for background processing
        self._job_pump = JobPump(
            jobs=self._jobs,
            check_dataset_file_changes=self._check_dataset_file_changes,
            check_file_browser_changes=self._check_file_browser_changes,
        )
        self._file_browser_controller = FileBrowserController(
            session=self.session, get_viewer=lambda: self.viewer
        )
        self._file_watch: FileWatchController | None = None
        # Use a getter that applies queued moves per frame (capped)
        self._pending_row_delta = 0
        self._pending_col_delta = 0
        self._count_buf: int | None = None
        # Allow tuning via env; default to 3 steps/frame
        try:
            from ..utils import _get_int_env

            env_value = os.environ.get("PULKA_MAX_STEPS_PER_FRAME")
            override = getattr(viewer, "_max_steps_per_frame_override", None)
            if env_value is not None:
                self._max_steps_per_frame = max(
                    1, _get_int_env("PULKA_MAX_STEPS_PER_FRAME", None, 3)
                )
            elif isinstance(override, int) and override > 0:
                self._max_steps_per_frame = override
            else:
                self._max_steps_per_frame = 3
        except Exception:
            self._max_steps_per_frame = 3
        self._base_max_steps_per_frame = self._max_steps_per_frame
        use_ptk_table = self._should_use_ptk_table()
        self._last_status_fragments: StyleAndTextTuples | None = None
        self._last_status_plain: str | None = None
        layout_parts = build_screen_layout(
            viewer=self.viewer,
            use_ptk_table=use_ptk_table,
            build_ptk_table_control=self._build_table_control,
            get_table_text=self._get_table_text,
            get_status_text=self._get_status_text,
            insight_enabled=lambda: self._insight_enabled,
            insight_allowed=self._insight_is_allowed,
        )
        self._use_ptk_table = layout_parts.use_ptk_table
        self._table_control = layout_parts.table_control
        self._status_control = layout_parts.status_control
        self._table_window = layout_parts.table_window
        self._status_window = layout_parts.status_window
        self._column_insight_panel = layout_parts.column_insight_panel
        self._transforms_insight_panel = layout_parts.transforms_insight_panel
        self._insight_panel = layout_parts.insight_panel
        self._insight_control = layout_parts.insight_control
        self._insight_window = layout_parts.insight_window
        self._insight_border = layout_parts.insight_border
        self._insight_border_padding = layout_parts.insight_border_padding
        self._insight_container = layout_parts.insight_container
        self.window = layout_parts.window
        self._view_stack_unsubscribe = self.view_stack.add_active_viewer_listener(
            self._on_active_viewer_changed
        )
        self._modal_manager = ModalManager(window=self.window, table_window=self._table_window)
        self._presenter = StatusPresenter(
            get_viewer=lambda: self.viewer,
            refresh=self.refresh,
            modals=self._modal_manager,
            get_app=lambda: self.app,
        )
        self._file_ops = FileOpsController(
            file_browser=self._file_browser_controller,
            presenter=self._presenter,
            get_viewer=lambda: self.viewer,
            refresh=self.refresh,
            handle_file_browser_refresh=self._handle_file_browser_refresh,
            invalidate=self._invalidate_app,
        )
        self._dataset_reload = DatasetReloadController(
            session=self.session,
            get_viewer=lambda: self.viewer,
            get_file_watch=lambda: self._file_watch,
            refresh=self.refresh,
            recorder_getter=lambda: getattr(self, "_recorder", None),
            open_reload_error_modal=lambda error_text: self._open_reload_error_modal(
                error_text=error_text
            ),
        )
        self._search_history: list[str] = []
        self._row_search_history: list[str] = []
        self._expr_filter_history: list[str] = []
        self._sql_filter_history: list[str] = []
        self._transform_history: list[str] = []
        self._command_history: list[str] = []
        self._command_history.append("write output.parquet")
        self._shell_history: list[str] = []
        self._col_search_history: list[str] = []
        self._col_search_state = _ColumnSearchState()
        self._clipboard_region: _ClipboardRegionState | None = None

        kb = build_key_bindings(self)

        self.app = Application(
            layout=Layout(self.window, focused_element=self._table_window),
            key_bindings=kb,
            full_screen=True,
            mouse_support=self._use_ptk_table,
            style=theme.APP_STYLE,
        )
        self._install_prompt_toolkit_perf_hooks()

        self._viewer_ui_hooks = PromptToolkitViewerUIHooks(self.app)
        self._install_terminal_resize_handler()
        attach_hooks = getattr(self._table_control, "attach_ui_hooks", None)
        if callable(attach_hooks):
            hooks = attach_hooks(self._viewer_ui_hooks)
            if hooks is not None:
                self._viewer_ui_hooks = hooks
        self.session.set_viewer_ui_hooks(self._viewer_ui_hooks)
        self._file_watch = FileWatchController(
            dataset_path_getter=lambda: getattr(self.session, "dataset_path", None),
            viewer_getter=lambda: self.viewer,
            hooks_getter=lambda: getattr(self, "_viewer_ui_hooks", None),
            on_dataset_change=lambda path, snapshot: self._schedule_file_change_prompt(
                path, snapshot
            ),
            on_file_browser_refresh=lambda sheet: self._handle_file_browser_refresh(sheet),
            on_file_browser_error=lambda exc: self._handle_file_browser_error(exc),
        )

        self._insight_controller = ColumnInsightController(
            viewer=self.viewer,
            panel=self._column_insight_panel,
            recorder=self._recorder,
            invalidate=self.app.invalidate,
            call_soon=self._viewer_ui_hooks.call_soon,
        )
        self._apply_insight_state(refresh=True)
        self._file_watch.sync(force=True)
        self._file_watch.check(force=True)
        self._refresh_transform_insight()

    def _handle_before_render(self, app: Application) -> None:
        _ = app
        self._sync_terminal_metrics_if_needed()

    def _install_prompt_toolkit_perf_hooks(self) -> None:
        renderer = getattr(self.app, "renderer", None)
        if renderer is None:
            return
        if getattr(renderer, "_pulka_perf_wrapped", False):
            return

        original_render = renderer.render

        def _wrapped_render(_self_renderer: Any, *args: Any, **kwargs: Any) -> Any:
            recorder = self._recorder if self._recorder and self._recorder.enabled else None
            if recorder is None:
                return original_render(*args, **kwargs)

            layout = args[0] if args else kwargs.get("layout")
            original_layout_render = None
            build_ns = 0

            if layout is not None:
                original_layout_render = getattr(layout, "render", None)
                if callable(original_layout_render):

                    def _timed_layout_render(*render_args: Any, **render_kwargs: Any) -> Any:
                        nonlocal build_ns
                        start_ns = perf_counter_ns()
                        try:
                            return original_layout_render(*render_args, **render_kwargs)
                        finally:
                            build_ns += perf_counter_ns() - start_ns

                    layout.render = _timed_layout_render

            total_start_ns = perf_counter_ns()
            try:
                return original_render(*args, **kwargs)
            finally:
                total_ns = perf_counter_ns() - total_start_ns
                if layout is not None and original_layout_render is not None:
                    layout.render = original_layout_render
                if build_ns:
                    recorder.record_perf(
                        phase="render.ptk.screen_build",
                        duration_ms=build_ns / 1_000_000,
                        payload={"context": "tui"},
                    )
                    diff_ns = max(0, total_ns - build_ns)
                    recorder.record_perf(
                        phase="render.ptk.diff_output",
                        duration_ms=diff_ns / 1_000_000,
                        payload={"context": "tui"},
                    )

        renderer.render = MethodType(_wrapped_render, renderer)
        with suppress(Exception):
            renderer._pulka_perf_wrapped = True

    def _install_terminal_resize_handler(self) -> None:
        if self._terminal_resize_handler_installed:
            return
        existing = getattr(self.app, "__dict__", {}).get("_on_resize")
        if callable(existing) and getattr(existing, "_pulka_wrapped", False):
            self._terminal_resize_handler_installed = True
            return

        on_resize = getattr(self.app, "_on_resize", None)
        if callable(on_resize):
            original_on_resize = on_resize

            def _wrapped_on_resize() -> None:
                self._sync_terminal_metrics_if_needed()
                original_on_resize()

            with suppress(Exception):
                _wrapped_on_resize._pulka_wrapped = True
            self.app._on_resize = _wrapped_on_resize  # type: ignore[method-assign]
            self._terminal_resize_handler_installed = True
            return

        with suppress(Exception):
            self.app.before_render.add_handler(self._handle_before_render)

    def _sync_terminal_metrics_if_needed(self) -> None:
        try:
            size = self.app.output.get_size()
        except Exception:
            return
        size_tuple = (size.columns, size.rows)
        if size_tuple == self._last_terminal_size:
            return
        self._last_terminal_size = size_tuple
        previous_terminal_allowed = self._insight_terminal_allowed
        with suppress(Exception):
            self._update_viewer_metrics()
        with suppress(Exception):
            self.viewer.clamp()
        if self._insight_terminal_allowed != previous_terminal_allowed:
            self._apply_insight_visibility(refresh=self._insight_enabled)

    def _should_use_ptk_table(self) -> bool:
        return use_prompt_toolkit_table()

    def _initial_insight_enabled(self) -> bool:
        env_value = os.getenv("PULKA_INSIGHT_PANEL")
        if env_value is None:
            try:
                from shutil import get_terminal_size

                cols = get_terminal_size(fallback=(0, 0)).columns
            except Exception:
                cols = 0
            if cols:
                return cols >= _INSIGHT_MIN_COLS_DEFAULT
            return True
        normalized = env_value.strip().lower()
        if normalized in {"0", "false", "off", "no"}:
            return False
        if normalized in {"1", "true", "on", "yes"}:
            return True
        return True

    def _insight_has_room(self, cols: int) -> bool:
        if cols <= 0:
            return True
        return cols >= _INSIGHT_MIN_COLS_DEFAULT

    def _update_insight_terminal_allowed(self, cols: int) -> bool:
        allowed = self._insight_has_room(cols)
        if allowed == self._insight_terminal_allowed:
            return False
        self._insight_terminal_allowed = allowed
        return True

    def _insight_is_allowed(self) -> bool:
        return self._insight_allowed and self._insight_terminal_allowed

    def _insight_is_effective(self) -> bool:
        return self._insight_enabled and self._insight_is_allowed()

    def _apply_budget_plan(self, plan) -> None:
        multiplier = float(getattr(plan, "coalesce_multiplier", 1.0) or 1.0)
        base = getattr(self, "_base_max_steps_per_frame", self._max_steps_per_frame)
        if multiplier > 1.0:
            boosted = max(base, int(round(base * multiplier)))
            self._max_steps_per_frame = max(base, min(12, boosted))
        else:
            self._max_steps_per_frame = base

    def _build_table_control(self):
        from .controls.table_control import TableControl

        return TableControl(
            self.viewer,
            apply_pending_moves=self._apply_pending_moves,
            poll_background_jobs=self._poll_background_jobs,
            set_status=self._set_status_from_table,
            apply_budget_plan=self._apply_budget_plan,
            on_cell_click=self._handle_clipboard_region_click,
            recorder=self._recorder,
        )

    def _clear_g_buf(self) -> None:
        """Reset the pending g-command and count state."""
        if hasattr(self, "_g_buf"):
            self._g_buf = 0
        self._clear_count_buf()

    def _clear_count_buf(self) -> None:
        self._count_buf = None

    def _append_count_digit(self, digit: int) -> bool:
        if digit == 0 and self._count_buf is None:
            return False
        next_value = (self._count_buf or 0) * 10 + digit
        self._count_buf = min(next_value, 999)
        return True

    def _consume_count(self, default: int = 1) -> int:
        count = self._count_buf if self._count_buf is not None else default
        self._count_buf = None
        return max(1, count)

    def _resolve_viewport_cursor_indices(self) -> tuple[int, int] | None:
        viewer = self.viewer
        columns = list(getattr(viewer, "visible_cols", []) or [])
        if not columns:
            return None
        try:
            current_col = viewer.current_colname()
        except Exception:
            current_col = None
        if not current_col:
            cur_col = getattr(viewer, "cur_col", None)
            if isinstance(cur_col, int):
                all_cols = getattr(viewer, "columns", [])
                if 0 <= cur_col < len(all_cols):
                    current_col = all_cols[cur_col]
        if not current_col:
            return None
        try:
            col_index = columns.index(current_col)
        except ValueError:
            return None

        row_positions = list(getattr(viewer, "visible_row_positions", []) or [])
        if not row_positions:
            return None
        cur_row = getattr(viewer, "cur_row", None)
        if not isinstance(cur_row, int):
            return None
        try:
            row_index = row_positions.index(cur_row)
        except ValueError:
            row0 = getattr(viewer, "row0", 0)
            row_index = max(0, min(len(row_positions) - 1, cur_row - row0))
        return row_index, col_index

    @staticmethod
    def _sanitize_clipboard_cell(value: object) -> str:
        text = "" if value is None else str(value)
        return text.replace("\t", " ").replace("\n", " ").replace("\r", " ")

    @classmethod
    def _sanitize_markdown_cell(cls, value: object) -> str:
        text = cls._sanitize_clipboard_cell(value)
        return text.replace("|", "\\|")

    @classmethod
    def _sanitize_ascii_cell(cls, value: object) -> str:
        text = cls._sanitize_clipboard_cell(value)
        return text.replace("|", "\\|")

    def _start_clipboard_region(self, *, format_name: str = "tsv") -> None:
        with suppress(Exception):
            self.viewer._ui_state.pop("clipboard_region_anchor", None)
        self._pending_row_delta = 0
        self._pending_col_delta = 0
        columns = list(getattr(self.viewer, "visible_cols", []) or [])
        row_positions = list(getattr(self.viewer, "visible_row_positions", []) or [])
        if not columns or not row_positions:
            self.viewer.status_message = "no visible cells to copy"
            return
        self._clipboard_region = _ClipboardRegionState(format=format_name)
        self.viewer.status_message = "select first corner cell, Enter to set (Esc to cancel)"

    def _start_clipboard_region_markdown(self) -> None:
        self._start_clipboard_region(format_name="markdown")

    def _start_clipboard_region_ascii(self) -> None:
        self._start_clipboard_region(format_name="ascii")

    def _start_clipboard_region_unicode(self) -> None:
        self._start_clipboard_region(format_name="unicode")

    def _set_clipboard_region_start(self) -> None:
        state = self._clipboard_region
        if state is None:
            return
        indices = self._resolve_viewport_cursor_indices()
        if indices is None:
            self.viewer.status_message = "no visible cells to copy"
            return
        row_index, col_visible_index = indices
        row_positions = list(getattr(self.viewer, "visible_row_positions", []) or [])
        columns = list(getattr(self.viewer, "visible_cols", []) or [])
        if not row_positions or not columns:
            self.viewer.status_message = "no visible cells to copy"
            return
        if row_index < 0 or row_index >= len(row_positions):
            self.viewer.status_message = "no visible cells to copy"
            return
        if col_visible_index < 0 or col_visible_index >= len(columns):
            self.viewer.status_message = "no visible cells to copy"
            return

        row_id = row_positions[row_index]
        col_name = columns[col_visible_index]
        col_index = None
        all_cols = list(getattr(self.viewer, "columns", []) or [])
        with suppress(ValueError):
            col_index = all_cols.index(col_name)
        if col_index is None:
            self.viewer.status_message = "no visible cells to copy"
            return

        state.row_id = row_id
        state.col_index = col_index
        state.awaiting_start = False
        with suppress(Exception):
            self.viewer._ui_state["clipboard_region_anchor"] = (row_id, col_index)
        self.viewer.status_message = "select second corner cell, Enter to copy (Esc to cancel)"

    def _set_clipboard_region_start_at(self, row_id: int, col_name: str) -> None:
        state = self._clipboard_region
        if state is None:
            return
        row_positions = list(getattr(self.viewer, "visible_row_positions", []) or [])
        columns = list(getattr(self.viewer, "visible_cols", []) or [])
        if not row_positions or not columns:
            self.viewer.status_message = "no visible cells to copy"
            return
        if row_id not in row_positions:
            self.viewer.status_message = "selection outside viewport"
            return
        if col_name not in columns:
            self.viewer.status_message = "selection outside viewport"
            return

        all_cols = list(getattr(self.viewer, "columns", []) or [])
        try:
            col_index = all_cols.index(col_name)
        except ValueError:
            self.viewer.status_message = "selection outside viewport"
            return

        state.row_id = row_id
        state.col_index = col_index
        state.awaiting_start = False
        with suppress(Exception):
            self.viewer._ui_state["clipboard_region_anchor"] = (row_id, col_index)
        self.viewer.status_message = "select second corner cell, Enter to copy (Esc to cancel)"

    def _cancel_clipboard_region(self) -> None:
        if self._clipboard_region is None:
            return
        with suppress(Exception):
            self.viewer._ui_state.pop("clipboard_region_anchor", None)
        self._clipboard_region = None
        self._pending_row_delta = 0
        self._pending_col_delta = 0
        self.viewer.status_message = "clipboard region canceled"

    def _finalize_clipboard_region(self) -> None:
        state = self._clipboard_region
        if state is None:
            return
        with suppress(Exception):
            self.viewer._ui_state.pop("clipboard_region_anchor", None)
        self._clipboard_region = None
        self._pending_row_delta = 0
        self._pending_col_delta = 0
        if state.awaiting_start or state.row_id is None or state.col_index is None:
            self.viewer.status_message = "select first corner cell first"
            return
        indices = self._resolve_viewport_cursor_indices()
        if indices is None:
            self.viewer.status_message = "no visible cells to copy"
            return
        self._copy_clipboard_region_to_clipboard(state, indices)

    def _finalize_clipboard_region_at(self, end: tuple[int, int]) -> None:
        state = self._clipboard_region
        if state is None:
            return
        with suppress(Exception):
            self.viewer._ui_state.pop("clipboard_region_anchor", None)
        self._clipboard_region = None
        self._pending_row_delta = 0
        self._pending_col_delta = 0
        if state.awaiting_start or state.row_id is None or state.col_index is None:
            self.viewer.status_message = "select first corner cell first"
            return
        self._copy_clipboard_region_to_clipboard(state, end)

    def _handle_clipboard_region_click(self, row_id: int, col_name: str) -> None:
        state = self._clipboard_region
        if state is None:
            return
        row_positions = list(getattr(self.viewer, "visible_row_positions", []) or [])
        columns = list(getattr(self.viewer, "visible_cols", []) or [])
        if not row_positions or not columns:
            self.viewer.status_message = "no visible cells to copy"
            return
        if row_id not in row_positions:
            self.viewer.status_message = "selection outside viewport"
            return
        if col_name not in columns:
            self.viewer.status_message = "selection outside viewport"
            return
        row_index = row_positions.index(row_id)
        col_visible_index = columns.index(col_name)
        if state.awaiting_start:
            self._set_clipboard_region_start_at(row_id, col_name)
        else:
            self._finalize_clipboard_region_at((row_index, col_visible_index))

    def _copy_clipboard_region_to_clipboard(
        self, start: _ClipboardRegionState, _end: tuple[int, int]
    ) -> None:
        viewer = self.viewer
        columns = list(getattr(viewer, "visible_cols", []) or [])
        row_positions = list(getattr(viewer, "visible_row_positions", []) or [])
        if not columns or not row_positions:
            viewer.status_message = "no visible data to copy"
            return

        if start.row_id is None or start.col_index is None:
            viewer.status_message = "select first corner cell first"
            return
        end_row_index, end_col_visible_index = _end
        if end_row_index < 0 or end_row_index >= len(row_positions):
            viewer.status_message = "selection outside viewport"
            return
        if end_col_visible_index < 0 or end_col_visible_index >= len(columns):
            viewer.status_message = "selection outside viewport"
            return

        end_row_id = row_positions[end_row_index]
        end_col_name = columns[end_col_visible_index]
        all_cols = list(getattr(viewer, "columns", []) or [])
        end_col_index: int | None = None
        with suppress(ValueError):
            end_col_index = all_cols.index(end_col_name)
        if end_col_index is None:
            viewer.status_message = "selection outside viewport"
            return

        row_lo_id = min(start.row_id, end_row_id)
        row_hi_id = max(start.row_id, end_row_id)
        col_lo = min(start.col_index, end_col_index)
        col_hi = max(start.col_index, end_col_index)

        visible_columns_fn = getattr(viewer, "visible_columns", None)
        if callable(visible_columns_fn):
            visible_columns = list(visible_columns_fn() or [])
        else:
            hidden_cols = set(getattr(viewer, "_hidden_cols", set()) or set())
            visible_columns = [name for name in all_cols if name not in hidden_cols]

        visible_set = set(visible_columns)
        selected_columns = [
            name
            for idx, name in enumerate(all_cols)
            if col_lo <= idx <= col_hi and name in visible_set
        ]
        if not selected_columns:
            viewer.status_message = "no columns to copy"
            return

        selection_active = False
        has_selection = getattr(viewer, "has_active_selection", None)
        if callable(has_selection):
            selection_active = has_selection()
        else:
            selection_active = bool(
                getattr(viewer, "_selected_row_ids", None)
                or getattr(viewer, "_selection_filter_expr", None)
                or getattr(viewer, "_value_selection_filter", None)
            )

        selection_lookup = set(getattr(viewer, "_selected_row_ids", set()) or set())
        selection_expr = None
        if selection_active:
            selection_expr = getattr(viewer, "_selection_filter_expr", None)
            if not selection_expr:
                value_expr_fn = getattr(viewer, "_value_selection_filter_expr", None)
                if callable(value_expr_fn):
                    try:
                        selection_expr = value_expr_fn()
                    except Exception:
                        selection_expr = None

        row_start = min(row_lo_id, row_hi_id)
        row_end = max(row_lo_id, row_hi_id)
        if row_end < row_start:
            viewer.status_message = "no rows to copy"
            return

        fetch_columns = list(selected_columns)
        if selection_expr:
            fetch_columns = list(getattr(viewer, "columns", []) or [])
            if not fetch_columns:
                viewer.status_message = "no columns to copy"
                return

        format_name = start.format if start.format else "tsv"
        sanitize = self._sanitize_clipboard_cell
        if format_name in {"markdown", "ascii", "unicode"}:
            sanitize = (
                self._sanitize_markdown_cell
                if format_name == "markdown"
                else self._sanitize_ascii_cell
            )
            header_cells = [sanitize(name) for name in selected_columns]
            rows: list[list[str]] = []
        else:
            header_line = "\t".join(sanitize(name) for name in selected_columns)
            lines = [header_line]
        rows_copied = 0

        row_provider = getattr(viewer, "row_provider", None)
        if row_provider is None:
            viewer.status_message = "clipboard copy unavailable"
            return

        plan = getattr(viewer, "_current_plan", None)
        plan_value = plan() if callable(plan) else None

        page_size = getattr(row_provider, "page_size", 512)
        try:
            chunk_size = max(1, int(page_size))
        except (TypeError, ValueError):
            chunk_size = 512

        cursor = row_start
        while cursor <= row_end:
            fetch_count = min(chunk_size, row_end - cursor + 1)
            try:
                table_slice, _ = row_provider.get_slice(
                    plan_value, fetch_columns, cursor, fetch_count
                )
            except Exception as exc:
                viewer.status_message = f"clipboard copy failed: {exc}"[:120]
                return

            height = getattr(table_slice, "height", 0)
            if height <= 0:
                break

            base_row = table_slice.start_offset if table_slice.start_offset is not None else cursor
            row_positions = range(base_row, base_row + height)

            selection_matches = None
            if selection_expr:
                resolver = getattr(viewer, "_selection_matches_for_slice", None)
                if callable(resolver):
                    selection_matches = resolver(table_slice, row_positions, selection_expr)
                if selection_matches is None and not selection_lookup:
                    viewer.status_message = "selection unavailable for clipboard copy"
                    return

            id_resolver = getattr(viewer, "_row_identifier_for_slice", None)
            if selection_active and not callable(id_resolver):
                viewer.status_message = "selection unavailable for clipboard copy"
                return

            column_values = [table_slice.column(name).formatted(0) for name in selected_columns]
            for idx in range(height):
                if selection_active:
                    row_id = id_resolver(
                        table_slice,
                        idx,
                        row_positions=row_positions,
                        absolute_row=base_row + idx,
                    )
                    if row_id is None:
                        continue
                    if selection_lookup or selection_matches is not None:
                        if row_id not in selection_lookup and (
                            selection_matches is None or row_id not in selection_matches
                        ):
                            continue
                    else:
                        continue

                row_values = []
                for col_values in column_values:
                    try:
                        cell = col_values[idx]
                    except Exception:
                        cell = ""
                    row_values.append(sanitize(cell))
                if format_name in {"markdown", "ascii", "unicode"}:
                    rows.append(row_values)
                else:
                    lines.append("\t".join(row_values))
                rows_copied += 1

            next_cursor = base_row + height
            if next_cursor <= cursor:
                cursor += max(1, height)
            else:
                cursor = next_cursor

        if rows_copied <= 0:
            viewer.status_message = (
                "no selected rows in region" if selection_active else "no rows to copy"
            )
            return

        if format_name == "markdown":
            header_line = "| " + " | ".join(header_cells) + " |"
            sep_line = "| " + " | ".join("---" for _ in header_cells) + " |"
            row_lines = ["| " + " | ".join(row) + " |" for row in rows]
            payload = "\n".join([header_line, sep_line, *row_lines])
        elif format_name in {"ascii", "unicode"}:
            widths = [len(cell) for cell in header_cells]
            for row in rows:
                for idx, cell in enumerate(row):
                    widths[idx] = max(widths[idx], len(cell))

            def _pad_cells(cells: list[str]) -> list[str]:
                return [
                    f" {cell}{' ' * (width - len(cell))} "
                    for cell, width in zip(cells, widths, strict=True)
                ]

            if format_name == "unicode":
                top = "┌" + "┬".join("─" * (width + 2) for width in widths) + "┐"
                mid = "├" + "┼".join("─" * (width + 2) for width in widths) + "┤"
                bottom = "└" + "┴".join("─" * (width + 2) for width in widths) + "┘"
                vert = "│"
            else:
                top = "+" + "+".join("-" * (width + 2) for width in widths) + "+"
                mid = top
                bottom = top
                vert = "|"
            header = vert + vert.join(_pad_cells(header_cells)) + vert
            row_lines = [vert + vert.join(_pad_cells(row)) + vert for row in rows]
            payload = "\n".join([top, header, mid, *row_lines, bottom])
        else:
            payload = "\n".join(lines)
        self._copy_to_clipboard(
            payload,
            success_message=(f"copied {rows_copied}x{len(selected_columns)} region to clipboard"),
            failure_message="clipboard unavailable",
        )

    def _on_active_viewer_changed(self, viewer: Viewer) -> None:
        previous = getattr(self, "viewer", None)
        self.viewer = viewer
        self._runtime.prepare_viewer(viewer)
        controller = getattr(self, "_file_browser_controller", None)
        if controller is not None:
            with suppress(Exception):
                controller.on_viewer_changed(viewer)
        table_control = getattr(self, "_table_control", None)
        update_viewer = getattr(table_control, "update_viewer", None)
        if callable(update_viewer):
            update_viewer(viewer)
        if previous is not None and previous is not viewer:
            self._clear_column_search()
        controller = getattr(self, "_insight_controller", None)
        if controller is not None:
            controller.on_viewer_changed(viewer)
        fallback_enabled = getattr(self, "_insight_enabled", self._insight_base_default)
        insight_state = resolve_insight_state(viewer, fallback_enabled=fallback_enabled)
        allowed_changed = insight_state.allowed != self._insight_allowed
        enabled_changed = insight_state.enabled != self._insight_enabled
        self._insight_enabled = insight_state.enabled
        self._insight_user_enabled = insight_state.user_enabled
        self._insight_allowed = insight_state.allowed
        if allowed_changed or enabled_changed or insight_state.effective:
            self._apply_insight_state(refresh=insight_state.effective)
        self._transform_signature = None
        self._transforms_active = False
        self._return_to_column_after_transforms = False
        self._cancel_transform_autoclose()
        self._refresh_transform_insight()
        self._prune_stale_jobs()
        file_watch = getattr(self, "_file_watch", None)
        if file_watch is not None:
            file_watch.sync(force=True)

    def _prune_stale_jobs(self) -> None:
        active = set(self.view_stack.viewers)
        for stale_viewer in list(self._jobs):
            if stale_viewer not in active:
                self.cancel_job(stale_viewer)

    def register_job(self, viewer: Viewer, job: object | None) -> None:
        """Register a background job handle for a viewer."""

        if job is None:
            self.cancel_job(viewer)
            return
        previous = self._jobs.get(viewer)
        if previous is job:
            return
        if previous is not None and hasattr(previous, "cancel"):
            with suppress(Exception):
                previous.cancel()
        self._jobs[viewer] = job

    def cancel_job(self, viewer: Viewer) -> None:
        """Cancel and remove the background job for a viewer, if any."""

        job = self._jobs.pop(viewer, None)
        if job is not None and hasattr(job, "cancel"):
            with suppress(Exception):
                job.cancel()

    def _mutate_context(self, context: CommandContext) -> None:
        context.screen = self
        context.ui = self

    def commands_help_entries(self) -> Sequence[object]:
        return getattr(self, "_key_only_help_entries", ())

    def _finalise_runtime_result(
        self, result: CommandRuntimeResult
    ) -> CommandDispatchResult | None:
        if result.message:
            self.viewer.status_message = result.message
        dispatch = result.dispatch
        if dispatch and dispatch.spec.name == "search":
            self._clear_column_search()
        return dispatch

    def _execute_command(
        self, name: str, args: list[str] | None = None, *, repeat: int = 1
    ) -> CommandDispatchResult | None:
        """Execute a command through the session command runtime."""

        invocation_args = list(args or [])
        recorder = self._recorder if self._recorder and self._recorder.enabled else None
        perf_ctx = (
            recorder.perf_timer(
                "input.command",
                payload={"context": "tui", "command": name},
            )
            if recorder
            else nullcontext()
        )
        with perf_ctx:
            result = self._runtime.invoke(
                name,
                args=invocation_args,
                repeat=repeat,
                source="tui",
                context_mutator=self._mutate_context,
            )
        self._apply_insight_state(refresh=self._insight_enabled)
        return self._finalise_runtime_result(result)

    def _queue_move(self, dr: int = 0, dc: int = 0) -> None:
        # Accumulate deltas; they'll be applied (capped) during next paint
        if dr != 0:
            self._pending_row_delta += dr
            # prevent runaway accumulation while still allowing page-sized deltas
            row_limit = max(100, abs(dr))
            self._pending_row_delta = max(-row_limit, min(row_limit, self._pending_row_delta))
        if dc != 0:
            self._pending_col_delta += dc
            col_limit = max(100, abs(dc))
            self._pending_col_delta = max(-col_limit, min(col_limit, self._pending_col_delta))

    def _record_key_event(self, event) -> None:
        # Record with structured recorder if available
        if self._count_buf is not None:
            try:
                first_key = event.key_sequence[0].key if event.key_sequence else None
            except Exception:
                first_key = None
            if not (isinstance(first_key, str) and first_key.isdigit()):
                self._clear_count_buf()
        if self._recorder and self._recorder.enabled:
            try:
                sequence = [kp.key for kp in event.key_sequence]
                data = [kp.data for kp in event.key_sequence]
            except Exception:
                sequence = []
                data = []
            payload = {"sequence": sequence, "data": data}
            payload["repeat"] = bool(getattr(event, "is_repeat", False))
            self._recorder.record("key", payload)

    def _toggle_recorder(self, event) -> None:
        """Toggle the structured recorder on/off from the TUI."""
        recorder = self._recorder
        if recorder is None:
            self.viewer.status_message = "recorder unavailable"
            self.refresh()
            return

        self._record_key_event(event)

        try:
            user_command = event.key_sequence[0].key if event.key_sequence else "@"
        except Exception:
            user_command = "@"

        if recorder.enabled:
            recorder.record(
                "control", {"action": "record_off", "source": "tui", "key": user_command}
            )
            path = recorder.flush_and_clear(reason="tui-toggle")
            recorder.disable()
            if path is not None:
                pending_message = f"flight recorder stopped - saved to {path.name}"
                self.viewer.status_message = pending_message
                self._copy_to_clipboard(
                    str(path),
                    success_message=(
                        f"flight recorder stopped - {path.name} (path copied to clipboard)"
                    ),
                    failure_message=pending_message,
                )
            else:
                self.viewer.status_message = "flight recorder disabled"
            self.refresh()
            return

        recorder.enable()
        recorder.ensure_env_recorded()
        source_path = getattr(self.viewer, "_source_path", None)
        schema = getattr(self.viewer.sheet, "schema", {})
        if source_path is not None:
            recorder.record_dataset_open(path=source_path, schema=schema, lazy=True)
        recorder.record("control", {"action": "record_on", "source": "tui", "key": user_command})
        self.viewer.status_message = "flight recorder started"
        self.refresh()

    def _handle_enter(self, event) -> None:
        if self._clipboard_region is not None:
            if self._clipboard_region.awaiting_start:
                self._set_clipboard_region_start()
            else:
                self._finalize_clipboard_region()
            return
        action = resolve_enter_action(self.viewer)
        if action is None:
            self._open_cell_value_modal(event)
            return
        if action.kind == "open-path":
            self._handle_open_path_action(action)
            return
        if action.kind == "apply-selection":
            self._apply_selection_action(action)
            return
        self._open_cell_value_modal(event)

    def _handle_open_path_action(self, action: SheetEnterAction) -> None:
        if action.open_as == "directory":
            self._enter_browser_directory(action.path)
            return
        if action.open_as == "dataset":
            self._open_dataset_from_action(action.path)
            return

    def _enter_browser_directory(self, target: Path | None) -> None:
        if target is None:
            self.viewer.status_message = "entry is not openable"
            self.refresh()
            return
        sheet = getattr(self.viewer, "sheet", None)
        builder = getattr(sheet, "at_path", None)
        if not callable(builder):
            self.viewer.status_message = "browser cannot change directory"
            self.refresh()
            return
        try:
            new_sheet = builder(target)
        except FileNotFoundError:
            self.viewer.status_message = f"directory not found: {target}"
            self.refresh()
            return
        except Exception as exc:
            self.viewer.status_message = f"dir open failed: {exc}"
            self.refresh()
            return

        refresh_from_disk = getattr(new_sheet, "refresh_from_disk", None)
        if callable(refresh_from_disk):
            with suppress(Exception):
                refresh_from_disk()

        source_label = resolve_display_path(new_sheet, fallback=str(target))
        self.viewer.replace_sheet(new_sheet, source_path=source_label)
        with suppress(Exception):
            self.viewer._clear_selection_state()
        with suppress(Exception):
            self.viewer.row_count_tracker.ensure_total_rows()
        message = getattr(new_sheet, "status_message", None)
        if not message:
            count = new_sheet.row_count() or 0
            message = f"{count} entries"
        self.viewer.status_message = message
        self._after_file_browser_directory_change()
        self.refresh()

    def _open_dataset_from_action(self, target: Path | None) -> None:
        if target is None:
            self.viewer.status_message = "entry is not openable"
            self.refresh()
            return
        try:
            self.session.open_dataset_viewer(target, base_viewer=self.viewer)
        except FileNotFoundError:
            self.viewer.status_message = f"path not found: {target}"
            self.refresh()
            return
        except Exception as exc:
            self.viewer.status_message = f"open failed: {exc}"
            self.refresh()
            return
        self.viewer.status_message = f"opened {target.name or target}"
        self._file_watch.sync(force=True)
        self.refresh()

    def _after_file_browser_directory_change(self) -> None:
        self._file_watch.sync_file_browser(force=True)
        self._apply_insight_state(refresh=True)
        with suppress(Exception):
            self.viewer.row_count_tracker.ensure_total_rows()

    def _path_completion_base_dir(self) -> Path:
        sheet = getattr(self.viewer, "sheet", None)
        directory = getattr(sheet, "directory", None) if sheet is not None else None
        if directory:
            with suppress(Exception):
                return Path(directory)
        command_cwd = getattr(self.session, "command_cwd", None)
        if command_cwd:
            with suppress(Exception):
                return Path(command_cwd)
        dataset_path = getattr(self.session, "dataset_path", None)
        if dataset_path:
            with suppress(Exception):
                ds_path = Path(dataset_path)
                return ds_path if ds_path.is_dir() else ds_path.parent
        return Path.cwd()

    def _open_file_from_browser(self, target: Path) -> None:
        try:
            self.session.open_dataset_viewer(target, base_viewer=self.viewer)
        except Exception as exc:
            self.viewer.status_message = f"open failed: {exc}"
            self.refresh()
            return
        self.viewer.status_message = f"opened {target.name or target}"
        self._file_watch.sync(force=True)
        self.refresh()

    def _file_browser_delete_targets(self, sheet) -> list[object]:
        viewer = getattr(self, "viewer", None)
        if viewer is None:
            return []
        controller = self._file_browser_controller
        targets, status = controller.resolve_entries(deletable=True, viewer=viewer)
        if status:
            viewer.status_message = status
        if targets:
            return targets
        self.refresh()
        return []

    def _file_browser_entries(self, sheet) -> list[object]:
        viewer = getattr(self, "viewer", None)
        if viewer is None:
            return []
        controller = self._file_browser_controller
        targets, status = controller.resolve_entries(viewer=viewer)
        if status:
            viewer.status_message = status
        if targets:
            return targets
        self.refresh()
        return []

    def _open_file_delete_modal(self, event) -> None:
        viewer = getattr(self, "viewer", None)
        sheet = getattr(viewer, "sheet", None)
        if viewer is None or sheet is None or not resolve_sheet_traits(sheet).is_file_browser:
            return

        targets = self._file_browser_delete_targets(sheet)
        if not targets:
            return

        count = len(targets)
        title = "Delete item" if count == 1 else "Delete items"
        name = targets[0].path.name if count == 1 else None
        has_dir = any(getattr(entry, "is_dir", False) for entry in targets)

        try:
            file_count, impact_errors = sheet.deletion_impact(targets)
        except Exception:
            file_count, impact_errors = 0, []

        message_lines = []
        if count == 1:
            if has_dir:
                message_lines.append(f"Delete folder {name or 'item'} and its contents?")
            else:
                message_lines.append(f"Delete {name or 'file'}?")
        else:
            kind = "items" if has_dir else "files"
            message_lines.append(f"Delete all {count} selected {kind}?")

        suffix = "" if file_count == 1 else "s"
        recurse_note = " recursively" if has_dir else ""
        message_lines.append(f"This will delete {file_count} file{suffix}{recurse_note}.")
        if impact_errors:
            path, err = impact_errors[0]
            message_lines.append(f"Count may be incomplete ({path}: {err})")

        body = tui_modals.build_lines_body(message_lines)
        app = event.app

        def _resolve(confirmed: bool) -> None:
            self._remove_modal(app)
            if confirmed:
                self._delete_file_browser_entries(sheet, targets)

        yes_button = Button(text="Yes", handler=lambda: _resolve(True))
        cancel_button = Button(text="Cancel", handler=lambda: _resolve(False))

        dialog = Dialog(
            title=title,
            body=body,
            buttons=[yes_button, cancel_button],
        )
        self._display_modal(
            app,
            dialog,
            focus=yes_button,
            context_type="file_delete",
            payload={"count": count},
            width=60,
        )

    def _delete_file_browser_entries(self, sheet, entries: Sequence[object]) -> None:
        viewer = getattr(self, "viewer", None)
        if viewer is None:
            return

        try:
            result: FileDeletionResult = self._file_browser_controller.delete_entries(
                sheet, entries
            )
        except Exception as exc:
            viewer.status_message = f"delete failed: {exc}"
            self.refresh()
            return

        with suppress(Exception):
            viewer.clear_row_selection()

        message = "No items deleted"
        if result.deleted:
            if len(result.deleted) == 1:
                target = result.deleted[0]
                message = f"Deleted {target.name or target}"
            else:
                message = f"Deleted {len(result.deleted)} items"

        if result.errors:
            path, error = result.errors[0]
            prefix = f"{message}; " if result.deleted else "Delete failed: "
            message = f"{prefix}{path.name}: {error}"

        if result.changed or result.errors:
            self._handle_file_browser_refresh(sheet)

        viewer.status_message = message[:120]
        self.refresh()

    def _open_confirmation_modal(
        self,
        *,
        title: str,
        message_lines: Sequence[str],
        on_confirm: Callable[[], None],
        context_type: str | None = None,
        payload: dict[str, object] | None = None,
    ) -> None:
        self._presenter.open_confirmation_modal(
            title=title,
            message_lines=message_lines,
            on_confirm=on_confirm,
            context_type=context_type,
            payload=payload,
        )

    def _open_simple_status_modal(self, title: str, lines: Sequence[str]) -> None:
        self._presenter.open_status_modal(title=title, lines=lines)

    def _request_file_transfer(
        self, operation: str, dest: str, *, source_paths: Sequence[Path] | None = None
    ) -> None:
        self._file_ops.request_transfer(operation, dest, source_paths=source_paths)

    def _request_file_rename(self, new_name: str) -> None:
        self._file_ops.request_rename(new_name)

    def _request_file_mkdir(self, path: str) -> None:
        self._file_ops.request_mkdir(path)

    def _perform_file_transfer(
        self, operation: str, targets: list[tuple[Path, Path]], *, allow_overwrite: bool = False
    ) -> None:
        self._file_ops.perform_transfer(operation, targets, allow_overwrite=allow_overwrite)

    def _reload_dataset(self) -> None:
        """Reload the currently open dataset if it originated from a path."""

        self._dataset_reload.reload_dataset()

    @staticmethod
    def _is_missing_error(exc: Exception) -> bool:
        return DatasetReloadController.is_missing_error(exc)

    def _handle_missing_dataset(self, dataset_path: Path) -> None:
        """Switch to file browser when the backing file disappears."""

        self._dataset_reload.handle_missing_dataset(dataset_path)

    def _handle_reload_error(self, exc: Exception, dataset_path: Path) -> None:
        self._dataset_reload.handle_reload_error(exc, dataset_path)

    def _apply_insight_state(self, *, refresh: bool = False) -> None:
        viewer = getattr(self, "viewer", None)
        if viewer is not None:
            insight_state = set_insight_state(
                viewer,
                enabled=self._insight_enabled,
                user_enabled=self._insight_user_enabled,
            )
            self._insight_enabled = insight_state.enabled
            self._insight_user_enabled = insight_state.user_enabled
            self._insight_allowed = insight_state.allowed
        with suppress(Exception):
            self._update_viewer_metrics()
        self._apply_insight_visibility(refresh=refresh)

    def _apply_insight_visibility(self, *, refresh: bool = False) -> None:
        controller = getattr(self, "_insight_controller", None)
        effective = self._insight_is_effective()
        if controller is not None:
            controller.set_enabled(effective)
            if effective and refresh:
                with suppress(Exception):
                    controller.on_refresh()
        if not effective:
            if not self._insight_allowed:
                self._column_insight_panel.set_unavailable("Insight unavailable for this view.")
                self._transforms_insight_panel.set_unavailable("Insight unavailable for this view.")
            else:
                self._transforms_insight_panel.set_transforms(filters=(), sorts=())
        app = getattr(self, "app", None)
        if app is not None:
            app.invalidate()

    def set_insight_panel(self, enabled: bool | None = None) -> bool:
        """Toggle or explicitly set the insight sidecar visibility."""

        if enabled is None:
            enabled = not self._insight_enabled
        if enabled:
            target_mode = "transform" if self._transforms_active else "column"
            self._insight_panel.set_mode(target_mode)
            self._cancel_transform_autoclose()
        self._insight_enabled = bool(enabled)
        self._insight_user_enabled = True
        self._apply_insight_state(refresh=self._insight_enabled)
        return self._insight_enabled

    def set_insight_panel_mode(self, mode: str | None = None) -> str:
        """Set or toggle the insight panel mode, ensuring it is visible."""

        if mode is None:
            self._insight_panel.toggle_mode()
        elif mode in {"column", "transform"}:
            self._insight_panel.set_mode(mode)
        self._cancel_transform_autoclose()
        self._insight_enabled = True
        self._insight_user_enabled = True
        self._apply_insight_state(refresh=True)
        return self._insight_panel.mode

    def _refresh_transform_insight(self) -> None:
        viewer = getattr(self, "viewer", None)
        if viewer is None or self._transforms_insight_panel is None:
            return
        plan_controller = getattr(viewer, "plan_controller", None)
        plan = plan_controller.current_plan() if plan_controller is not None else None
        if plan is None:
            self._transforms_insight_panel.set_unavailable(
                "Transforms require a plan-backed sheet."
            )
            self._apply_transform_state(has_transforms=False, signature_changed=False)
            return
        filters = plan.filter_clauses
        sorts = plan.sort
        signature = (filters, sorts)
        signature_changed = signature != self._transform_signature
        self._transform_signature = signature
        self._transforms_insight_panel.set_transforms(filters=filters, sorts=sorts)
        self._apply_transform_state(
            has_transforms=bool(filters or sorts),
            signature_changed=signature_changed,
        )

    def _apply_transform_state(self, *, has_transforms: bool, signature_changed: bool) -> None:
        if has_transforms:
            if not self._transforms_active:
                self._return_to_column_after_transforms = (
                    self._insight_is_effective() and self._insight_panel.mode == "column"
                )
            self._transforms_active = True
            if signature_changed or not self._insight_is_effective():
                self._insight_panel.set_mode("transform")
                self._invalidate_app()
            self._cancel_transform_autoclose()
            if not self._insight_is_effective() and self._insight_is_allowed():
                self._insight_enabled = True
                self._apply_insight_state(refresh=True)
            return

        if not self._transforms_active:
            return

        self._transforms_active = False
        if self._return_to_column_after_transforms:
            self._return_to_column_after_transforms = False
            self._insight_panel.set_mode("column")
            if not self._insight_is_effective() and self._insight_is_allowed():
                self._insight_enabled = True
                self._apply_insight_state(refresh=True)
            else:
                self._invalidate_app()
            return

        self._insight_panel.set_mode("transform")
        self._schedule_transform_autoclose()
        self._invalidate_app()

    def _schedule_transform_autoclose(self) -> None:
        if not self._insight_is_allowed():
            return
        self._cancel_transform_autoclose()

        def _dispatch() -> None:
            hooks = getattr(self, "_viewer_ui_hooks", None)
            if hooks is None:
                return
            hooks.call_soon(self._finalize_transform_autoclose)

        timer = threading.Timer(3.0, _dispatch)
        timer.daemon = True
        self._transform_autoclose_timer = timer
        timer.start()

    def _cancel_transform_autoclose(self) -> None:
        timer = self._transform_autoclose_timer
        if timer is None:
            return
        timer.cancel()
        self._transform_autoclose_timer = None

    def _finalize_transform_autoclose(self) -> None:
        self._transform_autoclose_timer = None
        if self._transforms_active or self._return_to_column_after_transforms:
            return
        if self._insight_panel.mode != "transform":
            return
        if not self._insight_is_allowed():
            return
        if not self._insight_enabled:
            return
        self._insight_enabled = False
        self._apply_insight_state(refresh=False)

    def _copy_to_clipboard(
        self,
        payload: str,
        *,
        success_message: str | None = None,
        failure_message: str | None = None,
    ) -> bool:
        """Copy text without letting clipboard output corrupt the TUI layout."""

        app = get_app_or_none()
        if app is None or not app.is_running:
            success = copy_to_clipboard(
                payload,
                max_osc52_bytes=_CLIPBOARD_RUN_IN_TERMINAL_THRESHOLD,
            )
            if success_message is not None or failure_message is not None:
                self.viewer.status_message = success_message if success else failure_message
            return success

        payload_bytes = len(payload.encode("utf-8", "replace"))
        if payload_bytes <= _CLIPBOARD_RUN_IN_TERMINAL_THRESHOLD:
            success = copy_to_clipboard(
                payload,
                max_osc52_bytes=_CLIPBOARD_RUN_IN_TERMINAL_THRESHOLD,
            )
            if success_message is not None or failure_message is not None:
                self.viewer.status_message = success_message if success else failure_message
            return success

        async def _copy_async() -> None:
            try:
                success = await run_in_terminal(
                    lambda: copy_to_clipboard(
                        payload,
                        max_osc52_bytes=_CLIPBOARD_RUN_IN_TERMINAL_THRESHOLD,
                    ),
                    in_executor=True,
                )
            except Exception:
                success = False
            if success_message is not None or failure_message is not None:
                self.viewer.status_message = success_message if success else failure_message
                app.invalidate()

        app.create_background_task(_copy_async())
        return True

    def _apply_pending_moves(self) -> None:
        # Apply up to N moves per axis this frame, for smoother scrolling
        steps = min(self._max_steps_per_frame, abs(self._pending_row_delta))
        if steps:
            if self._pending_row_delta > 0:
                self.viewer.move_rows(steps)
                self._pending_row_delta -= steps
            else:
                self.viewer.move_rows(-steps)
                self._pending_row_delta += steps
        steps = min(self._max_steps_per_frame, abs(self._pending_col_delta))
        if steps:
            if self._pending_col_delta > 0:
                for _ in range(steps):
                    self.viewer.move_right()
                self._pending_col_delta -= steps
            else:
                for _ in range(steps):
                    self.viewer.move_left()
                self._pending_col_delta += steps

    def _get_table_text(self):
        # Coalesce rapid key repeats by applying pending deltas here
        self._apply_pending_moves()
        self._poll_background_jobs()
        # Import the render table function here to avoid circular imports
        from ..render.table import render_table

        recorder = self._recorder if getattr(self, "_recorder", None) else None
        try:
            if recorder and recorder.enabled:
                with recorder.perf_timer(
                    "render.table",
                    payload={"context": "tui", "trigger": "refresh"},
                ):
                    body = render_table(self.viewer)
            else:
                body = render_table(self.viewer)
        except Exception as exc:  # pragma: no cover - safety net
            self.viewer.set_status(f"render error: {exc}"[:120], severity="error")
            return ANSI("").__pt_formatted_text__()

        # Precompute status text so the footer stays in sync with the latest render
        from ..render.status_bar import render_status_line

        status_fragments: StyleAndTextTuples = []
        try:
            if recorder and recorder.enabled:
                with recorder.perf_timer(
                    "render.status",
                    payload={"context": "tui", "trigger": "refresh"},
                ):
                    status_fragments = render_status_line(self.viewer)
            else:
                status_fragments = render_status_line(self.viewer)
        except Exception as exc:  # pragma: no cover - safety net
            self.viewer.set_status(f"render status error: {exc}"[:120], severity="error")
            status_fragments = []
        self._set_status_from_table(status_fragments)
        self.viewer.acknowledge_status_rendered()
        status_text = self._last_status_plain or ""
        if self._recorder and self._recorder.enabled:
            state_snapshot = viewer_state_snapshot(self.viewer)
            self._recorder.record_state(state_snapshot)
            if status_text:
                self._recorder.record_status(status_text)
            frame_capture = f"{body}\n{status_text}" if status_text else body
            if self._insight_is_effective():
                panel_block = self._insight_panel.render_for_recorder()
                if panel_block:
                    frame_capture = f"{frame_capture}\n\n{panel_block}"
            self._recorder.record_frame(
                frame_text=frame_capture,
                frame_hash=frame_hash(frame_capture),
            )
        return ANSI(body).__pt_formatted_text__()

    def _get_status_text(self):
        viewer = self.viewer
        status_dirty = bool(viewer.is_status_dirty())
        if status_dirty:
            # Import lazily to avoid circular dependency at module import time
            from ..render.status_bar import render_status_line

            self._set_status_from_table(render_status_line(viewer))
            viewer.acknowledge_status_rendered()
        elif self._last_status_fragments is None:
            from ..render.status_bar import render_status_line

            self._set_status_from_table(render_status_line(viewer))
            viewer.acknowledge_status_rendered()
        return self._last_status_fragments or [("", "")]

    def _set_status_from_table(self, fragments: StyleAndTextTuples) -> None:
        stored = list(fragments)
        self._last_status_fragments = stored
        self._last_status_plain = "".join(part for _, part in stored)

    def _insight_sidecar_width(self) -> int:
        if not self._insight_is_effective():
            return 0
        # Insight column, plus border and padding containers.
        return self._insight_panel.width + 2

    def _update_viewer_metrics(self) -> None:
        hooks = getattr(self, "_viewer_ui_hooks", None)
        cols, _rows = NullViewerUIHooks().get_terminal_size((100, 30))
        if hooks is not None:
            with suppress(Exception):
                cols, _rows = hooks.get_terminal_size((cols, _rows))
        self._update_insight_terminal_allowed(cols)
        insight_width = self._insight_sidecar_width()
        if insight_width:
            width_override = max(20, cols - insight_width)
            self.viewer.set_view_width_override(width_override)
        else:
            self.viewer.set_view_width_override(None)
        self.viewer.update_terminal_metrics()

    def _pop_viewer(self) -> None:
        removed = self.view_stack.pop()
        if removed is None:
            return
        self.cancel_job(removed)

    def refresh(self, *, skip_metrics: bool = False):
        if not skip_metrics:
            previous_terminal_allowed = self._insight_terminal_allowed
            self._update_viewer_metrics()
            if self._insight_terminal_allowed != previous_terminal_allowed:
                self._apply_insight_visibility(refresh=self._insight_enabled)
        self.viewer.clamp()
        self._check_dataset_file_changes(force=True)
        self._check_file_browser_changes(force=True)
        self._refresh_transform_insight()
        controller = getattr(self, "_insight_controller", None)
        if controller is not None:
            if self._file_watch.dataset_prompt_active:
                self._column_insight_panel.set_unavailable(
                    "File changed; reload to resume insight."
                )
                self._transforms_insight_panel.set_unavailable(
                    "File changed; reload to resume insight."
                )
            else:
                controller.on_refresh()
        with suppress(Exception):
            self._viewer_ui_hooks.invalidate()

    def _invalidate_app(self) -> None:
        hooks = getattr(self, "_viewer_ui_hooks", None)
        if hooks is not None:
            with suppress(Exception):
                hooks.invalidate()
            return
        with suppress(Exception):
            self.app.invalidate()

    def _poll_background_jobs(self) -> None:
        self._job_pump.poll()

    def _handle_file_browser_refresh(self, sheet) -> None:
        viewer = getattr(self, "viewer", None)
        if viewer is None or viewer.sheet is not sheet:
            return
        self._file_browser_controller.refresh_sheet(sheet, viewer)
        hooks = getattr(self, "_viewer_ui_hooks", None)
        if hooks is not None:
            with suppress(Exception):
                hooks.invalidate()
        else:
            self.app.invalidate()

    def _handle_file_browser_error(self, exc: Exception) -> None:
        viewer = getattr(self, "viewer", None)
        if viewer is not None:
            viewer.status_message = f"dir refresh failed: {exc}"
        hooks = getattr(self, "_viewer_ui_hooks", None)
        if hooks is not None:
            with suppress(Exception):
                hooks.invalidate()
        else:
            self.app.invalidate()

    def _check_dataset_file_changes(self, *, force: bool = False) -> None:
        if self._file_watch is None:
            return
        self._file_watch.check_dataset(force=force)

    def _check_file_browser_changes(self, *, force: bool = False) -> None:
        if self._file_watch is None:
            return
        self._file_watch.check_file_browser(force=force)

    @property
    def _file_watch_prompt_active(self) -> bool:
        return bool(self._file_watch and self._file_watch.dataset_prompt_active)

    @_file_watch_prompt_active.setter
    def _file_watch_prompt_active(self, active: bool) -> None:
        if self._file_watch is None:
            return
        if active:
            self._file_watch.set_dataset_prompt_active(True)
        else:
            self._file_watch.clear_dataset_prompt()

    def _schedule_file_change_prompt(
        self,
        path: Path,
        snapshot: FileSnapshot | None,
    ) -> None:
        def _open_prompt() -> None:
            self._open_dataset_file_change_modal(path=path, snapshot=snapshot)

        hooks = getattr(self, "_viewer_ui_hooks", None)
        if hooks is None:
            _open_prompt()
            return
        hooks.call_soon(_open_prompt)

    def _open_dataset_file_change_modal(
        self,
        *,
        path: Path,
        snapshot: FileSnapshot | None,
    ) -> None:
        if self._file_watch.dataset_path != path:
            self._file_watch.clear_dataset_prompt()
            return
        if snapshot is None:
            snapshot = FileSnapshot(
                mtime_ns=None,
                size=None,
                inode=None,
                missing=True,
                error=None,
            )
        if snapshot.missing:
            self._open_missing_dataset_modal(path=path, snapshot=snapshot)
            return
        message_lines = [
            f"{path} changed on disk while Pulka is running.",
        ]
        if snapshot.missing:
            missing_reason = snapshot.error or "The file may have been deleted or replaced."
            message_lines.append(missing_reason)
        else:
            message_lines.append("Reload to view the latest data or keep the current snapshot.")

        body = tui_modals.build_lines_body(message_lines)
        app = self.app

        def _resolve(reload_file: bool) -> None:
            self._remove_modal(app)
            self._complete_file_change_prompt(reload_file=reload_file)
            if not reload_file:
                self.refresh()

        reload_button = Button(text="Reload file", handler=lambda: _resolve(True))
        keep_button = Button(text="Keep current view", handler=lambda: _resolve(False))

        dialog = Dialog(
            title="File changed",
            body=body,
            buttons=[reload_button, keep_button],
        )
        self._display_modal(
            app,
            dialog,
            focus=reload_button,
            context_type="file_change",
            payload={"path": str(path)},
            width=80,
        )

    def _complete_file_change_prompt(self, *, reload_file: bool) -> None:
        self._file_watch.complete_dataset_prompt(refresh_snapshot=not reload_file)
        if reload_file:
            self._reload_dataset()
            return
        self.viewer.status_message = "file changed on disk (kept current view)"
        self._file_watch.sync_dataset(force=True)

    def run(self):
        try:
            self.app.run()
        finally:
            unsubscribe = getattr(self, "_view_stack_unsubscribe", None)
            if unsubscribe is not None:
                unsubscribe()
            self._file_watch.stop()
            session = self.session
            last_path = _last_open_dataset_path(session)
            if session is not None and self._on_shutdown is not None:
                with suppress(Exception):
                    self._on_shutdown(session)
            if session is not None:
                with suppress(Exception):
                    session.close()
                recorder = getattr(session, "recorder", None)
                if recorder is not None and recorder.enabled:
                    with suppress(Exception):
                        recorder.on_process_exit(reason="tui")
            if last_path is not None:
                print(f"Pulka {pulka_version}")
                print(last_path)

    def _display_modal(
        self,
        app,
        container,
        *,
        focus=None,
        context_type: str | None = None,
        payload: dict[str, object] | None = None,
        width: int | None = None,
        height: int | None = None,
    ) -> None:
        self._modal_manager.display(
            app,
            container,
            focus=focus,
            context_type=context_type,
            payload=payload,
            width=width,
            height=height,
        )

    def _calculate_modal_dimensions(
        self,
        app,
        *,
        target_width: int,
        target_height: int,
    ) -> tuple[int, int]:
        """Determine modal dimensions respecting terminal size constraints."""

        return self._modal_manager.calculate_dimensions(
            app,
            target_width=target_width,
            target_height=target_height,
            chrome_height=_CELL_MODAL_CHROME_HEIGHT,
        )

    def _remove_modal(self, app, *, restore_focus: bool = True) -> None:
        self._modal_manager.remove(app, restore_focus=restore_focus)

    def _build_read_only_modal_dialog(
        self,
        *,
        app,
        title: str,
        text_area: TextArea,
        extra_buttons: Sequence[Button] | None = None,
        ok_on_right: bool = False,
    ) -> tuple[Dialog, Button]:
        """Create a dialog with a read-only text area and shared controls."""

        def _close_modal(target_app) -> None:
            self._remove_modal(target_app)
            self.refresh()

        tui_modals.bind_close_keys(text_area, on_close=_close_modal)

        body = Box(body=HSplit([text_area], padding=1), padding=1)

        ok_button = Button(text="OK", handler=lambda: _close_modal(app))

        if ok_on_right:
            buttons = list(extra_buttons or [])
            buttons.append(ok_button)
        else:
            buttons = [ok_button]
            if extra_buttons:
                buttons.extend(extra_buttons)
        dialog = Dialog(title=title, body=body, buttons=buttons)
        return dialog, ok_button

    def _open_missing_dataset_modal(
        self,
        *,
        path: Path,
        snapshot: FileSnapshot,
    ) -> None:
        app = self.app
        message_lines = [
            f"{path} is no longer available.",
        ]
        if snapshot.error:
            message_lines.append(snapshot.error)
        message_lines.append("You will be redirected to the file browser sheet.")

        body = tui_modals.build_lines_body(message_lines)

        def _confirm() -> None:
            self._remove_modal(app)
            self._handle_missing_dataset(path)

        ok_button = Button(text="OK", handler=_confirm)

        dialog = Dialog(
            title="File missing",
            body=body,
            buttons=[ok_button],
        )

        self._display_modal(
            app,
            dialog,
            focus=ok_button,
            context_type="missing_dataset",
            payload={"path": str(path)},
            width=80,
        )

    def _open_reload_error_modal(self, *, error_text: str) -> None:
        app = self.app
        target_width = 80
        target_height = 40
        width, height = self._calculate_modal_dimensions(
            app,
            target_width=target_width,
            target_height=target_height,
        )
        text_area_height = max(3, height - _CELL_MODAL_CHROME_HEIGHT)

        text_area = TextArea(
            text=error_text,
            read_only=True,
            scrollbar=True,
            wrap_lines=True,
            height=text_area_height,
        )

        def _close_modal(target_app) -> None:
            self._remove_modal(target_app)
            self.refresh()

        tui_modals.bind_close_keys(text_area, on_close=_close_modal)

        body = Box(body=HSplit([text_area], padding=1), padding=1)

        def _copy_error() -> None:
            self._copy_to_clipboard(
                error_text,
                success_message="copied reload error to clipboard",
                failure_message="clipboard unavailable",
            )
            app.invalidate()

        copy_button = Button(text="Copy to clipboard", handler=_copy_error)
        close_button = Button(text="Close", handler=lambda: _close_modal(app))

        dialog = Dialog(
            title="Reload failed",
            body=body,
            buttons=[copy_button, close_button],
        )

        self._display_modal(
            app,
            dialog,
            focus=copy_button,
            context_type="reload_error",
            width=width,
            height=height,
        )

    def _open_cell_value_modal(self, event) -> None:
        """Open a modal showing details about the currently focused cell."""

        if not self.viewer.columns:
            return

        column_name = self.viewer.columns[self.viewer.cur_col]
        row_index = self.viewer.cur_row

        value = None
        value_error: str | None = None
        try:
            slice_ = self.viewer.sheet.fetch_slice(row_index, 1, [column_name])
            if isinstance(slice_, TableSlice):
                table_slice = slice_
            elif isinstance(slice_, pl.DataFrame):
                schema = getattr(self.viewer.sheet, "schema", {})
                table_slice = table_slice_from_dataframe(slice_, schema)
            else:
                table_slice = table_slice_from_dataframe(
                    pl.DataFrame(slice_), getattr(self.viewer.sheet, "schema", {})
                )

            if table_slice.height > 0 and column_name in table_slice.column_names:
                value = table_slice.column(column_name).values[0]
        except Exception as exc:  # pragma: no cover - defensive
            value_error = str(exc)

        target_width = 60
        target_height = 40
        width, height = self._calculate_modal_dimensions(
            event.app,
            target_width=target_width,
            target_height=target_height,
        )
        content_width = max(20, width - 6)

        def _render_text(*, pretty: bool) -> str:
            if value_error is not None:
                return f"Error: {value_error}"
            if pretty:
                console_buffer = StringIO()
                console = Console(
                    record=True,
                    width=content_width,
                    highlight=False,
                    file=console_buffer,
                )
                console.print(Pretty(value, expand_all=True, overflow="fold"))
                return console.export_text(clear=False)
            if isinstance(value, str):
                return value
            return str(value)

        render_pretty = value_error is None and not isinstance(value, str)
        rendered_text = _render_text(pretty=render_pretty)

        def _toggle_label(*, pretty: bool) -> str:
            return "Raw" if pretty else "Pretty"

        # Account for dialog chrome (label, padding, frame, and buttons) so the
        # text area fits within the requested height without triggering the
        # "window too small" warning from prompt_toolkit.
        text_area_height = max(3, height - _CELL_MODAL_CHROME_HEIGHT)

        text_area = TextArea(
            text=rendered_text,
            read_only=True,
            scrollbar=True,
            wrap_lines=True,
            height=text_area_height,
        )

        def _toggle_render() -> None:
            nonlocal render_pretty
            render_pretty = not render_pretty
            text_area.text = _render_text(pretty=render_pretty)
            text_area.buffer.cursor_position = 0
            toggle_button.text = _toggle_label(pretty=render_pretty)
            event.app.layout.focus(text_area)
            event.app.invalidate()

        toggle_button = Button(
            text=_toggle_label(pretty=render_pretty),
            handler=_toggle_render,
        )

        dialog, ok_button = self._build_read_only_modal_dialog(
            app=event.app,
            title=f"Cell {column_name} @ row {row_index + 1}",
            text_area=text_area,
            extra_buttons=[toggle_button],
            ok_on_right=True,
        )

        self._display_modal(
            event.app,
            dialog,
            focus=text_area,
            context_type="cell_value",
            payload={"column": column_name, "row": row_index},
            width=width,
            height=height,
        )

    def _apply_selection_action(self, action: SheetEnterAction) -> None:
        """Apply a selection-derived projection to the parent view."""

        if len(self.view_stack) < _STACK_MIN_SIZE:
            self.viewer.status_message = "no source view"
            self.refresh()
            return

        summary_viewer = self.viewer
        source_viewer = self.view_stack.parent
        if source_viewer is None:
            summary_viewer.status_message = "no source view"
            self.refresh()
            return

        selected_lookup = {name for name in action.columns if isinstance(name, str)}
        if not selected_lookup:
            summary_viewer.status_message = "select at least one column"
            self.refresh()
            return

        ordered_columns = [name for name in source_viewer.columns if name in selected_lookup]
        if not ordered_columns:
            summary_viewer.status_message = "no matching columns to keep"
            self.refresh()
            return

        try:
            source_viewer.keep_columns(ordered_columns)
        except Exception as exc:  # pragma: no cover - defensive
            summary_viewer.status_message = f"keep columns error: {exc}"[:120]
            self.refresh()
            return

        with suppress(Exception):
            summary_viewer.clear_row_selection()
        if action.pop_viewer:
            self._pop_viewer()
        self.refresh()

    def _filter_by_pick(self) -> None:
        """Apply filter based on the currently selected value in a frequency view."""
        # Get the frequency viewer (current view) and the source viewer (parent)
        if len(self.view_stack) < _STACK_MIN_SIZE:
            return

        freq_viewer = self.viewer
        source_viewer = self.view_stack.parent
        if source_viewer is None:
            return

        # Ensure we're in a frequency view
        if not hasattr(freq_viewer, "is_freq_view") or not getattr(
            freq_viewer, "is_freq_view", False
        ):
            return

        selected_ids = set(getattr(freq_viewer, "_selected_row_ids", set()))
        values: list[object] = []
        if selected_ids:
            values = _ordered_freq_values(freq_viewer, selected_ids)
        if not values:
            try:
                values = [freq_viewer.sheet.get_value_at(freq_viewer.cur_row)]
            except Exception:
                self.viewer.status_message = "unable to pick value"
                return

        # Apply the filter to the source view
        try:
            # Build filter expression for the source column
            source_col = freq_viewer.freq_source_col
            if source_col is None:
                self.viewer.status_message = "unknown frequency source"
                return
            filter_expr = build_filter_expr_for_values(source_col, values)

            result = self._runtime.invoke(
                "filter_expr",
                args=[filter_expr, "append"],
                source="tui",
                viewer=source_viewer,
                context_mutator=self._mutate_context,
            )
        except Exception as exc:
            self.viewer.status_message = f"filter error: {exc}"
        else:
            if result.message:
                self.viewer.status_message = result.message
                return
            self.viewer.status_message = None
            with suppress(Exception):
                freq_viewer.clear_row_selection()
            self._pop_viewer()
            self.refresh()

    def _open_filter_modal(self, event, *, initial_text: str | None = None) -> None:
        # Implementation details omitted for brevity to focus on key logic
        title = "Expression Filter"
        prompt_text = "Polars expression (use c.<column>) - Enter: replace existing"
        current_expr_filter = ""
        with suppress(Exception):
            current_expr_filter = _format_expr_filters_for_modal(
                getattr(self.viewer, "filters", ())
            )
        default_text = initial_text or current_expr_filter or ""
        if not default_text:
            current_col = None
            with suppress(Exception):
                current_col = self.viewer.current_colname()
            if current_col:
                if current_col.isidentifier():
                    default_text = f"c.{current_col}"
                else:
                    safe_name = current_col.replace('"', '\\"')
                    default_text = f'c["{safe_name}"]'

        def accept(buff):
            raw_text = buff.text
            text = raw_text.strip()
            if text.lower() == "cancel":
                self.viewer.status_message = "filter canceled"
                self._remove_modal(event.app)
                self.refresh()
                return True

            try:
                args = [text] if text else []
                result = self._runtime.invoke(
                    "filter_expr",
                    args=args,
                    source="tui",
                    context_mutator=self._mutate_context,
                    propagate=(FilterError,),
                )
            except FilterError as err:
                self._open_error_modal(
                    event,
                    "Filter Error",
                    str(err),
                    retry=lambda ev: self._open_filter_modal(ev, initial_text=raw_text),
                )
            except Exception as exc:
                self._open_error_modal(
                    event,
                    "Unexpected Error",
                    str(exc),
                    retry=lambda ev: self._open_filter_modal(ev, initial_text=raw_text),
                )
            else:
                dispatch = self._finalise_runtime_result(result)
                status_error = self._status_error_message(("filter error",))
                error_message = result.message or status_error
                if error_message:
                    self._open_error_modal(
                        event,
                        "Filter Error",
                        error_message,
                        retry=lambda ev: self._open_filter_modal(ev, initial_text=raw_text),
                    )
                    return True
                if dispatch is not None:
                    self._record_expr_filter(text)
                self.viewer.status_message = None
                self._remove_modal(event.app)
                self.refresh()
            return True

        filter_field = TextArea(
            text=default_text,
            multiline=True,
            height=4,
            accept_handler=accept,
            history=None,
            completer=ColumnNameCompleter(self.viewer.columns),
            complete_while_typing=True,
        )
        filter_field.buffer.cursor_position = len(default_text)

        tui_modals.bind_enter_to_accept(filter_field)
        body = tui_modals.build_prompt_body(prompt_text, filter_field)
        dialog = Dialog(title=title, body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=filter_field,
            context_type="expr_filter",
            payload={"field": filter_field},
            width=80,
        )

    def _open_filter_modal_with_text(self, event, text: str) -> None:
        self._open_filter_modal(event, initial_text=text)

    def _open_transform_modal(self, event, *, initial_text: str | None = None) -> None:
        if self.viewer.plan_controller.current_plan() is None:
            self.viewer.status_message = "transform unsupported for this view"
            self.refresh()
            return
        title = "Transform"
        prompt_text = "Polars LazyFrame transform (lf -> LazyFrame) - Enter: derived view"
        history = InMemoryHistory()
        for item in self._transform_history:
            history.append_string(item)

        default_text = initial_text or "lf"
        if initial_text is None:
            current_col = None
            with suppress(Exception):
                current_col = self.viewer.current_colname()
            if current_col:
                if current_col.isidentifier():
                    default_text = f"lf.with_columns(c.{current_col})"
                else:
                    safe_name = current_col.replace('"', '\\"')
                    default_text = f'lf.with_columns(c["{safe_name}"])'

        def accept(buff):
            raw_text = buff.text
            text = raw_text.strip()
            if text.lower() == "cancel":
                self.viewer.status_message = "transform canceled"
                self._remove_modal(event.app)
                self.refresh()
                return True

            try:
                result = self._runtime.invoke(
                    "transform_expr",
                    args=[text],
                    source="tui",
                    context_mutator=self._mutate_context,
                    propagate=(TransformError,),
                )
            except TransformError as err:
                self._open_error_modal(
                    event,
                    "Transform Error",
                    str(err),
                    retry=lambda ev: self._open_transform_modal(ev, initial_text=raw_text),
                )
            except Exception as exc:
                self._open_error_modal(
                    event,
                    "Unexpected Error",
                    str(exc),
                    retry=lambda ev: self._open_transform_modal(ev, initial_text=raw_text),
                )
            else:
                dispatch = self._finalise_runtime_result(result)
                error_message = result.message or self._status_error_message(("transform error",))
                if error_message:
                    self._open_error_modal(
                        event,
                        "Transform Error",
                        error_message,
                        retry=lambda ev: self._open_transform_modal(ev, initial_text=raw_text),
                    )
                    return True
                if dispatch is not None:
                    self._record_transform(text)
                self.viewer.status_message = None
                self._remove_modal(event.app)
                self.refresh()
            return True

        transform_field = TextArea(
            text=default_text,
            multiline=True,
            height=6,
            accept_handler=accept,
            history=history,
            completer=ColumnNameCompleter(self.viewer.columns),
            complete_while_typing=True,
        )
        transform_field.buffer.cursor_position = len(default_text)

        tui_modals.bind_enter_to_accept(transform_field)
        body = tui_modals.build_prompt_body(prompt_text, transform_field)
        dialog = Dialog(title=title, body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=transform_field,
            context_type="transform",
            payload={"field": transform_field},
            width=90,
        )

    def _open_sql_filter_modal(self, event, *, initial_text: str | None = None) -> None:
        title = "SQL Filter"
        prompt_text = "Polars SQL WHERE clause (omit WHERE) - Enter: replace existing"
        current_sql_filter = ""
        with suppress(Exception):
            current_sql_filter = next(
                (
                    clause.text
                    for clause in getattr(self.viewer, "filters", ())
                    if clause.kind == "sql"
                ),
                "",
            )
        default_text = (
            initial_text or current_sql_filter or getattr(self.viewer, "sql_filter_text", "") or ""
        )
        if not default_text:
            current_col = None
            with suppress(Exception):
                current_col = self.viewer.current_colname()
            if current_col:
                if current_col.isidentifier():
                    default_text = current_col
                else:
                    default_text = ColumnNameCompleter._quote_identifier(current_col)

        def accept(buff):
            raw_text = buff.text
            text = raw_text.strip()
            if text.lower() == "cancel":
                self.viewer.status_message = "SQL filter canceled"
                self._remove_modal(event.app)
                self.refresh()
                return True

            args = [text] if text else []
            try:
                result = self._runtime.invoke(
                    "filter_sql",
                    args=args,
                    source="tui",
                    context_mutator=self._mutate_context,
                    propagate=(FilterError,),
                )
            except Exception as exc:
                self._open_error_modal(
                    event,
                    "SQL Filter Error",
                    str(exc),
                    retry=lambda ev: self._open_sql_filter_modal(ev, initial_text=raw_text),
                )
            else:
                dispatch = self._finalise_runtime_result(result)
                status_error = self._status_error_message(("sql filter error",))
                error_message = result.message or status_error
                if error_message:
                    self._open_error_modal(
                        event,
                        "SQL Filter Error",
                        error_message,
                        retry=lambda ev: self._open_sql_filter_modal(ev, initial_text=raw_text),
                    )
                    return True
                if text and dispatch is not None:
                    self._record_sql_filter(text)
                self.viewer.status_message = None
                self._remove_modal(event.app)
                self.refresh()
            return True

        filter_field = TextArea(
            text=default_text,
            multiline=True,
            height=4,
            accept_handler=accept,
            history=None,
            completer=ColumnNameCompleter(self.viewer.columns, mode="sql"),
            complete_while_typing=True,
        )
        filter_field.buffer.cursor_position = len(default_text)

        tui_modals.bind_enter_to_accept(filter_field)
        body = tui_modals.build_prompt_body(prompt_text, filter_field)
        dialog = Dialog(title=title, body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=filter_field,
            context_type="sql_filter",
            payload={"field": filter_field},
            width=80,
        )

    def _open_sql_filter_modal_with_text(self, event, text: str) -> None:
        self._open_sql_filter_modal(event, initial_text=text)

    def _open_command_modal(self, event) -> None:
        history = InMemoryHistory()
        for item in self._command_history:
            history.append_string(item)

        def accept(buff):
            raw_text = buff.text
            command_text = raw_text.strip()

            if not command_text:
                self._remove_modal(event.app)
                self.refresh()
                return True

            lowered = command_text.split(None, 1)[0].lower()
            if lowered in {"transform", "transform_expr"}:
                self.viewer.status_message = "transform is available via Shift+E modal"
                self._remove_modal(event.app)
                self.refresh()
                return True

            result = self._runtime.dispatch_raw(
                command_text,
                source="tui",
                context_mutator=self._mutate_context,
            )
            dispatch = self._finalise_runtime_result(result)
            if dispatch is not None:
                self._record_command(command_text)
            self._remove_modal(event.app)
            self.refresh()
            return True

        command_field = TextArea(
            text="",
            multiline=False,
            accept_handler=accept,
            history=history,
            completer=FilesystemPathCompleter(self._path_completion_base_dir),
            complete_while_typing=True,
        )
        command_field.buffer.cursor_position = 0

        examples = []
        for spec in self.commands.iter_specs():
            hints = spec.ui_hints or {}
            example = hints.get("example") if isinstance(hints, dict) else hints.get("example")
            if example:
                examples.append(str(example))
        unique_examples: list[str] = []
        for example in examples:
            if example not in unique_examples:
                unique_examples.append(example)
        prompt = (
            "Command:"
            if not unique_examples
            else f"Command (e.g. {', '.join(unique_examples[:3])}):"
        )

        body = tui_modals.build_prompt_body(prompt, command_field)
        dialog = Dialog(title="Command", body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=command_field,
            context_type="command",
            payload={"field": command_field},
            width=60,
        )

    def _open_shell_modal(self, event) -> None:
        if isinstance(self.viewer.ui_hooks, NullViewerUIHooks):
            self.viewer.status_message = "shell commands unavailable in headless mode"
            self.refresh()
            return

        history = InMemoryHistory()
        for item in self._shell_history:
            history.append_string(item)

        def accept(buff):
            raw_text = buff.text
            command_text = raw_text.strip()

            if not command_text:
                self._remove_modal(event.app)
                self.refresh()
                return True

            self._record_shell_command(command_text)
            self._remove_modal(event.app)
            self.refresh()

            async def _run_shell() -> None:
                try:
                    exit_code = await run_in_terminal(
                        lambda: self._run_shell_command(command_text),
                        in_executor=True,
                    )
                except Exception as exc:
                    self.viewer.status_message = f"shell error: {exc}"[:120]
                    self.refresh()
                    return

                self.viewer.status_message = f"shell exit {exit_code}"
                self.refresh()

            if event.app is not None:
                event.app.create_background_task(_run_shell())
            else:
                exit_code = self._run_shell_command(command_text)
                self.viewer.status_message = f"shell exit {exit_code}"
                self.refresh()
            return True

        command_field = TextArea(
            text="",
            multiline=False,
            accept_handler=accept,
            history=history,
            completer=FilesystemPathCompleter(self._path_completion_base_dir),
            complete_while_typing=True,
        )
        command_field.buffer.cursor_position = 0
        field_kb = KeyBindings()

        @field_kb.add("tab")
        def _trigger_completion(event) -> None:
            buff = event.current_buffer
            if buff.complete_state is None:
                buff.start_completion(select_first=True)
            buff.complete_next()

        tui_modals.merge_text_area_key_bindings(command_field, field_kb)

        body = tui_modals.build_prompt_body("! command:", command_field)
        dialog = Dialog(title="Shell", body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=command_field,
            context_type="shell",
            payload={"field": command_field},
            width=60,
        )

    def _run_shell_command(self, command_text: str) -> int:
        shell = os.environ.get("SHELL") or "/bin/sh"
        cwd: Path | None = None
        with suppress(Exception):
            base_dir = self._path_completion_base_dir()
            if base_dir is not None and base_dir.exists():
                cwd = base_dir
        exit_code = 1
        try:
            result = subprocess.run(
                [shell, "-lc", command_text],
                check=False,
                stdin=subprocess.DEVNULL,
                cwd=cwd,
            )
            exit_code = result.returncode
        except FileNotFoundError:
            result = subprocess.run(
                ["/bin/sh", "-lc", command_text],
                check=False,
                stdin=subprocess.DEVNULL,
                cwd=cwd,
            )
            exit_code = result.returncode
        except Exception as exc:
            print(f"shell error: {exc}")

        with suppress(EOFError):
            input("Press Enter to return to Pulka...")
        return exit_code

    def _open_row_search_modal(self, event) -> None:
        def accept(buff):
            text = buff.text.strip()
            if not text:
                self._remove_modal(event.app)
                self.refresh()
                return True

            if text.lower() == "cancel":
                self.viewer.status_message = "row selection canceled"
                self._remove_modal(event.app)
                self.refresh()
                return True

            try:
                columns = ()
                try:
                    columns = (self.viewer.columns[self.viewer.cur_col],)
                except Exception:
                    columns = self.viewer.columns[:1]
                self.viewer.select_rows_containing(text, columns=columns)
                self._record_row_search(text)
            except Exception as exc:
                self.viewer.status_message = f"Row selection error: {exc}"
            self._remove_modal(event.app)
            self.refresh()
            return True

        history = InMemoryHistory()
        for item in self._row_search_history:
            history.append_string(item)

        search_field = TextArea(
            text="",
            multiline=False,
            accept_handler=accept,
            history=history,
        )
        search_field.buffer.cursor_position = 0

        body = tui_modals.build_prompt_body(
            "Substring (current column, case-insensitive):",
            search_field,
        )
        dialog = Dialog(title="Select Rows (current column)", body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=search_field,
            context_type="row_search",
            payload={"field": search_field},
            width=60,
        )

    def _open_search_modal(self, event) -> None:
        def accept(buff):
            text = buff.text.strip()
            if text.lower() == "cancel":
                self.viewer.status_message = "search canceled"
                self._remove_modal(event.app)
                self.refresh()
                return True

            # Apply search to the viewer
            try:
                self.viewer.set_search(text)
                current = self.viewer.search_text
                self._clear_column_search()
                self._record_search(text)
                if current:
                    self.viewer.search(forward=True, include_current=True)
                self._remove_modal(event.app)
                self.refresh()
            except Exception as exc:
                self.viewer.status_message = f"Search error: {exc}"
            return True

        history = InMemoryHistory()
        for item in self._search_history:
            history.append_string(item)

        search_field = TextArea(
            text="",
            multiline=False,
            accept_handler=accept,
            history=history,
        )
        search_field.buffer.cursor_position = 0

        body = tui_modals.build_prompt_body(
            "Substring (current column, case-insensitive):",
            search_field,
        )
        dialog = Dialog(title="Search", body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=search_field,
            context_type="search",
            payload={"field": search_field},
            width=60,
        )

    def _open_column_search_modal(self, event) -> None:
        """Open the column search modal with history and tab completion."""

        def accept(buff):
            raw_text = buff.text
            query = raw_text.strip()

            if not query or query.lower() == "cancel":
                self.viewer.status_message = "column search canceled"
                self._clear_column_search()
                self._remove_modal(event.app)
                self.refresh()
                return True

            self._remove_modal(event.app)
            success = self._apply_column_search(query)
            self._record_column_search(query)
            if not success:
                self._clear_column_search()
            self.refresh()
            return True

        history = InMemoryHistory()
        for item in self._col_search_history:
            history.append_string(item)

        search_field = TextArea(
            text="",
            multiline=False,
            accept_handler=accept,
            history=history,
            completer=ColumnNameCompleter(self.viewer.columns, mode="plain"),
            complete_while_typing=True,
        )
        search_field.buffer.cursor_position = 0

        body = tui_modals.build_prompt_body("Column name (prefix or substring):", search_field)
        dialog = Dialog(title="Column Search", body=body, buttons=[])
        self._display_modal(
            event.app,
            dialog,
            focus=search_field,
            context_type="column_search",
            payload={"field": search_field},
            width=60,
        )

    def _record_search(self, text: str) -> None:
        text = text.strip()
        if not text:
            return
        with suppress(ValueError):
            self._search_history.remove(text)
        self._search_history.append(text)
        if len(self._search_history) > _HISTORY_MAX_SIZE:
            del self._search_history[0]

    def _record_row_search(self, text: str) -> None:
        cleaned = text.strip()
        if not cleaned:
            return
        with suppress(ValueError):
            self._row_search_history.remove(cleaned)
        self._row_search_history.append(cleaned)
        if len(self._row_search_history) > _HISTORY_MAX_SIZE:
            del self._row_search_history[0]

    def _record_command(self, text: str) -> None:
        cleaned = text.strip()
        if not cleaned:
            return
        with suppress(ValueError):
            self._command_history.remove(cleaned)
        self._command_history.append(cleaned)
        if len(self._command_history) > _HISTORY_MAX_SIZE:
            del self._command_history[0]

    def _record_shell_command(self, text: str) -> None:
        cleaned = text.strip()
        if not cleaned:
            return
        with suppress(ValueError):
            self._shell_history.remove(cleaned)
        self._shell_history.append(cleaned)
        if len(self._shell_history) > _HISTORY_MAX_SIZE:
            del self._shell_history[0]

    def _record_column_search(self, text: str) -> None:
        cleaned = text.strip()
        if not cleaned:
            return
        with suppress(ValueError):
            self._col_search_history.remove(cleaned)
        self._col_search_history.append(cleaned)
        if len(self._col_search_history) > _HISTORY_MAX_SIZE:
            del self._col_search_history[0]

    def _apply_column_search(self, query: str) -> bool:
        """Compute matches for ``query`` and focus the first result."""

        matches = self._compute_column_search_matches(query)
        state = self._col_search_state
        state.set(query, matches, current_col=self.viewer.cur_col)
        if not matches:
            self.viewer.status_message = f"column search: no match for '{query}'"
            return False

        target = state.position or 0
        if self._focus_column_search_match(target):
            return True

        self.viewer.status_message = f"column search: unable to focus '{query}'"
        return False

    def _iter_column_search_candidates(self) -> Iterator[tuple[int, str]]:
        """Yield candidate column indices and names for column search ranking."""

        state = viewer_public_state(self.viewer)
        columns: list[str]
        hidden: set[str]
        if state is None:  # pragma: no cover - defensive
            columns = list(getattr(self.viewer, "columns", ()))
            hidden = set(getattr(self.viewer, "_hidden_cols", ()))
        else:
            columns = list(state.columns)
            hidden = set(state.hidden_columns)

        for idx, name in enumerate(columns):
            if name in hidden:
                continue
            yield idx, name

    def _compute_column_search_matches(self, query: str) -> list[int]:
        """Rank matching columns by how closely they match ``query``."""

        query_lower = query.lower()
        ranked: list[tuple[tuple[int, int], int]] = []

        for idx, name in self._iter_column_search_candidates():
            lowered = name.lower()
            if query_lower not in lowered:
                continue
            if lowered == query_lower:
                priority = 0
            elif lowered.startswith(query_lower):
                priority = 1
            else:
                priority = 2
            ranked.append(((priority, idx), idx))

        ranked.sort(key=lambda item: item[0])
        return [idx for _, idx in ranked]

    def _focus_column_search_match(self, position: int) -> bool:
        matches = self._col_search_state.matches
        if position < 0 or position >= len(matches):
            return False

        match_idx = matches[position]
        if match_idx >= len(self.viewer.columns):
            self._recompute_column_search_matches()
            matches = self._col_search_state.matches
            if position < 0 or position >= len(matches):
                return False
            match_idx = matches[position]

        col_name = self.viewer.columns[match_idx]
        moved = self.viewer.goto_col(col_name)
        if moved:
            self._col_search_state.position = position
            total = len(matches)
            self.viewer.status_message = f"column search: {col_name} ({position + 1}/{total})"
        return moved

    def _handle_column_search_navigation(self, *, forward: bool) -> bool:
        """Navigate among column search matches in response to ``n``/``N``."""

        state = self._col_search_state
        if not state.query or not state.matches:
            return False

        self._recompute_column_search_matches()
        matches = state.matches
        if not matches:
            self.viewer.status_message = f"column search: no match for '{state.query}'"
            self._clear_column_search()
            return True

        try:
            anchor = matches.index(self.viewer.cur_col)
        except ValueError:
            anchor = -1 if forward else len(matches)

        step = 1 if forward else -1
        target = anchor + step
        if 0 <= target < len(matches):
            if self._focus_column_search_match(target):
                return True
            self.viewer.status_message = "column search: unable to focus match"
            return True

        direction = "next" if forward else "previous"
        self.viewer.status_message = f"column search: no {direction} match"
        return True

    def _clear_column_search(self) -> None:
        """Reset column search bookkeeping so ``n``/``N`` fall back to row search."""

        self._col_search_state.clear()

    def _recompute_column_search_matches(self) -> None:
        """Refresh cached matches for the active column search query."""

        state = self._col_search_state
        if not state.query:
            state.clear()
            return

        matches = self._compute_column_search_matches(state.query)
        state.recompute(matches, current_col=self.viewer.cur_col)

    def _status_error_message(self, prefixes: Sequence[str]) -> str | None:
        """Return the current status message when it matches one of ``prefixes``."""

        message = self.viewer.status_message
        if not message:
            return None
        normalized = message.strip().lower()
        for prefix in prefixes:
            if normalized.startswith(prefix):
                return message
        return None

    def _record_expr_filter(self, text: str) -> None:
        cleaned = text.strip()
        if not cleaned:
            return
        with suppress(ValueError):
            self._expr_filter_history.remove(cleaned)
        self._expr_filter_history.append(cleaned)
        if len(self._expr_filter_history) > _HISTORY_MAX_SIZE:
            del self._expr_filter_history[0]

    def _record_sql_filter(self, text: str) -> None:
        cleaned = text.strip()
        if not cleaned:
            return
        with suppress(ValueError):
            self._sql_filter_history.remove(cleaned)
        self._sql_filter_history.append(cleaned)
        if len(self._sql_filter_history) > _HISTORY_MAX_SIZE:
            del self._sql_filter_history[0]

    def _record_transform(self, text: str) -> None:
        cleaned = text.strip()
        if not cleaned:
            return
        with suppress(ValueError):
            self._transform_history.remove(cleaned)
        self._transform_history.append(cleaned)
        if len(self._transform_history) > _HISTORY_MAX_SIZE:
            del self._transform_history[0]

    def _open_error_modal(self, event, title: str, error_message: str, *, retry=None) -> None:
        """Open a modal dialog to display error messages with proper formatting."""
        text_area = TextArea(
            text=error_message,
            read_only=True,
            scrollbar=True,
            wrap_lines=True,
        )
        msg_kb = KeyBindings()

        def _close(app, event_obj=None) -> None:
            self._remove_modal(app)
            if retry is not None:
                retry_event = event_obj
                if retry_event is None:
                    retry_event = SimpleNamespace(app=app)
                retry(retry_event)
            else:
                self.refresh()

        @msg_kb.add("escape")
        def _close_and_reopen_filter(event) -> None:
            _close(event.app, event)

        @msg_kb.add("enter")
        def _close_enter(event) -> None:
            _close(event.app, event)

        tui_modals.merge_text_area_key_bindings(text_area, msg_kb)

        content = HSplit([text_area], padding=0)
        body = Box(body=content, padding=1)
        go_back_button = Button(text="Go back", handler=lambda: _close(event.app))
        dialog = Dialog(title=f"⚠ Error: {title}", body=body, buttons=[go_back_button])
        self._display_modal(
            event.app,
            dialog,
            focus=go_back_button,
            context_type="error",
            width=80,
        )

    def _open_text_modal(self, event, title: str, text: str) -> None:
        target_width = 60
        target_height = 40
        width, height = self._calculate_modal_dimensions(
            event.app,
            target_width=target_width,
            target_height=target_height,
        )
        text_area_height = max(3, height - _CELL_MODAL_CHROME_HEIGHT)

        text_area = TextArea(
            text=text,
            read_only=True,
            scrollbar=True,
            wrap_lines=True,
            height=text_area_height,
        )

        dialog, ok_button = self._build_read_only_modal_dialog(
            app=event.app,
            title=title,
            text_area=text_area,
        )

        self._display_modal(
            event.app,
            dialog,
            focus=ok_button,
            context_type="message",
            width=width,
            height=height,
        )
