1use std::collections::{HashMap, VecDeque};
29use std::sync::{Arc, RwLock};
30use std::time::Duration;
31
32use crate::config::{ConfigLayer, DEFAULT_SESSION_CACHE_MAX_RESULTS};
33use crate::core::row::Row;
34use crate::native::NativeCommandRegistry;
35use crate::plugin::PluginManager;
36use crate::repl::HistoryShellContext;
37
38use super::command_output::CliCommandResult;
39use super::runtime::{AppClients, AppRuntime, LaunchContext, RuntimeContext, UiState};
40use super::timing::TimingSummary;
41
42#[derive(Debug, Clone, Copy, Default)]
43pub struct DebugTimingBadge {
45 pub level: u8,
47 pub(crate) summary: TimingSummary,
48}
49
50#[derive(Clone, Default, Debug)]
53pub struct DebugTimingState {
54 inner: Arc<RwLock<Option<DebugTimingBadge>>>,
55}
56
57impl DebugTimingState {
58 pub fn set(&self, badge: DebugTimingBadge) {
60 if let Ok(mut guard) = self.inner.write() {
61 *guard = Some(badge);
62 }
63 }
64
65 pub fn clear(&self) {
67 if let Ok(mut guard) = self.inner.write() {
68 *guard = None;
69 }
70 }
71
72 pub fn badge(&self) -> Option<DebugTimingBadge> {
74 self.inner.read().map(|value| *value).unwrap_or(None)
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct ReplScopeFrame {
81 command: String,
82}
83
84impl ReplScopeFrame {
85 pub fn new(command: impl Into<String>) -> Self {
96 Self {
97 command: command.into(),
98 }
99 }
100
101 pub fn command(&self) -> &str {
103 self.command.as_str()
104 }
105}
106
107#[derive(Debug, Clone, Default, PartialEq, Eq)]
112pub struct ReplScopeStack {
113 frames: Vec<ReplScopeFrame>,
114}
115
116impl ReplScopeStack {
117 pub fn is_root(&self) -> bool {
119 self.frames.is_empty()
120 }
121
122 pub fn enter(&mut self, command: impl Into<String>) {
124 self.frames.push(ReplScopeFrame::new(command));
125 }
126
127 pub fn leave(&mut self) -> Option<ReplScopeFrame> {
129 self.frames.pop()
130 }
131
132 pub fn commands(&self) -> Vec<String> {
134 self.frames
135 .iter()
136 .map(|frame| frame.command.clone())
137 .collect()
138 }
139
140 pub fn contains_command(&self, command: &str) -> bool {
142 self.frames
143 .iter()
144 .any(|frame| frame.command.eq_ignore_ascii_case(command))
145 }
146
147 pub fn display_label(&self) -> Option<String> {
162 if self.is_root() {
163 None
164 } else {
165 Some(
166 self.frames
167 .iter()
168 .map(|frame| frame.command.as_str())
169 .collect::<Vec<_>>()
170 .join(" / "),
171 )
172 }
173 }
174
175 pub fn history_prefix(&self) -> String {
189 if self.is_root() {
190 String::new()
191 } else {
192 format!(
193 "{} ",
194 self.frames
195 .iter()
196 .map(|frame| frame.command.as_str())
197 .collect::<Vec<_>>()
198 .join(" ")
199 )
200 }
201 }
202
203 pub fn prefixed_tokens(&self, tokens: &[String]) -> Vec<String> {
219 let prefix = self.commands();
220 if prefix.is_empty() || tokens.starts_with(&prefix) {
221 return tokens.to_vec();
222 }
223 let mut full = prefix;
224 full.extend_from_slice(tokens);
225 full
226 }
227
228 pub fn help_tokens(&self) -> Vec<String> {
241 let mut tokens = self.commands();
242 if !tokens.is_empty() {
243 tokens.push("--help".to_string());
244 }
245 tokens
246 }
247}
248
249#[non_exhaustive]
251#[must_use]
252pub struct AppSession {
253 pub prompt_prefix: String,
255 pub history_enabled: bool,
257 pub history_shell: HistoryShellContext,
259 pub prompt_timing: DebugTimingState,
261 pub(crate) startup_prompt_timing_pending: bool,
262 pub scope: ReplScopeStack,
264 pub last_rows: Vec<Row>,
266 pub last_failure: Option<LastFailure>,
268 pub result_cache: HashMap<String, Vec<Row>>,
270 pub cache_order: VecDeque<String>,
272 pub(crate) command_cache: HashMap<String, CliCommandResult>,
273 pub(crate) command_cache_order: VecDeque<String>,
274 pub max_cached_results: usize,
276 pub config_overrides: ConfigLayer,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct LastFailure {
283 pub command_line: String,
285 pub summary: String,
287 pub detail: String,
289}
290
291impl AppSession {
292 pub fn builder() -> AppSessionBuilder {
311 AppSessionBuilder::new()
312 }
313
314 pub fn with_cache_limit(max_cached_results: usize) -> Self {
330 let bounded = max_cached_results.max(1);
331 Self {
332 prompt_prefix: "osp".to_string(),
333 history_enabled: true,
334 history_shell: HistoryShellContext::default(),
335 prompt_timing: DebugTimingState::default(),
336 startup_prompt_timing_pending: true,
337 scope: ReplScopeStack::default(),
338 last_rows: Vec::new(),
339 last_failure: None,
340 result_cache: HashMap::new(),
341 cache_order: VecDeque::new(),
342 command_cache: HashMap::new(),
343 command_cache_order: VecDeque::new(),
344 max_cached_results: bounded,
345 config_overrides: ConfigLayer::default(),
346 }
347 }
348
349 pub(crate) fn from_resolved_config(config: &crate::config::ResolvedConfig) -> Self {
351 let session_cache_max_results = crate::app::host::config_usize(
352 config,
353 "session.cache.max_results",
354 DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
355 );
356 Self::with_cache_limit(session_cache_max_results)
357 }
358
359 pub(crate) fn from_resolved_config_with_overrides(
362 config: &crate::config::ResolvedConfig,
363 config_overrides: ConfigLayer,
364 ) -> Self {
365 Self::with_cache_limit(crate::app::host::config_usize(
366 config,
367 "session.cache.max_results",
368 DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
369 ))
370 .with_config_overrides(config_overrides)
371 }
372
373 pub fn with_prompt_prefix(mut self, prompt_prefix: impl Into<String>) -> Self {
375 self.prompt_prefix = prompt_prefix.into();
376 self
377 }
378
379 pub fn with_history_enabled(mut self, history_enabled: bool) -> Self {
381 self.history_enabled = history_enabled;
382 self
383 }
384
385 pub fn with_history_shell(mut self, history_shell: HistoryShellContext) -> Self {
387 self.history_shell = history_shell;
388 self
389 }
390
391 pub fn with_config_overrides(mut self, config_overrides: ConfigLayer) -> Self {
393 self.config_overrides = config_overrides;
394 self
395 }
396
397 pub fn record_result(&mut self, command_line: &str, rows: Vec<Row>) {
399 let key = command_line.trim().to_string();
400 if key.is_empty() {
401 return;
402 }
403
404 self.last_rows = rows.clone();
405 if !self.result_cache.contains_key(&key)
406 && self.result_cache.len() >= self.max_cached_results
407 && let Some(evict_key) = self.cache_order.pop_front()
408 {
409 self.result_cache.remove(&evict_key);
410 }
411
412 self.cache_order.retain(|item| item != &key);
413 self.cache_order.push_back(key.clone());
414 self.result_cache.insert(key, rows);
415 }
416
417 pub fn record_failure(
419 &mut self,
420 command_line: &str,
421 summary: impl Into<String>,
422 detail: impl Into<String>,
423 ) {
424 let command_line = command_line.trim().to_string();
425 if command_line.is_empty() {
426 return;
427 }
428 self.last_failure = Some(LastFailure {
429 command_line,
430 summary: summary.into(),
431 detail: detail.into(),
432 });
433 }
434
435 pub fn cached_rows(&self, command_line: &str) -> Option<&[Row]> {
437 self.result_cache
438 .get(command_line.trim())
439 .map(|rows| rows.as_slice())
440 }
441
442 pub(crate) fn record_cached_command(&mut self, cache_key: &str, result: &CliCommandResult) {
443 let cache_key = cache_key.trim().to_string();
444 if cache_key.is_empty() {
445 return;
446 }
447
448 if !self.command_cache.contains_key(&cache_key)
449 && self.command_cache.len() >= self.max_cached_results
450 && let Some(evict_key) = self.command_cache_order.pop_front()
451 {
452 self.command_cache.remove(&evict_key);
453 }
454
455 self.command_cache_order.retain(|item| item != &cache_key);
456 self.command_cache_order.push_back(cache_key.clone());
457 self.command_cache.insert(cache_key, result.clone());
458 }
459
460 pub(crate) fn cached_command(&self, cache_key: &str) -> Option<CliCommandResult> {
461 self.command_cache.get(cache_key.trim()).cloned()
462 }
463
464 pub fn record_prompt_timing(
466 &self,
467 level: u8,
468 total: Duration,
469 parse: Option<Duration>,
470 execute: Option<Duration>,
471 render: Option<Duration>,
472 ) {
473 if level == 0 {
474 self.prompt_timing.clear();
475 return;
476 }
477
478 self.prompt_timing.set(DebugTimingBadge {
479 level,
480 summary: TimingSummary {
481 total,
482 parse,
483 execute,
484 render,
485 },
486 });
487 }
488
489 pub fn seed_startup_prompt_timing(&mut self, level: u8, total: Duration) {
491 if !self.startup_prompt_timing_pending {
492 return;
493 }
494 self.startup_prompt_timing_pending = false;
495 if level == 0 {
496 return;
497 }
498
499 self.prompt_timing.set(DebugTimingBadge {
500 level,
501 summary: TimingSummary {
502 total,
503 parse: None,
504 execute: None,
505 render: None,
506 },
507 });
508 }
509
510 pub fn sync_history_shell_context(&self) {
512 self.history_shell.set_prefix(self.scope.history_prefix());
513 }
514}
515
516impl Default for AppSession {
517 fn default() -> Self {
518 Self::with_cache_limit(DEFAULT_SESSION_CACHE_MAX_RESULTS as usize)
519 }
520}
521
522#[must_use]
527pub struct AppSessionBuilder {
528 prompt_prefix: String,
529 history_enabled: bool,
530 history_shell: HistoryShellContext,
531 max_cached_results: usize,
532 config_overrides: ConfigLayer,
533}
534
535impl Default for AppSessionBuilder {
536 fn default() -> Self {
537 Self::new()
538 }
539}
540
541impl AppSessionBuilder {
542 pub fn new() -> Self {
544 Self {
545 prompt_prefix: "osp".to_string(),
546 history_enabled: true,
547 history_shell: HistoryShellContext::default(),
548 max_cached_results: DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
549 config_overrides: ConfigLayer::default(),
550 }
551 }
552
553 pub fn with_prompt_prefix(mut self, prompt_prefix: impl Into<String>) -> Self {
555 self.prompt_prefix = prompt_prefix.into();
556 self
557 }
558
559 pub fn with_history_enabled(mut self, history_enabled: bool) -> Self {
561 self.history_enabled = history_enabled;
562 self
563 }
564
565 pub fn with_history_shell(mut self, history_shell: HistoryShellContext) -> Self {
567 self.history_shell = history_shell;
568 self
569 }
570
571 pub fn with_cache_limit(mut self, max_cached_results: usize) -> Self {
573 self.max_cached_results = max_cached_results;
574 self
575 }
576
577 pub fn with_config_overrides(mut self, config_overrides: ConfigLayer) -> Self {
579 self.config_overrides = config_overrides;
580 self
581 }
582
583 pub fn build(self) -> AppSession {
585 AppSession::with_cache_limit(self.max_cached_results)
586 .with_prompt_prefix(self.prompt_prefix)
587 .with_history_enabled(self.history_enabled)
588 .with_history_shell(self.history_shell)
589 .with_config_overrides(self.config_overrides)
590 }
591}
592
593pub(crate) struct AppStateInit {
594 pub context: RuntimeContext,
595 pub config: crate::config::ResolvedConfig,
596 pub render_settings: crate::ui::RenderSettings,
597 pub message_verbosity: crate::ui::messages::MessageLevel,
598 pub debug_verbosity: u8,
599 pub plugins: crate::plugin::PluginManager,
600 pub native_commands: NativeCommandRegistry,
601 pub themes: crate::ui::theme_loader::ThemeCatalog,
602 pub launch: LaunchContext,
603}
604
605pub(crate) struct AppStateParts {
606 pub runtime: AppRuntime,
607 pub session: AppSession,
608 pub clients: AppClients,
609}
610
611impl AppStateParts {
612 fn from_init(init: AppStateInit, session_override: Option<AppSession>) -> Self {
613 let clients = AppClients::new(init.plugins, init.native_commands);
614 let config = crate::app::ConfigState::new(init.config);
615 let ui = crate::app::UiState::new(
616 init.render_settings,
617 init.message_verbosity,
618 init.debug_verbosity,
619 );
620 let auth = crate::app::AuthState::from_resolved_with_external_policies(
621 config.resolved(),
622 clients.plugins(),
623 clients.native_commands(),
624 );
625 let runtime = AppRuntime::new(init.context, config, ui, auth, init.themes, init.launch);
626 let session = session_override
627 .unwrap_or_else(|| AppSession::from_resolved_config(runtime.config.resolved()));
628
629 Self {
630 runtime,
631 session,
632 clients,
633 }
634 }
635}
636
637#[non_exhaustive]
639#[must_use]
640pub struct AppState {
641 pub runtime: AppRuntime,
643 pub session: AppSession,
645 pub clients: AppClients,
647}
648
649impl AppState {
650 pub fn from_resolved_config(
678 context: RuntimeContext,
679 config: crate::config::ResolvedConfig,
680 ) -> miette::Result<Self> {
681 AppStateBuilder::from_resolved_config(context, config).map(AppStateBuilder::build)
682 }
683
684 #[cfg(test)]
685 pub(crate) fn new(init: AppStateInit) -> Self {
686 Self::from_parts(AppStateParts::from_init(init, None))
687 }
688
689 pub(crate) fn from_parts(parts: AppStateParts) -> Self {
690 Self {
691 runtime: parts.runtime,
692 session: parts.session,
693 clients: parts.clients,
694 }
695 }
696
697 pub(crate) fn replace_parts(&mut self, parts: AppStateParts) {
698 self.runtime = parts.runtime;
699 self.session = parts.session;
700 self.clients = parts.clients;
701 }
702
703 pub fn prompt_prefix(&self) -> String {
705 self.session.prompt_prefix.clone()
706 }
707
708 pub fn sync_history_shell_context(&self) {
710 self.session.sync_history_shell_context();
711 }
712
713 pub fn record_repl_rows(&mut self, command_line: &str, rows: Vec<Row>) {
715 self.session.record_result(command_line, rows);
716 }
717
718 pub fn record_repl_failure(
720 &mut self,
721 command_line: &str,
722 summary: impl Into<String>,
723 detail: impl Into<String>,
724 ) {
725 self.session.record_failure(command_line, summary, detail);
726 }
727
728 pub fn last_repl_rows(&self) -> Vec<Row> {
730 self.session.last_rows.clone()
731 }
732
733 pub fn last_repl_failure(&self) -> Option<LastFailure> {
735 self.session.last_failure.clone()
736 }
737
738 pub fn cached_repl_rows(&self, command_line: &str) -> Option<Vec<Row>> {
740 self.session
741 .cached_rows(command_line)
742 .map(ToOwned::to_owned)
743 }
744
745 pub fn repl_cache_size(&self) -> usize {
747 self.session.result_cache.len()
748 }
749}
750
751#[must_use]
786pub struct AppStateBuilder {
787 context: RuntimeContext,
788 config: crate::config::ResolvedConfig,
789 ui: UiState,
790 launch: LaunchContext,
791 plugins: Option<PluginManager>,
792 native_commands: NativeCommandRegistry,
793 session: Option<AppSession>,
794 themes: Option<crate::ui::theme_loader::ThemeCatalog>,
795}
796
797impl AppStateBuilder {
798 pub fn new(
805 context: RuntimeContext,
806 config: crate::config::ResolvedConfig,
807 ui: UiState,
808 ) -> Self {
809 Self {
810 context,
811 config,
812 ui,
813 launch: LaunchContext::default(),
814 plugins: None,
815 native_commands: NativeCommandRegistry::default(),
816 session: None,
817 themes: None,
818 }
819 }
820
821 pub fn from_resolved_config(
827 context: RuntimeContext,
828 config: crate::config::ResolvedConfig,
829 ) -> miette::Result<Self> {
830 let host_inputs = crate::app::assembly::ResolvedHostInputs::derive(
833 &context,
834 &config,
835 &LaunchContext::default(),
836 crate::app::assembly::RenderSettingsSeed::DefaultAuto,
837 None,
838 None,
839 None,
840 )?;
841 crate::ui::theme_loader::log_theme_issues(&host_inputs.themes.issues);
842 Ok(Self {
843 context,
844 config,
845 ui: host_inputs.ui,
846 launch: LaunchContext::default(),
847 plugins: None,
848 native_commands: NativeCommandRegistry::default(),
849 session: Some(host_inputs.default_session),
850 themes: Some(host_inputs.themes),
851 })
852 }
853
854 pub fn with_launch(mut self, launch: LaunchContext) -> Self {
858 self.launch = launch;
859 self
860 }
861
862 pub fn with_plugins(mut self, plugins: PluginManager) -> Self {
867 self.plugins = Some(plugins);
868 self
869 }
870
871 pub fn with_native_commands(mut self, native_commands: NativeCommandRegistry) -> Self {
876 self.native_commands = native_commands;
877 self
878 }
879
880 pub fn with_session(mut self, session: AppSession) -> Self {
885 self.session = Some(session);
886 self
887 }
888
889 pub(crate) fn with_themes(mut self, themes: crate::ui::theme_loader::ThemeCatalog) -> Self {
891 self.themes = Some(themes);
892 self
893 }
894
895 pub fn build(self) -> AppState {
901 let themes = self.themes.unwrap_or_else(|| {
902 let themes = crate::ui::theme_loader::load_theme_catalog(&self.config);
903 crate::ui::theme_loader::log_theme_issues(&themes.issues);
904 themes
905 });
906 let plugins = self
907 .plugins
908 .unwrap_or_else(|| default_plugin_manager(&self.config, &self.launch));
909
910 let crate::app::UiState {
911 render_settings,
912 message_verbosity,
913 debug_verbosity,
914 ..
915 } = self.ui;
916
917 AppState::from_parts(AppStateParts::from_init(
918 AppStateInit {
919 context: self.context,
920 config: self.config,
921 render_settings,
922 message_verbosity,
923 debug_verbosity,
924 plugins,
925 native_commands: self.native_commands,
926 themes,
927 launch: self.launch,
928 },
929 self.session,
930 ))
931 }
932}
933
934fn default_plugin_manager(
935 config: &crate::config::ResolvedConfig,
936 launch: &LaunchContext,
937) -> PluginManager {
938 crate::app::assembly::build_plugin_manager(config, launch, None)
939}