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        UIEvent::PromptReplayHit { key, was_replayed } => {
58            if *was_replayed {
59                format!("📋 [prompt-replay] Replayed stored prompt: {key}")
60            } else {
61                format!("📋 [prompt-replay] Generated fresh prompt: {key}")
62            }
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::reducer::event::PipelinePhase;
71    use crate::reducer::ui_event::{XmlOutputContext, XmlOutputType};
72
73    #[test]
74    fn test_render_phase_transition() {
75        let event = UIEvent::PhaseTransition {
76            from: Some(PipelinePhase::Planning),
77            to: PipelinePhase::Development,
78        };
79        let output = render_ui_event(&event);
80        assert!(output.contains("🔨"));
81        assert!(output.contains("Development"));
82    }
83
84    #[test]
85    fn test_render_iteration_progress() {
86        let event = UIEvent::IterationProgress {
87            current: 2,
88            total: 5,
89        };
90        let output = render_ui_event(&event);
91        assert!(output.contains("2/5"));
92        assert!(output.contains("🔄"));
93    }
94
95    #[test]
96    fn test_render_review_progress() {
97        let event = UIEvent::ReviewProgress { pass: 1, total: 3 };
98        let output = render_ui_event(&event);
99        assert!(output.contains("1/3"));
100        assert!(output.contains("👁"));
101    }
102
103    #[test]
104    fn test_render_agent_activity() {
105        let event = UIEvent::AgentActivity {
106            agent: "claude".to_string(),
107            message: "Processing request".to_string(),
108        };
109        let output = render_ui_event(&event);
110        assert!(output.contains("[claude]"));
111        assert!(output.contains("Processing request"));
112    }
113
114    #[test]
115    fn test_render_xml_output_routes_to_xml_module() {
116        let event = UIEvent::XmlOutput {
117            xml_type: XmlOutputType::DevelopmentResult,
118            content: r"<ralph-development-result>
119<ralph-status>completed</ralph-status>
120<ralph-summary>Done</ralph-summary>
121</ralph-development-result>"
122                .to_string(),
123            context: Some(XmlOutputContext::default()),
124        };
125        let output = render_ui_event(&event);
126        // Should be semantically rendered, not raw XML
127        assert!(output.contains("✅") || output.contains("Completed"));
128        assert!(output.contains("Done"));
129    }
130
131    #[test]
132    fn test_phase_emoji_via_ui_event() {
133        // Verify all phases have non-empty emojis via UIEvent::phase_emoji
134        let phases = [
135            PipelinePhase::Planning,
136            PipelinePhase::Development,
137            PipelinePhase::Review,
138            PipelinePhase::CommitMessage,
139            PipelinePhase::FinalValidation,
140            PipelinePhase::Finalizing,
141            PipelinePhase::Complete,
142            PipelinePhase::Interrupted,
143        ];
144        phases.into_iter().for_each(|phase| {
145            let emoji = UIEvent::phase_emoji(&phase);
146            assert!(!emoji.is_empty(), "Phase {phase:?} should have an emoji");
147        });
148    }
149}