vtcode-tui 0.98.2

Reusable TUI primitives and session API for VT Code-style terminal interfaces
use std::{collections::VecDeque, sync::Arc, time::Instant};

#[cfg(test)]
use anstyle::Color as AnsiColorEnum;
use anstyle::RgbColor;
use ratatui::crossterm::event::{
    Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent,
    MouseEventKind,
};

use ratatui::{
    Frame,
    layout::{Constraint, Layout, Rect},
    text::{Line, Span, Text},
    widgets::{Clear, ListState, Widget},
};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};

use super::{
    style::{measure_text_width, ratatui_color_from_ansi, ratatui_style_from_inline},
    types::{
        InlineCommand, InlineEvent, InlineHeaderContext, InlineListSelection, InlineMessageKind,
        InlineTextStyle, InlineTheme, OverlayRequest,
    },
};
use crate::config::constants::ui;
use crate::core_tui::types::LocalAgentEntry;
use crate::options::FullscreenInteractionSettings;
use crate::ui::tui::widgets::SessionWidget;

mod frame_layout;
mod header;
mod impl_events;
mod impl_init;
mod impl_input;
mod impl_layout;
mod impl_logs;
mod impl_render;
mod impl_scroll;
mod impl_style;
pub(crate) mod inline_list;
mod input;
pub(crate) mod input_manager;
pub(crate) mod list_navigator;
pub(crate) mod list_panel;
mod message;
pub mod modal;
pub mod mouse_selection;
mod navigation;
mod queue;
pub mod render;
mod scroll;
pub mod styling;
mod text_utils;
mod transcript;
pub mod utils;
pub mod wrapping;

// New modular components (refactored from main session.rs)
mod command;
mod editing;

pub mod config;
mod driver;
mod events;
pub(crate) mod message_renderer;
mod messages;
mod reflow;
pub(crate) mod reverse_search;
mod spinner;
mod state;
pub mod terminal_capabilities;
mod terminal_title;
#[cfg(test)]
mod tests;
mod tool_renderer;
mod transcript_links;
mod vim;

use self::input_manager::InputManager;
pub(crate) use self::message::TranscriptLine;
use self::message::{MessageLabels, MessageLine};
use self::modal::{ModalState, WizardModalState};

use self::config::AppearanceConfig;
pub(crate) use self::input::status_requires_shimmer;
use self::mouse_selection::MouseSelectionState;
use self::queue::QueueOverlay;
use self::scroll::ScrollManager;
pub(crate) use self::spinner::spinner_frame_for_phase;
use self::spinner::{ShimmerState, ThinkingSpinner};
use self::styling::SessionStyles;
use self::transcript::TranscriptReflowCache;
use self::transcript_links::TranscriptFileLinkTarget;
pub(crate) use self::transcript_links::TranscriptLinkClickAction;
use self::vim::VimState;
#[cfg(test)]
use super::types::InlineHeaderHighlight;
// TaskPlan integration intentionally omitted in this UI crate.
use crate::ui::tui::log::{LogEntry, highlight_log_entry};

const USER_PREFIX: &str = "";
const PLACEHOLDER_COLOR: RgbColor =
    RgbColor(ui::PLACEHOLDER_R, ui::PLACEHOLDER_G, ui::PLACEHOLDER_B);
const MAX_LOG_LINES: usize = 256;
const MAX_LOG_DRAIN_PER_TICK: usize = 256;

