Skip to main content

systemprompt_cli/presentation/
renderer.rs

1use futures_util::StreamExt;
2use systemprompt_traits::{
3    Phase, ServiceInfo, ServiceState, ServiceType, StartupEvent, StartupEventReceiver,
4};
5
6use super::state::RenderState;
7use indicatif::{ProgressBar, ProgressStyle};
8use std::time::Duration;
9use systemprompt_logging::services::cli::BrandColors;
10
11use super::widgets::{CompletionMessage, ServiceTable, StartupBanner, render_warning};
12
13#[derive(Debug)]
14pub struct StartupRenderer {
15    receiver: StartupEventReceiver,
16    state: RenderState,
17}
18
19impl StartupRenderer {
20    pub fn new(receiver: StartupEventReceiver) -> Self {
21        Self {
22            receiver,
23            state: RenderState::new(),
24        }
25    }
26
27    pub async fn run(mut self) {
28        StartupBanner::render(Some("Starting services..."));
29
30        while let Some(event) = self.receiver.next().await {
31            if self.handle_event(event) {
32                break;
33            }
34        }
35    }
36
37    fn handle_event(&mut self, event: StartupEvent) -> bool {
38        let Some(event) = self.handle_phase_event(event) else {
39            return false;
40        };
41        let Some(event) = self.handle_service_event(event) else {
42            return false;
43        };
44        let Some(event) = self.handle_status_event(event) else {
45            return false;
46        };
47        self.handle_terminal_event(event)
48    }
49
50    fn handle_phase_event(&mut self, event: StartupEvent) -> Option<StartupEvent> {
51        match event {
52            StartupEvent::PhaseStarted { phase } => {
53                self.state.finish_all_spinners();
54                self.state.current_phase = Some(phase);
55                self.state.is_blocking = phase.is_blocking();
56                if matches!(phase, Phase::McpServers | Phase::Agents) {
57                    let spinner = Self::create_phase_spinner(phase.name());
58                    self.state
59                        .spinners
60                        .insert(format!("phase_{}", phase.name()), spinner);
61                }
62            },
63            StartupEvent::PhaseCompleted { phase } => {
64                let phase_key = format!("phase_{}", phase.name());
65                if let Some(spinner) = self.state.spinners.remove(&phase_key) {
66                    spinner.finish_and_clear();
67                    let (running, total) = match phase {
68                        Phase::McpServers => self.state.mcp_count,
69                        Phase::Agents => self.state.agent_count,
70                        _ => (0, 0),
71                    };
72                    systemprompt_logging::CliService::info(&format!(
73                        "  {} {} ({}/{})",
74                        BrandColors::running("✓"),
75                        phase.name(),
76                        running,
77                        total
78                    ));
79                }
80            },
81            StartupEvent::PhaseFailed { phase, error } => {
82                let phase_key = format!("phase_{}", phase.name());
83                if let Some(spinner) = self.state.spinners.remove(&phase_key) {
84                    spinner.finish_and_clear();
85                    systemprompt_logging::CliService::info(&format!(
86                        "  {} {} failed: {}",
87                        BrandColors::stopped("✗"),
88                        phase.name(),
89                        error
90                    ));
91                } else {
92                    render_warning(&format!("{} failed: {}", phase.name(), error));
93                }
94            },
95            other => return Some(other),
96        }
97        None
98    }
99
100    fn handle_service_event(&mut self, event: StartupEvent) -> Option<StartupEvent> {
101        match event {
102            StartupEvent::McpServerReady {
103                name,
104                port,
105                startup_time,
106                tools: _,
107            } => {
108                self.state.add_service(ServiceInfo {
109                    name,
110                    service_type: ServiceType::Mcp,
111                    port: Some(port),
112                    state: ServiceState::Running,
113                    startup_time: Some(startup_time),
114                });
115            },
116            StartupEvent::McpServerFailed { name, error } => {
117                render_warning(&format!("MCP {} failed: {}", name, error));
118                self.state.add_service(ServiceInfo {
119                    name,
120                    service_type: ServiceType::Mcp,
121                    port: None,
122                    state: ServiceState::Failed,
123                    startup_time: None,
124                });
125            },
126            StartupEvent::McpReconciliationComplete { running, required } => {
127                self.state.mcp_count = (running, required);
128            },
129            StartupEvent::AgentReady {
130                name,
131                port,
132                startup_time,
133            } => {
134                self.state.add_service(ServiceInfo {
135                    name,
136                    service_type: ServiceType::Agent,
137                    port: Some(port),
138                    state: ServiceState::Running,
139                    startup_time: Some(startup_time),
140                });
141            },
142            StartupEvent::AgentFailed { name, error } => {
143                render_warning(&format!("Agent {} failed: {}", name, error));
144                self.state.add_service(ServiceInfo {
145                    name,
146                    service_type: ServiceType::Agent,
147                    port: None,
148                    state: ServiceState::Failed,
149                    startup_time: None,
150                });
151            },
152            StartupEvent::AgentReconciliationComplete { running, total } => {
153                self.state.agent_count = (running, total);
154            },
155            other => return Some(other),
156        }
157        None
158    }
159
160    fn handle_status_event(&mut self, event: StartupEvent) -> Option<StartupEvent> {
161        match event {
162            StartupEvent::PortConflict { port, pid } => {
163                render_warning(&format!("Port {} in use by PID {}", port, pid));
164            },
165            StartupEvent::SchedulerInitializing => {
166                let spinner = Self::create_phase_spinner("Scheduler");
167                self.state.spinners.insert("scheduler".to_owned(), spinner);
168            },
169            StartupEvent::SchedulerReady { job_count } => {
170                if let Some(spinner) = self.state.spinners.remove("scheduler") {
171                    spinner.finish_and_clear();
172                    systemprompt_logging::CliService::info(&format!(
173                        "  {} Scheduler ({} jobs)",
174                        BrandColors::running("✓"),
175                        job_count
176                    ));
177                }
178            },
179            StartupEvent::Warning { message, context } => {
180                self.state.warnings.push(message.clone());
181                match context {
182                    Some(ctx) => render_warning(&format!("{}: {}", message, ctx)),
183                    None => render_warning(&message),
184                }
185            },
186            StartupEvent::Error { message, fatal } => {
187                if fatal {
188                    self.state.finish_all_spinners();
189                }
190                render_warning(&format!("ERROR: {}", message));
191            },
192            other => return Some(other),
193        }
194        None
195    }
196
197    fn handle_terminal_event(&mut self, event: StartupEvent) -> bool {
198        match event {
199            StartupEvent::StartupComplete {
200                duration,
201                api_url,
202                services,
203            } => {
204                self.state.finish_all_spinners();
205                for svc in services {
206                    if !self.state.services.iter().any(|s| s.name == svc.name) {
207                        self.state.services.push(svc);
208                    }
209                }
210                if !self.state.services.is_empty() {
211                    ServiceTable::render("Services", &self.state.services);
212                }
213                CompletionMessage::render_success(duration, &api_url);
214                true
215            },
216            StartupEvent::StartupFailed { error, duration } => {
217                self.state.finish_all_spinners();
218                CompletionMessage::render_failure(duration, &error);
219                true
220            },
221            _ => false,
222        }
223    }
224
225    fn create_phase_spinner(name: &str) -> ProgressBar {
226        let spinner = ProgressBar::new_spinner();
227        spinner.set_style(
228            ProgressStyle::default_spinner()
229                .template("  {spinner:.cyan} {msg}")
230                .unwrap_or_else(|_| ProgressStyle::default_spinner())
231                .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
232        );
233        spinner.set_message(format!("{}...", name));
234        spinner.enable_steady_tick(Duration::from_millis(80));
235        spinner
236    }
237}