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