Skip to main content

osp_cli/app/
session.rs

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