1use std::collections::{HashMap, VecDeque};
28use std::sync::{Arc, RwLock};
29use std::time::Duration;
30
31use crate::config::{ConfigLayer, DEFAULT_SESSION_CACHE_MAX_RESULTS};
32use crate::core::row::Row;
33use crate::native::NativeCommandRegistry;
34use crate::plugin::PluginManager;
35use crate::repl::HistoryShellContext;
36
37use super::command_output::CliCommandResult;
38use super::runtime::{AppClients, AppRuntime, LaunchContext, RuntimeContext, UiState};
39use super::timing::TimingSummary;
40
41#[derive(Debug, Clone, Copy, Default)]
42pub struct DebugTimingBadge {
44 pub level: u8,
46 pub(crate) summary: TimingSummary,
47}
48
49#[derive(Clone, Default, Debug)]
52pub struct DebugTimingState {
53 inner: Arc<RwLock<Option<DebugTimingBadge>>>,
54}
55
56impl DebugTimingState {
57 pub fn set(&self, badge: DebugTimingBadge) {
59 if let Ok(mut guard) = self.inner.write() {
60 *guard = Some(badge);
61 }
62 }
63
64 pub fn clear(&self) {
66 if let Ok(mut guard) = self.inner.write() {
67 *guard = None;
68 }
69 }
70
71 pub fn badge(&self) -> Option<DebugTimingBadge> {
73 self.inner.read().map(|value| *value).unwrap_or(None)
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ReplScopeFrame {
80 command: String,
81}
82
83impl ReplScopeFrame {
84 pub fn new(command: impl Into<String>) -> Self {
95 Self {
96 command: command.into(),
97 }
98 }
99
100 pub fn command(&self) -> &str {
102 self.command.as_str()
103 }
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq)]
111pub struct ReplScopeStack {
112 frames: Vec<ReplScopeFrame>,
113}
114
115impl ReplScopeStack {
116 pub fn is_root(&self) -> bool {
118 self.frames.is_empty()
119 }
120
121 pub fn enter(&mut self, command: impl Into<String>) {
123 self.frames.push(ReplScopeFrame::new(command));
124 }
125
126 pub fn leave(&mut self) -> Option<ReplScopeFrame> {
128 self.frames.pop()
129 }
130
131 pub fn commands(&self) -> Vec<String> {
133 self.frames
134 .iter()
135 .map(|frame| frame.command.clone())
136 .collect()
137 }
138
139 pub fn contains_command(&self, command: &str) -> bool {
141 self.frames
142 .iter()
143 .any(|frame| frame.command.eq_ignore_ascii_case(command))
144 }
145
146 pub fn display_label(&self) -> Option<String> {
161 if self.is_root() {
162 None
163 } else {
164 Some(
165 self.frames
166 .iter()
167 .map(|frame| frame.command.as_str())
168 .collect::<Vec<_>>()
169 .join(" / "),
170 )
171 }
172 }
173
174 pub fn history_prefix(&self) -> String {
188 if self.is_root() {
189 String::new()
190 } else {
191 format!(
192 "{} ",
193 self.frames
194 .iter()
195 .map(|frame| frame.command.as_str())
196 .collect::<Vec<_>>()
197 .join(" ")
198 )
199 }
200 }
201
202 pub fn prefixed_tokens(&self, tokens: &[String]) -> Vec<String> {
218 let prefix = self.commands();
219 if prefix.is_empty() || tokens.starts_with(&prefix) {
220 return tokens.to_vec();
221 }
222 let mut full = prefix;
223 full.extend_from_slice(tokens);
224 full
225 }
226
227 pub fn help_tokens(&self) -> Vec<String> {
240 let mut tokens = self.commands();
241 if !tokens.is_empty() {
242 tokens.push("--help".to_string());
243 }
244 tokens
245 }
246}
247
248#[non_exhaustive]
250pub struct AppSession {
251 pub prompt_prefix: String,
253 pub history_enabled: bool,
255 pub history_shell: HistoryShellContext,
257 pub prompt_timing: DebugTimingState,
259 pub(crate) startup_prompt_timing_pending: bool,
260 pub scope: ReplScopeStack,
262 pub last_rows: Vec<Row>,
264 pub last_failure: Option<LastFailure>,
266 pub result_cache: HashMap<String, Vec<Row>>,
268 pub cache_order: VecDeque<String>,
270 pub(crate) command_cache: HashMap<String, CliCommandResult>,
271 pub(crate) command_cache_order: VecDeque<String>,
272 pub max_cached_results: usize,
274 pub config_overrides: ConfigLayer,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct LastFailure {
281 pub command_line: String,
283 pub summary: String,
285 pub detail: String,
287}
288
289impl AppSession {
290 pub fn builder() -> AppSessionBuilder {
292 AppSessionBuilder::new()
293 }
294
295 pub fn with_cache_limit(max_cached_results: usize) -> Self {
297 let bounded = max_cached_results.max(1);
298 Self {
299 prompt_prefix: "osp".to_string(),
300 history_enabled: true,
301 history_shell: HistoryShellContext::default(),
302 prompt_timing: DebugTimingState::default(),
303 startup_prompt_timing_pending: true,
304 scope: ReplScopeStack::default(),
305 last_rows: Vec::new(),
306 last_failure: None,
307 result_cache: HashMap::new(),
308 cache_order: VecDeque::new(),
309 command_cache: HashMap::new(),
310 command_cache_order: VecDeque::new(),
311 max_cached_results: bounded,
312 config_overrides: ConfigLayer::default(),
313 }
314 }
315
316 pub(crate) fn from_resolved_config(config: &crate::config::ResolvedConfig) -> Self {
318 let session_cache_max_results = crate::app::host::config_usize(
319 config,
320 "session.cache.max_results",
321 DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
322 );
323 Self::builder()
324 .with_cache_limit(session_cache_max_results)
325 .build()
326 }
327
328 pub(crate) fn from_resolved_config_with_overrides(
331 config: &crate::config::ResolvedConfig,
332 config_overrides: ConfigLayer,
333 ) -> Self {
334 Self::builder()
335 .with_cache_limit(crate::app::host::config_usize(
336 config,
337 "session.cache.max_results",
338 DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
339 ))
340 .with_config_overrides(config_overrides)
341 .build()
342 }
343
344 pub fn record_result(&mut self, command_line: &str, rows: Vec<Row>) {
346 let key = command_line.trim().to_string();
347 if key.is_empty() {
348 return;
349 }
350
351 self.last_rows = rows.clone();
352 if !self.result_cache.contains_key(&key)
353 && self.result_cache.len() >= self.max_cached_results
354 && let Some(evict_key) = self.cache_order.pop_front()
355 {
356 self.result_cache.remove(&evict_key);
357 }
358
359 self.cache_order.retain(|item| item != &key);
360 self.cache_order.push_back(key.clone());
361 self.result_cache.insert(key, rows);
362 }
363
364 pub fn record_failure(
366 &mut self,
367 command_line: &str,
368 summary: impl Into<String>,
369 detail: impl Into<String>,
370 ) {
371 let command_line = command_line.trim().to_string();
372 if command_line.is_empty() {
373 return;
374 }
375 self.last_failure = Some(LastFailure {
376 command_line,
377 summary: summary.into(),
378 detail: detail.into(),
379 });
380 }
381
382 pub fn cached_rows(&self, command_line: &str) -> Option<&[Row]> {
384 self.result_cache
385 .get(command_line.trim())
386 .map(|rows| rows.as_slice())
387 }
388
389 pub(crate) fn record_cached_command(&mut self, cache_key: &str, result: &CliCommandResult) {
390 let cache_key = cache_key.trim().to_string();
391 if cache_key.is_empty() {
392 return;
393 }
394
395 if !self.command_cache.contains_key(&cache_key)
396 && self.command_cache.len() >= self.max_cached_results
397 && let Some(evict_key) = self.command_cache_order.pop_front()
398 {
399 self.command_cache.remove(&evict_key);
400 }
401
402 self.command_cache_order.retain(|item| item != &cache_key);
403 self.command_cache_order.push_back(cache_key.clone());
404 self.command_cache.insert(cache_key, result.clone());
405 }
406
407 pub(crate) fn cached_command(&self, cache_key: &str) -> Option<CliCommandResult> {
408 self.command_cache.get(cache_key.trim()).cloned()
409 }
410
411 pub fn record_prompt_timing(
413 &self,
414 level: u8,
415 total: Duration,
416 parse: Option<Duration>,
417 execute: Option<Duration>,
418 render: Option<Duration>,
419 ) {
420 if level == 0 {
421 self.prompt_timing.clear();
422 return;
423 }
424
425 self.prompt_timing.set(DebugTimingBadge {
426 level,
427 summary: TimingSummary {
428 total,
429 parse,
430 execute,
431 render,
432 },
433 });
434 }
435
436 pub fn seed_startup_prompt_timing(&mut self, level: u8, total: Duration) {
438 if !self.startup_prompt_timing_pending {
439 return;
440 }
441 self.startup_prompt_timing_pending = false;
442 if level == 0 {
443 return;
444 }
445
446 self.prompt_timing.set(DebugTimingBadge {
447 level,
448 summary: TimingSummary {
449 total,
450 parse: None,
451 execute: None,
452 render: None,
453 },
454 });
455 }
456
457 pub fn sync_history_shell_context(&self) {
459 self.history_shell.set_prefix(self.scope.history_prefix());
460 }
461}
462
463pub struct AppSessionBuilder {
467 prompt_prefix: String,
468 history_enabled: bool,
469 history_shell: HistoryShellContext,
470 max_cached_results: usize,
471 config_overrides: ConfigLayer,
472}
473
474impl Default for AppSessionBuilder {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480impl AppSessionBuilder {
481 pub fn new() -> Self {
483 Self {
484 prompt_prefix: "osp".to_string(),
485 history_enabled: true,
486 history_shell: HistoryShellContext::default(),
487 max_cached_results: DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
488 config_overrides: ConfigLayer::default(),
489 }
490 }
491
492 pub fn with_prompt_prefix(mut self, prompt_prefix: impl Into<String>) -> Self {
494 self.prompt_prefix = prompt_prefix.into();
495 self
496 }
497
498 pub fn with_history_enabled(mut self, history_enabled: bool) -> Self {
500 self.history_enabled = history_enabled;
501 self
502 }
503
504 pub fn with_history_shell(mut self, history_shell: HistoryShellContext) -> Self {
506 self.history_shell = history_shell;
507 self
508 }
509
510 pub fn with_cache_limit(mut self, max_cached_results: usize) -> Self {
512 self.max_cached_results = max_cached_results;
513 self
514 }
515
516 pub fn with_config_overrides(mut self, config_overrides: ConfigLayer) -> Self {
518 self.config_overrides = config_overrides;
519 self
520 }
521
522 pub fn build(self) -> AppSession {
524 let mut session = AppSession::with_cache_limit(self.max_cached_results);
525 session.prompt_prefix = self.prompt_prefix;
526 session.history_enabled = self.history_enabled;
527 session.history_shell = self.history_shell;
528 session.config_overrides = self.config_overrides;
529 session
530 }
531}
532
533pub(crate) struct AppStateInit {
534 pub context: RuntimeContext,
535 pub config: crate::config::ResolvedConfig,
536 pub render_settings: crate::ui::RenderSettings,
537 pub message_verbosity: crate::ui::messages::MessageLevel,
538 pub debug_verbosity: u8,
539 pub plugins: crate::plugin::PluginManager,
540 pub native_commands: NativeCommandRegistry,
541 pub themes: crate::ui::theme_loader::ThemeCatalog,
542 pub launch: LaunchContext,
543}
544
545pub(crate) struct AppStateParts {
546 pub runtime: AppRuntime,
547 pub session: AppSession,
548 pub clients: AppClients,
549}
550
551impl AppStateParts {
552 fn from_init(init: AppStateInit, session_override: Option<AppSession>) -> Self {
553 let clients = AppClients::new(init.plugins, init.native_commands);
554 let config = crate::app::ConfigState::new(init.config);
555 let ui = crate::app::UiState::builder(init.render_settings)
556 .with_message_verbosity(init.message_verbosity)
557 .with_debug_verbosity(init.debug_verbosity)
558 .build();
559 let auth = crate::app::AuthState::from_resolved_with_external_policies(
560 config.resolved(),
561 clients.plugins(),
562 clients.native_commands(),
563 );
564 let runtime = AppRuntime::new(init.context, config, ui, auth, init.themes, init.launch);
565 let session = session_override
566 .unwrap_or_else(|| AppSession::from_resolved_config(runtime.config.resolved()));
567
568 Self {
569 runtime,
570 session,
571 clients,
572 }
573 }
574}
575
576#[non_exhaustive]
578pub struct AppState {
579 pub runtime: AppRuntime,
581 pub session: AppSession,
583 pub clients: AppClients,
585}
586
587impl AppState {
588 pub fn builder(
590 context: RuntimeContext,
591 config: crate::config::ResolvedConfig,
592 ui: UiState,
593 ) -> AppStateBuilder {
594 AppStateBuilder::new(context, config, ui)
595 }
596
597 pub fn from_resolved_config(
625 context: RuntimeContext,
626 config: crate::config::ResolvedConfig,
627 ) -> miette::Result<Self> {
628 AppStateBuilder::from_resolved_config(context, config).map(AppStateBuilder::build)
629 }
630
631 #[cfg(test)]
632 pub(crate) fn new(init: AppStateInit) -> Self {
633 Self::from_parts(AppStateParts::from_init(init, None))
634 }
635
636 pub(crate) fn from_parts(parts: AppStateParts) -> Self {
637 Self {
638 runtime: parts.runtime,
639 session: parts.session,
640 clients: parts.clients,
641 }
642 }
643
644 pub(crate) fn replace_parts(&mut self, parts: AppStateParts) {
645 self.runtime = parts.runtime;
646 self.session = parts.session;
647 self.clients = parts.clients;
648 }
649
650 pub fn prompt_prefix(&self) -> String {
652 self.session.prompt_prefix.clone()
653 }
654
655 pub fn sync_history_shell_context(&self) {
657 self.session.sync_history_shell_context();
658 }
659
660 pub fn record_repl_rows(&mut self, command_line: &str, rows: Vec<Row>) {
662 self.session.record_result(command_line, rows);
663 }
664
665 pub fn record_repl_failure(
667 &mut self,
668 command_line: &str,
669 summary: impl Into<String>,
670 detail: impl Into<String>,
671 ) {
672 self.session.record_failure(command_line, summary, detail);
673 }
674
675 pub fn last_repl_rows(&self) -> Vec<Row> {
677 self.session.last_rows.clone()
678 }
679
680 pub fn last_repl_failure(&self) -> Option<LastFailure> {
682 self.session.last_failure.clone()
683 }
684
685 pub fn cached_repl_rows(&self, command_line: &str) -> Option<Vec<Row>> {
687 self.session
688 .cached_rows(command_line)
689 .map(ToOwned::to_owned)
690 }
691
692 pub fn repl_cache_size(&self) -> usize {
694 self.session.result_cache.len()
695 }
696}
697
698pub struct AppStateBuilder {
703 context: RuntimeContext,
704 config: crate::config::ResolvedConfig,
705 ui: UiState,
706 launch: LaunchContext,
707 plugins: Option<PluginManager>,
708 native_commands: NativeCommandRegistry,
709 session: Option<AppSession>,
710 themes: Option<crate::ui::theme_loader::ThemeCatalog>,
711}
712
713impl AppStateBuilder {
714 pub fn new(
717 context: RuntimeContext,
718 config: crate::config::ResolvedConfig,
719 ui: UiState,
720 ) -> Self {
721 Self {
722 context,
723 config,
724 ui,
725 launch: LaunchContext::default(),
726 plugins: None,
727 native_commands: NativeCommandRegistry::default(),
728 session: None,
729 themes: None,
730 }
731 }
732
733 pub fn from_resolved_config(
736 context: RuntimeContext,
737 config: crate::config::ResolvedConfig,
738 ) -> miette::Result<Self> {
739 let host_inputs = crate::app::assembly::ResolvedHostInputs::derive(
742 &context,
743 &config,
744 &LaunchContext::default(),
745 crate::app::assembly::RenderSettingsSeed::DefaultAuto,
746 None,
747 None,
748 None,
749 )?;
750 crate::ui::theme_loader::log_theme_issues(&host_inputs.themes.issues);
751 Ok(Self {
752 context,
753 config,
754 ui: host_inputs.ui,
755 launch: LaunchContext::default(),
756 plugins: None,
757 native_commands: NativeCommandRegistry::default(),
758 session: Some(host_inputs.default_session),
759 themes: Some(host_inputs.themes),
760 })
761 }
762
763 pub fn with_launch(mut self, launch: LaunchContext) -> Self {
765 self.launch = launch;
766 self
767 }
768
769 pub fn with_plugins(mut self, plugins: PluginManager) -> Self {
771 self.plugins = Some(plugins);
772 self
773 }
774
775 pub fn with_native_commands(mut self, native_commands: NativeCommandRegistry) -> Self {
777 self.native_commands = native_commands;
778 self
779 }
780
781 pub fn with_session(mut self, session: AppSession) -> Self {
783 self.session = Some(session);
784 self
785 }
786
787 pub(crate) fn with_themes(mut self, themes: crate::ui::theme_loader::ThemeCatalog) -> Self {
789 self.themes = Some(themes);
790 self
791 }
792
793 pub fn build(self) -> AppState {
795 let themes = self.themes.unwrap_or_else(|| {
796 let themes = crate::ui::theme_loader::load_theme_catalog(&self.config);
797 crate::ui::theme_loader::log_theme_issues(&themes.issues);
798 themes
799 });
800 let plugins = self
801 .plugins
802 .unwrap_or_else(|| default_plugin_manager(&self.config, &self.launch));
803
804 let crate::app::UiState {
805 render_settings,
806 message_verbosity,
807 debug_verbosity,
808 ..
809 } = self.ui;
810
811 AppState::from_parts(AppStateParts::from_init(
812 AppStateInit {
813 context: self.context,
814 config: self.config,
815 render_settings,
816 message_verbosity,
817 debug_verbosity,
818 plugins,
819 native_commands: self.native_commands,
820 themes,
821 launch: self.launch,
822 },
823 self.session,
824 ))
825 }
826}
827
828fn default_plugin_manager(
829 config: &crate::config::ResolvedConfig,
830 launch: &LaunchContext,
831) -> PluginManager {
832 crate::app::assembly::build_plugin_manager(config, launch, None)
833}