use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{
Block, BorderType, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
},
Frame,
};
use tmai_core::state::AppState;
pub struct HelpScreen;
impl HelpScreen {
pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
let help_lines = Self::build_help_content(state);
let total_lines = help_lines.len();
let visible_height = area.height.saturating_sub(2) as usize;
let max_scroll = total_lines.saturating_sub(visible_height);
let scroll = (state.view.help_scroll as usize).min(max_scroll);
let block = Block::default()
.title(" Help (j/k or ↑/↓ to scroll, q to close) ")
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Cyan));
let paragraph = Paragraph::new(help_lines)
.block(block)
.scroll((scroll as u16, 0));
frame.render_widget(paragraph, area);
if total_lines > visible_height {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"));
let mut scrollbar_state = ScrollbarState::new(max_scroll).position(scroll);
frame.render_stateful_widget(
scrollbar,
area.inner(ratatui::layout::Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
}
fn build_help_content(state: &AppState) -> Vec<Line<'static>> {
let scope_str = "All Sessions".to_string();
vec![
Self::title_line("tmai - Tactful Multi Agent Interface"),
Line::from(""),
Self::section_header("Current Settings"),
Line::from(vec![
Span::styled(" Monitor Scope: ", Style::default().fg(Color::DarkGray)),
Span::styled(scope_str, Style::default().fg(Color::Magenta)),
]),
Line::from(vec![
Span::styled(" Sort Method: ", Style::default().fg(Color::DarkGray)),
Span::styled(
state.sort_by.display_name().to_string(),
Style::default().fg(Color::Blue),
),
]),
Line::from(vec![
Span::styled(" Agents: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{} monitored", state.agents.len()),
Style::default().fg(Color::Green),
),
]),
Line::from(""),
Self::section_header("Navigation"),
Self::key_line("j / ↓", "Select next agent in list"),
Self::key_line("k / ↑", "Select previous agent in list"),
Self::key_line("g", "Jump to first agent"),
Self::key_line("G", "Jump to last agent"),
Self::key_line("Ctrl+d", "Scroll preview pane down (half page)"),
Self::key_line("Ctrl+u", "Scroll preview pane up (half page)"),
Line::from(""),
Self::section_header("Agent Actions"),
Self::key_line("f", "Focus the selected pane in tmux"),
Self::description_line(" Switches tmux to show the selected agent's pane"),
Self::key_line("x", "Kill the selected pane (with confirmation)"),
Self::description_line(" Terminates the agent process and closes the pane"),
Self::key_line("W", "Restart as IPC-wrapped (non-IPC Claude Code only)"),
Self::description_line(
" Resumes the session with PTY wrapping for high-precision detection",
),
Self::key_line("U", "Fetch subscription usage from Claude Code"),
Self::description_line(
" Spawns a temporary Claude Code to run /usage, displays below agent list",
),
Line::from(""),
Self::section_header("Approval"),
Self::key_line("y", "Approve / select Yes"),
Self::key_line("n", "Select No (UserQuestion only)"),
Self::description_line(" For other options, use number keys or input mode"),
Line::from(""),
Self::section_header("AskUserQuestion (Selection Dialogs)"),
Self::key_line("1-9", "Select option by number"),
Self::description_line(" All keys support full-width input (IME on)"),
Self::description_line(" Single-select: immediately confirms the choice"),
Self::description_line(" Multi-select: toggles the option on/off"),
Self::key_line("Space", "Toggle current option (multi-select mode)"),
Self::key_line("Enter", "Confirm selection (multi-select mode)"),
Self::description_line(" On 'Type something': opens input mode"),
Line::from(""),
Self::section_header("Input Mode"),
Self::key_line("i", "Enter input mode to type text"),
Self::key_line("/", "Enter input mode (alternative)"),
Self::description_line(" While in input mode:"),
Self::key_line(" Enter", "Send the typed text to agent"),
Self::key_line(" Esc", "Cancel and exit input mode"),
Self::key_line(" ← / →", "Move cursor left/right"),
Self::key_line(" Home/End", "Jump to start/end of input"),
Self::key_line(" Backspace", "Delete character before cursor"),
Line::from(""),
Self::section_header("Passthrough Mode"),
Self::key_line("p", "Enter passthrough mode"),
Self::key_line("→", "Enter passthrough mode (alternative)"),
Self::description_line(" All keystrokes are sent directly to the agent's pane."),
Self::description_line(" Useful for complex interactions not covered by shortcuts."),
Self::key_line(" Esc", "Exit passthrough mode"),
Line::from(""),
Self::section_header("View Options"),
Self::key_line("Tab/S-Tab", "Adjust split ratio (±10%, auto-saved)"),
Self::description_line(" Tab=expand list, Shift+Tab=expand preview"),
Self::key_line("l", "Toggle split direction (layout)"),
Self::description_line(" Horizontal (left/right) ↔ Vertical (top/bottom)"),
Self::key_line("s", "Cycle sort method (Team → Repository → Directory)"),
Self::disabled_key_line("m", "Cycle monitor scope (temporarily disabled)"),
Line::from(""),
Self::section_header("Agent Teams"),
Self::key_line("t", "Show task overlay for selected team member"),
Self::key_line("T", "Show team overview (all teams and members)"),
Self::key_line("w", "Show worktree overview (all repos and worktrees)"),
Self::description_line(" In worktree view: v=view diff, c=create, d=delete, l=launch"),
Line::from(""),
Self::section_header("Review & Security"),
Self::key_line("R", "Request fresh-session code review for selected agent"),
Self::description_line(" Launches a new Claude session to review git diff"),
Self::key_line("S", "Security monitor (scan Claude Code configs)"),
Self::description_line(" Checks settings.json, mcp.json, and hooks for risks"),
Line::from(""),
Self::section_header("Creating New Agents"),
Self::key_line("Enter", "On [+] entry: start create process wizard"),
Self::description_line(" 1. Choose placement: New Session / New Window / Split Pane"),
Self::description_line(" 2. Select target session (if applicable)"),
Self::description_line(" 3. Choose working directory"),
Self::description_line(" 4. Select agent type (Claude Code, Codex, Gemini, etc.)"),
Line::from(""),
Self::section_header("General"),
Self::key_line("h / ?", "Toggle this help screen"),
Self::key_line("q / Esc", "Quit tmai (or close help if open)"),
Line::from(""),
Self::section_header("Supported Agents"),
Self::description_line(" Claude Code - Anthropic's AI coding assistant"),
Self::description_line(" OpenCode - Open source AI coding assistant"),
Self::description_line(" Codex CLI - OpenAI's command-line tool"),
Self::description_line(" Gemini CLI - Google's AI assistant"),
Line::from(""),
Self::section_header("Agent Status Indicators"),
Self::status_line("✓ Idle", "Agent is waiting for input", Color::Green),
Self::status_line("⠋ Processing", "Agent is working on a task", Color::Yellow),
Self::status_line(
"? Approval",
"Agent needs yes/no confirmation",
Color::Magenta,
),
Self::status_line("⚠ Error", "Agent encountered an error", Color::Red),
Line::from(""),
Line::from(vec![Span::styled(
"Press q or Esc to close this help screen",
Style::default().fg(Color::DarkGray),
)]),
]
}
fn title_line(text: &str) -> Line<'static> {
Line::from(vec![Span::styled(
text.to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)])
}
fn section_header(text: &str) -> Line<'static> {
Line::from(vec![Span::styled(
format!("─── {} ───", text),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)])
}
fn key_line(key: &str, description: &str) -> Line<'static> {
Line::from(vec![
Span::styled(
format!(" {:14}", key),
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::styled(description.to_string(), Style::default().fg(Color::White)),
])
}
fn disabled_key_line(key: &str, description: &str) -> Line<'static> {
Line::from(vec![
Span::styled(
format!(" {:14}", key),
Style::default().fg(Color::DarkGray),
),
Span::styled(
description.to_string(),
Style::default().fg(Color::DarkGray),
),
])
}
fn description_line(text: &str) -> Line<'static> {
Line::from(vec![Span::styled(
text.to_string(),
Style::default().fg(Color::DarkGray),
)])
}
fn status_line(status: &str, description: &str, color: Color) -> Line<'static> {
Line::from(vec![
Span::styled(format!(" {:14}", status), Style::default().fg(color)),
Span::styled(description.to_string(), Style::default().fg(Color::White)),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_line() {
let line = HelpScreen::key_line("test", "description");
assert_eq!(line.spans.len(), 2);
}
}