Skip to main content

vtcode_tui/core_tui/
session.rs

1use std::{collections::VecDeque, sync::Arc, time::Instant};
2
3#[cfg(test)]
4use anstyle::Color as AnsiColorEnum;
5use anstyle::RgbColor;
6use ratatui::crossterm::event::{
7    Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, MouseEvent, MouseEventKind,
8};
9
10use ratatui::{
11    Frame,
12    layout::{Constraint, Layout, Rect},
13    text::{Line, Span, Text},
14    widgets::{Clear, ListState, Widget},
15};
16use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
17
18use super::{
19    style::{measure_text_width, ratatui_color_from_ansi, ratatui_style_from_inline},
20    types::{
21        InlineCommand, InlineEvent, InlineHeaderContext, InlineListSelection, InlineMessageKind,
22        InlineTextStyle, InlineTheme, OverlayRequest,
23    },
24};
25use crate::config::constants::ui;
26use crate::ui::tui::widgets::SessionWidget;
27
28mod frame_layout;
29mod header;
30mod impl_events;
31mod impl_init;
32mod impl_input;
33mod impl_layout;
34mod impl_logs;
35mod impl_render;
36mod impl_scroll;
37mod impl_style;
38pub(crate) mod inline_list;
39mod input;
40pub(crate) mod input_manager;
41pub(crate) mod list_navigator;
42pub(crate) mod list_panel;
43mod message;
44pub mod modal;
45pub mod mouse_selection;
46mod navigation;
47mod queue;
48pub mod render;
49mod scroll;
50pub mod styling;
51mod text_utils;
52mod transcript;
53pub mod utils;
54pub mod wrapping;
55
56// New modular components (refactored from main session.rs)
57mod command;
58mod editing;
59
60pub mod config;
61mod driver;
62mod events;
63pub(crate) mod message_renderer;
64mod messages;
65mod reflow;
66pub(crate) mod reverse_search;
67mod spinner;
68mod state;
69pub mod terminal_capabilities;
70mod terminal_title;
71#[cfg(test)]
72mod tests;
73mod tool_renderer;
74mod transcript_links;
75mod vim;
76
77use self::input_manager::InputManager;
78pub(crate) use self::message::TranscriptLine;
79use self::message::{MessageLabels, MessageLine};
80use self::modal::{ModalState, WizardModalState};
81
82use self::config::AppearanceConfig;
83pub(crate) use self::input::status_requires_shimmer;
84use self::mouse_selection::MouseSelectionState;
85use self::queue::QueueOverlay;
86use self::scroll::ScrollManager;
87use self::spinner::{ShimmerState, ThinkingSpinner};
88use self::styling::SessionStyles;
89use self::transcript::TranscriptReflowCache;
90use self::transcript_links::TranscriptFileLinkTarget;
91use self::vim::VimState;
92#[cfg(test)]
93use super::types::InlineHeaderHighlight;
94// TaskPlan integration intentionally omitted in this UI crate.
95use crate::ui::tui::log::{LogEntry, highlight_log_entry};
96
97const USER_PREFIX: &str = "";
98const PLACEHOLDER_COLOR: RgbColor =
99    RgbColor(ui::PLACEHOLDER_R, ui::PLACEHOLDER_G, ui::PLACEHOLDER_B);
100const MAX_LOG_LINES: usize = 256;
101const MAX_LOG_DRAIN_PER_TICK: usize = 256;
102
103#[derive(Clone, Debug)]
104struct CollapsedPaste {
105    line_index: usize,
106    full_text: String,
107}
108
109#[derive(Clone, Debug, Default)]
110pub(crate) struct SuggestedPromptState {
111    pub(crate) active: bool,
112}
113
114pub(crate) enum ActiveOverlay {
115    Modal(Box<ModalState>),
116    Wizard(Box<WizardModalState>),
117}
118
119impl ActiveOverlay {
120    fn as_modal(&self) -> Option<&ModalState> {
121        match self {
122            Self::Modal(state) => Some(state),
123            Self::Wizard(_) => None,
124        }
125    }
126
127    fn as_modal_mut(&mut self) -> Option<&mut ModalState> {
128        match self {
129            Self::Modal(state) => Some(state),
130            Self::Wizard(_) => None,
131        }
132    }
133
134    fn as_wizard(&self) -> Option<&WizardModalState> {
135        match self {
136            Self::Wizard(state) => Some(state),
137            Self::Modal(_) => None,
138        }
139    }
140
141    fn as_wizard_mut(&mut self) -> Option<&mut WizardModalState> {
142        match self {
143            Self::Wizard(state) => Some(state),
144            Self::Modal(_) => None,
145        }
146    }
147
148    fn restore_input(&self) -> bool {
149        match self {
150            Self::Modal(state) => state.restore_input,
151            Self::Wizard(_) => true,
152        }
153    }
154
155    fn restore_cursor(&self) -> bool {
156        match self {
157            Self::Modal(state) => state.restore_cursor,
158            Self::Wizard(_) => true,
159        }
160    }
161}
162
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
164pub(crate) enum MouseDragTarget {
165    #[default]
166    None,
167    Transcript,
168    Input,
169}
170
171pub struct Session {
172    // --- Managers (Phase 2) ---
173    /// Manages user input, cursor, and command history
174    pub(crate) input_manager: InputManager,
175    /// Manages scroll state and viewport metrics
176    pub(crate) scroll_manager: ScrollManager,
177    user_scrolled: bool,
178
179    // --- Message Management ---
180    pub(crate) lines: Vec<MessageLine>,
181    collapsed_pastes: Vec<CollapsedPaste>,
182    pub(crate) theme: InlineTheme,
183    pub(crate) styles: SessionStyles,
184    pub(crate) appearance: AppearanceConfig,
185    pub(crate) header_context: InlineHeaderContext,
186    pub(crate) header_rows: u16,
187    pub(crate) labels: MessageLabels,
188
189    // --- Prompt/Input Display ---
190    prompt_prefix: String,
191    prompt_style: InlineTextStyle,
192    placeholder: Option<String>,
193    placeholder_style: Option<InlineTextStyle>,
194    pub(crate) input_status_left: Option<String>,
195    pub(crate) input_status_right: Option<String>,
196    input_compact_mode: bool,
197
198    // --- UI State ---
199    #[allow(dead_code)]
200    navigation_state: ListState,
201    input_enabled: bool,
202    cursor_visible: bool,
203    pub(crate) needs_redraw: bool,
204    pub(crate) needs_full_clear: bool,
205    /// Track if transcript content changed (not just scroll position)
206    pub(crate) transcript_content_changed: bool,
207    should_exit: bool,
208    scroll_cursor_steady_until: Option<Instant>,
209    last_shimmer_active: bool,
210    pub(crate) view_rows: u16,
211    pub(crate) input_height: u16,
212    pub(crate) transcript_rows: u16,
213    pub(crate) transcript_width: u16,
214    pub(crate) transcript_view_top: usize,
215    transcript_area: Option<Rect>,
216    input_area: Option<Rect>,
217    bottom_panel_area: Option<Rect>,
218    modal_list_area: Option<Rect>,
219    transcript_file_link_targets: Vec<TranscriptFileLinkTarget>,
220    hovered_transcript_file_link: Option<usize>,
221    last_mouse_position: Option<(u16, u16)>,
222
223    // --- Logging ---
224    log_receiver: Option<UnboundedReceiver<LogEntry>>,
225    log_lines: VecDeque<Arc<Text<'static>>>,
226    log_cached_text: Option<Arc<Text<'static>>>,
227    log_evicted: bool,
228    pub(crate) show_logs: bool,
229
230    // --- Rendering ---
231    transcript_cache: Option<TranscriptReflowCache>,
232    /// Cache of visible lines by (scroll_offset, width) - shared via Arc for zero-copy reads
233    /// Avoids expensive clone on cache hits
234    pub(crate) visible_lines_cache: Option<(usize, u16, Arc<Vec<TranscriptLine>>)>,
235    pub(crate) queued_inputs: Vec<String>,
236    queue_overlay_cache: Option<QueueOverlay>,
237    queue_overlay_version: u64,
238    active_overlay: Option<ActiveOverlay>,
239    overlay_queue: VecDeque<OverlayRequest>,
240    last_overlay_list_selection: Option<InlineListSelection>,
241    last_overlay_list_was_last: bool,
242    line_revision_counter: u64,
243    /// Track the first line that needs reflow/update to avoid O(N) scans
244    first_dirty_line: Option<usize>,
245    in_tool_code_fence: bool,
246
247    // --- Prompt Suggestions ---
248    pub(crate) suggested_prompt_state: SuggestedPromptState,
249
250    // --- Thinking Indicator ---
251    pub(crate) thinking_spinner: ThinkingSpinner,
252    pub(crate) shimmer_state: ShimmerState,
253
254    // --- Reverse Search ---
255    pub(crate) reverse_search_state: reverse_search::ReverseSearchState,
256
257    // --- PTY Session Management ---
258    pub(crate) active_pty_sessions: Option<Arc<std::sync::atomic::AtomicUsize>>,
259
260    // --- Clipboard for yank/paste operations ---
261    #[allow(dead_code)]
262    pub(crate) clipboard: String,
263    pub(crate) vim_state: VimState,
264
265    // --- Mouse Text Selection ---
266    pub(crate) mouse_selection: MouseSelectionState,
267    pub(crate) mouse_drag_target: MouseDragTarget,
268
269    pub(crate) skip_confirmations: bool,
270
271    // --- Performance Caching ---
272    pub(crate) header_lines_cache: Option<Vec<Line<'static>>>,
273    pub(crate) header_height_cache: hashbrown::HashMap<u16, u16>,
274    pub(crate) queued_inputs_preview_cache: Option<Vec<String>>,
275
276    // --- Terminal Title ---
277    /// Product/app name used in terminal title branding
278    pub(crate) app_name: String,
279    /// Workspace root path for dynamic title generation
280    pub(crate) workspace_root: Option<std::path::PathBuf>,
281    /// Last set terminal title to avoid redundant updates
282    last_terminal_title: Option<String>,
283
284    // --- Streaming State ---
285    /// Track if the assistant is currently streaming a final answer.
286    /// When true, user input should be queued instead of submitted immediately
287    /// to prevent race conditions with turn completion (see GitHub #12569).
288    pub(crate) is_streaming_final_answer: bool,
289}