#[derive(Clone, Debug)]
struct CollapsedPaste {
    line_index: usize,
    full_text: String,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct SuggestedPromptState {
    pub(crate) active: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum InlinePromptSuggestionSource {
    Llm,
    Local,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct InlinePromptSuggestionState {
    pub(crate) suggestion: Option<String>,
    pub(crate) source: Option<InlinePromptSuggestionSource>,
}

pub(crate) enum ActiveOverlay {
    Modal(Box<ModalState>),
    Wizard(Box<WizardModalState>),
}

impl ActiveOverlay {
    fn as_modal(&self) -> Option<&ModalState> {
        match self {
            Self::Modal(state) => Some(state),
            Self::Wizard(_) => None,
        }
    }

    fn as_modal_mut(&mut self) -> Option<&mut ModalState> {
        match self {
            Self::Modal(state) => Some(state),
            Self::Wizard(_) => None,
        }
    }

    fn as_wizard(&self) -> Option<&WizardModalState> {
        match self {
            Self::Wizard(state) => Some(state),
            Self::Modal(_) => None,
        }
    }

    fn as_wizard_mut(&mut self) -> Option<&mut WizardModalState> {
        match self {
            Self::Wizard(state) => Some(state),
            Self::Modal(_) => None,
        }
    }

    fn restore_input(&self) -> bool {
        match self {
            Self::Modal(state) => state.restore_input,
            Self::Wizard(_) => true,
        }
    }

    fn restore_cursor(&self) -> bool {
        match self {
            Self::Modal(state) => state.restore_cursor,
            Self::Wizard(_) => true,
        }
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum MouseDragTarget {
    #[default]
    None,
    Transcript,
    ModalText,
    Input,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct FullscreenSessionState {
    pub(crate) active: bool,
    pub(crate) interaction: FullscreenInteractionSettings,
}

pub struct Session {
    // --- Managers (Phase 2) ---
    /// Manages user input, cursor, and command history
    pub(crate) input_manager: InputManager,
    /// Manages scroll state and viewport metrics
    pub(crate) scroll_manager: ScrollManager,
    user_scrolled: bool,

    // --- Message Management ---
    pub(crate) lines: Vec<MessageLine>,
    collapsed_pastes: Vec<CollapsedPaste>,
    pub(crate) theme: InlineTheme,
    pub(crate) styles: SessionStyles,
    pub(crate) appearance: AppearanceConfig,
    pub(crate) header_context: InlineHeaderContext,
    pub(crate) header_rows: u16,
    pub(crate) labels: MessageLabels,

    // --- Prompt/Input Display ---
    prompt_prefix: String,
    prompt_style: InlineTextStyle,
    placeholder: Option<String>,
    placeholder_style: Option<InlineTextStyle>,
    pub(crate) input_status_left: Option<String>,
    pub(crate) input_status_right: Option<String>,
    /// Transient "copied" confirmation shown in the input status row.
    copy_notification_until: Option<Instant>,
    input_compact_mode: bool,

    // --- UI State ---
    #[allow(dead_code)]
    navigation_state: ListState,
    input_enabled: bool,
    cursor_visible: bool,
    pub(crate) needs_redraw: bool,
    pub(crate) needs_full_clear: bool,
    /// Track whether the transcript viewport must be cleared before repainting.
    pub(crate) transcript_clear_required: bool,
    should_exit: bool,
    scroll_cursor_steady_until: Option<Instant>,
    last_shimmer_active: bool,
    pub(crate) view_rows: u16,
    pub(crate) input_height: u16,
    pub(crate) transcript_rows: u16,
    pub(crate) transcript_width: u16,
    pub(crate) transcript_view_top: usize,
    transcript_area: Option<Rect>,
    input_area: Option<Rect>,
    bottom_panel_area: Option<Rect>,
    modal_list_area: Option<Rect>,
    modal_text_areas: Vec<Rect>,
    transcript_file_link_targets: Vec<TranscriptFileLinkTarget>,
    modal_link_targets: Vec<TranscriptFileLinkTarget>,
    hovered_transcript_file_link: Option<usize>,
    last_mouse_position: Option<(u16, u16)>,
    last_link_open: Option<(String, Instant)>,
    pending_link_open: Option<String>,
    held_key_modifiers: KeyModifiers,

    // --- Logging ---
    log_receiver: Option<UnboundedReceiver<LogEntry>>,
    log_lines: VecDeque<Arc<Text<'static>>>,
    log_cached_text: Option<Arc<Text<'static>>>,
    log_evicted: bool,
    pub(crate) show_logs: bool,

    // --- Rendering ---
    transcript_cache: Option<TranscriptReflowCache>,
    /// Cache of visible lines by (scroll_offset, width) - shared via Arc for zero-copy reads
    /// Avoids expensive clone on cache hits
    pub(crate) visible_lines_cache: Option<(usize, u16, usize, Arc<Vec<TranscriptLine>>)>,
    pub(crate) queued_inputs: Vec<String>,
    pub(crate) local_agents: Vec<LocalAgentEntry>,
    pub(crate) local_agents_drawer_visible: bool,
    pub(crate) subprocess_entries: Vec<String>,
    pub(crate) subagent_preview: Option<String>,
    queue_overlay_cache: Option<QueueOverlay>,
    queue_overlay_version: u64,
    active_overlay: Option<ActiveOverlay>,
    overlay_queue: VecDeque<OverlayRequest>,
    last_overlay_list_selection: Option<InlineListSelection>,
    last_overlay_list_was_last: bool,
    line_revision_counter: u64,
    /// Track the first line that needs reflow/update to avoid O(N) scans
    first_dirty_line: Option<usize>,
    in_tool_code_fence: bool,

    // --- Prompt Suggestions ---
    pub(crate) suggested_prompt_state: SuggestedPromptState,
    pub(crate) inline_prompt_suggestion: InlinePromptSuggestionState,

    // --- Thinking Indicator ---
    pub(crate) thinking_spinner: ThinkingSpinner,
    pub(crate) shimmer_state: ShimmerState,

    // --- Reverse Search ---
    pub(crate) reverse_search_state: reverse_search::ReverseSearchState,

    // --- PTY Session Management ---
    pub(crate) active_pty_sessions: Option<Arc<std::sync::atomic::AtomicUsize>>,

    // --- Clipboard for yank/paste operations ---
    #[allow(dead_code)]
    pub(crate) clipboard: String,
    pub(crate) vim_state: VimState,

    // --- Mouse Text Selection ---
    pub(crate) mouse_selection: MouseSelectionState,
    pub(crate) mouse_drag_target: MouseDragTarget,
    pub(crate) fullscreen: FullscreenSessionState,

    pub(crate) skip_confirmations: bool,

    // --- Performance Caching ---
    pub(crate) header_lines_cache: Option<Vec<Line<'static>>>,
    pub(crate) header_height_cache: hashbrown::HashMap<u16, u16>,
    pub(crate) queued_inputs_preview_cache: Option<Vec<String>>,
    pub(crate) subprocess_entries_preview_cache: Option<Vec<String>>,

    // --- Terminal Title ---
    /// Product/app name used in terminal title branding
    pub(crate) app_name: String,
    /// Workspace root path for dynamic title generation
    pub(crate) workspace_root: Option<std::path::PathBuf>,
    /// Raw config items for terminal title rendering (`None` means use defaults).
    pub(crate) terminal_title_items: Option<Vec<String>>,
    /// Active thread label shown in terminal title when configured.
    pub(crate) terminal_title_thread_label: Option<String>,
    /// Active git branch shown in terminal title when configured.
    pub(crate) terminal_title_git_branch: Option<String>,
    /// Latest task tracker progress label extracted from the task panel.
    pub(crate) terminal_title_task_progress: Option<String>,
    /// Last set terminal title to avoid redundant updates
    last_terminal_title: Option<String>,

    // --- Streaming State ---
    /// Track if the assistant is currently streaming a final answer.
    /// When true, user input should be queued instead of submitted immediately
    /// to prevent race conditions with turn completion (see GitHub #12569).
    pub(crate) is_streaming_final_answer: bool,
}