Skip to main content

ralph_workflow/rendering/
ui_event.rs

1//! UI event rendering dispatch.
2//!
3//! This is the single entrypoint for all UI event rendering.
4//! The event loop calls `render_ui_event()` and displays the result.
5
6use crate::reducer::ui_event::UIEvent;
7
8/// Render a `UIEvent` to a displayable string.
9///
10/// This is the single entrypoint for all UI event rendering.
11/// The event loop calls this function and displays the result.
12#[must_use]
13pub fn render_ui_event(event: &UIEvent) -> String {
14    match event {
15        UIEvent::PhaseTransition { to, .. } => {
16            format!("{} {}", UIEvent::phase_emoji(to), to)
17        }
18        UIEvent::IterationProgress { current, total } => {
19            format!("🔄 Development iteration {current}/{total}")
20        }
21        UIEvent::ReviewProgress { pass, total } => {
22            format!("👁 Review pass {pass}/{total}")
23        }
24        UIEvent::AgentActivity { agent, message } => {
25            format!("🤖 [{agent}] {message}")
26        }
27        UIEvent::PushCompleted {
28            remote,
29            branch,
30            commit_sha,
31        } => {
32            let short = &commit_sha[..7.min(commit_sha.len())];
33            format!("⬆️  Pushed {short} to {remote}/{branch}")
34        }
35        UIEvent::PushFailed {
36            remote,
37            branch,
38            error,
39        } => {
40            format!("⚠️  Push failed for {remote}/{branch}: {error}")
41        }
42        UIEvent::PullRequestCreated { url, number } => {
43            if *number > 0 {
44                format!("🔀 PR created #{number}: {url}")
45            } else {
46                format!("🔀 PR created: {url}")
47            }
48        }
49        UIEvent::PullRequestFailed { error } => {
50            format!("⚠️  PR creation failed: {error}")
51        }
52        UIEvent::XmlOutput {
53            xml_type,
54            content,
55            context,
56        } => super::xml::render_xml(xml_type, content, context),
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::reducer::event::PipelinePhase;
64    use crate::reducer::ui_event::{XmlOutputContext, XmlOutputType};
65
66    #[test]
67    fn test_render_phase_transition() {
68        let event = UIEvent::PhaseTransition {
69            from: Some(PipelinePhase::Planning),
70            to: PipelinePhase::Development,
71        };
72        let output = render_ui_event(&event);
73        assert!(output.contains("🔨"));
74        assert!(output.contains("Development"));
75    }
76
77    #[test]
78    fn test_render_iteration_progress() {
79        let event = UIEvent::IterationProgress {
80            current: 2,
81            total: 5,
82        };
83        let output = render_ui_event(&event);
84        assert!(output.contains("2/5"));
85        assert!(output.contains("🔄"));
86    }
87
88    #[test]
89    fn test_render_review_progress() {
90        let event = UIEvent::ReviewProgress { pass: 1, total: 3 };
91        let output = render_ui_event(&event);
92        assert!(output.contains("1/3"));
93        assert!(output.contains("👁"));
94    }
95
96    #[test]
97    fn test_render_agent_activity() {
98        let event = UIEvent::AgentActivity {
99            agent: "claude".to_string(),
100            message: "Processing request".to_string(),
101        };
102        let output = render_ui_event(&event);
103        assert!(output.contains("[claude]"));
104        assert!(output.contains("Processing request"));
105    }
106
107    #[test]
108    fn test_render_xml_output_routes_to_xml_module() {
109        let event = UIEvent::XmlOutput {
110            xml_type: XmlOutputType::DevelopmentResult,
111            content: r"<ralph-development-result>
112<ralph-status>completed</ralph-status>
113<ralph-summary>Done</ralph-summary>
114</ralph-development-result>"
115                .to_string(),
116            context: Some(XmlOutputContext::default()),
117        };
118        let output = render_ui_event(&event);
119        // Should be semantically rendered, not raw XML
120        assert!(output.contains("✅") || output.contains("Completed"));
121        assert!(output.contains("Done"));
122    }
123
124    #[test]
125    fn test_phase_emoji_via_ui_event() {
126        // Verify all phases have non-empty emojis via UIEvent::phase_emoji
127        let phases = [
128            PipelinePhase::Planning,
129            PipelinePhase::Development,
130            PipelinePhase::Review,
131            PipelinePhase::CommitMessage,
132            PipelinePhase::FinalValidation,
133            PipelinePhase::Finalizing,
134            PipelinePhase::Complete,
135            PipelinePhase::Interrupted,
136        ];
137        for phase in phases {
138            let emoji = UIEvent::phase_emoji(&phase);
139            assert!(!emoji.is_empty(), "Phase {phase:?} should have an emoji");
140        }
141    }
142}