1use ratatui::{
11 layout::{Alignment, Constraint, Layout, Rect},
12 style::{Color, Modifier, Style, Stylize},
13 text::{Line, Span},
14 widgets::{Block, BorderType, Borders, Clear, List, ListItem, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
15 Frame,
16};
17
18use crate::commands::spawn::monitor::AgentStatus;
19
20use super::app::{App, FocusedPanel, ViewMode, WaveTaskState};
21
22const BG_PRIMARY: Color = Color::Rgb(15, 23, 42); const BG_SECONDARY: Color = Color::Rgb(30, 41, 59); const BG_TERMINAL: Color = Color::Rgb(22, 22, 22); const TEXT_PRIMARY: Color = Color::Rgb(226, 232, 240); const TEXT_MUTED: Color = Color::Rgb(100, 116, 139); const TEXT_TERMINAL: Color = Color::Rgb(200, 200, 200); const BORDER_DEFAULT: Color = Color::Rgb(51, 65, 85); const BORDER_ACTIVE: Color = Color::Rgb(96, 165, 250); const ACCENT: Color = Color::Rgb(96, 165, 250); const STATUS_STARTING: Color = Color::Rgb(148, 163, 184); const STATUS_RUNNING: Color = Color::Rgb(34, 197, 94); const STATUS_COMPLETED: Color = Color::Rgb(96, 165, 250); const STATUS_FAILED: Color = Color::Rgb(248, 113, 113); pub fn render(frame: &mut Frame, app: &mut App) {
42 let area = frame.area();
43
44 frame.render_widget(
46 Block::default().style(Style::default().bg(BG_PRIMARY)),
47 area,
48 );
49
50 match app.view_mode {
51 ViewMode::Split => render_split_view(frame, area, app),
52 ViewMode::Fullscreen => render_fullscreen_view(frame, area, app),
53 ViewMode::Input => render_input_view(frame, area, app),
54 }
55
56 if app.show_help {
58 render_help_overlay(frame, area, app);
59 }
60}
61
62fn render_split_view(frame: &mut Frame, area: Rect, app: &mut App) {
63 let [header_area, content_area, footer_area] = Layout::vertical([
65 Constraint::Length(3),
66 Constraint::Fill(1),
67 Constraint::Length(2),
68 ])
69 .areas(area);
70
71 render_header(frame, header_area, app);
72 render_three_panel_content(frame, content_area, app);
73 render_footer(frame, footer_area, app);
74}
75
76fn render_fullscreen_view(frame: &mut Frame, area: Rect, app: &App) {
77 let [header_area, terminal_area, footer_area] = Layout::vertical([
79 Constraint::Length(2),
80 Constraint::Fill(1),
81 Constraint::Length(2),
82 ])
83 .areas(area);
84
85 render_fullscreen_header(frame, header_area, app);
86 render_terminal_output(frame, terminal_area, app, true);
87 render_fullscreen_footer(frame, footer_area);
88}
89
90fn render_input_view(frame: &mut Frame, area: Rect, app: &App) {
91 let [header_area, terminal_area, input_area, footer_area] = Layout::vertical([
93 Constraint::Length(2),
94 Constraint::Fill(1),
95 Constraint::Length(3),
96 Constraint::Length(2),
97 ])
98 .areas(area);
99
100 render_fullscreen_header(frame, header_area, app);
101 render_terminal_output(frame, terminal_area, app, true);
102 render_input_bar(frame, input_area, app);
103 render_input_footer(frame, footer_area);
104}
105
106fn render_input_bar(frame: &mut Frame, area: Rect, app: &App) {
107 let input_text = format!("▸ {}", app.input_buffer);
108
109 let input = Paragraph::new(Line::from(vec![
110 Span::styled(&input_text, Style::default().fg(TEXT_PRIMARY)),
111 Span::styled("█", Style::default().fg(ACCENT)), ]))
113 .block(
114 Block::default()
115 .borders(Borders::ALL)
116 .border_type(BorderType::Rounded)
117 .border_style(Style::default().fg(ACCENT))
118 .title(Line::from(" Send to Agent ").fg(ACCENT))
119 .style(Style::default().bg(BG_SECONDARY))
120 .padding(Padding::horizontal(1)),
121 );
122
123 frame.render_widget(input, area);
124}
125
126fn render_input_footer(frame: &mut Frame, area: Rect) {
127 let help_text = " Enter Send · Esc Cancel · Type your message... ";
128
129 let footer = Paragraph::new(Line::from(vec![
130 Span::styled(help_text, Style::default().fg(TEXT_MUTED)),
131 ]))
132 .alignment(Alignment::Center)
133 .style(Style::default().bg(BG_PRIMARY));
134
135 frame.render_widget(footer, area);
136}
137
138fn render_header(frame: &mut Frame, area: Rect, app: &App) {
139 let (starting, running, completed, failed) = app.status_counts();
140
141 let ralph_indicator = if app.ralph_mode {
143 vec![
144 Span::styled(" 🔄 ", Style::default()),
145 Span::styled("RALPH ", Style::default().fg(Color::Rgb(255, 165, 0)).bold()),
146 ]
147 } else {
148 vec![]
149 };
150
151 let mut spans = vec![
153 Span::styled(" ", Style::default()),
154 Span::styled(&app.session_name, Style::default().fg(ACCENT).bold()),
155 ];
156 spans.extend(ralph_indicator);
157 spans.extend(vec![
158 Span::styled(" ", Style::default()),
159 Span::styled("◉ ", Style::default().fg(STATUS_STARTING)),
161 Span::styled("Starting ", Style::default().fg(TEXT_MUTED).dim()),
162 Span::styled(format!("{} ", starting), Style::default().fg(STATUS_STARTING)),
163 Span::styled("◉ ", Style::default().fg(STATUS_RUNNING)),
165 Span::styled("Running ", Style::default().fg(TEXT_MUTED).dim()),
166 Span::styled(format!("{} ", running), Style::default().fg(STATUS_RUNNING)),
167 Span::styled("◉ ", Style::default().fg(STATUS_COMPLETED)),
169 Span::styled("Done ", Style::default().fg(TEXT_MUTED).dim()),
170 Span::styled(format!("{} ", completed), Style::default().fg(STATUS_COMPLETED)),
171 Span::styled("◉ ", Style::default().fg(STATUS_FAILED)),
173 Span::styled("Failed ", Style::default().fg(TEXT_MUTED).dim()),
174 Span::styled(format!("{}", failed), Style::default().fg(STATUS_FAILED)),
175 ]);
176 let status_line = Line::from(spans);
177
178 let header = Paragraph::new(status_line)
179 .block(
180 Block::default()
181 .borders(Borders::BOTTOM)
182 .border_style(Style::default().fg(BORDER_DEFAULT))
183 .style(Style::default().bg(BG_SECONDARY))
184 .padding(Padding::horizontal(1)),
185 );
186
187 frame.render_widget(header, area);
188}
189
190fn render_fullscreen_header(frame: &mut Frame, area: Rect, app: &App) {
191 let agent_name = app
192 .selected_agent()
193 .map(|a| format!("{}: {}", a.task_id, a.task_title))
194 .unwrap_or_else(|| "No agent".to_string());
195
196 let title = Line::from(vec![
197 Span::styled(" ", Style::default()),
198 Span::styled(&agent_name, Style::default().fg(ACCENT).bold()),
199 ]);
200
201 let header = Paragraph::new(title)
202 .block(
203 Block::default()
204 .borders(Borders::BOTTOM)
205 .border_style(Style::default().fg(BORDER_ACTIVE))
206 .style(Style::default().bg(BG_SECONDARY)),
207 );
208
209 frame.render_widget(header, area);
210}
211
212fn render_three_panel_content(frame: &mut Frame, area: Rect, app: &mut App) {
213 let has_agents = !app.agents().is_empty();
216 let has_waves = !app.waves.is_empty();
217
218 let constraints = if has_waves && has_agents {
219 vec![
220 Constraint::Percentage(35), Constraint::Percentage(25), Constraint::Percentage(40), ]
224 } else if has_waves {
225 vec![
226 Constraint::Percentage(50), Constraint::Length(3), Constraint::Percentage(50), ]
230 } else if has_agents {
231 vec![
232 Constraint::Length(3), Constraint::Percentage(40), Constraint::Percentage(60), ]
236 } else {
237 vec![
238 Constraint::Length(3),
239 Constraint::Length(3),
240 Constraint::Fill(1),
241 ]
242 };
243
244 let [waves_area, agents_area, output_area] = Layout::vertical(constraints).areas(area);
245
246 render_waves_panel(frame, waves_area, app);
247 render_agents_panel(frame, agents_area, app);
248 render_terminal_output(frame, output_area, app, false);
249}
250
251fn render_waves_panel(frame: &mut Frame, area: Rect, app: &App) {
252 let is_focused = app.focused_panel == FocusedPanel::Waves;
253 let border_color = if is_focused { BORDER_ACTIVE } else { BORDER_DEFAULT };
254 let title_color = if is_focused { ACCENT } else { TEXT_MUTED };
255
256 let ready_count = app.ready_task_count();
257 let selected_count = app.selected_task_count();
258 let title = if selected_count > 0 {
259 format!(" Waves & Tasks ({} selected / {} ready) ", selected_count, ready_count)
260 } else {
261 format!(" Waves & Tasks ({} ready) ", ready_count)
262 };
263
264 let block = Block::default()
265 .borders(Borders::ALL)
266 .border_type(BorderType::Rounded)
267 .border_style(Style::default().fg(border_color))
268 .title(Line::from(title).fg(title_color))
269 .style(Style::default().bg(BG_SECONDARY))
270 .padding(Padding::new(1, 1, 0, 0));
271
272 if app.waves.is_empty() {
273 let empty_msg = Paragraph::new("No actionable tasks")
274 .style(Style::default().fg(TEXT_MUTED))
275 .block(block);
276 frame.render_widget(empty_msg, area);
277 return;
278 }
279
280 let mut all_items: Vec<ListItem> = Vec::new();
282 let mut task_index = 0;
283
284 for wave in &app.waves {
285 let ready_in_wave = wave.tasks.iter().filter(|t| t.state == WaveTaskState::Ready).count();
287 let wave_header = Line::from(vec![
288 Span::styled(
289 format!("Wave {} ", wave.number),
290 Style::default().fg(ACCENT).bold(),
291 ),
292 Span::styled(
293 format!("({} tasks, {} ready)", wave.tasks.len(), ready_in_wave),
294 Style::default().fg(TEXT_MUTED),
295 ),
296 ]);
297 all_items.push(ListItem::new(wave_header));
298
299 for task in &wave.tasks {
301 let is_selected_in_list = task_index == app.wave_task_index && is_focused;
302 let is_selected_for_spawn = app.selected_tasks.contains(&task.id);
303
304 let state_icon = match task.state {
305 WaveTaskState::Ready => ("○", STATUS_COMPLETED), WaveTaskState::Running => ("●", STATUS_RUNNING), WaveTaskState::Done => ("✓", STATUS_COMPLETED), WaveTaskState::Blocked => ("◌", TEXT_MUTED), WaveTaskState::InProgress => ("◐", STATUS_RUNNING), };
311
312 let checkbox = if is_selected_for_spawn {
313 "[x]"
314 } else if task.state == WaveTaskState::Ready {
315 "[ ]"
316 } else {
317 " "
318 };
319
320 let max_len = 40;
322 let title_display = if task.title.len() > max_len {
323 format!("{}…", &task.title[..max_len - 1])
324 } else {
325 task.title.clone()
326 };
327
328 let complexity = if task.complexity > 0 {
329 format!(" [{}]", task.complexity)
330 } else {
331 String::new()
332 };
333
334 let line = Line::from(vec![
335 Span::styled(
336 if is_selected_in_list { "▸ " } else { " " },
337 Style::default().fg(ACCENT),
338 ),
339 Span::styled(
340 format!("{} ", checkbox),
341 Style::default().fg(if is_selected_for_spawn { ACCENT } else { TEXT_MUTED }),
342 ),
343 Span::styled(format!("{} ", state_icon.0), Style::default().fg(state_icon.1)),
344 Span::styled(
345 format!("{} ", task.id),
346 Style::default().fg(TEXT_MUTED),
347 ),
348 Span::styled(
349 title_display,
350 Style::default()
351 .fg(if is_selected_in_list { ACCENT } else { TEXT_PRIMARY })
352 .add_modifier(if is_selected_in_list { Modifier::BOLD } else { Modifier::empty() }),
353 ),
354 Span::styled(complexity, Style::default().fg(TEXT_MUTED)),
355 ]);
356
357 all_items.push(ListItem::new(line));
358 task_index += 1;
359 }
360 }
361
362 let visible_items: Vec<ListItem> = all_items
364 .into_iter()
365 .skip(app.wave_scroll_offset)
366 .collect();
367
368 let list = List::new(visible_items).block(block);
369 frame.render_widget(list, area);
370}
371
372fn render_agents_panel(frame: &mut Frame, area: Rect, app: &mut App) {
373 let is_focused = app.focused_panel == FocusedPanel::Agents;
374 let border_color = if is_focused { BORDER_ACTIVE } else { BORDER_DEFAULT };
375 let title_color = if is_focused { ACCENT } else { TEXT_MUTED };
376
377 let total = app.agents().len();
379 let running = app.agents().iter().filter(|a| a.status == AgentStatus::Running).count();
380 let selected = app.selected;
381
382 let inner_height = area.height.saturating_sub(3) as usize; if selected < app.agents_scroll_offset {
387 app.agents_scroll_offset = selected;
388 } else if total > 0 && inner_height > 0 && selected >= app.agents_scroll_offset + inner_height {
389 app.agents_scroll_offset = selected.saturating_sub(inner_height - 1);
390 }
391
392 let scroll_offset = app.agents_scroll_offset;
393
394 let title = if total > inner_height && inner_height > 0 {
396 let visible_end = (scroll_offset + inner_height).min(total);
397 format!(" Agents ({} running / {} total) [{}-{}] ", running, total, scroll_offset + 1, visible_end)
398 } else {
399 format!(" Agents ({} running / {} total) ", running, total)
400 };
401
402 let agents = app.agents();
404
405 let block = Block::default()
406 .borders(Borders::ALL)
407 .border_type(BorderType::Rounded)
408 .border_style(Style::default().fg(border_color))
409 .title(Line::from(title).fg(title_color))
410 .style(Style::default().bg(BG_SECONDARY))
411 .padding(Padding::new(1, 1, 0, 0));
412
413 if agents.is_empty() {
414 let empty_msg = Paragraph::new("No agents spawned yet")
415 .style(Style::default().fg(TEXT_MUTED))
416 .block(block);
417 frame.render_widget(empty_msg, area);
418 return;
419 }
420
421 let items: Vec<ListItem> = agents
422 .iter()
423 .enumerate()
424 .skip(scroll_offset)
425 .take(inner_height.max(1))
426 .map(|(i, agent)| {
427 let is_selected = i == selected && is_focused;
428
429 let status_icon = match agent.status {
430 AgentStatus::Starting => ("◐", STATUS_STARTING),
431 AgentStatus::Running => ("●", STATUS_RUNNING),
432 AgentStatus::Completed => ("✓", STATUS_COMPLETED),
433 AgentStatus::Failed => ("✗", STATUS_FAILED),
434 };
435
436 let max_len = 35;
438 let title = if agent.task_title.len() > max_len {
439 format!("{}…", &agent.task_title[..max_len - 1])
440 } else {
441 agent.task_title.clone()
442 };
443
444 let line = Line::from(vec![
445 Span::styled(
446 if is_selected { "▸ " } else { " " },
447 Style::default().fg(ACCENT),
448 ),
449 Span::styled(format!("{} ", status_icon.0), Style::default().fg(status_icon.1)),
450 Span::styled(format!("{}: ", agent.task_id), Style::default().fg(TEXT_MUTED)),
451 Span::styled(
452 title,
453 Style::default()
454 .fg(if is_selected { ACCENT } else { TEXT_PRIMARY })
455 .add_modifier(if is_selected { Modifier::BOLD } else { Modifier::empty() }),
456 ),
457 ]);
458
459 ListItem::new(line)
460 })
461 .collect();
462
463 let list = List::new(items).block(block);
464 frame.render_widget(list, area);
465}
466
467fn render_terminal_output(frame: &mut Frame, area: Rect, app: &App, fullscreen: bool) {
468 let is_focused = app.focused_panel == FocusedPanel::Output || fullscreen;
469
470 let title = if fullscreen {
471 " Terminal (Esc to exit) ".to_string()
472 } else if let Some(agent) = app.selected_agent() {
473 format!(" Output: {} ", agent.task_id)
474 } else {
475 " Live Output ".to_string()
476 };
477
478 let border_color = if is_focused { BORDER_ACTIVE } else { BORDER_DEFAULT };
479 let title_color = if is_focused { ACCENT } else { TEXT_MUTED };
480
481 let block = Block::default()
482 .borders(Borders::ALL)
483 .border_type(BorderType::Rounded)
484 .border_style(Style::default().fg(border_color))
485 .title(Line::from(title).fg(title_color))
486 .style(Style::default().bg(BG_TERMINAL))
487 .padding(Padding::new(1, 0, 0, 0)); let inner = block.inner(area);
490 frame.render_widget(block, area);
491
492 let visible_height = inner.height as usize;
494 let output = &app.live_output;
495
496 let total_lines = output.len();
499 let end_idx = total_lines.saturating_sub(app.scroll_offset);
500 let start_idx = end_idx.saturating_sub(visible_height);
501
502 let text_width = inner.width.saturating_sub(2);
504 let text_area = Rect::new(inner.x, inner.y, text_width, inner.height);
505
506 let visible_lines: Vec<Line> = output
507 .iter()
508 .skip(start_idx)
509 .take(visible_height)
510 .map(|line| Line::from(Span::styled(line.as_str(), Style::default().fg(TEXT_TERMINAL))))
511 .collect();
512
513 let paragraph = Paragraph::new(visible_lines);
514 frame.render_widget(paragraph, text_area);
515
516 if total_lines > visible_height {
518 let scrollbar_area = Rect::new(
519 inner.x + inner.width.saturating_sub(1),
520 inner.y,
521 1,
522 inner.height,
523 );
524
525 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
526 .begin_symbol(None)
527 .end_symbol(None)
528 .track_symbol(Some(" "))
529 .thumb_symbol("▐");
530
531 let mut scrollbar_state = ScrollbarState::new(total_lines)
532 .position(start_idx);
533
534 frame.render_stateful_widget(
535 scrollbar,
536 scrollbar_area,
537 &mut scrollbar_state,
538 );
539 }
540}
541
542fn render_footer(frame: &mut Frame, area: Rect, app: &App) {
543 let ralph_hint = if app.ralph_mode { "R Ralph OFF" } else { "R Ralph" };
545 let help_text = match app.focused_panel {
546 FocusedPanel::Waves => format!(" Tab Panel · j/k Navigate · Space Select · a All · s Spawn · {} · ? Help · q Quit ", ralph_hint),
547 FocusedPanel::Agents => format!(" Tab Panel · j/k Navigate · Enter View · i Input · x Stop · {} · ? Help · q Quit ", ralph_hint),
548 FocusedPanel::Output => format!(" Tab Panel · ↑↓ Scroll · G Bottom · Enter Fullscreen · {} · ? Help · q Quit ", ralph_hint),
549 };
550
551 let mut line = Line::from(vec![
552 Span::styled(help_text, Style::default().fg(TEXT_MUTED)),
553 ]);
554
555 if let Some(ref error) = app.error {
557 line = Line::from(vec![
558 Span::styled(" ⚠ ", Style::default().fg(STATUS_FAILED)),
559 Span::styled(error.as_str(), Style::default().fg(STATUS_FAILED)),
560 ]);
561 }
562
563 let footer = Paragraph::new(line)
564 .alignment(Alignment::Center)
565 .style(Style::default().bg(BG_PRIMARY));
566
567 frame.render_widget(footer, area);
568}
569
570fn render_fullscreen_footer(frame: &mut Frame, area: Rect) {
571 let help_text = " ↑↓ Scroll · j/k Switch · G Bottom · i Input · Esc Back · q Quit ";
572
573 let footer = Paragraph::new(Line::from(vec![
574 Span::styled(help_text, Style::default().fg(TEXT_MUTED)),
575 ]))
576 .alignment(Alignment::Center)
577 .style(Style::default().bg(BG_PRIMARY));
578
579 frame.render_widget(footer, area);
580}
581
582fn render_help_overlay(frame: &mut Frame, area: Rect, app: &App) {
583 let overlay_width = 55.min(area.width.saturating_sub(4));
584 let overlay_height = 22.min(area.height.saturating_sub(2)); let x = (area.width.saturating_sub(overlay_width)) / 2;
586 let y = (area.height.saturating_sub(overlay_height)) / 2;
587 let overlay_area = Rect::new(x, y, overlay_width, overlay_height);
588
589 frame.render_widget(Clear, overlay_area);
590
591 let mode_hint = match app.view_mode {
592 ViewMode::Split => "Three-Panel",
593 ViewMode::Fullscreen => "Fullscreen",
594 ViewMode::Input => "Input Mode",
595 };
596
597 let panel_hint = match app.focused_panel {
598 FocusedPanel::Waves => "Waves",
599 FocusedPanel::Agents => "Agents",
600 FocusedPanel::Output => "Output",
601 };
602
603 let help_text = vec![
604 Line::from(vec![
605 Span::styled(" Tab ", Style::default().fg(ACCENT)),
606 Span::styled("Panel ", Style::default().fg(TEXT_PRIMARY)),
607 Span::styled(" j/k ", Style::default().fg(ACCENT)),
608 Span::styled("Navigate ", Style::default().fg(TEXT_PRIMARY)),
609 Span::styled(" r ", Style::default().fg(ACCENT)),
610 Span::styled("Refresh", Style::default().fg(TEXT_PRIMARY)),
611 ]),
612 Line::from(""),
613 Line::from(Span::styled(" Waves:", Style::default().fg(TEXT_MUTED))),
614 Line::from(vec![
615 Span::styled(" Space ", Style::default().fg(ACCENT)),
616 Span::styled("Select ", Style::default().fg(TEXT_PRIMARY)),
617 Span::styled(" a ", Style::default().fg(ACCENT)),
618 Span::styled("All ", Style::default().fg(TEXT_PRIMARY)),
619 Span::styled(" c ", Style::default().fg(ACCENT)),
620 Span::styled("Clear ", Style::default().fg(TEXT_PRIMARY)),
621 Span::styled(" s ", Style::default().fg(ACCENT)),
622 Span::styled("Spawn", Style::default().fg(TEXT_PRIMARY)),
623 ]),
624 Line::from(""),
625 Line::from(Span::styled(" Agents:", Style::default().fg(TEXT_MUTED))),
626 Line::from(vec![
627 Span::styled(" Enter ", Style::default().fg(ACCENT)),
628 Span::styled("View ", Style::default().fg(TEXT_PRIMARY)),
629 Span::styled(" i ", Style::default().fg(ACCENT)),
630 Span::styled("Input ", Style::default().fg(TEXT_PRIMARY)),
631 Span::styled(" x ", Style::default().fg(ACCENT)),
632 Span::styled("Stop", Style::default().fg(TEXT_PRIMARY)),
633 ]),
634 Line::from(""),
635 Line::from(Span::styled(" Output:", Style::default().fg(TEXT_MUTED))),
636 Line::from(vec![
637 Span::styled(" ↑/↓ ", Style::default().fg(ACCENT)),
638 Span::styled("Scroll ", Style::default().fg(TEXT_PRIMARY)),
639 Span::styled(" G ", Style::default().fg(ACCENT)),
640 Span::styled("Bottom ", Style::default().fg(TEXT_PRIMARY)),
641 Span::styled(" Enter ", Style::default().fg(ACCENT)),
642 Span::styled("Fullscreen", Style::default().fg(TEXT_PRIMARY)),
643 ]),
644 Line::from(""),
645 Line::from(vec![
646 Span::styled(" ? ", Style::default().fg(ACCENT)),
647 Span::styled("Help ", Style::default().fg(TEXT_PRIMARY)),
648 Span::styled(" q/Esc ", Style::default().fg(ACCENT)),
649 Span::styled("Quit", Style::default().fg(TEXT_PRIMARY)),
650 ]),
651 Line::from(""),
652 Line::from(vec![
653 Span::styled(format!(" Mode: {} | Panel: {}", mode_hint, panel_hint), Style::default().fg(TEXT_MUTED)),
654 ]),
655 ];
656
657 let help_block = Block::default()
658 .borders(Borders::ALL)
659 .border_type(BorderType::Rounded)
660 .border_style(Style::default().fg(ACCENT))
661 .title(Line::from(" Keybindings ").fg(ACCENT).bold())
662 .title_alignment(Alignment::Center)
663 .style(Style::default().bg(BG_SECONDARY));
664
665 let help_para = Paragraph::new(help_text).block(help_block);
666 frame.render_widget(help_para, overlay_area);
667}