1use std::collections::{HashMap, VecDeque};
4use std::sync::{Arc, RwLock};
5use std::time::Duration;
6
7use crate::config::{ConfigLayer, DEFAULT_SESSION_CACHE_MAX_RESULTS};
8use crate::core::command_policy::CommandPolicyRegistry;
9use crate::core::row::Row;
10use crate::native::NativeCommandRegistry;
11use crate::repl::HistoryShellContext;
12
13use super::command_output::CliCommandResult;
14use super::runtime::{
15 AppClients, AppRuntime, AuthState, ConfigState, LaunchContext, RuntimeContext, UiState,
16};
17use super::timing::TimingSummary;
18
19#[derive(Debug, Clone, Copy, Default)]
20pub struct DebugTimingBadge {
21 pub level: u8,
22 pub(crate) summary: TimingSummary,
23}
24
25#[derive(Clone, Default, Debug)]
26pub struct DebugTimingState {
27 inner: Arc<RwLock<Option<DebugTimingBadge>>>,
28}
29
30impl DebugTimingState {
31 pub fn set(&self, badge: DebugTimingBadge) {
32 if let Ok(mut guard) = self.inner.write() {
33 *guard = Some(badge);
34 }
35 }
36
37 pub fn clear(&self) {
38 if let Ok(mut guard) = self.inner.write() {
39 *guard = None;
40 }
41 }
42
43 pub fn badge(&self) -> Option<DebugTimingBadge> {
44 self.inner.read().map(|value| *value).unwrap_or(None)
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ReplScopeFrame {
50 command: String,
51}
52
53impl ReplScopeFrame {
54 pub fn new(command: impl Into<String>) -> Self {
55 Self {
56 command: command.into(),
57 }
58 }
59
60 pub fn command(&self) -> &str {
61 self.command.as_str()
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
66pub struct ReplScopeStack {
67 frames: Vec<ReplScopeFrame>,
68}
69
70impl ReplScopeStack {
71 pub fn is_root(&self) -> bool {
72 self.frames.is_empty()
73 }
74
75 pub fn enter(&mut self, command: impl Into<String>) {
76 self.frames.push(ReplScopeFrame::new(command));
77 }
78
79 pub fn leave(&mut self) -> Option<ReplScopeFrame> {
80 self.frames.pop()
81 }
82
83 pub fn commands(&self) -> Vec<String> {
84 self.frames
85 .iter()
86 .map(|frame| frame.command.clone())
87 .collect()
88 }
89
90 pub fn contains_command(&self, command: &str) -> bool {
91 self.frames
92 .iter()
93 .any(|frame| frame.command.eq_ignore_ascii_case(command))
94 }
95
96 pub fn display_label(&self) -> Option<String> {
97 if self.is_root() {
98 None
99 } else {
100 Some(
101 self.frames
102 .iter()
103 .map(|frame| frame.command.as_str())
104 .collect::<Vec<_>>()
105 .join(" / "),
106 )
107 }
108 }
109
110 pub fn history_prefix(&self) -> String {
111 if self.is_root() {
112 String::new()
113 } else {
114 format!(
115 "{} ",
116 self.frames
117 .iter()
118 .map(|frame| frame.command.as_str())
119 .collect::<Vec<_>>()
120 .join(" ")
121 )
122 }
123 }
124
125 pub fn prefixed_tokens(&self, tokens: &[String]) -> Vec<String> {
126 let prefix = self.commands();
127 if prefix.is_empty() || tokens.starts_with(&prefix) {
128 return tokens.to_vec();
129 }
130 let mut full = prefix;
131 full.extend_from_slice(tokens);
132 full
133 }
134
135 pub fn help_tokens(&self) -> Vec<String> {
136 let mut tokens = self.commands();
137 if !tokens.is_empty() {
138 tokens.push("--help".to_string());
139 }
140 tokens
141 }
142}
143
144pub struct AppSession {
145 pub prompt_prefix: String,
146 pub history_enabled: bool,
147 pub history_shell: HistoryShellContext,
148 pub prompt_timing: DebugTimingState,
149 pub scope: ReplScopeStack,
150 pub last_rows: Vec<Row>,
151 pub last_failure: Option<LastFailure>,
152 pub result_cache: HashMap<String, Vec<Row>>,
153 pub cache_order: VecDeque<String>,
154 pub(crate) command_cache: HashMap<String, CliCommandResult>,
155 pub(crate) command_cache_order: VecDeque<String>,
156 pub max_cached_results: usize,
157 pub config_overrides: ConfigLayer,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct LastFailure {
162 pub command_line: String,
163 pub summary: String,
164 pub detail: String,
165}
166
167impl AppSession {
168 pub fn with_cache_limit(max_cached_results: usize) -> Self {
169 let bounded = max_cached_results.max(1);
170 Self {
171 prompt_prefix: "osp".to_string(),
172 history_enabled: true,
173 history_shell: HistoryShellContext::default(),
174 prompt_timing: DebugTimingState::default(),
175 scope: ReplScopeStack::default(),
176 last_rows: Vec::new(),
177 last_failure: None,
178 result_cache: HashMap::new(),
179 cache_order: VecDeque::new(),
180 command_cache: HashMap::new(),
181 command_cache_order: VecDeque::new(),
182 max_cached_results: bounded,
183 config_overrides: ConfigLayer::default(),
184 }
185 }
186
187 pub fn record_result(&mut self, command_line: &str, rows: Vec<Row>) {
188 let key = command_line.trim().to_string();
189 if key.is_empty() {
190 return;
191 }
192
193 self.last_rows = rows.clone();
194 if !self.result_cache.contains_key(&key)
195 && self.result_cache.len() >= self.max_cached_results
196 && let Some(evict_key) = self.cache_order.pop_front()
197 {
198 self.result_cache.remove(&evict_key);
199 }
200
201 self.cache_order.retain(|item| item != &key);
202 self.cache_order.push_back(key.clone());
203 self.result_cache.insert(key, rows);
204 }
205
206 pub fn record_failure(
207 &mut self,
208 command_line: &str,
209 summary: impl Into<String>,
210 detail: impl Into<String>,
211 ) {
212 let command_line = command_line.trim().to_string();
213 if command_line.is_empty() {
214 return;
215 }
216 self.last_failure = Some(LastFailure {
217 command_line,
218 summary: summary.into(),
219 detail: detail.into(),
220 });
221 }
222
223 pub fn cached_rows(&self, command_line: &str) -> Option<&[Row]> {
224 self.result_cache
225 .get(command_line.trim())
226 .map(|rows| rows.as_slice())
227 }
228
229 pub(crate) fn record_cached_command(&mut self, cache_key: &str, result: &CliCommandResult) {
230 let cache_key = cache_key.trim().to_string();
231 if cache_key.is_empty() {
232 return;
233 }
234
235 if !self.command_cache.contains_key(&cache_key)
236 && self.command_cache.len() >= self.max_cached_results
237 && let Some(evict_key) = self.command_cache_order.pop_front()
238 {
239 self.command_cache.remove(&evict_key);
240 }
241
242 self.command_cache_order.retain(|item| item != &cache_key);
243 self.command_cache_order.push_back(cache_key.clone());
244 self.command_cache.insert(cache_key, result.clone());
245 }
246
247 pub(crate) fn cached_command(&self, cache_key: &str) -> Option<CliCommandResult> {
248 self.command_cache.get(cache_key.trim()).cloned()
249 }
250
251 pub fn record_prompt_timing(
252 &self,
253 level: u8,
254 total: Duration,
255 parse: Option<Duration>,
256 execute: Option<Duration>,
257 render: Option<Duration>,
258 ) {
259 if level == 0 {
260 self.prompt_timing.clear();
261 return;
262 }
263
264 self.prompt_timing.set(DebugTimingBadge {
265 level,
266 summary: TimingSummary {
267 total,
268 parse,
269 execute,
270 render,
271 },
272 });
273 }
274
275 pub fn sync_history_shell_context(&self) {
276 self.history_shell.set_prefix(self.scope.history_prefix());
277 }
278}
279
280pub(crate) struct AppStateInit {
281 pub context: RuntimeContext,
282 pub config: crate::config::ResolvedConfig,
283 pub render_settings: crate::ui::RenderSettings,
284 pub message_verbosity: crate::ui::messages::MessageLevel,
285 pub debug_verbosity: u8,
286 pub plugins: crate::plugin::PluginManager,
287 pub native_commands: NativeCommandRegistry,
288 pub themes: crate::ui::theme_loader::ThemeCatalog,
289 pub launch: LaunchContext,
290}
291
292pub struct AppState {
293 pub runtime: AppRuntime,
294 pub session: AppSession,
295 pub clients: AppClients,
296}
297
298impl AppState {
299 pub(crate) fn new(init: AppStateInit) -> Self {
300 let config_state = ConfigState::new(init.config);
301 let mut auth_state = AuthState::from_resolved(config_state.resolved());
302 let plugin_policy = init
303 .plugins
304 .command_policy_registry()
305 .unwrap_or_else(|err| {
306 tracing::warn!(error = %err, "failed to build plugin command policy registry");
307 CommandPolicyRegistry::default()
308 });
309 let external_policy = merge_policy_registries(
310 plugin_policy,
311 init.native_commands.command_policy_registry(),
312 );
313 auth_state.replace_external_policy(external_policy);
314 let session_cache_max_results = crate::app::host::config_usize(
315 config_state.resolved(),
316 "session.cache.max_results",
317 DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
318 );
319
320 Self {
321 runtime: AppRuntime {
322 context: init.context,
323 config: config_state,
324 ui: UiState {
325 render_settings: init.render_settings,
326 message_verbosity: init.message_verbosity,
327 debug_verbosity: init.debug_verbosity,
328 },
329 auth: auth_state,
330 themes: init.themes,
331 launch: init.launch,
332 },
333 session: AppSession::with_cache_limit(session_cache_max_results),
334 clients: AppClients::new(init.plugins, init.native_commands),
335 }
336 }
337
338 pub fn prompt_prefix(&self) -> String {
339 self.session.prompt_prefix.clone()
340 }
341
342 pub fn sync_history_shell_context(&self) {
343 self.session.sync_history_shell_context();
344 }
345
346 pub fn record_repl_rows(&mut self, command_line: &str, rows: Vec<Row>) {
347 self.session.record_result(command_line, rows);
348 }
349
350 pub fn record_repl_failure(
351 &mut self,
352 command_line: &str,
353 summary: impl Into<String>,
354 detail: impl Into<String>,
355 ) {
356 self.session.record_failure(command_line, summary, detail);
357 }
358
359 pub fn last_repl_rows(&self) -> Vec<Row> {
360 self.session.last_rows.clone()
361 }
362
363 pub fn last_repl_failure(&self) -> Option<LastFailure> {
364 self.session.last_failure.clone()
365 }
366
367 pub fn cached_repl_rows(&self, command_line: &str) -> Option<Vec<Row>> {
368 self.session
369 .cached_rows(command_line)
370 .map(ToOwned::to_owned)
371 }
372
373 pub fn repl_cache_size(&self) -> usize {
374 self.session.result_cache.len()
375 }
376}
377
378fn merge_policy_registries(
379 mut left: CommandPolicyRegistry,
380 right: CommandPolicyRegistry,
381) -> CommandPolicyRegistry {
382 for policy in right.entries() {
383 left.register(policy.clone());
384 }
385 left
386}