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