1use ratatui::Frame;
5use ratatui::layout::{Alignment, Constraint, Rect};
6use ratatui::text::{Line, Span};
7use ratatui::widgets::{Block, Borders, Cell, Clear, Row, Table};
8
9use crate::layout::centered_rect;
10use crate::theme::Theme;
11
12const POPUP_HEIGHT: u16 = 32;
14
15pub fn render(frame: &mut Frame, area: Rect) {
16 let theme = Theme::default();
17
18 let popup = centered_rect(70, POPUP_HEIGHT, area);
19 frame.render_widget(Clear, popup);
20
21 let rows = vec![
22 Row::new([
23 Cell::from(Span::styled("Normal mode", theme.panel_title)),
24 Cell::from(""),
25 ]),
26 keybind_row("q", "quit"),
27 keybind_row("i", "enter insert mode"),
28 keybind_row("j / k", "scroll down / up"),
29 keybind_row("PgDn / PgUp", "page scroll down / up"),
30 keybind_row("End / Home", "jump to bottom / top"),
31 keybind_row("d", "toggle side panels"),
32 keybind_row("e", "expand tools"),
33 keybind_row("c", "compact tools"),
34 keybind_row(
35 "Tab",
36 "cycle panels (Chat/Skills/Memory/Resources/SubAgents)",
37 ),
38 keybind_row("a", "focus Sub-Agents panel"),
39 keybind_row("?", "toggle this help"),
40 Row::new([Cell::from(""), Cell::from("")]),
41 Row::new([
42 Cell::from(Span::styled(
43 "Sub-Agents panel (focused)",
44 theme.panel_title,
45 )),
46 Cell::from(""),
47 ]),
48 keybind_row("j / k", "navigate agent list"),
49 keybind_row("Enter", "view selected agent transcript"),
50 keybind_row("Esc", "close panel focus"),
51 Row::new([Cell::from(""), Cell::from("")]),
52 Row::new([
53 Cell::from(Span::styled("Subagent transcript view", theme.panel_title)),
54 Cell::from(""),
55 ]),
56 keybind_row("Esc", "return to main conversation"),
57 Row::new([
58 Cell::from(Span::styled("Insert mode", theme.panel_title)),
59 Cell::from(""),
60 ]),
61 keybind_row("Enter", "send message"),
62 keybind_row("Shift+Enter", "insert newline"),
63 keybind_row("Ctrl+J", "insert newline"),
64 keybind_row("Esc", "return to normal mode"),
65 keybind_row("Ctrl+U", "clear input"),
66 keybind_row("Ctrl+K", "clear queue"),
67 keybind_row("Up / Down", "navigate history"),
68 Row::new([Cell::from(""), Cell::from("")]),
69 Row::new([
70 Cell::from(Span::styled("Confirm mode", theme.panel_title)),
71 Cell::from(""),
72 ]),
73 keybind_row("y", "confirm"),
74 keybind_row("n / Esc", "cancel"),
75 ];
76
77 let header = Row::new([
78 Cell::from(Span::styled("Key", theme.highlight)),
79 Cell::from(Span::styled("Action", theme.highlight)),
80 ]);
81
82 let table = Table::new(
83 rows,
84 [Constraint::Percentage(35), Constraint::Percentage(65)],
85 )
86 .header(header)
87 .block(
88 Block::default()
89 .borders(Borders::ALL)
90 .border_style(theme.panel_border)
91 .title(" Help — press ? or Esc to close ")
92 .title_alignment(Alignment::Center),
93 );
94
95 frame.render_widget(table, popup);
96}
97
98fn keybind_row(key: &'static str, action: &'static str) -> Row<'static> {
99 Row::new([Cell::from(Line::from(key)), Cell::from(Line::from(action))])
100}
101
102#[cfg(test)]
103mod tests {
104 use insta::assert_snapshot;
105
106 use crate::test_utils::render_to_string;
107
108 #[test]
109 fn help_default() {
110 let output = render_to_string(80, 30, |frame, area| {
111 super::render(frame, area);
112 });
113 assert_snapshot!(output);
114 }
115}