vtcode-tui 0.98.1

Reusable TUI primitives and session API for VT Code-style terminal interfaces
use ratatui::layout::{Constraint, Layout, Rect};

use crate::config::constants::ui;

use super::{Session, render, slash};
use crate::core_tui::app::session::transient::TransientSurface;
use crate::core_tui::session::list_panel;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum BottomPanelKind {
    None,
    AgentPalette,
    FilePalette,
    HistoryPicker,
    SlashPalette,
    TaskPanel,
    LocalAgents,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct BottomPanelSpec {
    pub(super) kind: BottomPanelKind,
    pub(super) height: u16,
}

pub(super) fn resolve_bottom_panel_spec(
    session: &mut Session,
    viewport: Rect,
    header_height: u16,
    input_reserved_height: u16,
) -> BottomPanelSpec {
    let max_panel_height = viewport
        .height
        .saturating_sub(header_height)
        .saturating_sub(input_reserved_height)
        .saturating_sub(1);
    if max_panel_height == 0 || viewport.width == 0 {
        return BottomPanelSpec {
            kind: BottomPanelKind::None,
            height: 0,
        };
    }

    let split_context = SplitContext {
        width: viewport.width,
        max_panel_height,
    };

    let visible_surface = session.visible_bottom_docked_surface();
    let panel = match visible_surface {
        Some(TransientSurface::AgentPalette) => panel_from_split(
            session,
            split_context,
            BottomPanelKind::AgentPalette,
            render::split_inline_agent_palette_area,
        ),
        Some(TransientSurface::FilePalette) => panel_from_split(
            session,
            split_context,
            BottomPanelKind::FilePalette,
            render::split_inline_file_palette_area,
        ),
        Some(TransientSurface::HistoryPicker) => panel_from_split(
            session,
            split_context,
            BottomPanelKind::HistoryPicker,
            render::split_inline_history_picker_area,
        ),
        Some(TransientSurface::SlashPalette) => panel_from_split(
            session,
            split_context,
            BottomPanelKind::SlashPalette,
            slash::split_inline_slash_area,
        ),
        Some(TransientSurface::TaskPanel) => panel_from_split(
            session,
            split_context,
            BottomPanelKind::TaskPanel,
            split_inline_task_panel_area,
        ),
        Some(TransientSurface::LocalAgents) => panel_from_split(
            session,
            split_context,
            BottomPanelKind::LocalAgents,
            render::split_inline_local_agents_area,
        ),
        Some(
            TransientSurface::FloatingOverlay
            | TransientSurface::DiffPreview
            | TransientSurface::TranscriptReview,
        )
        | None => None,
    };

    if let Some(panel) = panel {
        return panel;
    }

    BottomPanelSpec {
        kind: BottomPanelKind::None,
        height: 0,
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct SplitContext {
    width: u16,
    max_panel_height: u16,
}

fn panel_from_split(
    session: &mut Session,
    ctx: SplitContext,
    kind: BottomPanelKind,
    split_fn: fn(&mut Session, Rect) -> (Rect, Option<Rect>),
) -> Option<BottomPanelSpec> {
    let height = probe_panel_height(session, ctx, split_fn);
    if height == 0 {
        None
    } else {
        Some(BottomPanelSpec {
            kind,
            height: normalize_panel_height(height, ctx.max_panel_height),
        })
    }
}

fn normalize_panel_height(raw_height: u16, max_panel_height: u16) -> u16 {
    if raw_height == 0 || max_panel_height == 0 {
        return 0;
    }

    let min_floor = ui::INLINE_LIST_PANEL_MIN_HEIGHT
        .min(max_panel_height)
        .max(1);
    raw_height.max(min_floor).min(max_panel_height)
}

fn split_inline_task_panel_area(session: &mut Session, area: Rect) -> (Rect, Option<Rect>) {
    let visible_lines = session.task_panel_lines.len().max(1);
    let desired_list_rows =
        list_panel::rows_to_u16(visible_lines.min(ui::INLINE_LIST_MAX_ROWS_MULTILINE));
    let fixed_rows = list_panel::fixed_section_rows(1, 1, false);
    list_panel::split_bottom_list_panel(area, fixed_rows, desired_list_rows)
}

fn probe_panel_height(
    session: &mut Session,
    ctx: SplitContext,
    split_fn: fn(&mut Session, Rect) -> (Rect, Option<Rect>),
) -> u16 {
    if ctx.width == 0 || ctx.max_panel_height == 0 {
        return 0;
    }

    let probe_area = Rect::new(0, 0, ctx.width, ctx.max_panel_height.saturating_add(1));
    let (_, panel_area) = split_fn(session, probe_area);
    panel_area.map(|area| area.height).unwrap_or(0)
}

pub(super) fn split_input_and_bottom_panel_area(
    area: Rect,
    panel_height: u16,
) -> (Rect, Option<Rect>) {
    if area.height == 0 || panel_height == 0 || area.height <= 1 {
        return (area, None);
    }

    let resolved_panel = panel_height.min(area.height.saturating_sub(1));
    if resolved_panel == 0 {
        return (area, None);
    }

    let input_height = area.height.saturating_sub(resolved_panel);
    let chunks = Layout::vertical([
        Constraint::Length(input_height.max(1)),
        Constraint::Length(resolved_panel),
    ])
    .split(area);
    (chunks[0], Some(chunks[1]))
}