opendev_tui/app/state.rs
1//! Persistent application state shared across renders.
2
3use std::collections::{HashMap, VecDeque};
4use std::sync::{Arc, Mutex};
5use std::time::Instant;
6
7use crate::history::CommandHistory;
8use crate::selection::SelectionState;
9use crate::widgets::{Toast, TodoDisplayItem, WelcomePanelState};
10
11use super::{
12 AutonomyLevel, DisplayMessage, OperationMode, PendingItem, ReasoningLevel, ToolExecution,
13};
14
15/// Persistent application state shared across renders.
16#[derive(Debug)]
17pub struct AppState {
18 /// Whether the app is running.
19 pub running: bool,
20 /// Current operation mode.
21 pub mode: OperationMode,
22 /// Autonomy level (Manual / Semi-Auto / Auto).
23 pub autonomy: AutonomyLevel,
24 /// Reasoning effort level (Off / Low / Medium / High).
25 pub reasoning_level: ReasoningLevel,
26 /// Active model name.
27 pub model: String,
28 /// Current working directory.
29 pub working_dir: String,
30 /// Cached path shortener for display (avoids repeated syscalls).
31 pub path_shortener: crate::formatters::PathShortener,
32 /// Git branch name (if in a repo).
33 pub git_branch: Option<String>,
34 /// Tokens used in current session.
35 pub tokens_used: u64,
36 /// Token limit for the session.
37 pub tokens_limit: u64,
38 /// Context window usage percentage (0.0 - 100.0+).
39 pub context_usage_pct: f64,
40 /// Session cost in USD.
41 pub session_cost: f64,
42 /// MCP server status: (connected, total).
43 pub mcp_status: Option<(usize, usize)>,
44 /// Whether any MCP server has errors.
45 pub mcp_has_errors: bool,
46 /// Whether the agent is currently processing.
47 pub agent_active: bool,
48 /// Conversation messages for display.
49 pub messages: Vec<DisplayMessage>,
50 /// Current task progress (while agent is working).
51 pub task_progress: Option<crate::widgets::progress::TaskProgress>,
52 /// Spinner state for animation.
53 pub spinner: crate::widgets::spinner::SpinnerState,
54 /// Current user input buffer.
55 pub input_buffer: String,
56 /// Cursor position within the input buffer.
57 pub input_cursor: usize,
58 /// Active tool executions.
59 pub active_tools: Vec<ToolExecution>,
60 /// Scroll offset for the conversation view (lines from bottom).
61 pub scroll_offset: u32,
62 /// Whether the user has scrolled up (disables auto-scroll).
63 pub user_scrolled: bool,
64 /// Autocomplete engine for `/` commands and `@` file mentions.
65 pub autocomplete: crate::autocomplete::AutocompleteEngine,
66 /// Number of running background tasks.
67 pub background_task_count: usize,
68 /// Info about a recently-backgrounded task: (task_id, when).
69 pub backgrounded_task_info: Option<(String, Instant)>,
70 /// Active subagent executions for nested display.
71 pub active_subagents: Vec<crate::widgets::nested_tool::SubagentDisplayState>,
72 /// Shared todo manager for syncing panel state with tool results.
73 pub todo_manager: Option<Arc<Mutex<opendev_runtime::TodoManager>>>,
74 /// Todo items from the current plan (for the todo progress panel).
75 pub todo_items: Vec<TodoDisplayItem>,
76 /// Whether the todo panel is expanded (true) or collapsed (false).
77 pub todo_expanded: bool,
78 /// Whether thinking blocks should start expanded (toggled by Ctrl+I).
79 pub thinking_expanded: bool,
80 /// Spinner tick counter for todo panel animation.
81 pub todo_spinner_tick: usize,
82 /// Optional plan name for the todo panel title.
83 pub plan_name: Option<String>,
84 /// File change stats for current session: (files, additions, deletions).
85 pub file_changes: Option<(usize, u64, u64)>,
86 /// Application version string.
87 pub version: String,
88 /// Animated welcome panel state.
89 pub welcome_panel: WelcomePanelState,
90 /// Cached terminal width for tick-time access.
91 pub terminal_width: u16,
92 /// Cached terminal height for tick-time access.
93 pub terminal_height: u16,
94 /// Unified queue for items waiting to be processed by the foreground agent.
95 /// Contains both user messages and completed background results, processed FIFO.
96 pub pending_queue: VecDeque<PendingItem>,
97 /// Dirty flag — set to `true` when state changes; cleared after render.
98 pub dirty: bool,
99 /// Generation counter for message/tool state changes.
100 /// Incremented whenever messages, tool results, or collapse state change.
101 pub message_generation: u64,
102 /// Cached conversation lines (static message portion only, excludes spinners).
103 pub cached_lines: Vec<ratatui::text::Line<'static>>,
104 /// Generation counter at which `cached_lines` was last built.
105 pub lines_generation: u64,
106 /// Per-message content hashes for incremental cache rebuilds.
107 pub per_message_hashes: Vec<u64>,
108 /// Per-message line counts tracking how many cached_lines each message produced.
109 pub per_message_line_counts: Vec<usize>,
110 /// Per-message markdown render cache, keyed by hash of (role + content).
111 pub markdown_cache: HashMap<u64, Vec<ratatui::text::Line<'static>>>,
112 /// Terminal width at which cached_lines were last built (for resize invalidation).
113 pub cached_width: u16,
114 /// Per-message culling state from the last cache rebuild.
115 /// Used to detect when scrolling changes which messages are visible vs culled.
116 pub per_message_culled: Vec<bool>,
117 /// Scroll offset at the time cached_lines were last built.
118 pub cached_scroll_offset: u32,
119 /// Scroll acceleration: last scroll direction (true = up, false = down).
120 pub scroll_last_direction: Option<bool>,
121 /// Scroll acceleration: timestamp of the last scroll key press.
122 pub scroll_last_time: Option<Instant>,
123 /// Scroll acceleration: current acceleration level (0 = base, increases).
124 pub scroll_accel_level: u8,
125 /// Active color theme for the TUI.
126 pub theme: crate::formatters::style_tokens::Theme,
127 /// Name of the active theme.
128 pub theme_name: crate::formatters::style_tokens::ThemeName,
129 /// Command history for Up/Down arrow navigation.
130 pub command_history: CommandHistory,
131 /// Flag set by /compact command; agent loop consumes and triggers compaction.
132 pub compact_requested: bool,
133 /// Whether manual compaction is currently in progress.
134 pub compaction_active: bool,
135 /// Plan mode flag — when true, next UserSubmit injects plan reminder.
136 pub pending_plan_request: bool,
137 /// Plan content to display in the conversation (consumed after first render).
138 pub plan_content_display: Option<String>,
139 /// Whether we're waiting for the current tool to finish before backgrounding.
140 pub backgrounding_pending: bool,
141 /// Background agent task manager.
142 pub bg_agent_manager: crate::managers::BackgroundAgentManager,
143 /// Whether the task watcher panel (Alt+B) is open.
144 pub task_watcher_open: bool,
145 /// Index of focused cell in the task watcher grid (0-based).
146 pub task_watcher_focus: usize,
147 /// Per-task scroll offset in the task watcher (index = task_idx, value = lines scrolled up).
148 pub task_watcher_cell_scrolls: Vec<usize>,
149 /// Page offset when tasks exceed grid capacity.
150 pub task_watcher_page: usize,
151 /// When all tasks finished (for auto-close after 3s grace).
152 pub task_watcher_all_done_at: Option<Instant>,
153 /// Last task completion flash: (task_id, when).
154 pub last_task_completion: Option<(String, Instant)>,
155 /// Active toast notifications.
156 pub toasts: Vec<Toast>,
157 /// Whether leader key (Ctrl+X) is pending.
158 pub leader_pending: bool,
159 /// Timestamp of leader key press (for timeout).
160 pub leader_timestamp: Option<Instant>,
161 /// Undo stack: tree hashes from snapshot manager.
162 pub undo_stack: Vec<String>,
163 /// Redo stack: tree hashes for redo.
164 pub redo_stack: Vec<String>,
165 /// Whether debug panel is open.
166 pub debug_panel_open: bool,
167 /// Session title (set by the agent).
168 pub session_title: Option<String>,
169 /// Maps background subagent IDs to their parent background task IDs.
170 pub bg_subagent_map: HashMap<String, String>,
171 /// Per-subagent cancellation tokens for individual kill support.
172 pub subagent_cancel_tokens: HashMap<String, tokio_util::sync::CancellationToken>,
173 /// Text selection state for mouse-based copy.
174 pub selection: SelectionState,
175 /// Force a full terminal clear before next draw (resets ratatui's diff buffer).
176 pub force_clear: bool,
177 /// Timestamp of last user-interactive event (key, mouse, scroll).
178 /// Used to detect tab-switch return via timing gap.
179 pub last_event_time: Option<Instant>,
180}
181
182impl Default for AppState {
183 fn default() -> Self {
184 Self {
185 running: true,
186 mode: OperationMode::Normal,
187 autonomy: AutonomyLevel::SemiAuto,
188 reasoning_level: ReasoningLevel::Medium,
189 model: String::from("claude-sonnet-4"),
190 working_dir: String::from("."),
191 path_shortener: crate::formatters::PathShortener::new(Some(".")),
192 git_branch: None,
193 tokens_used: 0,
194 tokens_limit: 200_000,
195 context_usage_pct: 0.0,
196 session_cost: 0.0,
197 mcp_status: None,
198 mcp_has_errors: false,
199 agent_active: false,
200 messages: Vec::new(),
201 task_progress: None,
202 spinner: crate::widgets::spinner::SpinnerState::new(),
203 input_buffer: String::new(),
204 input_cursor: 0,
205 active_tools: Vec::new(),
206 scroll_offset: 0,
207 user_scrolled: false,
208 autocomplete: crate::autocomplete::AutocompleteEngine::new(
209 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
210 ),
211 background_task_count: 0,
212 backgrounded_task_info: None,
213 active_subagents: Vec::new(),
214 todo_manager: None,
215 todo_items: Vec::new(),
216 todo_expanded: true,
217 thinking_expanded: false,
218 todo_spinner_tick: 0,
219 plan_name: None,
220 file_changes: None,
221 version: env!("CARGO_PKG_VERSION").to_string(),
222 welcome_panel: WelcomePanelState::new(),
223 terminal_width: 80,
224 terminal_height: 24,
225 pending_queue: VecDeque::new(),
226 dirty: true,
227 message_generation: 0,
228 cached_lines: Vec::new(),
229 lines_generation: u64::MAX, // Force initial build
230 per_message_hashes: Vec::new(),
231 per_message_line_counts: Vec::new(),
232 markdown_cache: HashMap::new(),
233 cached_width: 80,
234 per_message_culled: Vec::new(),
235 cached_scroll_offset: 0,
236 scroll_last_direction: None,
237 scroll_last_time: None,
238 scroll_accel_level: 0,
239 theme: crate::formatters::style_tokens::Theme::dark(),
240 theme_name: crate::formatters::style_tokens::ThemeName::Dark,
241 command_history: CommandHistory::new(),
242 compact_requested: false,
243 compaction_active: false,
244 pending_plan_request: false,
245 plan_content_display: None,
246 backgrounding_pending: false,
247 bg_agent_manager: crate::managers::BackgroundAgentManager::new(),
248 task_watcher_open: false,
249 task_watcher_focus: 0,
250 task_watcher_cell_scrolls: Vec::new(),
251 task_watcher_page: 0,
252 task_watcher_all_done_at: None,
253 last_task_completion: None,
254 toasts: Vec::new(),
255 leader_pending: false,
256 leader_timestamp: None,
257 undo_stack: Vec::new(),
258 redo_stack: Vec::new(),
259 debug_panel_open: false,
260 session_title: None,
261 bg_subagent_map: HashMap::new(),
262 subagent_cancel_tokens: HashMap::new(),
263 selection: SelectionState::default(),
264 force_clear: false,
265 last_event_time: None,
266 }
267 }
268}
269
270#[cfg(test)]
271#[path = "state_tests.rs"]
272mod tests;