1use super::swarm_view::{AgentMessageEntry, AgentToolCallDetail};
7use ratatui::{
8 Frame,
9 layout::{Constraint, Direction, Layout, Rect},
10 style::{Color, Modifier, Style},
11 text::{Line, Span},
12 widgets::{Block, Borders, Gauge, List, ListItem, ListState, Paragraph, Wrap},
13};
14
15#[derive(Debug, Clone)]
21pub enum RalphEvent {
22 Started {
24 project: String,
25 feature: String,
26 stories: Vec<RalphStoryInfo>,
27 max_iterations: usize,
28 },
29
30 IterationStarted {
32 iteration: usize,
33 max_iterations: usize,
34 },
35
36 StoryStarted { story_id: String },
38
39 StoryToolCall { story_id: String, tool_name: String },
41
42 StoryToolCallDetail {
44 story_id: String,
45 detail: AgentToolCallDetail,
46 },
47
48 StoryMessage {
50 story_id: String,
51 entry: AgentMessageEntry,
52 },
53
54 StoryQualityCheck {
56 story_id: String,
57 check_name: String,
58 passed: bool,
59 },
60
61 StoryComplete { story_id: String, passed: bool },
63
64 StoryOutput { story_id: String, output: String },
66
67 StoryError { story_id: String, error: String },
69
70 StoryMerge {
72 story_id: String,
73 success: bool,
74 summary: String,
75 },
76
77 StageComplete {
79 stage: usize,
80 completed: usize,
81 failed: usize,
82 },
83
84 Complete {
86 status: String,
87 passed: usize,
88 total: usize,
89 },
90
91 Error(String),
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum RalphStoryStatus {
102 Pending,
103 Blocked,
104 Running,
105 QualityCheck,
106 Passed,
107 Failed,
108}
109
110#[derive(Debug, Clone)]
116pub struct RalphStoryInfo {
117 pub id: String,
118 pub title: String,
119 pub status: RalphStoryStatus,
120 pub priority: u8,
121 pub depends_on: Vec<String>,
122 pub quality_checks: Vec<(String, bool)>,
124 pub tool_call_history: Vec<AgentToolCallDetail>,
126 pub messages: Vec<AgentMessageEntry>,
128 pub output: Option<String>,
130 pub error: Option<String>,
132 pub merge_summary: Option<String>,
134 pub steps: usize,
136 pub current_tool: Option<String>,
138}
139
140#[derive(Debug, Default)]
146pub struct RalphViewState {
147 pub active: bool,
149 pub project: String,
151 pub feature: String,
153 pub stories: Vec<RalphStoryInfo>,
155 pub current_iteration: usize,
157 pub max_iterations: usize,
159 pub complete: bool,
161 pub final_status: Option<String>,
163 pub error: Option<String>,
165 pub selected_index: usize,
167 pub detail_mode: bool,
169 pub detail_scroll: usize,
171 pub list_state: ListState,
173}
174
175impl RalphViewState {
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn mark_active(&mut self, project: impl Into<String>, feature: impl Into<String>) {
181 self.handle_event(RalphEvent::Started {
182 project: project.into(),
183 feature: feature.into(),
184 stories: Vec::new(),
185 max_iterations: 0,
186 });
187 }
188
189 pub fn handle_event(&mut self, event: RalphEvent) {
191 match event {
192 RalphEvent::Started {
193 project,
194 feature,
195 stories,
196 max_iterations,
197 } => {
198 self.active = true;
199 self.project = project;
200 self.feature = feature;
201 self.stories = stories;
202 self.max_iterations = max_iterations;
203 self.current_iteration = 0;
204 self.complete = false;
205 self.final_status = None;
206 self.error = None;
207 self.selected_index = 0;
208 self.detail_mode = false;
209 self.detail_scroll = 0;
210 self.list_state = ListState::default();
211 if !self.stories.is_empty() {
212 self.list_state.select(Some(0));
213 }
214 }
215 RalphEvent::IterationStarted {
216 iteration,
217 max_iterations,
218 } => {
219 self.current_iteration = iteration;
220 self.max_iterations = max_iterations;
221 }
222 RalphEvent::StoryStarted { story_id } => {
223 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
224 story.status = RalphStoryStatus::Running;
225 story.steps = 0;
226 story.current_tool = None;
227 story.quality_checks.clear();
228 story.tool_call_history.clear();
229 story.messages.clear();
230 story.output = None;
231 story.error = None;
232 }
233 }
234 RalphEvent::StoryToolCall {
235 story_id,
236 tool_name,
237 } => {
238 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
239 story.current_tool = Some(tool_name);
240 story.steps += 1;
241 }
242 }
243 RalphEvent::StoryToolCallDetail { story_id, detail } => {
244 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
245 story.current_tool = Some(detail.tool_name.clone());
246 story.steps += 1;
247 story.tool_call_history.push(detail);
248 }
249 }
250 RalphEvent::StoryMessage { story_id, entry } => {
251 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
252 story.messages.push(entry);
253 }
254 }
255 RalphEvent::StoryQualityCheck {
256 story_id,
257 check_name,
258 passed,
259 } => {
260 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
261 story.status = RalphStoryStatus::QualityCheck;
262 story.current_tool = None;
263 story.quality_checks.push((check_name, passed));
264 }
265 }
266 RalphEvent::StoryComplete { story_id, passed } => {
267 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
268 story.status = if passed {
269 RalphStoryStatus::Passed
270 } else {
271 RalphStoryStatus::Failed
272 };
273 story.current_tool = None;
274 }
275 }
276 RalphEvent::StoryOutput { story_id, output } => {
277 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
278 story.output = Some(output);
279 }
280 }
281 RalphEvent::StoryError { story_id, error } => {
282 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
283 story.error = Some(error.clone());
284 story.status = RalphStoryStatus::Failed;
285 }
286 }
287 RalphEvent::StoryMerge {
288 story_id,
289 success: _,
290 summary,
291 } => {
292 if let Some(story) = self.stories.iter_mut().find(|s| s.id == story_id) {
293 story.merge_summary = Some(summary);
294 }
295 }
296 RalphEvent::StageComplete { .. } => {
297 }
299 RalphEvent::Complete {
300 status,
301 passed: _,
302 total: _,
303 } => {
304 self.complete = true;
305 self.final_status = Some(status);
306 }
307 RalphEvent::Error(err) => {
308 self.error = Some(err);
309 }
310 }
311 }
312
313 pub fn select_prev(&mut self) {
315 if self.stories.is_empty() {
316 return;
317 }
318 self.selected_index = self.selected_index.saturating_sub(1);
319 self.list_state.select(Some(self.selected_index));
320 }
321
322 pub fn select_next(&mut self) {
324 if self.stories.is_empty() {
325 return;
326 }
327 self.selected_index = (self.selected_index + 1).min(self.stories.len() - 1);
328 self.list_state.select(Some(self.selected_index));
329 }
330
331 pub fn enter_detail(&mut self) {
333 if !self.stories.is_empty() {
334 self.detail_mode = true;
335 self.detail_scroll = 0;
336 }
337 }
338
339 pub fn exit_detail(&mut self) {
341 self.detail_mode = false;
342 self.detail_scroll = 0;
343 }
344
345 pub fn detail_scroll_down(&mut self, amount: usize) {
347 self.detail_scroll = self.detail_scroll.saturating_add(amount);
348 }
349
350 pub fn detail_scroll_up(&mut self, amount: usize) {
352 self.detail_scroll = self.detail_scroll.saturating_sub(amount);
353 }
354
355 pub fn selected_story(&self) -> Option<&RalphStoryInfo> {
357 self.stories.get(self.selected_index)
358 }
359
360 pub fn status_counts(&self) -> (usize, usize, usize, usize) {
362 let mut pending = 0;
363 let mut running = 0;
364 let mut passed = 0;
365 let mut failed = 0;
366 for story in &self.stories {
367 match story.status {
368 RalphStoryStatus::Pending | RalphStoryStatus::Blocked => pending += 1,
369 RalphStoryStatus::Running | RalphStoryStatus::QualityCheck => running += 1,
370 RalphStoryStatus::Passed => passed += 1,
371 RalphStoryStatus::Failed => failed += 1,
372 }
373 }
374 (pending, running, passed, failed)
375 }
376
377 pub fn progress(&self) -> f64 {
379 if self.stories.is_empty() {
380 return 0.0;
381 }
382 let (_, _, passed, failed) = self.status_counts();
383 ((passed + failed) as f64 / self.stories.len() as f64) * 100.0
384 }
385}
386
387pub fn render_ralph_view(f: &mut Frame, state: &mut RalphViewState, area: Rect) {
393 if state.detail_mode {
394 render_story_detail(f, state, area);
395 return;
396 }
397
398 let chunks = Layout::default()
399 .direction(Direction::Vertical)
400 .constraints([
401 Constraint::Length(4), Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ])
406 .split(area);
407
408 render_header(f, state, chunks[0]);
409 render_progress(f, state, chunks[1]);
410 render_story_list(f, state, chunks[2]);
411 render_footer(f, state, chunks[3]);
412}
413
414fn render_header(f: &mut Frame, state: &RalphViewState, area: Rect) {
415 let (pending, running, passed, failed) = state.status_counts();
416 let total = state.stories.len();
417
418 let title = if state.complete {
419 if state.error.is_some() {
420 " Ralph [ERROR] "
421 } else if failed > 0 {
422 " Ralph [PARTIAL] "
423 } else {
424 " Ralph [COMPLETE] "
425 }
426 } else {
427 " Ralph [ACTIVE] "
428 };
429
430 let border_color = if state.complete {
431 if state.error.is_some() || failed > 0 {
432 Color::Red
433 } else {
434 Color::Green
435 }
436 } else {
437 Color::Magenta
438 };
439
440 let lines = vec![
441 Line::from(vec![
442 Span::styled("Project: ", Style::default().fg(Color::DarkGray)),
443 Span::styled(&state.project, Style::default().fg(Color::White)),
444 Span::raw(" "),
445 Span::styled("Feature: ", Style::default().fg(Color::DarkGray)),
446 Span::styled(&state.feature, Style::default().fg(Color::White)),
447 ]),
448 Line::from(vec![
449 Span::styled(
450 format!(
451 "Iteration: {}/{}",
452 state.current_iteration, state.max_iterations
453 ),
454 Style::default().fg(Color::Cyan),
455 ),
456 Span::raw(" "),
457 Span::styled(format!("⏳{}", pending), Style::default().fg(Color::Yellow)),
458 Span::raw(" "),
459 Span::styled(format!("⚡{}", running), Style::default().fg(Color::Cyan)),
460 Span::raw(" "),
461 Span::styled(format!("✓{}", passed), Style::default().fg(Color::Green)),
462 Span::raw(" "),
463 Span::styled(format!("✗{}", failed), Style::default().fg(Color::Red)),
464 Span::raw(" "),
465 Span::styled(format!("/{}", total), Style::default().fg(Color::DarkGray)),
466 ]),
467 ];
468
469 let paragraph = Paragraph::new(lines).block(
470 Block::default()
471 .borders(Borders::ALL)
472 .title(title)
473 .border_style(Style::default().fg(border_color)),
474 );
475 f.render_widget(paragraph, area);
476}
477
478fn render_progress(f: &mut Frame, state: &RalphViewState, area: Rect) {
479 let progress = state.progress();
480 let (_, _, passed, _) = state.status_counts();
481 let total = state.stories.len();
482 let label = format!("{}/{} stories — {:.0}%", passed, total, progress);
483
484 let gauge = Gauge::default()
485 .block(Block::default().borders(Borders::ALL).title(" Progress "))
486 .gauge_style(Style::default().fg(Color::Magenta).bg(Color::DarkGray))
487 .percent(progress as u16)
488 .label(label);
489
490 f.render_widget(gauge, area);
491}
492
493fn render_story_list(f: &mut Frame, state: &mut RalphViewState, area: Rect) {
494 state.list_state.select(Some(state.selected_index));
495
496 let items: Vec<ListItem> = state
497 .stories
498 .iter()
499 .map(|story| {
500 let (icon, color) = match story.status {
501 RalphStoryStatus::Pending => ("○", Color::DarkGray),
502 RalphStoryStatus::Blocked => ("⊘", Color::Yellow),
503 RalphStoryStatus::Running => ("●", Color::Cyan),
504 RalphStoryStatus::QualityCheck => ("◎", Color::Magenta),
505 RalphStoryStatus::Passed => ("✓", Color::Green),
506 RalphStoryStatus::Failed => ("✗", Color::Red),
507 };
508
509 let mut spans = vec![
510 Span::styled(format!("{} ", icon), Style::default().fg(color)),
511 Span::styled(
512 format!("[{}] ", story.id),
513 Style::default().fg(Color::DarkGray),
514 ),
515 Span::styled(&story.title, Style::default().fg(Color::White)),
516 ];
517
518 if story.status == RalphStoryStatus::Running {
520 if let Some(ref tool) = story.current_tool {
521 spans.push(Span::styled(
522 format!(" → {}", tool),
523 Style::default()
524 .fg(Color::Yellow)
525 .add_modifier(Modifier::DIM),
526 ));
527 }
528 if story.steps > 0 {
529 spans.push(Span::styled(
530 format!(" (step {})", story.steps),
531 Style::default().fg(Color::DarkGray),
532 ));
533 }
534 }
535
536 if story.status == RalphStoryStatus::QualityCheck {
538 let qc_summary: Vec<&str> = story
539 .quality_checks
540 .iter()
541 .map(|(name, _passed)| name.as_str())
542 .collect();
543 if !qc_summary.is_empty() {
544 let checks: String = story
545 .quality_checks
546 .iter()
547 .map(|(name, passed)| {
548 if *passed {
549 format!("✓{}", name)
550 } else {
551 format!("✗{}", name)
552 }
553 })
554 .collect::<Vec<_>>()
555 .join(" ");
556 spans.push(Span::styled(
557 format!(" [{}]", checks),
558 Style::default().fg(Color::Magenta),
559 ));
560 }
561 }
562
563 if story.status == RalphStoryStatus::Passed && !story.quality_checks.is_empty() {
565 let all_passed = story.quality_checks.iter().all(|(_, p)| *p);
566 if all_passed {
567 spans.push(Span::styled(
568 " ✓QC",
569 Style::default()
570 .fg(Color::Green)
571 .add_modifier(Modifier::DIM),
572 ));
573 }
574 }
575
576 ListItem::new(Line::from(spans))
577 })
578 .collect();
579
580 let title = if state.stories.is_empty() {
581 " Stories (loading...) "
582 } else {
583 " Stories (↑↓:select Enter:detail) "
584 };
585
586 let list = List::new(items)
587 .block(Block::default().borders(Borders::ALL).title(title))
588 .highlight_style(
589 Style::default()
590 .add_modifier(Modifier::BOLD)
591 .bg(Color::DarkGray),
592 )
593 .highlight_symbol("▶ ");
594
595 f.render_stateful_widget(list, area, &mut state.list_state);
596}
597
598fn render_story_detail(f: &mut Frame, state: &RalphViewState, area: Rect) {
600 let story = match state.selected_story() {
601 Some(s) => s,
602 None => {
603 let p = Paragraph::new("No story selected").block(
604 Block::default()
605 .borders(Borders::ALL)
606 .title(" Story Detail "),
607 );
608 f.render_widget(p, area);
609 return;
610 }
611 };
612
613 let chunks = Layout::default()
614 .direction(Direction::Vertical)
615 .constraints([
616 Constraint::Length(6), Constraint::Min(1), Constraint::Length(1), ])
620 .split(area);
621
622 let (status_text, status_color) = match story.status {
624 RalphStoryStatus::Pending => ("○ Pending", Color::DarkGray),
625 RalphStoryStatus::Blocked => ("⊘ Blocked", Color::Yellow),
626 RalphStoryStatus::Running => ("● Running", Color::Cyan),
627 RalphStoryStatus::QualityCheck => ("◎ Quality Check", Color::Magenta),
628 RalphStoryStatus::Passed => ("✓ Passed", Color::Green),
629 RalphStoryStatus::Failed => ("✗ Failed", Color::Red),
630 };
631
632 let header_lines = vec![
633 Line::from(vec![
634 Span::styled("Story: ", Style::default().fg(Color::DarkGray)),
635 Span::styled(
636 format!("{} — {}", story.id, story.title),
637 Style::default()
638 .fg(Color::White)
639 .add_modifier(Modifier::BOLD),
640 ),
641 ]),
642 Line::from(vec![
643 Span::styled("Status: ", Style::default().fg(Color::DarkGray)),
644 Span::styled(status_text, Style::default().fg(status_color)),
645 Span::raw(" "),
646 Span::styled("Priority: ", Style::default().fg(Color::DarkGray)),
647 Span::styled(
648 format!("{}", story.priority),
649 Style::default().fg(Color::White),
650 ),
651 Span::raw(" "),
652 Span::styled("Steps: ", Style::default().fg(Color::DarkGray)),
653 Span::styled(
654 format!("{}", story.steps),
655 Style::default().fg(Color::White),
656 ),
657 ]),
658 Line::from(vec![
659 Span::styled("Deps: ", Style::default().fg(Color::DarkGray)),
660 Span::styled(
661 if story.depends_on.is_empty() {
662 "none".to_string()
663 } else {
664 story.depends_on.join(", ")
665 },
666 Style::default().fg(Color::DarkGray),
667 ),
668 ]),
669 Line::from({
671 let mut spans = vec![Span::styled(
672 "Quality: ",
673 Style::default().fg(Color::DarkGray),
674 )];
675 if story.quality_checks.is_empty() {
676 spans.push(Span::styled(
677 "not run yet",
678 Style::default().fg(Color::DarkGray),
679 ));
680 } else {
681 for (name, passed) in &story.quality_checks {
682 let (icon, color) = if *passed {
683 ("✓", Color::Green)
684 } else {
685 ("✗", Color::Red)
686 };
687 spans.push(Span::styled(
688 format!("{}{} ", icon, name),
689 Style::default().fg(color),
690 ));
691 }
692 }
693 spans
694 }),
695 ];
696
697 let title = format!(" Story Detail: {} ", story.id);
698 let header = Paragraph::new(header_lines).block(
699 Block::default()
700 .borders(Borders::ALL)
701 .title(title)
702 .border_style(Style::default().fg(status_color)),
703 );
704 f.render_widget(header, chunks[0]);
705
706 let mut content_lines: Vec<Line> = Vec::new();
708
709 if !story.tool_call_history.is_empty() {
711 content_lines.push(Line::from(Span::styled(
712 "─── Tool Call History ───",
713 Style::default()
714 .fg(Color::Cyan)
715 .add_modifier(Modifier::BOLD),
716 )));
717 content_lines.push(Line::from(""));
718
719 for (i, tc) in story.tool_call_history.iter().enumerate() {
720 let icon = if tc.success { "✓" } else { "✗" };
721 let icon_color = if tc.success { Color::Green } else { Color::Red };
722 content_lines.push(Line::from(vec![
723 Span::styled(format!(" {icon} "), Style::default().fg(icon_color)),
724 Span::styled(format!("#{} ", i + 1), Style::default().fg(Color::DarkGray)),
725 Span::styled(
726 &tc.tool_name,
727 Style::default()
728 .fg(Color::Yellow)
729 .add_modifier(Modifier::BOLD),
730 ),
731 ]));
732 if !tc.input_preview.is_empty() {
733 content_lines.push(Line::from(vec![
734 Span::raw(" "),
735 Span::styled("in: ", Style::default().fg(Color::DarkGray)),
736 Span::styled(
737 truncate_str(&tc.input_preview, 80),
738 Style::default().fg(Color::White),
739 ),
740 ]));
741 }
742 if !tc.output_preview.is_empty() {
743 content_lines.push(Line::from(vec![
744 Span::raw(" "),
745 Span::styled("out: ", Style::default().fg(Color::DarkGray)),
746 Span::styled(
747 truncate_str(&tc.output_preview, 80),
748 Style::default().fg(Color::White),
749 ),
750 ]));
751 }
752 }
753 content_lines.push(Line::from(""));
754 } else if story.steps > 0 {
755 content_lines.push(Line::from(Span::styled(
756 format!("─── {} tool calls (no detail captured) ───", story.steps),
757 Style::default().fg(Color::DarkGray),
758 )));
759 content_lines.push(Line::from(""));
760 }
761
762 if !story.messages.is_empty() {
764 content_lines.push(Line::from(Span::styled(
765 "─── Conversation ───",
766 Style::default()
767 .fg(Color::Cyan)
768 .add_modifier(Modifier::BOLD),
769 )));
770 content_lines.push(Line::from(""));
771
772 for msg in &story.messages {
773 let (role_color, role_label) = match msg.role.as_str() {
774 "user" => (Color::White, "USER"),
775 "assistant" => (Color::Cyan, "ASST"),
776 "tool" => (Color::Yellow, "TOOL"),
777 "system" => (Color::DarkGray, "SYS "),
778 _ => (Color::White, " "),
779 };
780 content_lines.push(Line::from(vec![Span::styled(
781 format!(" [{role_label}] "),
782 Style::default().fg(role_color).add_modifier(Modifier::BOLD),
783 )]));
784 for line in msg.content.lines().take(10) {
785 content_lines.push(Line::from(vec![
786 Span::raw(" "),
787 Span::styled(line, Style::default().fg(Color::White)),
788 ]));
789 }
790 if msg.content.lines().count() > 10 {
791 content_lines.push(Line::from(vec![
792 Span::raw(" "),
793 Span::styled("... (truncated)", Style::default().fg(Color::DarkGray)),
794 ]));
795 }
796 content_lines.push(Line::from(""));
797 }
798 }
799
800 if let Some(ref output) = story.output {
802 content_lines.push(Line::from(Span::styled(
803 "─── Output ───",
804 Style::default()
805 .fg(Color::Green)
806 .add_modifier(Modifier::BOLD),
807 )));
808 content_lines.push(Line::from(""));
809 for line in output.lines().take(20) {
810 content_lines.push(Line::from(Span::styled(
811 line,
812 Style::default().fg(Color::White),
813 )));
814 }
815 if output.lines().count() > 20 {
816 content_lines.push(Line::from(Span::styled(
817 "... (truncated)",
818 Style::default().fg(Color::DarkGray),
819 )));
820 }
821 content_lines.push(Line::from(""));
822 }
823
824 if let Some(ref summary) = story.merge_summary {
826 content_lines.push(Line::from(Span::styled(
827 "─── Merge ───",
828 Style::default()
829 .fg(Color::Magenta)
830 .add_modifier(Modifier::BOLD),
831 )));
832 content_lines.push(Line::from(""));
833 content_lines.push(Line::from(Span::styled(
834 summary.as_str(),
835 Style::default().fg(Color::White),
836 )));
837 content_lines.push(Line::from(""));
838 }
839
840 if let Some(ref err) = story.error {
842 content_lines.push(Line::from(Span::styled(
843 "─── Error ───",
844 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
845 )));
846 content_lines.push(Line::from(""));
847 for line in err.lines() {
848 content_lines.push(Line::from(Span::styled(
849 line,
850 Style::default().fg(Color::Red),
851 )));
852 }
853 content_lines.push(Line::from(""));
854 }
855
856 if content_lines.is_empty() {
858 content_lines.push(Line::from(Span::styled(
859 " Waiting for agent activity...",
860 Style::default().fg(Color::DarkGray),
861 )));
862 }
863
864 let content = Paragraph::new(content_lines)
865 .block(Block::default().borders(Borders::ALL))
866 .wrap(Wrap { trim: false })
867 .scroll((state.detail_scroll as u16, 0));
868 f.render_widget(content, chunks[1]);
869
870 let hints = Paragraph::new(Line::from(vec![
872 Span::styled(" Esc", Style::default().fg(Color::Yellow)),
873 Span::raw(": Back "),
874 Span::styled("PgUp/PgDn", Style::default().fg(Color::Yellow)),
875 Span::raw(": Scroll "),
876 Span::styled("↑/↓", Style::default().fg(Color::Yellow)),
877 Span::raw(": Prev/Next story"),
878 ]));
879 f.render_widget(hints, chunks[2]);
880}
881
882fn render_footer(f: &mut Frame, state: &RalphViewState, area: Rect) {
883 let content = if let Some(ref status) = state.final_status {
884 Line::from(vec![
885 Span::styled("Status: ", Style::default().fg(Color::DarkGray)),
886 Span::styled(
887 status.as_str(),
888 Style::default().fg(if status.contains("Completed") {
889 Color::Green
890 } else {
891 Color::Yellow
892 }),
893 ),
894 ])
895 } else if let Some(ref err) = state.error {
896 Line::from(vec![Span::styled(
897 format!("Error: {}", err),
898 Style::default().fg(Color::Red),
899 )])
900 } else {
901 Line::from(vec![Span::styled(
902 "Executing...",
903 Style::default().fg(Color::DarkGray),
904 )])
905 };
906
907 let paragraph = Paragraph::new(content)
908 .block(Block::default().borders(Borders::ALL).title(" Status "))
909 .wrap(Wrap { trim: true });
910 f.render_widget(paragraph, area);
911}
912
913fn truncate_str(s: &str, max: usize) -> String {
915 if s.len() <= max {
916 s.replace('\n', " ")
917 } else {
918 let mut end = max;
919 while end > 0 && !s.is_char_boundary(end) {
920 end -= 1;
921 }
922 format!("{}...", s[..end].replace('\n', " "))
923 }
924}